// SPDX-License-Identifier: AGPL-3.0-or-later import {type GuildID, guildIdToRoleId, type RoleID, type UserID} from '@app/api/BrandedTypes'; import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildMember} from '@app/api/models/GuildMember'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError'; import {UnknownGuildRoleError} from '@fluxer/errors/src/domains/guild/UnknownGuildRoleError'; import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas'; function ensureNotEveryoneRole(roleId: RoleID, guildId: GuildID, path: string): void { if (roleId === guildIdToRoleId(guildId)) { throw InputValidationError.fromCode(path, ValidationErrorCodes.INVALID_ROLE_ID); } } export class GuildMemberValidationService { constructor(private readonly guildRepository: GuildRepository) {} async validateAndGetRoleIds(params: { userId: UserID; guildId: GuildID; guildData: GuildResponse; targetId: UserID; targetMember: GuildMember; newRoles: Array; hasPermission: (permission: bigint) => Promise; canManageRoles: (targetUserId: UserID, targetRoleId: RoleID) => Promise; }): Promise> { const {userId, guildId, guildData, targetId, targetMember, newRoles, hasPermission, canManageRoles} = params; for (const roleId of newRoles) { ensureNotEveryoneRole(roleId, guildId, 'roles'); } const existingRoles = await this.guildRepository.listRolesByIds(newRoles, guildId); const existingRoleIds = existingRoles.map((role) => role.id); if (guildData && guildData.owner_id === userId.toString()) { return existingRoleIds; } if (!(await hasPermission(Permissions.MANAGE_ROLES))) { throw new MissingPermissionsError(); } const currentRoles = targetMember.roleIds; const rolesToRemove = [...currentRoles].filter((roleId) => !existingRoleIds.includes(roleId)); const rolesToAdd = existingRoleIds.filter((roleId) => !currentRoles.has(roleId)); const rolesToCheck = [...rolesToAdd, ...rolesToRemove].filter((roleId) => roleId !== guildIdToRoleId(guildId)); const checks = await Promise.all( rolesToCheck.map((roleId) => canManageRoles(targetId, roleId).then((ok) => ({roleId, ok}))), ); const denied = checks.find((check) => !check.ok); if (denied) { throw new MissingPermissionsError(); } return existingRoleIds; } async validateRoleAssignment(params: { guildData: GuildResponse; guildId: GuildID; userId: UserID; targetId: UserID; roleId: RoleID; hasPermission: (permission: bigint) => Promise; canManageRoles: (targetUserId: UserID, targetRoleId: RoleID) => Promise; }): Promise { const {guildData, guildId, userId, targetId, roleId, hasPermission, canManageRoles} = params; ensureNotEveryoneRole(roleId, guildId, 'role_id'); if (guildData && guildData.owner_id === userId.toString()) { const role = await this.guildRepository.getRole(roleId, guildId); if (!role || role.id === guildIdToRoleId(guildId)) { throw new UnknownGuildRoleError(); } } else { if (!(await hasPermission(Permissions.MANAGE_ROLES))) { throw new MissingPermissionsError(); } if (!(await canManageRoles(targetId, roleId))) { throw new MissingPermissionsError(); } } } }