refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,49 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {CreateAdminApiKeyResponse, ListAdminApiKeyResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
export async function createApiKey(
config: Config,
session: Session,
name: string,
acls: Array<string>,
): Promise<ApiResult<CreateAdminApiKeyResponse>> {
const client = new ApiClient(config, session);
return client.post<CreateAdminApiKeyResponse>('/admin/api-keys', {name, acls});
}
export async function listApiKeys(
config: Config,
session: Session,
): Promise<ApiResult<Array<ListAdminApiKeyResponse>>> {
const client = new ApiClient(config, session);
return client.get<Array<ListAdminApiKeyResponse>>('/admin/api-keys');
}
export async function revokeApiKey(config: Config, session: Session, key_id: string): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.deleteVoid(`/admin/api-keys/${key_id}`);
}

View File

@@ -0,0 +1,86 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {
AdminArchiveResponseSchema,
DownloadUrlResponseSchema,
ListArchivesResponseSchema,
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import type {z} from 'zod';
export type Archive = z.infer<typeof AdminArchiveResponseSchema>;
export type ListArchivesResponse = z.infer<typeof ListArchivesResponseSchema>;
export type ArchiveDownloadUrlResponse = z.infer<typeof DownloadUrlResponseSchema>;
export async function triggerUserArchive(
config: Config,
session: Session,
userId: string,
reason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/archives/user', {user_id: userId}, reason);
}
export async function triggerGuildArchive(
config: Config,
session: Session,
guildId: string,
reason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/archives/guild', {guild_id: guildId}, reason);
}
export async function listArchives(
config: Config,
session: Session,
subjectType: string,
subjectId?: string,
includeExpired: boolean = false,
requestedBy?: string,
): Promise<ApiResult<ListArchivesResponse>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
subject_type: subjectType,
include_expired: includeExpired,
...(subjectId ? {subject_id: subjectId} : {}),
...(requestedBy ? {requested_by: requestedBy} : {}),
};
return client.post<ListArchivesResponse>('/admin/archives/list', body);
}
export async function getArchiveDownloadUrl(
config: Config,
session: Session,
subjectType: string,
subjectId: string,
archiveId: string,
): Promise<ApiResult<ArchiveDownloadUrlResponse>> {
const client = new ApiClient(config, session);
return client.get<ArchiveDownloadUrlResponse>(`/admin/archives/${subjectType}/${subjectId}/${archiveId}/download`);
}

View File

@@ -0,0 +1,36 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {PurgeGuildAssetsResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
export async function purgeAssets(
config: Config,
session: Session,
ids: Array<string>,
reason?: string,
): Promise<ApiResult<PurgeGuildAssetsResponse>> {
const client = new ApiClient(config, session);
return client.post<PurgeGuildAssetsResponse>('/admin/assets/purge', {ids}, reason);
}

View File

@@ -0,0 +1,55 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {AuditLogsListResponseSchema} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import type {z} from 'zod';
type AuditLogsListResponse = z.infer<typeof AuditLogsListResponseSchema>;
export interface SearchAuditLogsParams {
query: string | undefined;
admin_user_id: string | undefined;
target_id: string | undefined;
limit: number;
offset: number;
}
export async function searchAuditLogs(
config: Config,
session: Session,
params: SearchAuditLogsParams,
): Promise<ApiResult<AuditLogsListResponse>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
limit: params.limit,
offset: params.offset,
...(params.query && params.query !== '' ? {query: params.query} : {}),
...(params.admin_user_id && params.admin_user_id !== '' ? {admin_user_id: params.admin_user_id} : {}),
...(params.target_id && params.target_id !== '' ? {target_id: params.target_id} : {}),
};
return client.post<AuditLogsListResponse>('/admin/audit-logs/search', body);
}

View File

@@ -0,0 +1,147 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {
BanCheckResponseSchema,
ListEmailBansResponseSchema,
ListIpBansResponseSchema,
ListPhoneBansResponseSchema,
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import type {z} from 'zod';
export type BanCheckResponse = z.infer<typeof BanCheckResponseSchema>;
export type ListIpBansResponse = z.infer<typeof ListIpBansResponseSchema>;
export type ListEmailBansResponse = z.infer<typeof ListEmailBansResponseSchema>;
export type ListPhoneBansResponse = z.infer<typeof ListPhoneBansResponseSchema>;
export async function banEmail(
config: Config,
session: Session,
email: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/bans/email/add', {email}, auditLogReason);
}
export async function unbanEmail(
config: Config,
session: Session,
email: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/bans/email/remove', {email}, auditLogReason);
}
export async function checkEmailBan(
config: Config,
session: Session,
email: string,
): Promise<ApiResult<BanCheckResponse>> {
const client = new ApiClient(config, session);
return client.post<BanCheckResponse>('/admin/bans/email/check', {email});
}
export async function banIp(
config: Config,
session: Session,
ip: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/bans/ip/add', {ip}, auditLogReason);
}
export async function unbanIp(
config: Config,
session: Session,
ip: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/bans/ip/remove', {ip}, auditLogReason);
}
export async function checkIpBan(config: Config, session: Session, ip: string): Promise<ApiResult<BanCheckResponse>> {
const client = new ApiClient(config, session);
return client.post<BanCheckResponse>('/admin/bans/ip/check', {ip});
}
export async function banPhone(
config: Config,
session: Session,
phone: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/bans/phone/add', {phone}, auditLogReason);
}
export async function unbanPhone(
config: Config,
session: Session,
phone: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/bans/phone/remove', {phone}, auditLogReason);
}
export async function checkPhoneBan(
config: Config,
session: Session,
phone: string,
): Promise<ApiResult<BanCheckResponse>> {
const client = new ApiClient(config, session);
return client.post<BanCheckResponse>('/admin/bans/phone/check', {phone});
}
export async function listIpBans(
config: Config,
session: Session,
limit: number = 200,
): Promise<ApiResult<ListIpBansResponse>> {
const client = new ApiClient(config, session);
return client.post<ListIpBansResponse>('/admin/bans/ip/list', {limit});
}
export async function listEmailBans(
config: Config,
session: Session,
limit: number = 200,
): Promise<ApiResult<ListEmailBansResponse>> {
const client = new ApiClient(config, session);
return client.post<ListEmailBansResponse>('/admin/bans/email/list', {limit});
}
export async function listPhoneBans(
config: Config,
session: Session,
limit: number = 200,
): Promise<ApiResult<ListPhoneBansResponse>> {
const client = new ApiClient(config, session);
return client.post<ListPhoneBansResponse>('/admin/bans/phone/list', {limit});
}

View File

@@ -0,0 +1,107 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {BulkOperationResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import type {z} from 'zod';
type BulkOperationResponseType = z.infer<typeof BulkOperationResponse>;
export async function bulkUpdateUserFlags(
config: Config,
session: Session,
user_ids: Array<string>,
add_flags: Array<string>,
remove_flags: Array<string>,
audit_log_reason?: string,
): Promise<ApiResult<BulkOperationResponseType>> {
const client = new ApiClient(config, session);
return client.post<BulkOperationResponseType>(
'/admin/bulk/update-user-flags',
{
user_ids,
add_flags,
remove_flags,
},
audit_log_reason,
);
}
export async function bulkUpdateGuildFeatures(
config: Config,
session: Session,
guild_ids: Array<string>,
add_features: Array<string>,
remove_features: Array<string>,
audit_log_reason?: string,
): Promise<ApiResult<BulkOperationResponseType>> {
const client = new ApiClient(config, session);
return client.post<BulkOperationResponseType>(
'/admin/bulk/update-guild-features',
{
guild_ids,
add_features,
remove_features,
},
audit_log_reason,
);
}
export async function bulkAddGuildMembers(
config: Config,
session: Session,
guild_id: string,
user_ids: Array<string>,
audit_log_reason?: string,
): Promise<ApiResult<BulkOperationResponseType>> {
const client = new ApiClient(config, session);
return client.post<BulkOperationResponseType>(
'/admin/bulk/add-guild-members',
{
guild_id,
user_ids,
},
audit_log_reason,
);
}
export async function bulkScheduleUserDeletion(
config: Config,
session: Session,
user_ids: Array<string>,
reason_code: number,
days_until_deletion: number,
public_reason?: string,
audit_log_reason?: string,
): Promise<ApiResult<BulkOperationResponseType>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
user_ids,
reason_code,
days_until_deletion,
...(public_reason ? {public_reason} : {}),
};
return client.post<BulkOperationResponseType>('/admin/bulk/schedule-user-deletion', body, audit_log_reason);
}

