// SPDX-License-Identifier: AGPL-3.0-or-later import {ReportAlreadyResolvedError} from '@fluxer/errors/src/domains/moderation/ReportAlreadyResolvedError'; import {UnknownReportError} from '@fluxer/errors/src/domains/moderation/UnknownReportError'; import type {ChannelID, MessageID, ReportID, UserID} from '../BrandedTypes'; import {createChannelID, createGuildID, createMessageID, createReportID, createUserID} from '../BrandedTypes'; import {fetchMany, fetchOne, upsertOne} from '../database/CassandraQueryExecution'; import {Db} from '../database/CassandraTypes'; import type { DSAReportEmailVerificationRow, DSAReportTicketRow, MessageReportSubmissionByReporterRow, } from '../database/types/ReportTypes'; import { DSAReportEmailVerifications, DSAReportTickets, IARSubmissions, MessageReportSubmissionsByReporter, } from '../Tables'; import type { IARMessageContext, IARMessageContextRow, IARSubmission, IARSubmissionRow, IReportRepository, } from './IReportRepository'; const GET_REPORT_QUERY = IARSubmissions.select({ where: IARSubmissions.where.eq('report_id'), limit: 1, }); const createFetchAllReportsPaginatedQuery = (limit: number) => IARSubmissions.select({ where: IARSubmissions.where.tokenGt('report_id', 'last_report_id'), limit, }); const GET_DSA_EMAIL_VERIFICATION_QUERY = DSAReportEmailVerifications.select({ where: DSAReportEmailVerifications.where.eq('email_lower'), limit: 1, }); const GET_DSA_REPORT_TICKET_QUERY = DSAReportTickets.select({ where: DSAReportTickets.where.eq('ticket'), limit: 1, }); const GET_MESSAGE_REPORT_SUBMISSION_BY_REPORTER_QUERY = MessageReportSubmissionsByReporter.select({ where: [ MessageReportSubmissionsByReporter.where.eq('reporter_id'), MessageReportSubmissionsByReporter.where.eq('channel_id'), MessageReportSubmissionsByReporter.where.eq('message_id'), ], limit: 1, }); function createFetchAllReportsFirstPageQuery(limit: number) { return IARSubmissions.select({limit}); } export class ReportRepository implements IReportRepository { async createReport(data: IARSubmissionRow): Promise { await upsertOne(IARSubmissions.insert(data)); return this.mapRowToSubmission(data); } async reserveMessageReportByReporter(data: MessageReportSubmissionByReporterRow): Promise { const existing = await fetchOne( GET_MESSAGE_REPORT_SUBMISSION_BY_REPORTER_QUERY.bind({ reporter_id: data.reporter_id, channel_id: data.channel_id, message_id: data.message_id, }), ); if (existing) { return false; } await upsertOne(MessageReportSubmissionsByReporter.insert(data)); return true; } async deleteMessageReportByReporter(reporterId: UserID, channelId: ChannelID, messageId: MessageID): Promise { await MessageReportSubmissionsByReporter.deleteByPk({ reporter_id: reporterId, channel_id: channelId, message_id: messageId, }); } async getReport(reportId: ReportID): Promise { const row = await fetchOne(GET_REPORT_QUERY.bind({report_id: reportId})); return row ? this.mapRowToSubmission(row) : null; } async resolveReport( reportId: ReportID, resolvedByAdminId: UserID, publicComment: string | null, auditLogReason: string | null, ): Promise { const report = await this.getReport(reportId); if (!report) { throw new UnknownReportError(); } if (report.status !== 0) { throw new ReportAlreadyResolvedError(); } const resolvedAt = new Date(); const newStatus = 1; await upsertOne( IARSubmissions.patchByPk( {report_id: reportId}, { resolved_at: Db.set(resolvedAt), resolved_by_admin_id: Db.set(resolvedByAdminId), public_comment: Db.set(publicComment), audit_log_reason: Db.set(auditLogReason), status: Db.set(newStatus), }, ), ); return { ...report, resolvedAt, resolvedByAdminId, publicComment, auditLogReason, status: newStatus, }; } private mapRowToSubmission(row: IARSubmissionRow): IARSubmission { return { reportId: createReportID(row.report_id), reporterId: row.reporter_id ? createUserID(row.reporter_id) : null, reporterEmail: row.reporter_email, reporterFullLegalName: row.reporter_full_legal_name, reporterCountryOfResidence: row.reporter_country_of_residence, reportedAt: row.reported_at, status: row.status, reportType: row.report_type, category: row.category, additionalInfo: row.additional_info, reportedUserId: row.reported_user_id ? createUserID(row.reported_user_id) : null, reportedUserAvatarHash: row.reported_user_avatar_hash, reportedGuildId: row.reported_guild_id ? createGuildID(row.reported_guild_id) : null, reportedGuildName: row.reported_guild_name, reportedGuildIconHash: row.reported_guild_icon_hash, reportedMessageId: row.reported_message_id ? createMessageID(row.reported_message_id) : null, reportedChannelId: row.reported_channel_id ? createChannelID(row.reported_channel_id) : null, reportedChannelName: row.reported_channel_name, messageContext: row.message_context ? this.mapMessageContext(row.message_context) : null, guildContextId: row.guild_context_id ? createGuildID(row.guild_context_id) : null, resolvedAt: row.resolved_at, resolvedByAdminId: row.resolved_by_admin_id ? createUserID(row.resolved_by_admin_id) : null, publicComment: row.public_comment, auditLogReason: row.audit_log_reason, reportedGuildInviteCode: row.reported_guild_invite_code, reportedGuildNsfw: row.reported_guild_nsfw ?? null, reportedGuildContentWarningLevel: row.reported_guild_content_warning_level ?? null, reportedGuildContentWarningText: row.reported_guild_content_warning_text ?? null, reportedChannelNsfwOverride: row.reported_channel_nsfw_override ?? null, reportedChannelContentWarningLevel: row.reported_channel_content_warning_level ?? null, reportedChannelContentWarningText: row.reported_channel_content_warning_text ?? null, reportedChannelEffectiveNsfw: row.reported_channel_effective_nsfw ?? null, reportedChannelEffectiveContentWarningLevel: row.reported_channel_effective_content_warning_level ?? null, reportedChannelEffectiveContentWarningText: row.reported_channel_effective_content_warning_text ?? null, }; } async listAllReportsPaginated(limit: number, lastReportId?: ReportID): Promise> { let reports: Array; if (lastReportId) { const query = createFetchAllReportsPaginatedQuery(limit); reports = await fetchMany(query.bind({last_report_id: lastReportId})); } else { const query = createFetchAllReportsFirstPageQuery(limit); reports = await fetchMany(query.bind({})); } return reports.map((report) => this.mapRowToSubmission(report)); } async upsertDsaEmailVerification(row: DSAReportEmailVerificationRow): Promise { await upsertOne(DSAReportEmailVerifications.insert(row)); } async getDsaEmailVerification(emailLower: string): Promise { const row = await fetchOne( GET_DSA_EMAIL_VERIFICATION_QUERY.bind({email_lower: emailLower}), ); return row ?? null; } async deleteDsaEmailVerification(emailLower: string): Promise { await DSAReportEmailVerifications.deleteByPk({email_lower: emailLower}); } async createDsaTicket(row: DSAReportTicketRow): Promise { await upsertOne(DSAReportTickets.insert(row)); } async getDsaTicket(ticket: string): Promise { const row = await fetchOne(GET_DSA_REPORT_TICKET_QUERY.bind({ticket})); return row ?? null; } async deleteDsaTicket(ticket: string): Promise { await DSAReportTickets.deleteByPk({ticket}); } private mapMessageContext(rawContext: Array): Array { const toBigintArray = (collection: ReadonlyArray | Set | null | undefined): Array => collection ? Array.from(collection) : []; return rawContext.map((msg) => ({ messageId: createMessageID(msg.message_id), authorId: createUserID(msg.author_id), channelId: msg.channel_id ? createChannelID(msg.channel_id) : null, authorUsername: msg.author_username, authorDiscriminator: msg.author_discriminator, authorAvatarHash: msg.author_avatar_hash, content: msg.content, timestamp: msg.timestamp, editedTimestamp: msg.edited_timestamp, type: msg.type, flags: msg.flags, mentionEveryone: msg.mention_everyone, mentionUsers: toBigintArray(msg.mention_users), mentionRoles: toBigintArray(msg.mention_roles), mentionChannels: toBigintArray(msg.mention_channels), attachments: msg.attachments ?? [], embeds: msg.embeds ?? [], stickers: msg.sticker_items ?? [], })); } }