/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import {createGuildID, createUserID, type GuildID, type UserID} from '~/BrandedTypes'; import {UnknownGuildError} from '~/Errors'; import type {IGuildRepository} from '~/guild/IGuildRepository'; import type {EntityAssetService, PreparedAssetUpload} from '~/infrastructure/EntityAssetService'; import type {Guild} from '~/Models'; import type { ClearGuildFieldsRequest, TransferGuildOwnershipRequest, UpdateGuildNameRequest, UpdateGuildSettingsRequest, } from '../../AdminModel'; import {mapGuildToAdminResponse} from '../../AdminModel'; import type {AdminAuditService} from '../AdminAuditService'; import type {AdminGuildUpdatePropagator} from './AdminGuildUpdatePropagator'; interface AdminGuildUpdateServiceDeps { guildRepository: IGuildRepository; entityAssetService: EntityAssetService; auditService: AdminAuditService; updatePropagator: AdminGuildUpdatePropagator; } export class AdminGuildUpdateService { constructor(private readonly deps: AdminGuildUpdateServiceDeps) {} async updateGuildFeatures({ guildId, addFeatures, removeFeatures, adminUserId, auditLogReason, }: { guildId: GuildID; addFeatures: Array; removeFeatures: Array; adminUserId: UserID; auditLogReason: string | null; }) { const {guildRepository, auditService, updatePropagator} = this.deps; const guild = await guildRepository.findUnique(guildId); if (!guild) { throw new UnknownGuildError(); } const newFeatures = new Set(guild.features); for (const feature of addFeatures) { newFeatures.add(feature); } for (const feature of removeFeatures) { newFeatures.delete(feature); } const guildRow = guild.toRow(); const updatedGuild = await guildRepository.upsert({ ...guildRow, features: newFeatures, }); await updatePropagator.dispatchGuildUpdate(guildId, updatedGuild); await auditService.createAuditLog({ adminUserId, targetType: 'guild', targetId: BigInt(guildId), action: 'update_features', auditLogReason, metadata: new Map([ ['add_features', addFeatures.join(',')], ['remove_features', removeFeatures.join(',')], ['new_features', Array.from(newFeatures).join(',')], ]), }); return { guild: mapGuildToAdminResponse(updatedGuild), }; } async clearGuildFields(data: ClearGuildFieldsRequest, adminUserId: UserID, auditLogReason: string | null) { const {guildRepository, entityAssetService, auditService, updatePropagator} = this.deps; const guildId = createGuildID(data.guild_id); const guild = await guildRepository.findUnique(guildId); if (!guild) { throw new UnknownGuildError(); } const guildRow = guild.toRow(); const updates: Partial = {}; const preparedAssets: Array = []; for (const field of data.fields) { if (field === 'icon') { const prepared = await entityAssetService.prepareAssetUpload({ assetType: 'icon', entityType: 'guild', entityId: guildId, previousHash: guild.iconHash, base64Image: null, errorPath: 'icon', }); preparedAssets.push(prepared); updates.icon_hash = prepared.newHash; } else if (field === 'banner') { const prepared = await entityAssetService.prepareAssetUpload({ assetType: 'banner', entityType: 'guild', entityId: guildId, previousHash: guild.bannerHash, base64Image: null, errorPath: 'banner', }); preparedAssets.push(prepared); updates.banner_hash = prepared.newHash; } else if (field === 'splash') { const prepared = await entityAssetService.prepareAssetUpload({ assetType: 'splash', entityType: 'guild', entityId: guildId, previousHash: guild.splashHash, base64Image: null, errorPath: 'splash', }); preparedAssets.push(prepared); updates.splash_hash = prepared.newHash; } else if (field === 'embed_splash') { const prepared = await entityAssetService.prepareAssetUpload({ assetType: 'embed_splash', entityType: 'guild', entityId: guildId, previousHash: guild.embedSplashHash, base64Image: null, errorPath: 'embed_splash', }); preparedAssets.push(prepared); updates.embed_splash_hash = prepared.newHash; } } let updatedGuild: Guild; try { updatedGuild = await guildRepository.upsert({ ...guildRow, ...updates, }); } catch (error) { await Promise.allSettled(preparedAssets.map((p) => entityAssetService.rollbackAssetUpload(p))); throw error; } await Promise.allSettled(preparedAssets.map((p) => entityAssetService.commitAssetChange({prepared: p}))); await updatePropagator.dispatchGuildUpdate(guildId, updatedGuild); await auditService.createAuditLog({ adminUserId, targetType: 'guild', targetId: BigInt(guildId), action: 'clear_fields', auditLogReason, metadata: new Map([['fields', data.fields.join(',')]]), }); } async updateGuildName(data: UpdateGuildNameRequest, adminUserId: UserID, auditLogReason: string | null) { const {guildRepository, auditService, updatePropagator} = this.deps; const guildId = createGuildID(data.guild_id); const guild = await guildRepository.findUnique(guildId); if (!guild) { throw new UnknownGuildError(); } const oldName = guild.name; const guildRow = guild.toRow(); const updatedGuild = await guildRepository.upsert({ ...guildRow, name: data.name, }); await updatePropagator.dispatchGuildUpdate(guildId, updatedGuild); await auditService.createAuditLog({ adminUserId, targetType: 'guild', targetId: BigInt(guildId), action: 'update_name', auditLogReason, metadata: new Map([ ['old_name', oldName], ['new_name', data.name], ]), }); return { guild: mapGuildToAdminResponse(updatedGuild), }; } async updateGuildSettings(data: UpdateGuildSettingsRequest, adminUserId: UserID, auditLogReason: string | null) { const {guildRepository, auditService, updatePropagator} = this.deps; const guildId = createGuildID(data.guild_id); const guild = await guildRepository.findUnique(guildId); if (!guild) { throw new UnknownGuildError(); } const guildRow = guild.toRow(); const updates: Partial = {}; const metadata = new Map(); if (data.verification_level !== undefined) { updates.verification_level = data.verification_level; metadata.set('verification_level', data.verification_level.toString()); } if (data.mfa_level !== undefined) { updates.mfa_level = data.mfa_level; metadata.set('mfa_level', data.mfa_level.toString()); } if (data.nsfw_level !== undefined) { updates.nsfw_level = data.nsfw_level; metadata.set('nsfw_level', data.nsfw_level.toString()); } if (data.explicit_content_filter !== undefined) { updates.explicit_content_filter = data.explicit_content_filter; metadata.set('explicit_content_filter', data.explicit_content_filter.toString()); } if (data.default_message_notifications !== undefined) { updates.default_message_notifications = data.default_message_notifications; metadata.set('default_message_notifications', data.default_message_notifications.toString()); } if (data.disabled_operations !== undefined) { updates.disabled_operations = data.disabled_operations; metadata.set('disabled_operations', data.disabled_operations.toString()); } const updatedGuild = await guildRepository.upsert({ ...guildRow, ...updates, }); await updatePropagator.dispatchGuildUpdate(guildId, updatedGuild); await auditService.createAuditLog({ adminUserId, targetType: 'guild', targetId: BigInt(guildId), action: 'update_settings', auditLogReason, metadata, }); return { guild: mapGuildToAdminResponse(updatedGuild), }; } async transferGuildOwnership( data: TransferGuildOwnershipRequest, adminUserId: UserID, auditLogReason: string | null, ) { const {guildRepository, auditService, updatePropagator} = this.deps; const guildId = createGuildID(data.guild_id); const guild = await guildRepository.findUnique(guildId); if (!guild) { throw new UnknownGuildError(); } const newOwnerId = createUserID(data.new_owner_id); const oldOwnerId = guild.ownerId; const guildRow = guild.toRow(); const updatedGuild = await guildRepository.upsert({ ...guildRow, owner_id: newOwnerId, }); await updatePropagator.dispatchGuildUpdate(guildId, updatedGuild); await auditService.createAuditLog({ adminUserId, targetType: 'guild', targetId: BigInt(guildId), action: 'transfer_ownership', auditLogReason, metadata: new Map([ ['old_owner_id', oldOwnerId.toString()], ['new_owner_id', newOwnerId.toString()], ]), }); return { guild: mapGuildToAdminResponse(updatedGuild), }; } }