View File

@@ -0,0 +1,216 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {type ApiError, parseApiResponse} from '@fluxer/admin/src/api/Errors';
import type {JsonValue} from '@fluxer/admin/src/api/JsonTypes';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig} from '@fluxer/admin/src/types/Config';
import {buildEndpointUrl, validateOutboundEndpointUrl} from '@fluxer/hono/src/security/OutboundEndpoint';
export type ApiResult<T> = {ok: true; data: T} | {ok: false; error: ApiError};
export interface RequestOptions {
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
path: string;
body?: JsonValue | string;
queryParams?: Record<string, string | number | boolean | undefined | null>;
auditLogReason?: string;
}
export class ApiClient {
private session: Session;
private apiEndpointUrl: URL;
constructor(config: AdminConfig, session: Session) {
this.session = session;
this.apiEndpointUrl = validateOutboundEndpointUrl(config.apiEndpoint, {
name: 'admin.apiEndpoint',
allowHttp: config.env !== 'production',
allowLocalhost: config.env !== 'production',
allowPrivateIpLiterals: config.env !== 'production',
});
}
private buildHeaders(auditLogReason?: string): Record<string, string> {
const headers: Record<string, string> = {
Authorization: `Bearer ${this.session.accessToken}`,
'Content-Type': 'application/json',
};
if (auditLogReason) {
headers['X-Audit-Log-Reason'] = auditLogReason;
}
return headers;
}
private buildUrl(path: string, queryParams?: Record<string, string | number | boolean | undefined | null>): string {
const baseUrl = buildEndpointUrl(this.apiEndpointUrl, path);
if (!queryParams) {
return baseUrl;
}
const params = new URLSearchParams();
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined && value !== null && value !== '') {
params.set(key, String(value));
}
}
const queryString = params.toString();
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
}
async request<T>(options: RequestOptions): Promise<ApiResult<T>> {
try {
const url = this.buildUrl(options.path, options.queryParams);
const headers = this.buildHeaders(options.auditLogReason);
const fetchOptions: RequestInit = {
method: options.method,
headers,
};
if (options.body !== undefined && options.method !== 'GET') {
if (typeof options.body === 'string') {
fetchOptions.body = options.body;
} else {
fetchOptions.body = JSON.stringify(options.body);
}
}
const response = await fetch(url, fetchOptions);
if (response.status === 204) {
return {ok: true, data: undefined as T};
}
if (response.ok) {
const contentLength = response.headers.get('content-length');
if (contentLength === '0') {
return {ok: true, data: undefined as T};
}
try {
const data = (await response.json()) as T;
return {ok: true, data};
} catch {
return {ok: true, data: undefined as T};
}
}
return parseApiResponse<T>(response);
} catch (e) {
return {
ok: false,
error: {type: 'networkError', message: (e as Error).message},
};
}
}
async get<T>(
path: string,
queryParams?: Record<string, string | number | boolean | undefined | null>,
): Promise<ApiResult<T>> {
return this.request<T>({
method: 'GET',
path,
...(queryParams !== undefined ? {queryParams} : {}),
});
}
async post<T>(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<T>> {
return this.request<T>({
method: 'POST',
path,
...(body !== undefined ? {body} : {}),
...(auditLogReason !== undefined ? {auditLogReason} : {}),
});
}
async postVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<void>> {
return this.request<void>({
method: 'POST',
path,
...(body !== undefined ? {body} : {}),
...(auditLogReason !== undefined ? {auditLogReason} : {}),
});
}
async patch<T>(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<T>> {
return this.request<T>({
method: 'PATCH',
path,
...(body !== undefined ? {body} : {}),
...(auditLogReason !== undefined ? {auditLogReason} : {}),
});
}
async patchVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<void>> {
return this.request<void>({
method: 'PATCH',
path,
...(body !== undefined ? {body} : {}),
...(auditLogReason !== undefined ? {auditLogReason} : {}),
});
}
async delete<T>(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<T>> {
return this.request<T>({
method: 'DELETE',
path,
...(body !== undefined ? {body} : {}),
...(auditLogReason !== undefined ? {auditLogReason} : {}),
});
}
async deleteVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<void>> {
return this.request<void>({
method: 'DELETE',
path,
...(body !== undefined ? {body} : {}),
...(auditLogReason !== undefined ? {auditLogReason} : {}),
});
}
async put<T>(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<T>> {
return this.request<T>({
method: 'PUT',
path,
...(body !== undefined ? {body} : {}),
...(auditLogReason !== undefined ? {auditLogReason} : {}),
});
}
async putVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<void>> {
return this.request<void>({
method: 'PUT',
path,
...(body !== undefined ? {body} : {}),
...(auditLogReason !== undefined ? {auditLogReason} : {}),
});
}
}
export function createApiClient(config: AdminConfig, session: Session): ApiClient {
return new ApiClient(config, session);
}

View File

@@ -0,0 +1,43 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {CodesResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import type {z} from 'zod';
type CodesResponseType = z.infer<typeof CodesResponse>;
export async function generateGiftCodes(
config: Config,
session: Session,
count: number,
product_type: string,
): Promise<ApiResult<Array<string>>> {
const client = new ApiClient(config, session);
const result = await client.post<CodesResponseType>('/admin/codes/gift', {count, product_type});
if (result.ok) {
return {ok: true, data: result.data.codes};
}
return result;
}

View File

@@ -0,0 +1,77 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {DiscoveryApplicationResponse} from '@fluxer/schema/src/domains/guild/GuildDiscoverySchemas';
import type {z} from 'zod';
type DiscoveryApplicationResponseType = z.infer<typeof DiscoveryApplicationResponse>;
export async function listDiscoveryApplications(
config: Config,
session: Session,
status: string,
limit?: number,
): Promise<ApiResult<Array<DiscoveryApplicationResponseType>>> {
const client = new ApiClient(config, session);
return await client.get<Array<DiscoveryApplicationResponseType>>('/admin/discovery/applications', {
status,
limit: limit ?? 25,
});
}
export async function approveDiscoveryApplication(
config: Config,
session: Session,
guildId: string,
reason?: string,
): Promise<ApiResult<DiscoveryApplicationResponseType>> {
const client = new ApiClient(config, session);
return await client.post<DiscoveryApplicationResponseType>(
`/admin/discovery/applications/${guildId}/approve`,
reason ? {reason} : {},
);
}
export async function rejectDiscoveryApplication(
config: Config,
session: Session,
guildId: string,
reason: string,
): Promise<ApiResult<DiscoveryApplicationResponseType>> {
const client = new ApiClient(config, session);
return await client.post<DiscoveryApplicationResponseType>(`/admin/discovery/applications/${guildId}/reject`, {
reason,
});
}
export async function removeFromDiscovery(
config: Config,
session: Session,
guildId: string,
reason: string,
): Promise<ApiResult<DiscoveryApplicationResponseType>> {
const client = new ApiClient(config, session);
return await client.post<DiscoveryApplicationResponseType>(`/admin/discovery/guilds/${guildId}/remove`, {reason});
}

View File

@@ -0,0 +1,325 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {JsonValue} from '@fluxer/admin/src/api/JsonTypes';
import {isJsonObject, parseJson} from '@fluxer/admin/src/api/JsonTypes';
export interface ValidationError {
path: string;
message: string;
code: string;
}
export type ApiError =
| {type: 'unauthorized'}
| {type: 'forbidden'; code: string; message: string; details: Record<string, string>}
| {type: 'notFound'; code: string; message: string}
| {type: 'badRequest'; code: string; message: string; errors: Array<ValidationError>}
| {type: 'rateLimited'; code: string; message: string; retryAfter: number; isGlobal: boolean}
| {type: 'clientError'; status: number; code: string; message: string}
| {type: 'serverError'; status: number; code: string | null; message: string}
| {type: 'networkError'; message: string}
| {type: 'parseError'; body: string; parseError: string};
export function isUnauthorized(error: ApiError): error is {type: 'unauthorized'} {
return error.type === 'unauthorized';
}
export function isForbidden(
error: ApiError,
): error is {type: 'forbidden'; code: string; message: string; details: Record<string, string>} {
return error.type === 'forbidden';
}
export function isNotFound(error: ApiError): error is {type: 'notFound'; code: string; message: string} {
return error.type === 'notFound';
}
export function getErrorMessage(error: ApiError): string {
switch (error.type) {
case 'unauthorized':
return 'Authentication required';
case 'forbidden':
return error.message;
case 'notFound':
return error.message;
case 'badRequest':
return error.message;
case 'rateLimited':
return error.message;
case 'clientError':
return error.message;
case 'serverError':
return error.message;
case 'networkError':
return error.message;
case 'parseError':
return 'Failed to parse API response';
}
}
export function getErrorCode(error: ApiError): string | null {
switch (error.type) {
case 'forbidden':
return error.code;
case 'notFound':
return error.code;
case 'badRequest':
return error.code;
case 'rateLimited':
return error.code;
case 'clientError':
return error.code;
case 'serverError':
return error.code;
default:
return null;
}
}
export function getErrorDisplayString(error: ApiError): string {
switch (error.type) {
case 'badRequest': {
const base = `${error.message} (${error.code})`;
if (error.errors.length === 0) return base;
const errorDetails = error.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
return `${base}\n${errorDetails}`;
}
case 'rateLimited':
return `${error.message} (Retry after ${Math.round(error.retryAfter)}s)`;
case 'forbidden':
return error.message;
case 'notFound':
return error.message;
case 'clientError':
return error.message;
case 'serverError':
return error.code ? `${error.message} (Error code: ${error.code})` : error.message;
default:
return getErrorMessage(error);
}
}
export function isRetryable(error: ApiError): boolean {
return error.type === 'rateLimited' || error.type === 'serverError' || error.type === 'networkError';
}
export function getRetryAfterSeconds(error: ApiError): number | null {
if (error.type === 'rateLimited') {
return error.retryAfter;
}
return null;
}
export function getValidationErrors(error: ApiError): Array<ValidationError> {
if (error.type === 'badRequest') {
return error.errors;
}
return [];
}
export function getErrorTitle(error: ApiError): string {
switch (error.type) {
case 'unauthorized':
return 'Authentication Required';
case 'forbidden':
return 'Permission Denied';
case 'notFound':
return 'Not Found';
case 'badRequest':
return 'Validation Error';
case 'rateLimited':
return 'Rate Limited';
case 'clientError':
return 'Client Error';
case 'serverError':
return 'Server Error';
case 'networkError':
return 'Network Error';
case 'parseError':
return 'Response Error';
}
}
export function getErrorSubtitle(error: ApiError): string {
switch (error.type) {
case 'unauthorized':
return 'Your session has expired. Please log in again.';
case 'forbidden':
return "You don't have permission to perform this action.";
case 'notFound':
return 'The requested resource could not be found.';
case 'badRequest':
return 'Please check your input and try again.';
case 'rateLimited':
return "You've made too many requests. Please wait before trying again.";
case 'clientError':
return 'The request was invalid or malformed.';
case 'serverError':
return 'An internal server error occurred. Please try again later.';
case 'networkError':
return 'Could not connect to the server. Please check your connection.';
case 'parseError':
return 'The server returned an invalid response.';
}
}
export function getErrorDetails(error: ApiError): Array<string> {
switch (error.type) {
case 'forbidden':
return [`Permission denied (Error code: ${error.code})`];
case 'notFound':
return [`Resource not found (Error code: ${error.code})`];
case 'badRequest': {
const codeDetail = `Validation failed (Error code: ${error.code})`;
const validationDetails = error.errors.map((e) => `${e.path}: ${e.message}`);
return [codeDetail, ...validationDetails];
}
case 'rateLimited':
return [
`Rate limit exceeded (Error code: ${error.code})`,
`Retry after: ${Math.round(error.retryAfter)} seconds`,
error.isGlobal ? 'This is a global rate limit' : 'This is an endpoint-specific rate limit',
];
case 'clientError':
return [`HTTP Status: ${error.status}`, `Client error (Error code: ${error.code})`];
case 'serverError': {
const statusDetail = `HTTP Status: ${error.status}`;
return error.code ? [statusDetail, `Error code: ${error.code}`] : [statusDetail];
}
case 'parseError': {
const preview = error.body.slice(0, 200).replace(/\n/g, ' ');
return ['Could not parse server response', `Response preview: ${preview}`];
}
default:
return [];
}
}
export async function parseApiResponse<T>(
response: Response,
): Promise<{ok: true; data: T} | {ok: false; error: ApiError}> {
if (response.ok) {
try {
const data = (await response.json()) as T;
return {ok: true, data};
} catch (e) {
const body = await response.text().catch(() => '');
return {ok: false, error: {type: 'parseError', body, parseError: String(e)}};
}
}
if (response.status === 401) {
return {ok: false, error: {type: 'unauthorized'}};
}
const bodyText = await response.text().catch(() => '');
const parsed = parseJson(bodyText);
const body = isJsonObject(parsed) ? parsed : {};
if (response.status === 403) {
return {
ok: false,
error: {
type: 'forbidden',
code: typeof body['code'] === 'string' ? body['code'] : 'FORBIDDEN',
message: typeof body['message'] === 'string' ? body['message'] : bodyText || 'Forbidden',
details: Object.fromEntries(
Object.entries(body).flatMap(([k, v]) => (typeof v === 'string' ? [[k, v] as const] : [])),
),
},
};
}
if (response.status === 404) {
return {
ok: false,
error: {
type: 'notFound',
code: typeof body['code'] === 'string' ? body['code'] : 'NOT_FOUND',
message: typeof body['message'] === 'string' ? body['message'] : 'The requested resource was not found.',
},
};
}
if (response.status === 400) {
const errors: Array<ValidationError> = [];
const errorList = Array.isArray(body['errors']) ? body['errors'] : [];
for (const e of errorList) {
if (typeof e !== 'object' || e === null || Array.isArray(e)) {
continue;
}
const errObj = e as {[key: string]: JsonValue};
errors.push({
path: typeof errObj['path'] === 'string' ? errObj['path'] : '',
message: typeof errObj['message'] === 'string' ? errObj['message'] : '',
code: typeof errObj['code'] === 'string' ? errObj['code'] : '',
});
}
return {
ok: false,
error: {
type: 'badRequest',
code: typeof body['code'] === 'string' ? body['code'] : 'BAD_REQUEST',
message: typeof body['message'] === 'string' ? body['message'] : 'Bad request',
errors,
},
};
}
if (response.status === 429) {
const retryAfterHeader = response.headers.get('retry-after');
const retryAfter = retryAfterHeader ? parseFloat(retryAfterHeader) : 60;
return {
ok: false,
error: {
type: 'rateLimited',
code: (body['code'] as string) ?? 'RATE_LIMITED',
message: (body['message'] as string) ?? 'Rate limited',
retryAfter,
isGlobal: (body['global'] as boolean) ?? false,
},
};
}
if (response.status >= 500) {
return {
ok: false,
error: {
type: 'serverError',
status: response.status,
code: (body['code'] as string) ?? null,
message: (body['message'] as string) ?? (bodyText || 'Server error'),
},
};
}
return {
ok: false,
error: {
type: 'clientError',
status: response.status,
code: (body['code'] as string) ?? 'CLIENT_ERROR',
message: (body['message'] as string) ?? (bodyText || `HTTP ${response.status}`),
},
};
}

View File

@@ -0,0 +1,44 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {ListGuildEmojisResponse, ListGuildStickersResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
export async function listGuildEmojis(
config: Config,
session: Session,
guild_id: string,
): Promise<ApiResult<ListGuildEmojisResponse>> {
const client = new ApiClient(config, session);
return client.get<ListGuildEmojisResponse>(`/admin/guilds/${guild_id}/emojis`);
}
export async function listGuildStickers(
config: Config,
session: Session,
guild_id: string,
): Promise<ApiResult<ListGuildStickersResponse>> {
const client = new ApiClient(config, session);
return client.get<ListGuildStickersResponse>(`/admin/guilds/${guild_id}/stickers`);
}

View File

@@ -0,0 +1,212 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {
ListGuildMembersResponse,
LookupGuildResponse,
SearchGuildsResponse,
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import type {z} from 'zod';
export type GuildLookupResult = NonNullable<z.infer<typeof LookupGuildResponse>['guild']>;
export type GuildChannel = GuildLookupResult['channels'][number];
export type GuildRole = GuildLookupResult['roles'][number];
export interface UpdateGuildSettingsOptions {
verification_level?: number | undefined;
mfa_level?: number | undefined;
nsfw_level?: number | undefined;
explicit_content_filter?: number | undefined;
default_message_notifications?: number | undefined;
disabled_operations?: number | undefined;
}
export async function lookupGuild(
config: Config,
session: Session,
guildId: string,
): Promise<ApiResult<GuildLookupResult | null>> {
const client = new ApiClient(config, session);
const result = await client.post<z.infer<typeof LookupGuildResponse>>('/admin/guilds/lookup', {guild_id: guildId});
if (result.ok) {
return {ok: true, data: result.data.guild};
}
return result;
}
export async function clearGuildFields(
config: Config,
session: Session,
guildId: string,
fields: Array<string>,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/guilds/clear-fields', {guild_id: guildId, fields});
}
export async function updateGuildFeatures(
config: Config,
session: Session,
guildId: string,
addFeatures: Array<string>,
removeFeatures: Array<string>,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/guilds/update-features', {
guild_id: guildId,
add_features: addFeatures,
remove_features: removeFeatures,
});
}
export async function updateGuildSettings(
config: Config,
session: Session,
guildId: string,
options: UpdateGuildSettingsOptions,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
guild_id: guildId,
...(options.verification_level !== undefined ? {verification_level: options.verification_level} : {}),
...(options.mfa_level !== undefined ? {mfa_level: options.mfa_level} : {}),
...(options.nsfw_level !== undefined ? {nsfw_level: options.nsfw_level} : {}),
...(options.explicit_content_filter !== undefined
? {explicit_content_filter: options.explicit_content_filter}
: {}),
...(options.default_message_notifications !== undefined
? {default_message_notifications: options.default_message_notifications}
: {}),
...(options.disabled_operations !== undefined ? {disabled_operations: options.disabled_operations} : {}),
};
return client.postVoid('/admin/guilds/update-settings', body);
}
export async function updateGuildName(
config: Config,
session: Session,
guildId: string,
name: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/guilds/update-name', {guild_id: guildId, name});
}
export async function updateGuildVanity(
config: Config,
session: Session,
guildId: string,
vanityUrlCode?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
guild_id: guildId,
...(vanityUrlCode !== undefined ? {vanity_url_code: vanityUrlCode} : {}),
};
return client.postVoid('/admin/guilds/update-vanity', body);
}
export async function transferGuildOwnership(
config: Config,
session: Session,
guildId: string,
newOwnerId: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/guilds/transfer-ownership', {guild_id: guildId, new_owner_id: newOwnerId});
}
export async function reloadGuild(config: Config, session: Session, guildId: string): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/guilds/reload', {guild_id: guildId});
}
export async function shutdownGuild(config: Config, session: Session, guildId: string): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/guilds/shutdown', {guild_id: guildId});
}
export async function deleteGuild(config: Config, session: Session, guildId: string): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/guilds/delete', {guild_id: guildId});
}
export async function forceAddUserToGuild(
config: Config,
session: Session,
userId: string,
guildId: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/guilds/force-add-user', {user_id: userId, guild_id: guildId});
}
export async function searchGuilds(
config: Config,
session: Session,
query: string,
limit: number = 25,
offset: number = 0,
): Promise<ApiResult<z.infer<typeof SearchGuildsResponse>>> {
const client = new ApiClient(config, session);
return client.post<z.infer<typeof SearchGuildsResponse>>('/admin/guilds/search', {query, limit, offset});
}
export async function listGuildMembers(
config: Config,
session: Session,
guildId: string,
limit: number = 50,
offset: number = 0,
): Promise<ApiResult<z.infer<typeof ListGuildMembersResponse>>> {
const client = new ApiClient(config, session);
return client.post<z.infer<typeof ListGuildMembersResponse>>('/admin/guilds/list-members', {
guild_id: guildId,
limit,
offset,
});
}
export async function banGuildMember(
config: Config,
session: Session,
guildId: string,
userId: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/guilds/ban-member', {guild_id: guildId, user_id: userId});
}
export async function kickGuildMember(
config: Config,
session: Session,
guildId: string,
userId: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/guilds/kick-member', {guild_id: guildId, user_id: userId});
}

