// SPDX-License-Identifier: AGPL-3.0-or-later import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; import type {JoinSourceType} from '@fluxer/constants/src/GuildConstants'; import {UnknownGuildMemberError} from '@fluxer/errors/src/domains/guild/UnknownGuildMemberError'; import type {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas'; import type {GuildMemberUpdateRequest} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas'; import type {IpInfoService} from '@pkgs/geoip/src/IpInfoService'; import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService'; import type {GuildID, InviteCode, RoleID, UserID} from '../../BrandedTypes'; import type {ChannelService} from '../../channel/services/ChannelService'; import type {EntityAssetService} from '../../infrastructure/EntityAssetService'; import type {IGatewayService} from '../../infrastructure/IGatewayService'; import type {UserCacheService} from '../../infrastructure/UserCacheService'; import type {LimitConfigService} from '../../limits/LimitConfigService'; import type {RequestCache} from '../../middleware/RequestCacheMiddleware'; import type {Guild} from '../../models/Guild'; import type {GuildMember} from '../../models/GuildMember'; import type {IUserRepository} from '../../user/IUserRepository'; import type {GuildAuditLogService} from '../GuildAuditLogService'; import type {IGuildRepositoryAggregate} from '../repositories/IGuildRepositoryAggregate'; import {GuildMemberAuditService} from './member/GuildMemberAuditService'; import {GuildMemberAuthService} from './member/GuildMemberAuthService'; import {GuildMemberEventService} from './member/GuildMemberEventService'; import {GuildMemberOperationsService} from './member/GuildMemberOperationsService'; import {GuildMemberRoleService} from './member/GuildMemberRoleService'; import {GuildMemberSearchIndexService} from './member/GuildMemberSearchIndexService'; import {GuildMemberValidationService} from './member/GuildMemberValidationService'; export class GuildMemberService { private readonly authService: GuildMemberAuthService; private readonly validationService: GuildMemberValidationService; private readonly auditService: GuildMemberAuditService; private readonly eventService: GuildMemberEventService; private readonly operationsService: GuildMemberOperationsService; private readonly roleService: GuildMemberRoleService; private readonly searchIndexService: GuildMemberSearchIndexService; private readonly userRepository: IUserRepository; constructor( private readonly guildRepository: IGuildRepositoryAggregate, channelService: ChannelService, userCacheService: UserCacheService, gatewayService: IGatewayService, entityAssetService: EntityAssetService, userRepository: IUserRepository, rateLimitService: IRateLimitService, private readonly guildAuditLogService: GuildAuditLogService, limitConfigService: LimitConfigService, ipInfoService: IpInfoService, ) { this.userRepository = userRepository; this.authService = new GuildMemberAuthService(gatewayService, userRepository); this.validationService = new GuildMemberValidationService(guildRepository, userRepository, ipInfoService); this.auditService = new GuildMemberAuditService(guildAuditLogService); this.eventService = new GuildMemberEventService(gatewayService, userCacheService); this.searchIndexService = new GuildMemberSearchIndexService(); this.operationsService = new GuildMemberOperationsService( guildRepository, channelService, userCacheService, gatewayService, entityAssetService, userRepository, rateLimitService, this.authService, this.validationService, this.guildAuditLogService, limitConfigService, this.searchIndexService, ); this.roleService = new GuildMemberRoleService( guildRepository, gatewayService, this.authService, this.validationService, ); } private getMemberSearchIndexOptions(guild: Guild | null | undefined) { if (!guild) { return null; } const includeDefault = guild.membersIndexedAt != null; return includeDefault ? {includeDefault} : null; } async getMembers(params: { userId: UserID; guildId: GuildID; limit?: number; after?: UserID; requestCache: RequestCache; }): Promise> { return this.operationsService.getMembers(params); } async getMember(params: { userId: UserID; targetId: UserID; guildId: GuildID; requestCache: RequestCache; }): Promise { return this.operationsService.getMember(params); } async updateMember( params: { userId: UserID; targetId: UserID; guildId: GuildID; data: GuildMemberUpdateRequest | Omit; requestCache: RequestCache; }, auditLogReason?: string | null, ): Promise { const {userId, targetId, guildId, data, requestCache} = params; const targetMember = await this.guildRepository.getMember(guildId, targetId); if (!targetMember) throw new UnknownGuildMemberError(); const previousSnapshot = this.auditService.serializeMemberForAudit(targetMember); const result = await this.operationsService.updateMember({ userId, targetId, guildId, data, requestCache, auditLogReason, }); const updatedMember = await this.guildRepository.getMember(guildId, targetId); if (!updatedMember) throw new UnknownGuildMemberError(); await this.eventService.dispatchGuildMemberUpdate({guildId, member: updatedMember, requestCache}); const targetUser = await this.userRepository.findUnique(targetId); if (targetUser) { const guild = await this.guildRepository.findUnique(guildId); const searchIndexOptions = this.getMemberSearchIndexOptions(guild); if (searchIndexOptions) { void this.searchIndexService.updateMember(updatedMember, targetUser, searchIndexOptions); } } const timeoutMetadata = (() => { if (data.communication_disabled_until === undefined) { return undefined; } const metadata: Record = {}; if (data.communication_disabled_until !== null) { metadata['communication_disabled_until'] = data.communication_disabled_until; } const trimmedReason = data.timeout_reason?.trim(); if (trimmedReason) { metadata['timeout_reason'] = trimmedReason; } return Object.keys(metadata).length > 0 ? metadata : undefined; })(); await this.auditService.recordAuditLog({ guildId, userId, action: AuditLogActionType.MEMBER_UPDATE, targetId: targetId, auditLogReason: auditLogReason ?? null, metadata: timeoutMetadata, changes: this.guildAuditLogService.computeChanges( previousSnapshot, this.auditService.serializeMemberForAudit(updatedMember), ), }); return result; } async addMemberRole( params: { userId: UserID; targetId: UserID; guildId: GuildID; roleId: RoleID; requestCache: RequestCache; }, auditLogReason?: string | null, ): Promise { const {userId, targetId, guildId, roleId, requestCache} = params; const targetMember = await this.guildRepository.getMember(guildId, targetId); if (!targetMember) throw new UnknownGuildMemberError(); const previousSnapshot = this.auditService.serializeMemberForAudit(targetMember); await this.roleService.addMemberRole(params); const updatedMember = await this.guildRepository.getMember(guildId, targetId); if (updatedMember) { await this.eventService.dispatchGuildMemberUpdate({guildId, member: updatedMember, requestCache}); const roleTargetUser = await this.userRepository.findUnique(targetId); if (roleTargetUser) { const guild = await this.guildRepository.findUnique(guildId); const searchIndexOptions = this.getMemberSearchIndexOptions(guild); if (searchIndexOptions) { void this.searchIndexService.updateMember(updatedMember, roleTargetUser, searchIndexOptions); } } await this.auditService.recordAuditLog({ guildId, userId, action: AuditLogActionType.MEMBER_ROLE_UPDATE, targetId: targetId, auditLogReason: auditLogReason ?? null, metadata: {role_id: roleId.toString(), action: 'add'}, changes: this.guildAuditLogService.computeChanges( previousSnapshot, this.auditService.serializeMemberForAudit(updatedMember), ), }); } } async systemAddMemberRole(params: { targetId: UserID; guildId: GuildID; roleId: RoleID; initiatorId: UserID; requestCache: RequestCache; }): Promise { const {targetId, guildId, roleId, initiatorId, requestCache} = params; const targetMember = await this.guildRepository.getMember(guildId, targetId); if (!targetMember) throw new UnknownGuildMemberError(); const previousSnapshot = this.auditService.serializeMemberForAudit(targetMember); await this.roleService.systemAddMemberRole({targetId, guildId, roleId}); const updatedMember = await this.guildRepository.getMember(guildId, targetId); if (updatedMember) { await this.eventService.dispatchGuildMemberUpdate({guildId, member: updatedMember, requestCache}); const roleTargetUser = await this.userRepository.findUnique(targetId); if (roleTargetUser) { const guild = await this.guildRepository.findUnique(guildId); const searchIndexOptions = this.getMemberSearchIndexOptions(guild); if (searchIndexOptions) { void this.searchIndexService.updateMember(updatedMember, roleTargetUser, searchIndexOptions); } } await this.auditService.recordAuditLog({ guildId, userId: initiatorId, action: AuditLogActionType.MEMBER_ROLE_UPDATE, targetId: targetId, auditLogReason: null, metadata: {role_id: roleId.toString(), action: 'add'}, changes: this.guildAuditLogService.computeChanges( previousSnapshot, this.auditService.serializeMemberForAudit(updatedMember), ), }); } } async removeMemberRole( params: { userId: UserID; targetId: UserID; guildId: GuildID; roleId: RoleID; requestCache: RequestCache; }, auditLogReason?: string | null, ): Promise { const {userId, targetId, guildId, roleId, requestCache} = params; const targetMember = await this.guildRepository.getMember(guildId, targetId); if (!targetMember) throw new UnknownGuildMemberError(); const previousSnapshot = this.auditService.serializeMemberForAudit(targetMember); await this.roleService.removeMemberRole(params); const updatedMember = await this.guildRepository.getMember(guildId, targetId); if (updatedMember) { await this.eventService.dispatchGuildMemberUpdate({guildId, member: updatedMember, requestCache}); const roleTargetUser = await this.userRepository.findUnique(targetId); if (roleTargetUser) { const guild = await this.guildRepository.findUnique(guildId); const searchIndexOptions = this.getMemberSearchIndexOptions(guild); if (searchIndexOptions) { void this.searchIndexService.updateMember(updatedMember, roleTargetUser, searchIndexOptions); } } await this.auditService.recordAuditLog({ guildId, userId, action: AuditLogActionType.MEMBER_ROLE_UPDATE, targetId: targetId, auditLogReason: auditLogReason ?? null, metadata: {role_id: roleId.toString(), action: 'remove'}, changes: this.guildAuditLogService.computeChanges( previousSnapshot, this.auditService.serializeMemberForAudit(updatedMember), ), }); } } async removeMember( params: { userId: UserID; targetId: UserID; guildId: GuildID; }, auditLogReason?: string | null, ): Promise { const {userId, targetId, guildId} = params; const targetMember = await this.guildRepository.getMember(guildId, targetId); if (!targetMember) throw new UnknownGuildMemberError(); await this.operationsService.removeMember(params); const guild = await this.guildRepository.findUnique(guildId); const searchIndexOptions = this.getMemberSearchIndexOptions(guild); if (searchIndexOptions) { void this.searchIndexService.deleteMember(guildId, targetId, searchIndexOptions); } await this.eventService.dispatchGuildMemberRemove({guildId, userId: targetId}); await this.auditService.recordAuditLog({ guildId, userId, action: AuditLogActionType.MEMBER_KICK, targetId: targetId, auditLogReason: auditLogReason ?? null, }); } async addUserToGuild(params: { userId: UserID; guildId: GuildID; sendJoinMessage?: boolean; skipGuildLimitCheck?: boolean; skipBanCheck?: boolean; skipRiskGate?: boolean; isTemporary?: boolean; joinSourceType?: JoinSourceType; sourceInviteCode?: InviteCode; inviterId?: UserID; requestCache: RequestCache; initiatorId?: UserID; }): Promise { return this.operationsService.addUserToGuild(params, this.eventService); } async leaveGuild( params: { userId: UserID; guildId: GuildID; }, _auditLogReason?: string | null, ): Promise { const {userId, guildId} = params; const member = await this.guildRepository.getMember(guildId, userId); if (!member) throw new UnknownGuildMemberError(); await this.operationsService.leaveGuild(params); const guild = await this.guildRepository.findUnique(guildId); const searchIndexOptions = this.getMemberSearchIndexOptions(guild); if (searchIndexOptions) { void this.searchIndexService.deleteMember(guildId, userId, searchIndexOptions); } await this.eventService.dispatchGuildMemberRemove({guildId, userId}); } }