This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330// SPDX-License-Identifier: AGPL-3.0-or-later
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError';import type { CancelBulkMessageDeletionRequest, ListUserChangeLogRequest, ListUserDmChannelsRequest, ListUserGroupDmChannelsRequest,} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';import type {ICacheService} from '@pkgs/cache/src/ICacheService';import type Stripe from 'stripe';import type {ApiContext} from '../../ApiContext';import {createChannelID, createUserID, type UserID} from '../../BrandedTypes';import type {IChannelRepository} from '../../channel/IChannelRepository';import type {IGuildRepositoryAggregate} from '../../guild/repositories/IGuildRepositoryAggregate';import type {IDiscriminatorService} from '../../infrastructure/DiscriminatorService';import type {EntityAssetService} from '../../infrastructure/EntityAssetService';import type {KVAccountDeletionQueueService} from '../../infrastructure/KVAccountDeletionQueueService';import type {KVBulkMessageDeletionQueueService} from '../../infrastructure/KVBulkMessageDeletionQueueService';import type {UserCacheService} from '../../infrastructure/UserCacheService';import {getBillingRepository} from '../../middleware/ServiceRegistry';import type {ReportService} from '../../report/ReportService';import type {IRiskHistoryRepository} from '../../risk/HistoricalOutcomeRepository';import type {IUserRepository} from '../../user/IUserRepository';import type {UserContactChangeLogService} from '../../user/services/UserContactChangeLogService';import {mapUserToAdminResponse} from '../models/UserTypes';import type {AdminAuditService} from './AdminAuditService';import type {AdminBanManagementService} from './AdminBanManagementService';import {AdminUserBanService} from './AdminUserBanService';import {AdminUserDeletionService} from './AdminUserDeletionService';import {AdminUserLookupService} from './AdminUserLookupService';import {AdminUserProfileService} from './AdminUserProfileService';import {AdminUserSecurityService} from './AdminUserSecurityService';import {AdminUserUpdatePropagator} from './AdminUserUpdatePropagator';
interface AdminUserServiceDeps { apiContext: ApiContext; guildRepository: IGuildRepositoryAggregate; channelRepository: IChannelRepository; discriminatorService: IDiscriminatorService; entityAssetService: EntityAssetService; auditService: AdminAuditService; userCacheService: UserCacheService; banManagementService: AdminBanManagementService; kvDeletionQueue: KVAccountDeletionQueueService; bulkMessageDeletionQueue: KVBulkMessageDeletionQueueService; stripe: Stripe | null; riskHistoryRepository: Pick<IRiskHistoryRepository, 'recordOutcomeForUser'>; reportService: ReportService;}
export class AdminUserService { readonly lookupService: AdminUserLookupService; readonly profileService: AdminUserProfileService; readonly securityService: AdminUserSecurityService; readonly banService: AdminUserBanService; readonly deletionService: AdminUserDeletionService; private readonly updatePropagator: AdminUserUpdatePropagator; private readonly contactChangeLogService: UserContactChangeLogService; private readonly auditService: AdminAuditService; private readonly userRepository: IUserRepository; private readonly guildRepository: IGuildRepositoryAggregate; private readonly channelRepository: IChannelRepository; private readonly bulkMessageDeletionQueue: KVBulkMessageDeletionQueueService; private readonly cacheService: ICacheService;
constructor(deps: AdminUserServiceDeps) { const {users, cache, gateway, contactChangeLog} = deps.apiContext.services; this.updatePropagator = new AdminUserUpdatePropagator({ userCacheService: deps.userCacheService, userRepository: users, guildRepository: deps.guildRepository, gatewayService: gateway, }); this.userRepository = users; this.guildRepository = deps.guildRepository; this.channelRepository = deps.channelRepository; this.auditService = deps.auditService; this.bulkMessageDeletionQueue = deps.bulkMessageDeletionQueue; this.cacheService = cache; this.lookupService = new AdminUserLookupService({ apiContext: deps.apiContext, }); this.profileService = new AdminUserProfileService({ apiContext: deps.apiContext, discriminatorService: deps.discriminatorService, entityAssetService: deps.entityAssetService, auditService: deps.auditService, updatePropagator: this.updatePropagator, guildRepository: deps.guildRepository, }); this.securityService = new AdminUserSecurityService({ apiContext: deps.apiContext, auditService: deps.auditService, updatePropagator: this.updatePropagator, riskHistoryRepository: deps.riskHistoryRepository, }); this.banService = new AdminUserBanService({ apiContext: deps.apiContext, auditService: deps.auditService, updatePropagator: this.updatePropagator, }); this.deletionService = new AdminUserDeletionService({ apiContext: deps.apiContext, auditService: deps.auditService, banManagementService: deps.banManagementService, reportService: deps.reportService, updatePropagator: this.updatePropagator, kvDeletionQueue: deps.kvDeletionQueue, stripe: deps.stripe, billingRepository: getBillingRepository(), }); this.contactChangeLogService = contactChangeLog; }
async listUserDmChannels(data: ListUserDmChannelsRequest) { const userId = createUserID(data.user_id); const user = await this.userRepository.findUnique(userId); if (!user) { throw new UnknownUserError(); } const channels = await this.userRepository.listHistoricalDmChannelsPaginated(userId, { limit: data.limit, beforeChannelId: data.before ? createChannelID(data.before) : undefined, afterChannelId: data.after ? createChannelID(data.after) : undefined, }); const channelModels = await this.channelRepository.listChannels(channels.map((channel) => channel.channelId)); const channelModelById = new Map( channelModels.filter((c): c is NonNullable<typeof c> => c != null).map((c) => [c.id, c]), ); const guildIds = channelModels .map((channel) => channel?.guildId) .filter((guildId): guildId is NonNullable<typeof guildId> => guildId !== null && guildId !== undefined); const guilds = await this.guildRepository.listGuilds(guildIds); const guildNsfwLevels = new Map(guilds.map((guild) => [guild.id.toString(), guild.nsfwLevel])); const channelNsfwById = new Map(channelModels.map((channel) => [channel?.id.toString(), channel?.isNsfw ?? null])); const channelGuildIdById = new Map( channelModels.map((channel) => [channel?.id.toString(), channel?.guildId ?? null]), ); const allRecipientIds = new Set<UserID>(); for (const channel of channels) { for (const recipientId of channel.recipientIds) { allRecipientIds.add(recipientId); } } for (const model of channelModels) { if (model?.ownerId) { allRecipientIds.add(model.ownerId); } } const resolvedUsers = await this.resolveUsers([...allRecipientIds]); return { channels: channels.map((channel) => { const model = channelModelById.get(channel.channelId); return { channel_id: channel.channelId.toString(), channel_type: channel.channelType, channel_nsfw: channelNsfwById.get(channel.channelId.toString()) ?? null, guild_nsfw_level: (() => { const guildId = channelGuildIdById.get(channel.channelId.toString()); if (!guildId) { return null; } return guildNsfwLevels.get(guildId.toString()) ?? null; })(), recipient_ids: channel.recipientIds.map((recipientId) => recipientId.toString()), recipients: channel.recipientIds .map((recipientId) => resolvedUsers.get(recipientId)) .filter((u): u is NonNullable<typeof u> => u != null), last_message_id: channel.lastMessageId?.toString() ?? null, is_open: channel.open, name: model?.name ?? null, icon: model?.iconHash ?? null, owner_id: model?.ownerId?.toString() ?? null, }; }), }; }
private async resolveUsers(userIds: Array<UserID>): Promise< Map< UserID, { id: string; username: string; discriminator: string; global_name: string | null; avatar: string | null; } > > { const results = new Map< UserID, { id: string; username: string; discriminator: string; global_name: string | null; avatar: string | null; } >(); const users = await Promise.all(userIds.map((id) => this.userRepository.findUnique(id))); for (let i = 0; i < userIds.length; i++) { const user = users[i]; if (user) { results.set(userIds[i], { id: user.id.toString(), username: user.username, discriminator: String(user.discriminator).padStart(4, '0'), global_name: user.globalName, avatar: user.avatarHash, }); } } return results; }
async listUserGroupDmChannels(data: ListUserGroupDmChannelsRequest) { const userId = createUserID(data.user_id); const user = await this.userRepository.findUnique(userId); if (!user) { throw new UnknownUserError(); } const summaries = await this.userRepository.listPrivateChannelSummaries(userId); const groupDmSummaries = summaries.filter((s) => s.isGroupDm); if (groupDmSummaries.length === 0) { return {channels: []}; } const channelModels = await this.channelRepository.listChannels(groupDmSummaries.map((s) => s.channelId)); const allUserIds = new Set<UserID>(); for (const model of channelModels) { if (model) { for (const recipientId of model.recipientIds) { allUserIds.add(recipientId); } if (model.ownerId) { allUserIds.add(model.ownerId); } } } const resolvedUsers = await this.resolveUsers([...allUserIds]); return { channels: groupDmSummaries.map((summary, i) => { const model = channelModels[i]; const recipientIds = model ? [...model.recipientIds] : []; return { channel_id: summary.channelId.toString(), channel_type: summary.channelType, channel_nsfw: model?.isNsfw ?? null, guild_nsfw_level: null, recipient_ids: recipientIds.map((id) => id.toString()), recipients: recipientIds .map((id) => resolvedUsers.get(id)) .filter((u): u is NonNullable<typeof u> => u != null), last_message_id: summary.lastMessageId?.toString() ?? null, is_open: summary.open, name: model?.name ?? null, icon: model?.iconHash ?? null, owner_id: model?.ownerId?.toString() ?? null, }; }), }; }
async listUserChangeLog(data: ListUserChangeLogRequest, acls: ReadonlySet<string>) { if (!acls.has(AdminACLs.USER_VIEW_CONTACT_LOG) && !acls.has(AdminACLs.WILDCARD)) { return {entries: [], next_page_token: null}; } const canViewEmail = acls.has(AdminACLs.USER_VIEW_EMAIL) || acls.has(AdminACLs.WILDCARD); const entries = await this.contactChangeLogService.listLogs({ userId: createUserID(data.user_id), limit: data.limit, beforeEventId: data.page_token, }); const lastEntry = entries.length === data.limit && entries.length > 0 ? entries.at(-1) : null; const nextPageToken = lastEntry?.event_id != null ? lastEntry.event_id.toString() : null; return { entries: entries.map((entry) => { const isEmailField = entry.field === 'email'; const shouldRedact = isEmailField && !canViewEmail; return { event_id: entry.event_id != null ? entry.event_id.toString() : String(entry.user_id), field: entry.field, old_value: shouldRedact ? '[redacted]' : (entry.old_value ?? null), new_value: shouldRedact ? '[redacted]' : (entry.new_value ?? null), reason: entry.reason ?? null, actor_user_id: entry.actor_user_id ? entry.actor_user_id.toString() : null, event_at: entry.event_at.toISOString(), }; }), next_page_token: nextPageToken, }; }
async cancelBulkMessageDeletion( data: CancelBulkMessageDeletionRequest, adminUserId: UserID, auditLogReason: string | null, acls: ReadonlySet<string>, ) { const userId = createUserID(data.user_id); const user = await this.userRepository.findUnique(userId); if (!user) { throw new UnknownUserError(); } const updatedUser = await this.userRepository.patchUpsert( userId, { pending_bulk_message_deletion_at: null, pending_bulk_message_deletion_channel_count: null, pending_bulk_message_deletion_message_count: null, }, user.toRow(), ); await this.bulkMessageDeletionQueue.removeFromQueue(userId); await this.auditService.createAuditLog({ adminUserId, targetType: 'user', targetId: BigInt(userId), action: 'cancel_bulk_message_deletion', auditLogReason, metadata: new Map(), }); return { user: await mapUserToAdminResponse(updatedUser, this.cacheService, acls), }; }}