View File

@@ -0,0 +1,79 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {
InstanceConfigResponse,
InstanceConfigUpdateRequest,
SnowflakeReservationEntry,
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
export async function getInstanceConfig(config: Config, session: Session): Promise<ApiResult<InstanceConfigResponse>> {
const client = new ApiClient(config, session);
return client.post<InstanceConfigResponse>('/admin/instance-config/get', {});
}
export async function updateInstanceConfig(
config: Config,
session: Session,
update: InstanceConfigUpdateRequest,
): Promise<ApiResult<InstanceConfigResponse>> {
const client = new ApiClient(config, session);
return client.post<InstanceConfigResponse>('/admin/instance-config/update', update as JsonObject);
}
export async function listSnowflakeReservations(
config: Config,
session: Session,
): Promise<ApiResult<Array<SnowflakeReservationEntry>>> {
const client = new ApiClient(config, session);
const result = await client.post<{reservations: Array<SnowflakeReservationEntry>}>(
'/admin/snowflake-reservations/list',
{},
);
if (result.ok) {
return {ok: true, data: result.data.reservations};
}
return result;
}
export async function addSnowflakeReservation(
config: Config,
session: Session,
email: string,
snowflake: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/snowflake-reservations/add', {email, snowflake});
}
export async function deleteSnowflakeReservation(
config: Config,
session: Session,
email: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/snowflake-reservations/delete', {email});
}

View File

@@ -0,0 +1,37 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
export type JsonPrimitive = string | number | boolean | null;
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
export type JsonObject = {[key: string]: JsonValue};
export type JsonArray = Array<JsonValue>;
export function parseJson(text: string): JsonValue | null {
try {
return JSON.parse(text) as JsonValue;
} catch {
return null;
}
}
export function isJsonObject(value: JsonValue | null): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

View File

@@ -0,0 +1,74 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {LimitConfigGetResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import type {z} from 'zod';
type LimitConfigResponse = z.infer<typeof LimitConfigGetResponse>;
type LimitRule = LimitConfigResponse['limit_config']['rules'][number];
export async function getLimitConfig(config: Config, session: Session): Promise<ApiResult<LimitConfigResponse>> {
const client = new ApiClient(config, session);
return client.post<LimitConfigResponse>('/admin/limit-config/get', {});
}
export async function updateLimitConfig(
config: Config,
session: Session,
limitConfigJsonValue: string,
): Promise<ApiResult<LimitConfigResponse>> {
const client = new ApiClient(config, session);
return client.post<LimitConfigResponse>('/admin/limit-config/update', `{"limit_config":${limitConfigJsonValue}}`);
}
export function getDefaultValue(response: LimitConfigResponse, ruleId: string, limitKey: string): number | null {
const ruleDefaults = response.defaults[ruleId];
if (!ruleDefaults) {
return null;
}
const value = ruleDefaults[limitKey];
return value !== undefined ? value : null;
}
export function isModified(rule: LimitRule, limitKey: string): boolean {
return rule.modifiedFields?.includes(limitKey) ?? false;
}
export function getKeysByCategory(response: LimitConfigResponse): Record<string, Array<string>> {
const result: Record<string, Array<string>> = {};
for (const key of response.limit_keys) {
const meta = response.metadata[key];
if (meta) {
const category = meta.category;
if (!result[category]) {
result[category] = [];
}
result[category].push(key);
}
}
return result;
}

View File

@@ -0,0 +1,131 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {
DeleteAllUserMessagesResponse,
MessageShredResponse,
} from '@fluxer/schema/src/domains/admin/AdminMessageSchemas';
import type {
LookupMessageResponse as LookupMessageResponseSchema,
MessageShredStatusResponse as MessageShredStatusResponseSchema,
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import type {z} from 'zod';
export type LookupMessageResponse = z.infer<typeof LookupMessageResponseSchema>;
export type MessageShredStatusResponse = z.infer<typeof MessageShredStatusResponseSchema>;
export type ShredEntry = {
channel_id: string;
message_id: string;
};
export async function deleteMessage(
config: Config,
session: Session,
channel_id: string,
message_id: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid(
'/admin/messages/delete',
{
channel_id,
message_id,
},
auditLogReason,
);
}
export async function lookupMessage(
config: Config,
session: Session,
channel_id: string,
message_id: string,
context_limit: number,
): Promise<ApiResult<LookupMessageResponse>> {
const client = new ApiClient(config, session);
return client.post<LookupMessageResponse>('/admin/messages/lookup', {
channel_id,
message_id,
context_limit,
});
}
export async function queueMessageShred(
config: Config,
session: Session,
user_id: string,
entries: Array<ShredEntry>,
): Promise<ApiResult<MessageShredResponse>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
user_id,
entries: entries.map((e) => ({channel_id: e.channel_id, message_id: e.message_id})),
};
return client.post<MessageShredResponse>('/admin/messages/shred', body);
}
export async function deleteAllUserMessages(
config: Config,
session: Session,
user_id: string,
dry_run: boolean,
): Promise<ApiResult<DeleteAllUserMessagesResponse>> {
const client = new ApiClient(config, session);
return client.post<DeleteAllUserMessagesResponse>('/admin/messages/delete-all', {
user_id,
dry_run,
});
}
export async function getMessageShredStatus(
config: Config,
session: Session,
job_id: string,
): Promise<ApiResult<MessageShredStatusResponse>> {
const client = new ApiClient(config, session);
return client.post<MessageShredStatusResponse>('/admin/messages/shred-status', {
job_id,
});
}
export async function lookupMessageByAttachment(
config: Config,
session: Session,
channel_id: string,
attachment_id: string,
filename: string,
context_limit: number,
): Promise<ApiResult<LookupMessageResponse>> {
const client = new ApiClient(config, session);
return client.post<LookupMessageResponse>('/admin/messages/lookup-by-attachment', {
channel_id,
attachment_id,
filename,
context_limit,
});
}

View File

@@ -0,0 +1,94 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {
ListReportsResponse,
ReportAdminResponseSchema,
SearchReportsResponse,
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import type {z} from 'zod';
export type Report = z.infer<typeof ReportAdminResponseSchema>;
export async function listReports(
config: Config,
session: Session,
status: number,
limit: number,
offset?: number,
): Promise<ApiResult<z.infer<typeof ListReportsResponse>>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
status,
limit,
...(offset !== undefined ? {offset} : {}),
};
return client.post<z.infer<typeof ListReportsResponse>>('/admin/reports/list', body);
}
export async function resolveReport(
config: Config,
session: Session,
reportId: string,
publicComment?: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
report_id: reportId,
...(publicComment !== undefined ? {public_comment: publicComment} : {}),
};
return client.postVoid('/admin/reports/resolve', body, auditLogReason);
}
export async function searchReports(
config: Config,
session: Session,
query?: string,
statusFilter?: number,
typeFilter?: number,
categoryFilter?: string,
limit: number = 25,
offset: number = 0,
): Promise<ApiResult<z.infer<typeof SearchReportsResponse>>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
limit,
offset,
...(query && query !== '' ? {query} : {}),
...(statusFilter !== undefined ? {status: statusFilter} : {}),
...(typeFilter !== undefined ? {report_type: typeFilter} : {}),
...(categoryFilter && categoryFilter !== '' ? {category: categoryFilter} : {}),
};
return client.post<z.infer<typeof SearchReportsResponse>>('/admin/reports/search', body);
}
export async function getReportDetail(config: Config, session: Session, reportId: string): Promise<ApiResult<Report>> {
const client = new ApiClient(config, session);
return client.get<Report>(`/admin/reports/${reportId}`);
}

