mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-10 20:52:12 +09:00
352 lines
13 KiB
TypeScript
352 lines
13 KiB
TypeScript
// 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<Array<GuildMemberResponse>> {
|
|
return this.operationsService.getMembers(params);
|
|
}
|
|
|
|
async getMember(params: {
|
|
userId: UserID;
|
|
targetId: UserID;
|
|
guildId: GuildID;
|
|
requestCache: RequestCache;
|
|
}): Promise<GuildMemberResponse> {
|
|
return this.operationsService.getMember(params);
|
|
}
|
|
|
|
async updateMember(
|
|
params: {
|
|
userId: UserID;
|
|
targetId: UserID;
|
|
guildId: GuildID;
|
|
data: GuildMemberUpdateRequest | Omit<GuildMemberUpdateRequest, 'roles'>;
|
|
requestCache: RequestCache;
|
|
},
|
|
auditLogReason?: string | null,
|
|
): Promise<GuildMemberResponse> {
|
|
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<string, string> = {};
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<GuildMember> {
|
|
return this.operationsService.addUserToGuild(params, this.eventService);
|
|
}
|
|
|
|
async leaveGuild(
|
|
params: {
|
|
userId: UserID;
|
|
guildId: GuildID;
|
|
},
|
|
_auditLogReason?: string | null,
|
|
): Promise<void> {
|
|
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});
|
|
}
|
|
}
|