// SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, GuildID, RoleID, UserID} from '@app/api/BrandedTypes'; import {createChannelID, createGuildID, createRoleID, createUserID} from '@app/api/BrandedTypes'; import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; import type {ChannelAuthService} from '@app/api/channel/services/channel_data/ChannelAuthService'; import type {ChannelUtilsService} from '@app/api/channel/services/channel_data/ChannelUtilsService'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; import {mapGuildToGuildResponse} from '@app/api/guild/GuildModel'; import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; import {ChannelHelpers} from '@app/api/guild/services/channel/ChannelHelpers'; import {contentModerationService} from '@app/api/infrastructure/ContentModerationService'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ILiveKitService} from '@app/api/infrastructure/ILiveKitService'; import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore'; import type {IInviteRepository} from '@app/api/invite/IInviteRepository'; import {Logger} from '@app/api/Logger'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Channel} from '@app/api/models/Channel'; import {ChannelPermissionOverwrite} from '@app/api/models/ChannelPermissionOverwrite'; import {deleteChannelMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup'; import type {IUserRepository} from '@app/api/user/IUserRepository'; import {serializeChannelForAudit} from '@app/api/utils/AuditSerializationUtils'; import {applyProtectedOverwriteBits} from '@app/api/utils/featureUtils'; import type {VoiceAvailabilityService} from '@app/api/voice/VoiceAvailabilityService'; import type {VoiceRegionAvailability} from '@app/api/voice/VoiceModel'; import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; import { ALL_PERMISSIONS, ChannelTypes, GUILD_TEXT_BASED_CHANNEL_TYPES, Permissions, } from '@fluxer/constants/src/ChannelConstants'; import {ContentWarningLevel, clampVoiceChannelBitrate, GuildFeatures} from '@fluxer/constants/src/GuildConstants'; import {MAX_CHANNELS_PER_CATEGORY} from '@fluxer/constants/src/LimitConstants'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {InvalidChannelTypeError} from '@fluxer/errors/src/domains/channel/InvalidChannelTypeError'; import {MaxCategoryChannelsError} from '@fluxer/errors/src/domains/channel/MaxCategoryChannelsError'; import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownChannelError'; import {CannotExecuteOnDmError} from '@fluxer/errors/src/domains/core/CannotExecuteOnDmError'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError'; import {resolveLimit} from '@fluxer/limits/src/LimitResolver'; import {ChannelNameType} from '@fluxer/schema/src/primitives/ChannelValidators'; import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService'; export interface ChannelUpdateData { name?: string; topic?: string | null; url?: string | null; parent_id?: bigint | null; bitrate?: number | null; user_limit?: number | null; voice_connection_limit?: number | null; nsfw?: boolean; nsfw_override?: boolean | null; content_warning_level?: number; content_warning_text?: string | null; rate_limit_per_user?: number; permission_overwrites?: Array<{ id: bigint; type: number; allow?: bigint; deny?: bigint; }> | null; rtc_region?: string | null; icon?: string | null; owner_id?: bigint | null; nicks?: Record | null; } export class ChannelOperationsService { constructor( private channelRepository: IChannelRepositoryAggregate, private userRepository: IUserRepository, private gatewayService: IGatewayService, private channelAuthService: ChannelAuthService, private channelUtilsService: ChannelUtilsService, private voiceRoomStore: IVoiceRoomStore, private liveKitService: ILiveKitService, private voiceAvailabilityService: VoiceAvailabilityService | null, private readonly guildAuditLogService: GuildAuditLogService, private inviteRepository: IInviteRepository, private webhookRepository: IWebhookRepository, private guildRepository: IGuildRepositoryAggregate, private limitConfigService: LimitConfigService, private rateLimitService: IRateLimitService, ) {} async getChannel({ userId, channelId, skipNsfwValidation, }: { userId: UserID; channelId: ChannelID; skipNsfwValidation?: boolean; }): Promise { const {channel} = await this.channelAuthService.getChannelAuthenticated({ userId, channelId, skipNsfwValidation, }); return channel; } async getPublicChannelData(channelId: ChannelID) { const channel = await this.channelRepository.channelData.findUnique(channelId); if (!channel) throw new UnknownChannelError(); return channel; } async getChannelMemberCount(channelId: ChannelID): Promise { const channel = await this.channelRepository.channelData.findUnique(channelId); if (!channel) throw new UnknownChannelError(); return channel.recipientIds.size; } async getChannelSystem(channelId: ChannelID): Promise { return await this.channelRepository.channelData.findUnique(channelId); } async editChannel({ userId, channelId, data, clientFeatures, requestCache, auditLogReason, }: { userId: UserID; channelId: ChannelID; data: ChannelUpdateData; clientFeatures: ReadonlySet; requestCache: RequestCache; auditLogReason: string | null; }): Promise { const {channel, guild, checkPermission} = await this.channelAuthService.getChannelAuthenticated({ userId, channelId, skipNsfwValidation: true, }); if (channel.type === ChannelTypes.GROUP_DM) { throw new InvalidChannelTypeError(); } if (!guild) throw new MissingPermissionsError(); await checkPermission(Permissions.MANAGE_CHANNELS); const guildIdValue = createGuildID(BigInt(guild.id)); contentModerationService.scanText(data.name ?? null, { userId, guildId: guildIdValue, channelId, messageId: null, surface: 'profile_field', }); contentModerationService.scanText(data.topic ?? null, { userId, guildId: guildIdValue, channelId, messageId: null, surface: 'profile_field', }); let channelName = data.name ?? channel.name; if (data.name !== undefined && channel.type === ChannelTypes.GUILD_TEXT) { const hasFlexibleNamesEnabled = guild.features?.includes(GuildFeatures.TEXT_CHANNEL_FLEXIBLE_NAMES) ?? false; if (!hasFlexibleNamesEnabled) { channelName = ChannelNameType.parse(data.name); } } if (data.rtc_region !== undefined && channel.type === ChannelTypes.GUILD_VOICE) { await checkPermission(Permissions.UPDATE_RTC_REGION); if (data.rtc_region !== null) { if (this.voiceAvailabilityService !== null) { const guildId = createGuildID(BigInt(guild.id)); const availableRegions = this.voiceAvailabilityService.getAvailableRegions({ requestingUserId: userId, guildId, guildFeatures: new Set(guild.features ?? []), }); const regionAllowed = availableRegions.some((region) => region.id === data.rtc_region && region.isAccessible); if (!regionAllowed) { throw InputValidationError.fromCode('rtc_region', ValidationErrorCodes.INVALID_OR_RESTRICTED_RTC_REGION, { region: data.rtc_region ?? 'unknown', }); } } else { const availableRegions = this.liveKitService.getRegionMetadata().map((region) => region.id); if (availableRegions.length > 0 && !availableRegions.includes(data.rtc_region)) { throw InputValidationError.fromCode('rtc_region', ValidationErrorCodes.INVALID_RTC_REGION, { region: data.rtc_region ?? 'unknown', availableRegions: availableRegions.join(', '), }); } } } } const previousPermissionOverwrites = channel.permissionOverwrites; let permissionOverwrites = channel.permissionOverwrites; if (data.permission_overwrites !== undefined) { const guildId = createGuildID(BigInt(guild.id)); await checkPermission(Permissions.MANAGE_ROLES); const isOwner = guild.owner_id === userId.toString(); const channelPermissions = await this.gatewayService.getUserPermissions({ guildId, userId, channelId: channel.id, }); if (!isOwner) { for (const overwrite of data.permission_overwrites ?? []) { const allowPerms = (overwrite.allow ? BigInt(overwrite.allow) : 0n) & ALL_PERMISSIONS; if ((allowPerms & ~channelPermissions) !== 0n) { throw new MissingPermissionsError(); } } const nextDeny = new Map(); for (const overwrite of data.permission_overwrites ?? []) { const targetKey = overwrite.type === 0 ? createRoleID(overwrite.id) : createUserID(overwrite.id); nextDeny.set(targetKey, (overwrite.deny ? BigInt(overwrite.deny) : 0n) & ALL_PERMISSIONS); } for (const [targetId, existing] of previousPermissionOverwrites ?? []) { const removedDeny = existing.deny & ~(nextDeny.get(targetId) ?? 0n); if ((removedDeny & ~channelPermissions) !== 0n) { throw new MissingPermissionsError(); } } } permissionOverwrites = new Map(); for (const overwrite of data.permission_overwrites ?? []) { const targetId = overwrite.type === 0 ? createRoleID(overwrite.id) : createUserID(overwrite.id); const existing = previousPermissionOverwrites?.get(targetId); const protectedBits = applyProtectedOverwriteBits( { allow: (overwrite.allow ? BigInt(overwrite.allow) : 0n) & ALL_PERMISSIONS, deny: (overwrite.deny ? BigInt(overwrite.deny) : 0n) & ALL_PERMISSIONS, }, { allow: existing?.allow ?? 0n, deny: existing?.deny ?? 0n, }, clientFeatures, ); permissionOverwrites.set( targetId, new ChannelPermissionOverwrite({ type: overwrite.type, allow_: protectedBits.allow, deny_: protectedBits.deny, }), ); } } const requestedParentId = data.parent_id !== undefined ? (data.parent_id ? createChannelID(data.parent_id) : null) : channel.parentId; if (data.parent_id !== undefined) { await this.validateParentCategory({ guildId: guildIdValue, channel, parentId: requestedParentId, validateCapacity: requestedParentId !== null && requestedParentId !== (channel.parentId ?? null), }); } let nextBitrate = channel.bitrate; if (data.bitrate !== undefined && channel.type === ChannelTypes.GUILD_VOICE) { nextBitrate = data.bitrate === null ? null : clampVoiceChannelBitrate(data.bitrate, guild.features ?? []); } const updatedChannelData = { ...channel.toRow(), name: channelName, topic: data.topic !== undefined ? data.topic : channel.topic, url: data.url !== undefined && channel.type === ChannelTypes.GUILD_LINK ? data.url : channel.url, parent_id: requestedParentId, bitrate: nextBitrate, user_limit: data.user_limit !== undefined && channel.type === ChannelTypes.GUILD_VOICE ? data.user_limit : channel.userLimit, voice_connection_limit: data.voice_connection_limit !== undefined && channel.type === ChannelTypes.GUILD_VOICE ? data.voice_connection_limit : channel.voiceConnectionLimit, rate_limit_per_user: data.rate_limit_per_user !== undefined && GUILD_TEXT_BASED_CHANNEL_TYPES.has(channel.type) ? data.rate_limit_per_user : channel.rateLimitPerUser, nsfw: resolveNsfwOverrideWrite(channel, data), content_warning_level: resolveContentWarningLevelWrite(channel, data), content_warning_text: resolveContentWarningTextWrite(channel, data), rtc_region: data.rtc_region !== undefined && channel.type === ChannelTypes.GUILD_VOICE ? data.rtc_region : channel.rtcRegion, permission_overwrites: new Map( Array.from(permissionOverwrites.entries()).map(([targetId, overwrite]) => [ targetId, overwrite.toPermissionOverwrite(), ]), ), }; const updatedChannel = await this.channelRepository.channelData.upsert(updatedChannelData); if ( data.rate_limit_per_user !== undefined && GUILD_TEXT_BASED_CHANNEL_TYPES.has(channel.type) && data.rate_limit_per_user !== channel.rateLimitPerUser ) { try { await this.rateLimitService.clearLimitsByIdentifierPrefix(`slowmode:${channelId}:`); } catch (error) { Logger.error( {error, channelId: channelId.toString()}, 'Failed to clear slowmode rate-limit state on channel edit', ); } } await this.channelUtilsService.dispatchChannelUpdate({channel: updatedChannel, requestCache}); if (channel.type === ChannelTypes.GUILD_CATEGORY && data.permission_overwrites !== undefined && guild) { await this.propagatePermissionsToSyncedChildren({ categoryChannel: updatedChannel, previousPermissionOverwrites, guildId: createGuildID(BigInt(guild.id)), requestCache, }); } if ( data.rtc_region !== undefined && channel.type === ChannelTypes.GUILD_VOICE && data.rtc_region !== channel.rtcRegion && this.voiceRoomStore ) { await this.handleRtcRegionSwitch({ guildId: createGuildID(BigInt(guild.id)), channelId, }); } const beforeSnapshot = serializeChannelForAudit(channel); const afterSnapshot = serializeChannelForAudit(updatedChannel); const changes = this.guildAuditLogService.computeChanges(beforeSnapshot, afterSnapshot); if (changes.length > 0) { const builder = this.guildAuditLogService .createBuilder(guildIdValue, userId) .withAction(AuditLogActionType.CHANNEL_UPDATE, channel.id.toString()) .withReason(auditLogReason) .withMetadata({ type: updatedChannel.type.toString(), }) .withChanges(changes); try { await builder.commit(); } catch (error) { Logger.error( { error, guildId: guildIdValue.toString(), userId: userId.toString(), action: AuditLogActionType.CHANNEL_UPDATE, targetId: channel.id.toString(), }, 'Failed to record guild audit log', ); } } if (data.permission_overwrites !== undefined) { await this.guildAuditLogService.recordPermissionOverwriteDiff({ guildId: guildIdValue, userId, channelId: updatedChannel.id, previous: previousPermissionOverwrites, next: updatedChannel.permissionOverwrites, reason: auditLogReason, }); } return updatedChannel; } async deleteChannel({ userId, channelId, requestCache, auditLogReason, }: { userId: UserID; channelId: ChannelID; requestCache: RequestCache; auditLogReason: string | null; }): Promise { const {channel, guild, checkPermission} = await this.channelAuthService.getChannelAuthenticated({ userId, channelId, skipNsfwValidation: true, }); if (this.channelAuthService.isPersonalNotesChannel({userId, channelId})) { throw new CannotExecuteOnDmError(); } if (guild) { await checkPermission(Permissions.MANAGE_CHANNELS); const guildId = createGuildID(BigInt(guild.id)); if (channel.type === ChannelTypes.GUILD_CATEGORY) { const guildChannels = await this.channelRepository.channelData.listGuildChannels(guildId); const childChannels = guildChannels.filter((ch: Channel) => ch.parentId === channelId); for (const childChannel of childChannels) { const updatedChild = await this.channelRepository.channelData.upsert({ ...childChannel.toRow(), parent_id: null, }); await this.channelUtilsService.dispatchChannelUpdate({channel: updatedChild, requestCache}); } } const [channelInvites, channelWebhooks] = await Promise.all([ this.inviteRepository.listChannelInvites(channelId), this.webhookRepository.listByChannel(channelId), ]); await Promise.all([ ...channelInvites.map((invite) => this.inviteRepository.delete(invite.code)), ...channelWebhooks.map((webhook) => this.webhookRepository.delete(webhook.id)), ]); await this.channelUtilsService.purgeChannelAttachments(channel); await this.channelRepository.messages.deleteAllChannelMessages(channelId); await deleteChannelMessageSearchDocuments(channelId, {context: {source: 'channel_delete'}}); await this.channelUtilsService.dispatchChannelDelete({channel, requestCache}); const guildIdValue = createGuildID(BigInt(guild.id)); const changes = this.guildAuditLogService.computeChanges(ChannelHelpers.serializeChannelForAudit(channel), null); const builder = this.guildAuditLogService .createBuilder(guildIdValue, userId) .withAction(AuditLogActionType.CHANNEL_DELETE, channel.id.toString()) .withReason(auditLogReason) .withMetadata({ type: channel.type.toString(), }) .withChanges(changes); try { await builder.commit(); } catch (error) { Logger.error( { error, guildId: guildIdValue.toString(), userId: userId.toString(), action: AuditLogActionType.CHANNEL_DELETE, targetId: channel.id.toString(), }, 'Failed to record guild audit log', ); } await this.channelRepository.channelData.delete(channelId, guildId); const guildModel = await this.guildRepository.findUnique(guildId); if (guildModel) { const guildRow = guildModel.toRow(); const patch: Partial = {}; if (guildRow.system_channel_id === channelId) patch.system_channel_id = null; if (guildRow.rules_channel_id === channelId) patch.rules_channel_id = null; if (guildRow.afk_channel_id === channelId) patch.afk_channel_id = null; if (Object.keys(patch).length > 0) { const updatedGuild = await this.guildRepository.upsertPartial(guildId, patch, guildRow); await this.gatewayService.dispatchGuild({ guildId, event: 'GUILD_UPDATE', data: mapGuildToGuildResponse(updatedGuild), }); } } } else { await this.userRepository.closeDmForUser(userId, channelId); await this.channelUtilsService.dispatchDmChannelDelete({channel, userId, requestCache}); } } async getAvailableRtcRegions({ userId, channelId, }: { userId: UserID; channelId: ChannelID; }): Promise> { if (this.voiceAvailabilityService === null) { return []; } const {channel, guild} = await this.channelAuthService.getChannelAuthenticated({ userId, channelId, skipNsfwValidation: true, }); if (channel.type !== ChannelTypes.GUILD_VOICE) { throw new InvalidChannelTypeError(); } if (!guild) { return []; } const guildId = createGuildID(BigInt(guild.id)); const regions = this.voiceAvailabilityService.getAvailableRegions({ requestingUserId: userId, guildId, guildFeatures: new Set(guild.features ?? []), }); const accessibleRegions = regions.filter((region) => region.isAccessible); return accessibleRegions.sort((a, b) => a.name.localeCompare(b.name)); } private async propagatePermissionsToSyncedChildren({ categoryChannel, previousPermissionOverwrites, guildId, requestCache, }: { categoryChannel: Channel; previousPermissionOverwrites: Map; guildId: GuildID; requestCache: RequestCache; }): Promise { const guildChannels = await this.channelRepository.channelData.listGuildChannels(guildId); const childChannels = guildChannels.filter((ch: Channel) => ch.parentId === categoryChannel.id); const syncedChannels: Array = []; for (const child of childChannels) { if (this.arePermissionsEqual(child.permissionOverwrites, previousPermissionOverwrites)) { syncedChannels.push(child); } } if (syncedChannels.length > 0) { await Promise.all( syncedChannels.map(async (child) => { const updatedChild = await this.channelRepository.channelData.upsert({ ...child.toRow(), permission_overwrites: new Map( Array.from(categoryChannel.permissionOverwrites.entries()).map(([targetId, overwrite]) => [ targetId, overwrite.toPermissionOverwrite(), ]), ), }); await this.channelUtilsService.dispatchChannelUpdate({channel: updatedChild, requestCache}); }), ); } } private arePermissionsEqual( perms1: Map, perms2: Map, ): boolean { if (perms1.size !== perms2.size) return false; for (const [targetId, overwrite1] of perms1.entries()) { const overwrite2 = perms2.get(targetId); if (!overwrite2) return false; if ( overwrite1.type !== overwrite2.type || overwrite1.allow !== overwrite2.allow || overwrite1.deny !== overwrite2.deny ) { return false; } } return true; } private async handleRtcRegionSwitch({guildId, channelId}: {guildId: GuildID; channelId: ChannelID}): Promise { if (!this.voiceRoomStore) { Logger.warn('[ChannelOperationsService] VoiceRoomStore not available, skipping region switch'); return; } await this.voiceRoomStore.deleteRoomServer(guildId, channelId); await this.gatewayService.switchVoiceRegion({guildId, channelId}); } private async ensureCategoryHasCapacity(params: {guildId: GuildID; categoryId: ChannelID}): Promise { const count = await this.gatewayService.getCategoryChannelCount(params); let maxChannels = MAX_CHANNELS_PER_CATEGORY; const guild = await this.guildRepository.findUnique(params.guildId); const ctx = createLimitMatchContext({user: null, guildFeatures: guild?.features ?? null}); const resolved = resolveLimit(this.limitConfigService.getConfigSnapshot(), ctx, 'max_channels_per_category', { evaluationContext: 'guild', }); if (Number.isFinite(resolved) && resolved >= 0) { maxChannels = Math.floor(resolved); } if (count >= maxChannels) { throw new MaxCategoryChannelsError(maxChannels); } } private async validateParentCategory(params: { guildId: GuildID; channel: Channel; parentId: ChannelID | null; validateCapacity: boolean; }): Promise { if (params.parentId === null) { return; } if (params.channel.type === ChannelTypes.GUILD_CATEGORY) { throw InputValidationError.fromCode('parent_id', ValidationErrorCodes.CATEGORIES_CANNOT_HAVE_PARENTS); } const guildChannels = await this.channelRepository.channelData.listGuildChannels(params.guildId); const parentChannel = guildChannels.find((channel) => channel.id === params.parentId); if (!parentChannel) { throw InputValidationError.fromCode('parent_id', ValidationErrorCodes.INVALID_PARENT_CHANNEL); } if (parentChannel.type !== ChannelTypes.GUILD_CATEGORY) { throw InputValidationError.fromCode('parent_id', ValidationErrorCodes.PARENT_MUST_BE_CATEGORY); } if (params.validateCapacity) { await this.ensureCategoryHasCapacity({guildId: params.guildId, categoryId: params.parentId}); } } async setChannelPermissionOverwrite(params: { userId: UserID; channelId: ChannelID; overwriteId: bigint; overwrite: { type: number; allow_: bigint; deny_: bigint; }; clientFeatures: ReadonlySet; requestCache: RequestCache; auditLogReason: string | null; }): Promise { const channel = await this.channelRepository.channelData.findUnique(params.channelId); if (!channel || !channel.guildId) throw new UnknownChannelError(); const canManageRoles = await this.gatewayService.checkPermission({ guildId: channel.guildId, userId: params.userId, channelId: channel.id, permission: Permissions.MANAGE_ROLES, }); if (!canManageRoles) throw new MissingPermissionsError(); const userPermissions = await this.gatewayService.getUserPermissions({ guildId: channel.guildId, userId: params.userId, channelId: channel.id, }); const targetId = params.overwrite.type === 0 ? createRoleID(params.overwriteId) : createUserID(params.overwriteId); const existing = channel.permissionOverwrites?.get(targetId); const protectedBits = applyProtectedOverwriteBits( { allow: params.overwrite.allow_ & ALL_PERMISSIONS, deny: params.overwrite.deny_ & ALL_PERMISSIONS, }, { allow: existing?.allow ?? 0n, deny: existing?.deny ?? 0n, }, params.clientFeatures, ); const sanitizedAllow = protectedBits.allow; const sanitizedDeny = protectedBits.deny; const hasAdministrator = (userPermissions & Permissions.ADMINISTRATOR) !== 0n; if (!hasAdministrator && (sanitizedAllow & ~userPermissions) !== 0n) throw new MissingPermissionsError(); const removedDeny = (existing?.deny ?? 0n) & ~sanitizedDeny; if (!hasAdministrator && (removedDeny & ~userPermissions) !== 0n) throw new MissingPermissionsError(); const previousPermissionOverwrites = channel.permissionOverwrites; const nextOverwrite = new ChannelPermissionOverwrite({ type: params.overwrite.type, allow_: sanitizedAllow, deny_: sanitizedDeny, }); const overwrites = new Map(channel.permissionOverwrites ?? []); overwrites.set(targetId, nextOverwrite); const updated = await this.channelRepository.channelData.upsert({ ...channel.toRow(), permission_overwrites: new Map( Array.from(overwrites.entries()).map(([id, ow]) => [id, ow.toPermissionOverwrite()]), ), }); await this.channelUtilsService.dispatchChannelUpdate({channel: updated, requestCache: params.requestCache}); if (channel.type === ChannelTypes.GUILD_CATEGORY) { await this.propagatePermissionsToSyncedChildren({ categoryChannel: updated, previousPermissionOverwrites, guildId: channel.guildId, requestCache: params.requestCache, }); } await this.guildAuditLogService.recordPermissionOverwriteDiff({ guildId: channel.guildId, userId: params.userId, channelId: channel.id, previous: existing ? new Map([[targetId, existing]]) : null, next: new Map([[targetId, nextOverwrite]]), reason: params.auditLogReason, }); } async deleteChannelPermissionOverwrite(params: { userId: UserID; channelId: ChannelID; overwriteId: bigint; requestCache: RequestCache; auditLogReason: string | null; }): Promise { const channel = await this.channelRepository.channelData.findUnique(params.channelId); if (!channel || !channel.guildId) throw new UnknownChannelError(); const canManageRoles = await this.gatewayService.checkPermission({ guildId: channel.guildId, userId: params.userId, channelId: channel.id, permission: Permissions.MANAGE_ROLES, }); if (!canManageRoles) throw new MissingPermissionsError(); const previousPermissionOverwrites = channel.permissionOverwrites; const overwrites = new Map(channel.permissionOverwrites ?? []); const removedRole = overwrites.get(createRoleID(params.overwriteId)); const removedUser = overwrites.get(createUserID(params.overwriteId)); const removed = removedRole ?? removedUser; if (removed) { const userPermissions = await this.gatewayService.getUserPermissions({ guildId: channel.guildId, userId: params.userId, channelId: channel.id, }); const hasAdministrator = (userPermissions & Permissions.ADMINISTRATOR) !== 0n; if (!hasAdministrator && (removed.deny & ~userPermissions) !== 0n) throw new MissingPermissionsError(); } overwrites.delete(createRoleID(params.overwriteId)); overwrites.delete(createUserID(params.overwriteId)); const updated = await this.channelRepository.channelData.upsert({ ...channel.toRow(), permission_overwrites: new Map( Array.from(overwrites.entries()).map(([id, ow]) => [id, ow.toPermissionOverwrite()]), ), }); await this.channelUtilsService.dispatchChannelUpdate({channel: updated, requestCache: params.requestCache}); if (channel.type === ChannelTypes.GUILD_CATEGORY) { await this.propagatePermissionsToSyncedChildren({ categoryChannel: updated, previousPermissionOverwrites, guildId: channel.guildId, requestCache: params.requestCache, }); } if (removed) { const removedTargetId = removed.type === 0 ? createRoleID(params.overwriteId) : createUserID(params.overwriteId); await this.guildAuditLogService.recordPermissionOverwriteDiff({ guildId: channel.guildId, userId: params.userId, channelId: channel.id, previous: new Map([[removedTargetId, removed]]), next: null, reason: params.auditLogReason, }); } } } function isWritableGuildChannel(type: number): boolean { return ( type === ChannelTypes.GUILD_TEXT || type === ChannelTypes.GUILD_VOICE || type === ChannelTypes.GUILD_LINK || type === ChannelTypes.GUILD_CATEGORY ); } function resolveNsfwOverrideWrite(channel: Channel, data: ChannelUpdateData): boolean | null { if (!isWritableGuildChannel(channel.type)) { return channel.nsfwOverride; } if (data.nsfw_override !== undefined) { return data.nsfw_override; } if (data.nsfw !== undefined) { return data.nsfw === true ? true : null; } return channel.nsfwOverride; } function resolveContentWarningLevelWrite(channel: Channel, data: ChannelUpdateData): number { if (!isWritableGuildChannel(channel.type)) { return channel.contentWarningLevel; } if (data.content_warning_level === undefined) { return channel.contentWarningLevel; } return data.content_warning_level === ContentWarningLevel.CONTENT_WARNING ? ContentWarningLevel.CONTENT_WARNING : ContentWarningLevel.INHERIT; } function resolveContentWarningTextWrite(channel: Channel, data: ChannelUpdateData): string | null { if (!isWritableGuildChannel(channel.type)) { return channel.contentWarningText; } if (data.content_warning_text === undefined) { return channel.contentWarningText; } const trimmed = data.content_warning_text == null ? null : data.content_warning_text.trim(); return trimmed && trimmed.length > 0 ? trimmed : null; }