View File

@@ -0,0 +1,62 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {
IndexRefreshStatusResponse,
RefreshSearchIndexResponse,
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
export async function refreshSearchIndex(
config: Config,
session: Session,
indexType: string,
reason?: string,
): Promise<ApiResult<RefreshSearchIndexResponse>> {
return refreshSearchIndexWithGuild(config, session, indexType, undefined, reason);
}
export async function refreshSearchIndexWithGuild(
config: Config,
session: Session,
indexType: string,
guildId?: string,
reason?: string,
): Promise<ApiResult<RefreshSearchIndexResponse>> {
const client = new ApiClient(config, session);
const body: Record<string, string> = {
index_type: indexType,
...(guildId ? {guild_id: guildId} : {}),
};
return client.post<RefreshSearchIndexResponse>('/admin/search/refresh-index', body, reason);
}
export async function getIndexRefreshStatus(
config: Config,
session: Session,
jobId: string,
): Promise<ApiResult<IndexRefreshStatusResponse>> {
const client = new ApiClient(config, session);
return client.post<IndexRefreshStatusResponse>('/admin/search/refresh-status', {job_id: jobId});
}

View File

@@ -0,0 +1,53 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {
GuildMemoryStatsResponse,
NodeStatsResponse,
ReloadAllGuildsResponse,
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
export async function getGuildMemoryStats(
config: Config,
session: Session,
limit: number = 25,
): Promise<ApiResult<GuildMemoryStatsResponse>> {
const client = new ApiClient(config, session);
return client.post<GuildMemoryStatsResponse>('/admin/gateway/memory-stats', {limit});
}
export async function reloadAllGuilds(
config: Config,
session: Session,
guildIds: Array<string>,
): Promise<ApiResult<ReloadAllGuildsResponse>> {
const client = new ApiClient(config, session);
return client.post<ReloadAllGuildsResponse>('/admin/gateway/reload-all', {guild_ids: guildIds});
}
export async function getNodeStats(config: Config, session: Session): Promise<ApiResult<NodeStatsResponse>> {
const client = new ApiClient(config, session);
return client.get<NodeStatsResponse>('/admin/gateway/stats');
}

View File

@@ -0,0 +1,68 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {ListSystemDmJobsResponse, SystemDmJobResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
export async function createSystemDmJob(
config: Config,
session: Session,
content: string,
registrationStart?: string,
registrationEnd?: string,
excludedGuildIds: Array<string> = [],
): Promise<ApiResult<SystemDmJobResponse>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
content,
...(registrationStart ? {registration_start: registrationStart} : {}),
...(registrationEnd ? {registration_end: registrationEnd} : {}),
...(excludedGuildIds.length > 0 ? {excluded_guild_ids: excludedGuildIds} : {}),
};
return client.post<SystemDmJobResponse>('/admin/system-dm-jobs', body);
}
export async function listSystemDmJobs(
config: Config,
session: Session,
limit: number = 25,
beforeJobId?: string,
): Promise<ApiResult<ListSystemDmJobsResponse>> {
const client = new ApiClient(config, session);
return client.get<ListSystemDmJobsResponse>('/admin/system-dm-jobs', {
limit,
before_job_id: beforeJobId,
});
}
export async function approveSystemDmJob(
config: Config,
session: Session,
jobId: string,
): Promise<ApiResult<SystemDmJobResponse>> {
const client = new ApiClient(config, session);
return client.post<SystemDmJobResponse>(`/admin/system-dm-jobs/${jobId}/approve`, {});
}

View File

@@ -0,0 +1,419 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {ListUserGuildsResponse} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas';
import type {
AdminUsersMeResponse,
ListUserChangeLogResponse,
ListUserDmChannelsResponse,
ListUserSessionsResponse,
LookupUserResponse,
UserAdminResponse,
} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
export async function getCurrentAdmin(config: Config, session: Session): Promise<ApiResult<UserAdminResponse | null>> {
const client = new ApiClient(config, session);
const result = await client.get<AdminUsersMeResponse>('/admin/users/me');
if (result.ok) {
return {ok: true, data: result.data.user};
}
if (result.error.type === 'notFound') {
return {ok: true, data: null};
}
return result;
}
export interface UserSearchResult {
users: Array<UserAdminResponse>;
has_more: boolean;
}
export async function searchUsers(
config: Config,
session: Session,
query: string,
page: number = 0,
limit: number = 25,
): Promise<ApiResult<UserSearchResult>> {
const client = new ApiClient(config, session);
const offset = Math.max(0, page) * limit;
const result = await client.post<{users: Array<UserAdminResponse>; total: number}>('/admin/users/search', {
query,
limit,
offset,
});
if (result.ok) {
const users = result.data.users;
const hasMore = offset + users.length < result.data.total;
return {ok: true, data: {users, has_more: hasMore}};
}
return result;
}
export async function lookupUser(
config: Config,
session: Session,
query: string,
): Promise<ApiResult<UserAdminResponse | null>> {
const client = new ApiClient(config, session);
const result = await client.post<LookupUserResponse>('/admin/users/lookup', {query});
if (result.ok) {
const user = result.data.users[0];
return {ok: true, data: user ?? null};
}
return result;
}
export async function lookupUsersByIds(
config: Config,
session: Session,
userIds: Array<string>,
): Promise<ApiResult<Array<UserAdminResponse>>> {
if (userIds.length === 0) {
return {ok: true, data: []};
}
const client = new ApiClient(config, session);
const result = await client.post<LookupUserResponse>('/admin/users/lookup', {user_ids: userIds});
if (result.ok) {
return {ok: true, data: result.data.users};
}
return result;
}
export async function listUserGuilds(
config: Config,
session: Session,
userId: string,
before?: string,
after?: string,
limit: number = 25,
withCounts: boolean = true,
): Promise<ApiResult<ListUserGuildsResponse>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
user_id: userId,
limit,
with_counts: withCounts,
...(before ? {before} : {}),
...(after ? {after} : {}),
};
const result = await client.post<ListUserGuildsResponse>('/admin/users/list-guilds', body);
if (result.ok) {
return {ok: true, data: result.data};
}
return result;
}
export async function listUserDmChannels(
config: Config,
session: Session,
userId: string,
before?: string,
after?: string,
limit: number = 50,
): Promise<ApiResult<ListUserDmChannelsResponse>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
user_id: userId,
limit,
...(before ? {before} : {}),
...(after ? {after} : {}),
};
const result = await client.post<ListUserDmChannelsResponse>('/admin/users/list-dm-channels', body);
if (result.ok) {
return {ok: true, data: result.data};
}
return result;
}
export async function listUserSessions(
config: Config,
session: Session,
userId: string,
): Promise<ApiResult<ListUserSessionsResponse>> {
const client = new ApiClient(config, session);
const result = await client.post<ListUserSessionsResponse>('/admin/users/list-sessions', {
user_id: userId,
});
if (result.ok) {
return {ok: true, data: result.data};
}
return result;
}
export async function listUserChangeLog(
config: Config,
session: Session,
userId: string,
): Promise<ApiResult<ListUserChangeLogResponse>> {
const client = new ApiClient(config, session);
const result = await client.post<ListUserChangeLogResponse>('/admin/users/change-log', {user_id: userId, limit: 50});
if (result.ok) {
return {ok: true, data: result.data};
}
return result;
}
export async function updateUserFlags(
config: Config,
session: Session,
userId: string,
addFlags: Array<string>,
removeFlags: Array<string>,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid(
'/admin/users/update-flags',
{user_id: userId, add_flags: addFlags, remove_flags: removeFlags},
auditLogReason,
);
}
export async function updateSuspiciousActivityFlags(
config: Config,
session: Session,
userId: string,
flags: number,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/update-suspicious-activity-flags', {user_id: userId, flags}, auditLogReason);
}
export async function setUserAcls(
config: Config,
session: Session,
userId: string,
acls: Array<string>,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/set-acls', {user_id: userId, acls}, auditLogReason);
}
export async function setUserTraits(
config: Config,
session: Session,
userId: string,
traits: Array<string>,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/set-traits', {user_id: userId, traits}, auditLogReason);
}
export async function disableMfa(
config: Config,
session: Session,
userId: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/disable-mfa', {user_id: userId}, auditLogReason);
}
export async function verifyEmail(
config: Config,
session: Session,
userId: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/verify-email', {user_id: userId}, auditLogReason);
}
export async function unlinkPhone(
config: Config,
session: Session,
userId: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/unlink-phone', {user_id: userId}, auditLogReason);
}
export async function terminateSessions(
config: Config,
session: Session,
userId: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/terminate-sessions', {user_id: userId}, auditLogReason);
}
export async function clearUserFields(
config: Config,
session: Session,
userId: string,
fields: Array<string>,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/clear-fields', {user_id: userId, fields}, auditLogReason);
}
export async function setBotStatus(
config: Config,
session: Session,
userId: string,
bot: boolean,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/set-bot-status', {user_id: userId, bot}, auditLogReason);
}
export async function setSystemStatus(
config: Config,
session: Session,
userId: string,
system: boolean,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/set-system-status', {user_id: userId, system}, auditLogReason);
}
export async function changeUsername(
config: Config,
session: Session,
userId: string,
username: string,
discriminator?: number,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
user_id: userId,
username,
...(discriminator !== undefined ? {discriminator} : {}),
};
return client.postVoid('/admin/users/change-username', body, auditLogReason);
}
export async function changeEmail(
config: Config,
session: Session,
userId: string,
email: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/change-email', {user_id: userId, email}, auditLogReason);
}
export async function scheduleDeletion(
config: Config,
session: Session,
userId: string,
reasonCode: number,
publicReason: string | undefined,
daysUntilDeletion: number,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
user_id: userId,
reason_code: reasonCode,
days_until_deletion: daysUntilDeletion,
...(publicReason ? {public_reason: publicReason} : {}),
};
return client.postVoid('/admin/users/schedule-deletion', body, auditLogReason);
}
export async function cancelDeletion(
config: Config,
session: Session,
userId: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/cancel-deletion', {user_id: userId}, auditLogReason);
}
export async function cancelBulkMessageDeletion(
config: Config,
session: Session,
userId: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/cancel-bulk-message-deletion', {user_id: userId}, auditLogReason);
}
export async function tempBanUser(
config: Config,
session: Session,
userId: string,
durationHours: number,
reason?: string,
privateReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
user_id: userId,
duration_hours: durationHours,
...(reason ? {reason} : {}),
};
return client.postVoid('/admin/users/temp-ban', body, privateReason);
}
export async function unbanUser(
config: Config,
session: Session,
userId: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/unban', {user_id: userId}, auditLogReason);
}
export async function changeDob(
config: Config,
session: Session,
userId: string,
dateOfBirth: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/change-dob', {user_id: userId, date_of_birth: dateOfBirth}, auditLogReason);
}
export async function sendPasswordReset(
config: Config,
session: Session,
userId: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/send-password-reset', {user_id: userId}, auditLogReason);
}

