This repository has no description
Something went wrong. Try again.
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372// SPDX-License-Identifier: AGPL-3.0-or-later
import type { BrowseChannelRequest, SearchChannelMessagesRequest,} from '@fluxer/schema/src/domains/admin/AdminMessageBrowseSchemas';import type { DeleteMessageRequest, LookupMessageByAttachmentRequest, LookupMessageRequest,} from '@fluxer/schema/src/domains/admin/AdminMessageSchemas';import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';import type {ApiContext} from '../../ApiContext';import { type AttachmentID, type ChannelID, createAttachmentID, createChannelID, createMessageID, createUserID, type MessageID, type UserID,} from '../../BrandedTypes';import type {IChannelRepository} from '../../channel/IChannelRepository';import {purgeMessageAttachments} from '../../channel/services/message/MessageHelpers';import { createMessageResponseDataService, type MessageResponseAccessContext, messageResponseAccessForChannel, messageResponseAccessForGuild,} from '../../channel/services/message/MessageResponseDataService';import type {NcmecAttachmentStatusResponse, NcmecSubmissionService} from '../../csam/NcmecSubmissionService';import type {IGuildRepositoryAggregate} from '../../guild/repositories/IGuildRepositoryAggregate';import {getPurgeQueue, getStorageService} from '../../middleware/ServiceSingletons';import {getMessageSearchService} from '../../SearchFactory';import {deleteMessageSearchDocuments} from '../../search/MessageSearchIndexCleanup';import {searchExistingMessages} from '../../search/MessageSearchResultReconciler';import type {AdminAuditService} from './AdminAuditService';
interface AdminMessageServiceDeps { apiContext: ApiContext; channelRepository: IChannelRepository; guildRepository: IGuildRepositoryAggregate; auditService: AdminAuditService; ncmecSubmissionService: NcmecSubmissionService;}
interface ChannelNsfwContext { channelNsfw: boolean | null; guildNsfwLevel: number | null; channelName: string | null; guildId: string | null; guildName: string | null;}
export class AdminMessageService { constructor(private readonly deps: AdminMessageServiceDeps) {}
async lookupAttachment({ channelId, attachmentId, filename, }: { channelId: ChannelID; attachmentId: AttachmentID; filename: string; }): Promise<{ message_id: MessageID | null; }> { const {channelRepository} = this.deps; const messageId = await channelRepository.lookupAttachmentByChannelAndFilename(channelId, attachmentId, filename); return { message_id: messageId, }; }
async lookupMessage(data: LookupMessageRequest) { const channelId = createChannelID(data.channel_id); const messageId = createMessageID(data.message_id); const channelNsfwContext = await this.getChannelNsfwContext(channelId); const messageResponses = ( await this.listMessageResponsesForAdmin({ channelId, limit: data.context_limit, around: messageId, }) ).reverse(); const attachmentStatuses = await this.getAttachmentStatusesForMessages(messageResponses); const priorReports = await this.getPriorReportsForMessages(messageResponses); const adminMessages = messageResponses.map((message) => this.mapMessageResponseToAdminMessage(message, channelNsfwContext, attachmentStatuses, priorReports), ); return { messages: adminMessages, message_responses: messageResponses, message_id: messageId.toString(), }; }
async lookupMessageByAttachment(data: LookupMessageByAttachmentRequest) { const channelId = createChannelID(data.channel_id); const attachmentId = createAttachmentID(data.attachment_id); const messageId = await this.deps.channelRepository.lookupAttachmentByChannelAndFilename( channelId, attachmentId, data.filename, ); if (!messageId) { return { messages: [], message_responses: [], message_id: null, }; } const result = await this.lookupMessage({ channel_id: data.channel_id, message_id: BigInt(messageId), context_limit: data.context_limit, }); return { messages: result.messages, message_responses: result.message_responses, message_id: messageId.toString(), }; }
async deleteMessage(data: DeleteMessageRequest, adminUserId: UserID, auditLogReason: string | null) { const {channelRepository, auditService} = this.deps; const {gateway: gatewayService} = this.deps.apiContext.services; const channelId = createChannelID(data.channel_id); const messageId = createMessageID(data.message_id); const channel = await channelRepository.findUnique(channelId); const message = await channelRepository.getMessage(channelId, messageId); if (message) { if (message.attachments.length > 0) { await purgeMessageAttachments(message, getStorageService(), getPurgeQueue()); } await channelRepository.deleteMessage( channelId, messageId, message.authorId || createUserID(0n), message.pinnedTimestamp || undefined, ); if (channel) { if (channel.guildId) { await gatewayService.dispatchGuild({ guildId: channel.guildId, event: 'MESSAGE_DELETE', data: { channel_id: channelId.toString(), id: messageId.toString(), }, }); } else { for (const recipientId of channel.recipientIds) { await gatewayService.dispatchPresence({ userId: recipientId, event: 'MESSAGE_DELETE', data: { channel_id: channelId.toString(), id: messageId.toString(), }, }); } } } await deleteMessageSearchDocuments([messageId], {context: {source: 'admin_message_delete'}}); } await auditService.createAuditLog({ adminUserId, targetType: 'message', targetId: BigInt(messageId), action: 'delete_message', auditLogReason, metadata: new Map([ ['channel_id', channelId.toString()], ['message_id', messageId.toString()], ]), }); return { success: true, }; }
async browseChannel(data: BrowseChannelRequest) { const {snowflake: snowflakeService} = this.deps.apiContext.services; const channelId = createChannelID(data.channel_id); const limit = data.limit ?? 50; const channelNsfwContext = await this.getChannelNsfwContext(channelId); let beforeId = data.before ? createMessageID(data.before) : undefined; const afterId = data.after ? createMessageID(data.after) : undefined; if (!beforeId && !afterId) { beforeId = createMessageID(await snowflakeService.generate()); } const messages = await this.listMessageResponsesForAdmin({ channelId, limit, before: beforeId, after: afterId, }); const messageResponses = afterId ? messages : [...messages].reverse(); const attachmentStatuses = await this.getAttachmentStatusesForMessages(messageResponses); const priorReports = await this.getPriorReportsForMessages(messageResponses); const adminMessages = messageResponses.map((message) => this.mapMessageResponseToAdminMessage(message, channelNsfwContext, attachmentStatuses, priorReports), ); return { messages: adminMessages, message_responses: messageResponses, has_more: messages.length >= limit, }; }
async searchChannelMessages(data: SearchChannelMessagesRequest) { const {channelRepository} = this.deps; const channelId = createChannelID(data.channel_id); const limit = data.limit ?? 25; const channelNsfwContext = await this.getChannelNsfwContext(channelId); const searchService = getMessageSearchService(); if (!searchService) { return {messages: [], message_responses: [], total: 0}; } const result = await searchExistingMessages({ searchService, messageRepository: channelRepository, query: data.query, filters: {channelIds: [channelId.toString()]}, hitsPerPage: limit, page: 1, }); const messageEntries = result.hits.map((hit) => ({ channelId: createChannelID(BigInt(hit.channelId)), messageId: createMessageID(BigInt(hit.id)), })); const resolvedMessages = await Promise.all( messageEntries.map(({channelId, messageId}) => this.getMessageResponseForAdmin(channelId, messageId)), ); const messageResponses = resolvedMessages.filter((message): message is MessageResponse => message !== null); const attachmentStatuses = await this.getAttachmentStatusesForMessages(messageResponses); const priorReports = await this.getPriorReportsForMessages(messageResponses); const adminMessages = messageResponses.map((message) => this.mapMessageResponseToAdminMessage(message, channelNsfwContext, attachmentStatuses, priorReports), ); return { messages: adminMessages, message_responses: messageResponses, total: result.total, }; }
private async getAttachmentStatusesForMessages( messages: Array<MessageResponse>, ): Promise<Map<string, NcmecAttachmentStatusResponse>> { const attachmentIds = messages.flatMap((message) => (message.attachments ?? []).map((attachment) => createAttachmentID(BigInt(attachment.id))), ); return this.deps.ncmecSubmissionService.getAttachmentStatuses(attachmentIds); }
private async getMessageResponseAccessForAdmin(channelId: ChannelID): Promise<MessageResponseAccessContext> { const channel = await this.deps.channelRepository.findUnique(channelId); return channel ? messageResponseAccessForChannel(channel) : messageResponseAccessForGuild(null); }
private async listMessageResponsesForAdmin(params: { channelId: ChannelID; limit: number; before?: MessageID; after?: MessageID; around?: MessageID; }): Promise<Array<MessageResponse>> { const access = await this.getMessageResponseAccessForAdmin(params.channelId); return createMessageResponseDataService().listMessages({ userId: createUserID(0n), channelId: params.channelId, limit: params.limit, before: params.before, after: params.after, around: params.around, access, }); }
private async getMessageResponseForAdmin( channelId: ChannelID, messageId: MessageID, ): Promise<MessageResponse | null> { const access = await this.getMessageResponseAccessForAdmin(channelId); return createMessageResponseDataService().getMessage({ userId: createUserID(0n), channelId, messageId, access, }); }
private async getPriorReportsForMessages(messages: Array<MessageResponse>): Promise<Map<string, Array<string>>> { const authorIds = messages.map((message) => createUserID(BigInt(message.author.id))); return this.deps.ncmecSubmissionService.getUserPriorReportIds(authorIds); }
private mapMessageResponseToAdminMessage( message: MessageResponse, channelNsfwContext: ChannelNsfwContext, attachmentStatuses: Map<string, NcmecAttachmentStatusResponse>, priorReports: Map<string, Array<string>>, ) { return { id: message.id, channel_id: message.channel_id ?? '', channel_name: channelNsfwContext.channelName, channel_nsfw: channelNsfwContext.channelNsfw, guild_id: channelNsfwContext.guildId, guild_name: channelNsfwContext.guildName, guild_nsfw_level: channelNsfwContext.guildNsfwLevel, author_id: message.author.id, author_username: message.author.username, author_global_name: message.author.global_name ?? null, author_discriminator: message.author.discriminator, author_avatar: message.author.avatar, content: message.content ?? '', timestamp: message.timestamp, user_prior_ncmec_report_ids: priorReports.get(message.author.id) ?? [], attachments: message.attachments?.map((attachment) => ({ id: attachment.id, filename: attachment.filename, url: attachment.url, nsfw: attachment.nsfw ?? null, content_type: attachment.content_type ?? null, width: attachment.width ?? null, height: attachment.height ?? null, size: attachment.size ?? null, ncmec_status: attachmentStatuses.get(attachment.id)?.status ?? 'not_submitted', ncmec_report_id: attachmentStatuses.get(attachment.id)?.ncmec_report_id ?? null, ncmec_failure_reason: attachmentStatuses.get(attachment.id)?.failure_reason ?? null, })) ?? [], }; }
private async getChannelNsfwContext(channelId: ChannelID): Promise<ChannelNsfwContext> { const {channelRepository, guildRepository} = this.deps; const channel = await channelRepository.findUnique(channelId); if (!channel) { return { channelNsfw: null, guildNsfwLevel: null, channelName: null, guildId: null, guildName: null, }; } if (!channel.guildId) { return { channelNsfw: channel.isNsfw, guildNsfwLevel: null, channelName: channel.name ?? null, guildId: null, guildName: null, }; } const guild = await guildRepository.findUnique(channel.guildId); return { channelNsfw: channel.isNsfw, guildNsfwLevel: guild?.nsfwLevel ?? null, channelName: channel.name ?? null, guildId: channel.guildId.toString(), guildName: guild?.name ?? null, }; }}