mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-09 12:12:25 +09:00
507 lines
17 KiB
TypeScript
507 lines
17 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
|
import {MissingACLError} from '@fluxer/errors/src/domains/core/MissingACLError';
|
|
import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError';
|
|
import {
|
|
BanGuildMemberBody,
|
|
ListGuildAuditLogsResponse,
|
|
ListGuildMembersQuery,
|
|
ListGuildsQuery,
|
|
UpdateGuildRequest,
|
|
} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas';
|
|
import {
|
|
GuildUpdateResponse,
|
|
ListGuildEmojisResponse,
|
|
ListGuildMembersResponse,
|
|
ListGuildStickersResponse,
|
|
LookupGuildResponse,
|
|
SearchGuildsResponse,
|
|
SuccessResponse,
|
|
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
|
import {GuildIdParam, GuildIdUserIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
|
import {GuildAuditLogListQuery} from '@fluxer/schema/src/domains/guild/GuildAuditLogSchemas';
|
|
import {createGuildID} from '../../BrandedTypes';
|
|
import {requireAdminACL, requireAnyAdminACL} from '../../middleware/AdminMiddleware';
|
|
import {RateLimitMiddleware} from '../../middleware/RateLimitMiddleware';
|
|
import {OpenAPI} from '../../middleware/ResponseTypeMiddleware';
|
|
import {AdminRateLimitConfigs} from '../../rate_limit_configs/AdminRateLimitConfig';
|
|
import type {HonoApp} from '../../types/HonoEnv';
|
|
import {Validator} from '../../Validator';
|
|
|
|
const GUILD_UPDATE_ACLS = [
|
|
AdminACLs.GUILD_UPDATE_NAME,
|
|
AdminACLs.GUILD_UPDATE_SETTINGS,
|
|
AdminACLs.GUILD_UPDATE_FEATURES,
|
|
AdminACLs.GUILD_UPDATE_VANITY,
|
|
AdminACLs.GUILD_TRANSFER_OWNERSHIP,
|
|
];
|
|
|
|
function hasGuildSettingsUpdate(body: UpdateGuildRequest): boolean {
|
|
return (
|
|
body.verification_level !== undefined ||
|
|
body.mfa_level !== undefined ||
|
|
body.nsfw_level !== undefined ||
|
|
body.nsfw !== undefined ||
|
|
body.content_warning_level !== undefined ||
|
|
body.content_warning_text !== undefined ||
|
|
body.explicit_content_filter !== undefined ||
|
|
body.default_message_notifications !== undefined ||
|
|
body.disabled_operations !== undefined
|
|
);
|
|
}
|
|
|
|
function hasGuildFeatureUpdate(body: UpdateGuildRequest): boolean {
|
|
return body.add_features !== undefined || body.remove_features !== undefined;
|
|
}
|
|
|
|
function selectGuildUpdateACLs(body: UpdateGuildRequest): Array<string> {
|
|
const required: Array<string> = [];
|
|
if (body.name !== undefined) {
|
|
required.push(AdminACLs.GUILD_UPDATE_NAME);
|
|
}
|
|
if (body.fields !== undefined || hasGuildSettingsUpdate(body)) {
|
|
required.push(AdminACLs.GUILD_UPDATE_SETTINGS);
|
|
}
|
|
if (hasGuildFeatureUpdate(body)) {
|
|
required.push(AdminACLs.GUILD_UPDATE_FEATURES);
|
|
}
|
|
if (body.vanity_url_code !== undefined) {
|
|
required.push(AdminACLs.GUILD_UPDATE_VANITY);
|
|
}
|
|
if (body.new_owner_id !== undefined) {
|
|
required.push(AdminACLs.GUILD_TRANSFER_OWNERSHIP);
|
|
}
|
|
if (required.length > 0) {
|
|
return required;
|
|
}
|
|
return Object.keys(body).length === 0 ? [] : [AdminACLs.WILDCARD];
|
|
}
|
|
|
|
function requireAllAdminACLs(granted: ReadonlySet<string>, required: ReadonlyArray<string>): void {
|
|
if (granted.has(AdminACLs.WILDCARD)) {
|
|
return;
|
|
}
|
|
for (const acl of required) {
|
|
if (!granted.has(acl)) {
|
|
throw new MissingACLError(acl);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function GuildAdminController(app: HonoApp) {
|
|
app.get(
|
|
'/admin/guilds',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_LOOKUP),
|
|
requireAdminACL(AdminACLs.GUILD_LOOKUP),
|
|
Validator('query', ListGuildsQuery),
|
|
OpenAPI({
|
|
operationId: 'list_admin_guilds',
|
|
summary: 'List guilds',
|
|
description:
|
|
'Searches guilds by name, ID, and other criteria. Supports full-text search and pagination through limit and offset. Requires GUILD_LOOKUP permission.',
|
|
responseSchema: SearchGuildsResponse,
|
|
statusCode: 200,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const query = ctx.req.valid('query');
|
|
return ctx.json(
|
|
await adminService.searchService.searchGuilds({
|
|
query: query.q,
|
|
limit: query.limit,
|
|
offset: query.offset,
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
app.get(
|
|
'/admin/guilds/:guild_id',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_LOOKUP),
|
|
requireAdminACL(AdminACLs.GUILD_LOOKUP),
|
|
Validator('param', GuildIdParam),
|
|
OpenAPI({
|
|
operationId: 'get_admin_guild',
|
|
summary: 'Get guild',
|
|
description:
|
|
'Retrieves complete guild details including metadata, settings, channels, roles, and statistics. Requires GUILD_LOOKUP permission.',
|
|
responseSchema: LookupGuildResponse,
|
|
statusCode: 200,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
return ctx.json(
|
|
await adminService.guildServiceAggregate.lookupService.lookupGuild({
|
|
guild_id: ctx.req.valid('param').guild_id,
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
app.patch(
|
|
'/admin/guilds/:guild_id',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_GUILD_MODIFY),
|
|
requireAnyAdminACL(GUILD_UPDATE_ACLS),
|
|
Validator('param', GuildIdParam),
|
|
Validator('json', UpdateGuildRequest),
|
|
OpenAPI({
|
|
operationId: 'update_admin_guild',
|
|
summary: 'Update guild',
|
|
description:
|
|
'Partially updates a guild. The permissions required are selected by the fields present in the body and are evaluated with all-of semantics: name requires GUILD_UPDATE_NAME, vanity_url_code requires GUILD_UPDATE_VANITY, new_owner_id requires GUILD_TRANSFER_OWNERSHIP, add_features and remove_features require GUILD_UPDATE_FEATURES, and fields together with every other setting requires GUILD_UPDATE_SETTINGS. A body with no fields applies no change. Every applied change is logged to the audit log.',
|
|
responseSchema: GuildUpdateResponse,
|
|
statusCode: 200,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const adminUserId = ctx.get('adminUserId');
|
|
const auditLogReason = ctx.get('auditLogReason');
|
|
const guildIdRaw = ctx.req.valid('param').guild_id;
|
|
const body = ctx.req.valid('json');
|
|
requireAllAdminACLs(ctx.get('adminUserAcls'), selectGuildUpdateACLs(body));
|
|
const {updateService, vanityService, lookupService} = adminService.guildServiceAggregate;
|
|
if (body.fields !== undefined) {
|
|
await updateService.clearGuildFields({guild_id: guildIdRaw, fields: body.fields}, adminUserId, auditLogReason);
|
|
}
|
|
if (hasGuildSettingsUpdate(body)) {
|
|
await updateService.updateGuildSettings(
|
|
{
|
|
guild_id: guildIdRaw,
|
|
verification_level: body.verification_level,
|
|
mfa_level: body.mfa_level,
|
|
nsfw_level: body.nsfw_level,
|
|
nsfw: body.nsfw,
|
|
content_warning_level: body.content_warning_level,
|
|
content_warning_text: body.content_warning_text,
|
|
explicit_content_filter: body.explicit_content_filter,
|
|
default_message_notifications: body.default_message_notifications,
|
|
disabled_operations: body.disabled_operations,
|
|
},
|
|
adminUserId,
|
|
auditLogReason,
|
|
);
|
|
}
|
|
if (hasGuildFeatureUpdate(body)) {
|
|
await updateService.updateGuildFeatures({
|
|
guildId: createGuildID(guildIdRaw),
|
|
addFeatures: body.add_features ?? [],
|
|
removeFeatures: body.remove_features ?? [],
|
|
adminUserId,
|
|
auditLogReason,
|
|
});
|
|
}
|
|
if (body.name !== undefined) {
|
|
await updateService.updateGuildName({guild_id: guildIdRaw, name: body.name}, adminUserId, auditLogReason);
|
|
}
|
|
if (body.vanity_url_code !== undefined) {
|
|
await vanityService.updateGuildVanity(
|
|
{guild_id: guildIdRaw, vanity_url_code: body.vanity_url_code},
|
|
adminUserId,
|
|
auditLogReason,
|
|
);
|
|
}
|
|
if (body.new_owner_id !== undefined) {
|
|
await updateService.transferGuildOwnership(
|
|
{guild_id: guildIdRaw, new_owner_id: body.new_owner_id},
|
|
adminUserId,
|
|
auditLogReason,
|
|
);
|
|
}
|
|
const {guild} = await lookupService.lookupGuild({guild_id: guildIdRaw});
|
|
if (!guild) {
|
|
throw new UnknownGuildError();
|
|
}
|
|
return ctx.json({
|
|
guild: {
|
|
id: guild.id,
|
|
name: guild.name,
|
|
features: guild.features,
|
|
owner_id: guild.owner_id,
|
|
icon: guild.icon,
|
|
banner: guild.banner,
|
|
member_count: guild.member_count,
|
|
nsfw_level: guild.nsfw_level,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
app.delete(
|
|
'/admin/guilds/:guild_id',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_GUILD_MODIFY),
|
|
requireAdminACL(AdminACLs.GUILD_DELETE),
|
|
Validator('param', GuildIdParam),
|
|
OpenAPI({
|
|
operationId: 'delete_admin_guild',
|
|
summary: 'Delete guild',
|
|
description:
|
|
'Permanently deletes a guild. Deletes all channels, messages, and settings. Irreversible operation with no recovery window. Logged to audit log. Requires GUILD_DELETE permission.',
|
|
responseSchema: SuccessResponse,
|
|
statusCode: 200,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const adminUserId = ctx.get('adminUserId');
|
|
const auditLogReason = ctx.get('auditLogReason');
|
|
return ctx.json(
|
|
await adminService.guildServiceAggregate.managementService.deleteGuild(
|
|
ctx.req.valid('param').guild_id,
|
|
adminUserId,
|
|
auditLogReason,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
app.get(
|
|
'/admin/guilds/:guild_id/members',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_LOOKUP),
|
|
requireAdminACL(AdminACLs.GUILD_LIST_MEMBERS),
|
|
Validator('param', GuildIdParam),
|
|
Validator('query', ListGuildMembersQuery),
|
|
OpenAPI({
|
|
operationId: 'list_admin_guild_members',
|
|
summary: 'List guild members',
|
|
description:
|
|
'Lists all guild members with pagination. Returns member IDs, join dates, and roles. Requires GUILD_LIST_MEMBERS permission.',
|
|
responseSchema: ListGuildMembersResponse,
|
|
statusCode: 200,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const query = ctx.req.valid('query');
|
|
return ctx.json(
|
|
await adminService.guildServiceAggregate.lookupService.listGuildMembers({
|
|
guild_id: ctx.req.valid('param').guild_id,
|
|
limit: query.limit,
|
|
offset: query.offset,
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
app.put(
|
|
'/admin/guilds/:guild_id/members/:user_id',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_GUILD_MODIFY),
|
|
requireAdminACL(AdminACLs.GUILD_FORCE_ADD_MEMBER),
|
|
Validator('param', GuildIdUserIdParam),
|
|
OpenAPI({
|
|
operationId: 'add_admin_guild_member',
|
|
summary: 'Add guild member',
|
|
description:
|
|
'Forcefully adds a user to a guild. Bypasses normal invite flow for administrative account recovery. Logged to audit log. Requires GUILD_FORCE_ADD_MEMBER permission.',
|
|
responseSchema: SuccessResponse,
|
|
statusCode: 200,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const adminUserId = ctx.get('adminUserId');
|
|
const auditLogReason = ctx.get('auditLogReason');
|
|
const requestCache = ctx.get('requestCache');
|
|
const params = ctx.req.valid('param');
|
|
return ctx.json(
|
|
await adminService.guildServiceAggregate.membershipService.forceAddUserToGuild({
|
|
data: {guild_id: params.guild_id, user_id: params.user_id},
|
|
requestCache,
|
|
adminUserId,
|
|
auditLogReason,
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
app.delete(
|
|
'/admin/guilds/:guild_id/members/:user_id',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_GUILD_MODIFY),
|
|
requireAdminACL(AdminACLs.GUILD_KICK_MEMBER),
|
|
Validator('param', GuildIdUserIdParam),
|
|
OpenAPI({
|
|
operationId: 'kick_admin_guild_member',
|
|
summary: 'Remove guild member',
|
|
description:
|
|
'Temporarily removes a user from a guild. User can rejoin. Logged to audit log. Requires GUILD_KICK_MEMBER permission.',
|
|
responseSchema: null,
|
|
statusCode: 204,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const adminUserId = ctx.get('adminUserId');
|
|
const auditLogReason = ctx.get('auditLogReason');
|
|
const params = ctx.req.valid('param');
|
|
await adminService.guildServiceAggregate.membershipService.kickMember(
|
|
{guild_id: params.guild_id, user_id: params.user_id},
|
|
adminUserId,
|
|
auditLogReason,
|
|
);
|
|
return ctx.body(null, 204);
|
|
},
|
|
);
|
|
app.put(
|
|
'/admin/guilds/:guild_id/bans/:user_id',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_GUILD_MODIFY),
|
|
requireAdminACL(AdminACLs.GUILD_BAN_MEMBER),
|
|
Validator('param', GuildIdUserIdParam),
|
|
Validator('json', BanGuildMemberBody),
|
|
OpenAPI({
|
|
operationId: 'ban_admin_guild_member',
|
|
summary: 'Ban guild member',
|
|
description:
|
|
'Bans a user from a guild, optionally deleting their recent messages. Prevents the user from joining until the ban expires or is removed. Logged to audit log. Requires GUILD_BAN_MEMBER permission.',
|
|
responseSchema: null,
|
|
statusCode: 204,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const adminUserId = ctx.get('adminUserId');
|
|
const auditLogReason = ctx.get('auditLogReason');
|
|
const params = ctx.req.valid('param');
|
|
await adminService.guildServiceAggregate.membershipService.banMember(
|
|
{...ctx.req.valid('json'), guild_id: params.guild_id, user_id: params.user_id},
|
|
adminUserId,
|
|
auditLogReason,
|
|
);
|
|
return ctx.body(null, 204);
|
|
},
|
|
);
|
|
app.get(
|
|
'/admin/guilds/:guild_id/emojis',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_LOOKUP),
|
|
requireAdminACL(AdminACLs.ASSET_PURGE),
|
|
Validator('param', GuildIdParam),
|
|
OpenAPI({
|
|
operationId: 'list_admin_guild_emojis',
|
|
summary: 'List guild emojis',
|
|
description:
|
|
'Lists all custom emojis in a guild. Returns ID, name, and creation date. Used for asset inventory and purge operations. Requires ASSET_PURGE permission.',
|
|
responseSchema: ListGuildEmojisResponse,
|
|
statusCode: 200,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const guildId = createGuildID(ctx.req.valid('param').guild_id);
|
|
return ctx.json(await adminService.guildServiceAggregate.lookupService.listGuildEmojis(guildId));
|
|
},
|
|
);
|
|
app.get(
|
|
'/admin/guilds/:guild_id/stickers',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_LOOKUP),
|
|
requireAdminACL(AdminACLs.ASSET_PURGE),
|
|
Validator('param', GuildIdParam),
|
|
OpenAPI({
|
|
operationId: 'list_admin_guild_stickers',
|
|
summary: 'List guild stickers',
|
|
description:
|
|
'Lists all stickers in a guild. Returns ID, name, and asset information. Used for asset inventory and purge operations. Requires ASSET_PURGE permission.',
|
|
responseSchema: ListGuildStickersResponse,
|
|
statusCode: 200,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const guildId = createGuildID(ctx.req.valid('param').guild_id);
|
|
return ctx.json(await adminService.guildServiceAggregate.lookupService.listGuildStickers(guildId));
|
|
},
|
|
);
|
|
app.get(
|
|
'/admin/guilds/:guild_id/audit-logs',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_LOOKUP),
|
|
requireAdminACL(AdminACLs.GUILD_AUDIT_LOG_VIEW),
|
|
Validator('param', GuildIdParam),
|
|
Validator('query', GuildAuditLogListQuery),
|
|
OpenAPI({
|
|
operationId: 'list_admin_guild_audit_logs',
|
|
summary: 'List guild audit logs',
|
|
description:
|
|
'Returns in-app guild audit log entries for a guild without requiring VIEW_AUDIT_LOG membership permission. Supports pagination via before/after log IDs and filtering by user_id or action_type. Requires GUILD_AUDIT_LOG_VIEW permission.',
|
|
responseSchema: ListGuildAuditLogsResponse,
|
|
statusCode: 200,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const query = ctx.req.valid('query');
|
|
return ctx.json(
|
|
await adminService.guildServiceAggregate.listGuildAuditLogs({
|
|
guild_id: ctx.req.valid('param').guild_id,
|
|
limit: query.limit,
|
|
before: query.before,
|
|
after: query.after,
|
|
user_id: query.user_id,
|
|
action_type: query.action_type,
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
app.post(
|
|
'/admin/guilds/:guild_id/reloads',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_GUILD_MODIFY),
|
|
requireAdminACL(AdminACLs.GUILD_RELOAD),
|
|
Validator('param', GuildIdParam),
|
|
OpenAPI({
|
|
operationId: 'create_admin_guild_reload',
|
|
summary: 'Reload guild',
|
|
description:
|
|
'Reloads a single guild state from database. Used to recover from corruption or sync issues. Logged to audit log. Requires GUILD_RELOAD permission.',
|
|
responseSchema: SuccessResponse,
|
|
statusCode: 200,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const adminUserId = ctx.get('adminUserId');
|
|
const auditLogReason = ctx.get('auditLogReason');
|
|
return ctx.json(
|
|
await adminService.guildServiceAggregate.managementService.reloadGuild(
|
|
ctx.req.valid('param').guild_id,
|
|
adminUserId,
|
|
auditLogReason,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
app.post(
|
|
'/admin/guilds/:guild_id/shutdowns',
|
|
RateLimitMiddleware(AdminRateLimitConfigs.ADMIN_GUILD_MODIFY),
|
|
requireAdminACL(AdminACLs.GUILD_SHUTDOWN),
|
|
Validator('param', GuildIdParam),
|
|
OpenAPI({
|
|
operationId: 'create_admin_guild_shutdown',
|
|
summary: 'Shut down guild',
|
|
description:
|
|
'Shuts down and unloads a guild from the gateway. Guild data remains in database. Used for emergency resource cleanup. Logged to audit log. Requires GUILD_SHUTDOWN permission.',
|
|
responseSchema: SuccessResponse,
|
|
statusCode: 200,
|
|
security: 'adminApiKey',
|
|
tags: 'Admin',
|
|
}),
|
|
async (ctx) => {
|
|
const adminService = ctx.get('adminService');
|
|
const adminUserId = ctx.get('adminUserId');
|
|
const auditLogReason = ctx.get('auditLogReason');
|
|
return ctx.json(
|
|
await adminService.guildServiceAggregate.managementService.shutdownGuild(
|
|
ctx.req.valid('param').guild_id,
|
|
adminUserId,
|
|
auditLogReason,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|