View File

@@ -0,0 +1,87 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {
ListVisionarySlotsResponse,
VisionarySlotOperationResponse,
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import type {z} from 'zod';
type ListVisionarySlotsResponseType = z.infer<typeof ListVisionarySlotsResponse>;
type VisionarySlotOperationResponseType = z.infer<typeof VisionarySlotOperationResponse>;
export async function listVisionarySlots(
config: Config,
session: Session,
): Promise<ApiResult<ListVisionarySlotsResponseType>> {
const client = new ApiClient(config, session);
return await client.get<ListVisionarySlotsResponseType>('/admin/visionary-slots');
}
export async function expandVisionarySlots(
config: Config,
session: Session,
count: number,
): Promise<ApiResult<VisionarySlotOperationResponseType>> {
const client = new ApiClient(config, session);
return await client.post<VisionarySlotOperationResponseType>('/admin/visionary-slots/expand', {count});
}
export async function shrinkVisionarySlots(
config: Config,
session: Session,
targetCount: number,
): Promise<ApiResult<VisionarySlotOperationResponseType>> {
const client = new ApiClient(config, session);
return await client.post<VisionarySlotOperationResponseType>('/admin/visionary-slots/shrink', {
target_count: targetCount,
});
}
export async function reserveVisionarySlot(
config: Config,
session: Session,
slotIndex: number,
userId: string | null,
): Promise<ApiResult<VisionarySlotOperationResponseType>> {
const client = new ApiClient(config, session);
return await client.post<VisionarySlotOperationResponseType>('/admin/visionary-slots/reserve', {
slot_index: slotIndex,
user_id: userId,
});
}
export async function swapVisionarySlots(
config: Config,
session: Session,
slotIndexA: number,
slotIndexB: number,
): Promise<ApiResult<VisionarySlotOperationResponseType>> {
const client = new ApiClient(config, session);
return await client.post<VisionarySlotOperationResponseType>('/admin/visionary-slots/swap', {
slot_index_a: slotIndexA,
slot_index_b: slotIndexB,
});
}

View File

@@ -0,0 +1,217 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import type {
CreateVoiceRegionRequest,
CreateVoiceServerRequest,
ListVoiceRegionsResponse,
ListVoiceServersResponse,
UpdateVoiceRegionRequest,
UpdateVoiceServerRequest,
VoiceRegionWithServersResponse,
VoiceServerAdminResponse,
} from '@fluxer/schema/src/domains/admin/AdminVoiceSchemas';
import type {z} from 'zod';
export interface GetVoiceRegionResponse {
region: VoiceRegionWithServersResponse | null;
}
export interface GetVoiceServerResponse {
server: VoiceServerAdminResponse | null;
}
export async function listVoiceRegions(
config: Config,
session: Session,
include_servers: boolean,
): Promise<ApiResult<ListVoiceRegionsResponse>> {
const client = new ApiClient(config, session);
return client.post<ListVoiceRegionsResponse>('/admin/voice/regions/list', {include_servers});
}
export async function getVoiceRegion(
config: Config,
session: Session,
id: string,
include_servers: boolean,
): Promise<ApiResult<GetVoiceRegionResponse>> {
const client = new ApiClient(config, session);
return client.post<GetVoiceRegionResponse>('/admin/voice/regions/get', {
id,
include_servers,
});
}
export interface CreateVoiceRegionParams
extends Omit<z.infer<typeof CreateVoiceRegionRequest>, 'allowed_guild_ids' | 'allowed_user_ids'> {
allowed_guild_ids: Array<string>;
allowed_user_ids?: Array<string>;
audit_log_reason?: string;
}
export async function createVoiceRegion(
config: Config,
session: Session,
params: CreateVoiceRegionParams,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid(
'/admin/voice/regions/create',
{
id: params.id,
name: params.name,
emoji: params.emoji,
latitude: params.latitude,
longitude: params.longitude,
is_default: params.is_default,
vip_only: params.vip_only,
required_guild_features: params.required_guild_features,
allowed_guild_ids: params.allowed_guild_ids,
allowed_user_ids: params.allowed_user_ids ?? [],
},
params.audit_log_reason,
);
}
export interface UpdateVoiceRegionParams
extends Omit<z.infer<typeof UpdateVoiceRegionRequest>, 'allowed_guild_ids' | 'allowed_user_ids'> {
allowed_guild_ids?: Array<string>;
allowed_user_ids?: Array<string>;
audit_log_reason?: string;
}
export async function updateVoiceRegion(
config: Config,
session: Session,
params: UpdateVoiceRegionParams,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
id: params.id,
...(params.name !== undefined ? {name: params.name} : {}),
...(params.emoji !== undefined ? {emoji: params.emoji} : {}),
...(params.latitude !== undefined ? {latitude: params.latitude} : {}),
...(params.longitude !== undefined ? {longitude: params.longitude} : {}),
...(params.is_default !== undefined ? {is_default: params.is_default} : {}),
...(params.vip_only !== undefined ? {vip_only: params.vip_only} : {}),
...(params.required_guild_features !== undefined ? {required_guild_features: params.required_guild_features} : {}),
...(params.allowed_guild_ids !== undefined ? {allowed_guild_ids: params.allowed_guild_ids} : {}),
...(params.allowed_user_ids !== undefined ? {allowed_user_ids: params.allowed_user_ids} : {}),
};
return client.postVoid('/admin/voice/regions/update', body, params.audit_log_reason);
}
export async function deleteVoiceRegion(
config: Config,
session: Session,
id: string,
audit_log_reason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/voice/regions/delete', {id}, audit_log_reason);
}
export async function listVoiceServers(
config: Config,
session: Session,
region_id: string,
): Promise<ApiResult<ListVoiceServersResponse>> {
const client = new ApiClient(config, session);
return client.post<ListVoiceServersResponse>('/admin/voice/servers/list', {region_id});
}
export interface CreateVoiceServerParams
extends Omit<z.infer<typeof CreateVoiceServerRequest>, 'allowed_guild_ids' | 'allowed_user_ids'> {
allowed_guild_ids: Array<string>;
allowed_user_ids?: Array<string>;
audit_log_reason?: string;
}
export async function createVoiceServer(
config: Config,
session: Session,
params: CreateVoiceServerParams,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid(
'/admin/voice/servers/create',
{
region_id: params.region_id,
server_id: params.server_id,
endpoint: params.endpoint,
api_key: params.api_key,
api_secret: params.api_secret,
is_active: params.is_active,
vip_only: params.vip_only,
required_guild_features: params.required_guild_features,
allowed_guild_ids: params.allowed_guild_ids,
allowed_user_ids: params.allowed_user_ids ?? [],
},
params.audit_log_reason,
);
}
export interface UpdateVoiceServerParams
extends Omit<z.infer<typeof UpdateVoiceServerRequest>, 'allowed_guild_ids' | 'allowed_user_ids'> {
allowed_guild_ids?: Array<string>;
allowed_user_ids?: Array<string>;
audit_log_reason?: string;
}
export async function updateVoiceServer(
config: Config,
session: Session,
params: UpdateVoiceServerParams,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
const body: JsonObject = {
region_id: params.region_id,
server_id: params.server_id,
...(params.endpoint !== undefined ? {endpoint: params.endpoint} : {}),
...(params.api_key !== undefined ? {api_key: params.api_key} : {}),
...(params.api_secret !== undefined ? {api_secret: params.api_secret} : {}),
...(params.is_active !== undefined ? {is_active: params.is_active} : {}),
...(params.vip_only !== undefined ? {vip_only: params.vip_only} : {}),
...(params.required_guild_features !== undefined ? {required_guild_features: params.required_guild_features} : {}),
...(params.allowed_guild_ids !== undefined ? {allowed_guild_ids: params.allowed_guild_ids} : {}),
...(params.allowed_user_ids !== undefined ? {allowed_user_ids: params.allowed_user_ids} : {}),
};
return client.postVoid('/admin/voice/servers/update', body, params.audit_log_reason);
}
export async function deleteVoiceServer(
config: Config,
session: Session,
region_id: string,
server_id: string,
audit_log_reason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid('/admin/voice/servers/delete', {region_id, server_id}, audit_log_reason);
}