mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(admin)!: move the admin api to rest and fix its defects (#2515)
This commit is contained in:
1 parent
a70924d4b0
commit
7f8f09ee51
143 files changed
+15238
-12790
No files matched your search
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
||||
import {z} from 'zod';
|
||||
|
||||
type AdminACL = (typeof AdminACLs)[keyof typeof AdminACLs];
|
||||
|
||||
const ADMIN_ACL_VALUES = Object.values(AdminACLs) as [AdminACL, ...Array<AdminACL>];
|
||||
|
||||
export const ADMIN_ACL_COUNT = ADMIN_ACL_VALUES.length;
|
||||
|
||||
export const AdminAclType = z.enum(ADMIN_ACL_VALUES);
|
||||
|
||||
export type AdminAclType = z.infer<typeof AdminAclType>;
|
||||
@@ -39,11 +39,20 @@ export const ApplicationAdminResponse = z.object({
|
||||
|
||||
export type ApplicationAdminResponse = z.infer<typeof ApplicationAdminResponse>;
|
||||
|
||||
export const LookupApplicationRequest = z.object({
|
||||
application_id: SnowflakeType.describe('ID of the application to look up'),
|
||||
export const AdminApplicationIdParam = z.object({
|
||||
application_id: SnowflakeType.describe('The ID of the OAuth2 application'),
|
||||
});
|
||||
|
||||
export type LookupApplicationRequest = z.infer<typeof LookupApplicationRequest>;
|
||||
export type AdminApplicationIdParam = z.infer<typeof AdminApplicationIdParam>;
|
||||
|
||||
export const ListApplicationsQuery = z.object({
|
||||
owner_id: SnowflakeType.optional().describe('Restrict the results to the applications owned by this user'),
|
||||
guild_id: SnowflakeType.optional().describe(
|
||||
'Restrict the results to the applications whose bot users are members of this guild',
|
||||
),
|
||||
});
|
||||
|
||||
export type ListApplicationsQuery = z.infer<typeof ListApplicationsQuery>;
|
||||
|
||||
export const LookupApplicationResponse = z.object({
|
||||
application: ApplicationAdminResponse.nullable(),
|
||||
@@ -51,32 +60,13 @@ export const LookupApplicationResponse = z.object({
|
||||
|
||||
export type LookupApplicationResponse = z.infer<typeof LookupApplicationResponse>;
|
||||
|
||||
export const ListUserApplicationsRequest = z.object({
|
||||
user_id: SnowflakeType.describe('ID of the user whose applications to list'),
|
||||
});
|
||||
|
||||
export type ListUserApplicationsRequest = z.infer<typeof ListUserApplicationsRequest>;
|
||||
|
||||
export const ListUserApplicationsResponse = z.object({
|
||||
export const ListApplicationsResponse = z.object({
|
||||
applications: z.array(ApplicationAdminResponse),
|
||||
});
|
||||
|
||||
export type ListUserApplicationsResponse = z.infer<typeof ListUserApplicationsResponse>;
|
||||
|
||||
export const ListGuildApplicationsRequest = z.object({
|
||||
guild_id: SnowflakeType.describe('ID of the guild whose installed bot applications to list'),
|
||||
});
|
||||
|
||||
export type ListGuildApplicationsRequest = z.infer<typeof ListGuildApplicationsRequest>;
|
||||
|
||||
export const ListGuildApplicationsResponse = z.object({
|
||||
applications: z.array(ApplicationAdminResponse),
|
||||
});
|
||||
|
||||
export type ListGuildApplicationsResponse = z.infer<typeof ListGuildApplicationsResponse>;
|
||||
export type ListApplicationsResponse = z.infer<typeof ListApplicationsResponse>;
|
||||
|
||||
export const TransferApplicationOwnershipRequest = z.object({
|
||||
application_id: SnowflakeType.describe('ID of the application to transfer'),
|
||||
new_owner_id: SnowflakeType.describe('ID of the user to transfer ownership to'),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
BanAvatarHashRequest,
|
||||
BanEmailRequest,
|
||||
BanFileShaRequest,
|
||||
BanIpRequest,
|
||||
BanPhraseRequest,
|
||||
BanProfileSubstringRequest,
|
||||
BanUrlDomainRequest,
|
||||
BanUrlRequest,
|
||||
CheckAvatarHashRequest,
|
||||
SuspiciousEmailDomainRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import {createQueryIntegerType} from '@fluxer/schema/src/primitives/QueryValidators';
|
||||
import {createStringType, Int32Type, SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const AdminBlocklistListType = z
|
||||
.enum([
|
||||
'ip',
|
||||
'email',
|
||||
'email-domain-suspicious',
|
||||
'phrase',
|
||||
'url',
|
||||
'url-domain',
|
||||
'file-sha',
|
||||
'avatar-hash',
|
||||
'profile-substring',
|
||||
])
|
||||
.describe('The blocklist an entry belongs to');
|
||||
|
||||
export type AdminBlocklistListType = z.infer<typeof AdminBlocklistListType>;
|
||||
|
||||
const BlocklistScopeType = BanProfileSubstringRequest.shape.scope;
|
||||
|
||||
export const BlocklistTypeParam = z.object({
|
||||
list_type: AdminBlocklistListType.describe('The blocklist to operate on'),
|
||||
});
|
||||
|
||||
export type BlocklistTypeParam = z.infer<typeof BlocklistTypeParam>;
|
||||
|
||||
export const BlocklistEntryParam = z.object({
|
||||
list_type: AdminBlocklistListType.describe('The blocklist the entry belongs to'),
|
||||
entry_value: createStringType(1, 2048).describe('The percent-encoded value of the blocklist entry'),
|
||||
});
|
||||
|
||||
export type BlocklistEntryParam = z.infer<typeof BlocklistEntryParam>;
|
||||
|
||||
export const AdminBlocklistScopeQuery = z.object({
|
||||
scope: BlocklistScopeType.optional().describe(
|
||||
'Profile field the entry is scoped to. Required on the profile-substring blocklist and rejected on every other blocklist.',
|
||||
),
|
||||
});
|
||||
|
||||
export type AdminBlocklistScopeQuery = z.infer<typeof AdminBlocklistScopeQuery>;
|
||||
|
||||
export const AdminBlocklistEntryListQuery = z.object({
|
||||
limit: createQueryIntegerType({minValue: 1, maxValue: 200, defaultValue: 50}).describe(
|
||||
'Maximum number of entries to return',
|
||||
),
|
||||
after: createStringType(1, 2048)
|
||||
.optional()
|
||||
.describe('Return entries ordered after this value, taken from the next_after cursor of the previous page'),
|
||||
scope: BlocklistScopeType.optional().describe(
|
||||
'Profile field to list. Required on the profile-substring blocklist and rejected on every other blocklist.',
|
||||
),
|
||||
});
|
||||
|
||||
export type AdminBlocklistEntryListQuery = z.infer<typeof AdminBlocklistEntryListQuery>;
|
||||
|
||||
const AdminBlocklistTypeResponse = z.object({
|
||||
list_type: AdminBlocklistListType,
|
||||
description: z.string().describe('What the blocklist matches and how matching is performed'),
|
||||
value_field: z.string().describe('The request body field that carries the entry value when adding to this blocklist'),
|
||||
fields: z.array(z.string()).max(8).describe('Fields entries of this blocklist accept beyond the value itself'),
|
||||
scoped: z.boolean().describe('Whether entries are scoped to a profile field and a scope must be supplied'),
|
||||
supports_bulk_create: z.boolean().describe('Whether PUT on the entry collection is accepted'),
|
||||
supports_bulk_delete: z.boolean().describe('Whether DELETE on the entry collection is accepted'),
|
||||
supports_update: z.boolean().describe('Whether PATCH on a single entry is accepted'),
|
||||
});
|
||||
|
||||
export const AdminBlocklistTypeListResponse = z.object({
|
||||
items: z.array(AdminBlocklistTypeResponse).max(50).describe('Every blocklist exposed by this instance'),
|
||||
});
|
||||
|
||||
const AdminBlocklistEntryResponse = z.object({
|
||||
list_type: AdminBlocklistListType,
|
||||
value: createStringType(1, 2048).describe('The canonical stored value of the entry'),
|
||||
scope: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe('The profile field the entry is scoped to, or null when the blocklist is unscoped'),
|
||||
category: z.string().nullable().describe('The category slug stored alongside the entry, or null'),
|
||||
severity: Int32Type.nullable().describe('The stored severity, or null when the blocklist has no severity'),
|
||||
source_url: z.string().nullable().describe('The upstream source the entry was imported from, or null'),
|
||||
notes: z.string().nullable().describe('The internal notes stored alongside the entry, or null'),
|
||||
content_type: z.string().nullable().describe('The MIME type hint stored alongside the entry, or null'),
|
||||
match_subdomains: z
|
||||
.boolean()
|
||||
.nullable()
|
||||
.describe('Whether subdomains are covered by the entry, or null when the blocklist has no such flag'),
|
||||
reason: z.string().nullable().describe('The stored reason for the entry, or null'),
|
||||
expires_at: z.string().nullable().describe('ISO 8601 timestamp when the entry expires, or null'),
|
||||
created_at: z.string().nullable().describe('ISO 8601 timestamp when the entry was added, or null'),
|
||||
created_by_user_id: SnowflakeStringType.nullable().describe('The admin who added the entry, or null when unknown'),
|
||||
});
|
||||
|
||||
export const AdminBlocklistEntryListResponse = z.object({
|
||||
items: z.array(AdminBlocklistEntryResponse).max(200).describe('The blocklist entries in this page, ordered by value'),
|
||||
has_more: z.boolean().describe('Whether another page can be fetched with the next_after cursor'),
|
||||
next_after: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe('Cursor to send as after on the next request, or null when this is the last page'),
|
||||
});
|
||||
|
||||
export const AdminBlocklistEntryCreateRequest = z
|
||||
.union([
|
||||
BanIpRequest,
|
||||
BanEmailRequest,
|
||||
SuspiciousEmailDomainRequest,
|
||||
BanPhraseRequest,
|
||||
BanUrlRequest,
|
||||
BanUrlDomainRequest,
|
||||
BanFileShaRequest,
|
||||
BanAvatarHashRequest,
|
||||
BanProfileSubstringRequest,
|
||||
])
|
||||
.describe('The entry to add, in the shape the blocklist named by list_type accepts');
|
||||
|
||||
export const AdminBlocklistBulkDeleteRequest = z
|
||||
.union([CheckAvatarHashRequest, BanProfileSubstringRequest])
|
||||
.describe('The entries to remove, in the shape the blocklist named by list_type accepts');
|
||||
|
||||
export const AdminBlocklistUrlUpdateRequest = BanUrlRequest.omit({url: true});
|
||||
|
||||
export const AdminBlocklistUrlDomainUpdateRequest = BanUrlDomainRequest.omit({domain: true});
|
||||
|
||||
export const AdminBlocklistFileShaUpdateRequest = BanFileShaRequest.omit({sha256_hex: true});
|
||||
|
||||
export const AdminBlocklistAvatarHashUpdateRequest = BanAvatarHashRequest.omit({hashes: true});
|
||||
|
||||
export const AdminBlocklistProfileSubstringUpdateRequest = BanProfileSubstringRequest.omit({substrings: true});
|
||||
|
||||
export const AdminBlocklistEntryUpdateRequest = z
|
||||
.union([
|
||||
AdminBlocklistUrlUpdateRequest,
|
||||
AdminBlocklistUrlDomainUpdateRequest,
|
||||
AdminBlocklistFileShaUpdateRequest,
|
||||
AdminBlocklistAvatarHashUpdateRequest,
|
||||
AdminBlocklistProfileSubstringUpdateRequest,
|
||||
])
|
||||
.describe('The stored fields to write, in the shape the blocklist named by list_type accepts');
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
BulkAddGuildMembersRequest,
|
||||
BulkUpdateGuildFeaturesRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas';
|
||||
import {BulkDeleteUserMessagesRequest} from '@fluxer/schema/src/domains/admin/AdminMessageSchemas';
|
||||
import {
|
||||
BulkScheduleUserDeletionRequest,
|
||||
BulkUpdateSuspiciousActivityFlagsRequest,
|
||||
BulkUpdateUserFlagsRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const AdminBulkTaskType = {
|
||||
UPDATE_USER_FLAGS: 'update_user_flags',
|
||||
UPDATE_SUSPICIOUS_ACTIVITY_FLAGS: 'update_suspicious_activity_flags',
|
||||
UPDATE_GUILD_FEATURES: 'update_guild_features',
|
||||
ADD_GUILD_MEMBERS: 'add_guild_members',
|
||||
SCHEDULE_USER_DELETION: 'schedule_user_deletion',
|
||||
DELETE_USER_MESSAGES: 'delete_user_messages',
|
||||
} as const;
|
||||
|
||||
export type AdminBulkTaskType = (typeof AdminBulkTaskType)[keyof typeof AdminBulkTaskType];
|
||||
|
||||
export const AdminBulkJobCreateRequest = z.discriminatedUnion('task', [
|
||||
BulkUpdateUserFlagsRequest.extend({
|
||||
task: z
|
||||
.literal(AdminBulkTaskType.UPDATE_USER_FLAGS)
|
||||
.describe('Adds and removes account flags on every targeted user'),
|
||||
}),
|
||||
BulkUpdateSuspiciousActivityFlagsRequest.extend({
|
||||
task: z
|
||||
.literal(AdminBulkTaskType.UPDATE_SUSPICIOUS_ACTIVITY_FLAGS)
|
||||
.describe('Adds and removes verification requirements on every targeted user'),
|
||||
}),
|
||||
BulkUpdateGuildFeaturesRequest.extend({
|
||||
task: z
|
||||
.literal(AdminBulkTaskType.UPDATE_GUILD_FEATURES)
|
||||
.describe('Adds and removes features on every targeted guild'),
|
||||
}),
|
||||
BulkAddGuildMembersRequest.extend({
|
||||
task: z.literal(AdminBulkTaskType.ADD_GUILD_MEMBERS).describe('Adds every targeted user to one guild'),
|
||||
}),
|
||||
BulkScheduleUserDeletionRequest.extend({
|
||||
task: z
|
||||
.literal(AdminBulkTaskType.SCHEDULE_USER_DELETION)
|
||||
.describe('Schedules account deletion for every targeted user'),
|
||||
}),
|
||||
BulkDeleteUserMessagesRequest.extend({
|
||||
task: z
|
||||
.literal(AdminBulkTaskType.DELETE_USER_MESSAGES)
|
||||
.describe('Deletes every message authored by each targeted user, across all channels'),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type AdminBulkJobCreateRequest = z.infer<typeof AdminBulkJobCreateRequest>;
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
GuildVerificationLevelSchema,
|
||||
NSFWLevelSchema,
|
||||
} from '@fluxer/schema/src/primitives/GuildValidators';
|
||||
import {createQueryIntegerType} from '@fluxer/schema/src/primitives/QueryValidators';
|
||||
import {
|
||||
createBitflagInt32Type,
|
||||
createNamedStringLiteralUnion,
|
||||
@@ -101,40 +102,20 @@ export const KickGuildMemberRequest = z.object({
|
||||
|
||||
export type KickGuildMemberRequest = z.infer<typeof KickGuildMemberRequest>;
|
||||
|
||||
export const SearchGuildsRequest = z.object({
|
||||
query: createStringType(1, 1024).optional(),
|
||||
limit: z.number().int().min(1).max(200).default(50),
|
||||
offset: z.number().int().min(0).default(0),
|
||||
export const GetProcessMemoryStatsQuery = z.object({
|
||||
limit: createQueryIntegerType({defaultValue: 100, minValue: 100, maxValue: 1000}).describe(
|
||||
'Maximum number of guild processes to return (100-1000, default 100)',
|
||||
),
|
||||
});
|
||||
|
||||
export type SearchGuildsRequest = z.infer<typeof SearchGuildsRequest>;
|
||||
export type GetProcessMemoryStatsQuery = z.infer<typeof GetProcessMemoryStatsQuery>;
|
||||
|
||||
export const ReloadGuildRequest = z.object({
|
||||
guild_id: SnowflakeType,
|
||||
});
|
||||
|
||||
export type ReloadGuildRequest = z.infer<typeof ReloadGuildRequest>;
|
||||
|
||||
export const ShutdownGuildRequest = z.object({
|
||||
guild_id: SnowflakeType,
|
||||
});
|
||||
|
||||
export type ShutdownGuildRequest = z.infer<typeof ShutdownGuildRequest>;
|
||||
|
||||
export const GetProcessMemoryStatsRequest = z.object({
|
||||
limit: z.number().int().min(100).max(1000).default(100),
|
||||
});
|
||||
|
||||
export type GetProcessMemoryStatsRequest = z.infer<typeof GetProcessMemoryStatsRequest>;
|
||||
|
||||
export const UpdateGuildFeaturesRequest = z.object({
|
||||
const UpdateGuildFeaturesRequest = z.object({
|
||||
guild_id: SnowflakeType.describe('ID of the guild to update'),
|
||||
add_features: z.array(GuildFeatureSchema).max(100).default([]).describe('Guild features to add'),
|
||||
remove_features: z.array(GuildFeatureSchema).max(100).default([]).describe('Guild features to remove'),
|
||||
});
|
||||
|
||||
export type UpdateGuildFeaturesRequest = z.infer<typeof UpdateGuildFeaturesRequest>;
|
||||
|
||||
export const ForceAddUserToGuildRequest = z.object({
|
||||
user_id: SnowflakeType.describe('ID of the user to add to the guild'),
|
||||
guild_id: SnowflakeType.describe('ID of the guild to add the user to'),
|
||||
@@ -158,12 +139,6 @@ export const ClearGuildFieldsRequest = z.object({
|
||||
|
||||
export type ClearGuildFieldsRequest = z.infer<typeof ClearGuildFieldsRequest>;
|
||||
|
||||
export const DeleteGuildRequest = z.object({
|
||||
guild_id: SnowflakeType.describe('ID of the guild to delete'),
|
||||
});
|
||||
|
||||
export type DeleteGuildRequest = z.infer<typeof DeleteGuildRequest>;
|
||||
|
||||
export const UpdateGuildVanityRequest = z.object({
|
||||
guild_id: SnowflakeType.describe('ID of the guild to update'),
|
||||
vanity_url_code: VanityURLCodeType.nullable().describe('New vanity URL code, or null to remove'),
|
||||
@@ -254,3 +229,43 @@ export type ListGuildAuditLogsRequest = z.infer<typeof ListGuildAuditLogsRequest
|
||||
export const ListGuildAuditLogsResponse = GuildAuditLogListResponse;
|
||||
|
||||
export type ListGuildAuditLogsResponse = z.infer<typeof ListGuildAuditLogsResponse>;
|
||||
|
||||
export const ListGuildsQuery = z.object({
|
||||
q: createStringType(1, 1024)
|
||||
.optional()
|
||||
.describe('Free-text query matched against the guild name and vanity URL code'),
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum guilds to return (1-200, default 50)',
|
||||
),
|
||||
offset: createQueryIntegerType({defaultValue: 0, minValue: 0, maxValue: 10000}).describe(
|
||||
'Guilds to skip before returning results (default 0)',
|
||||
),
|
||||
});
|
||||
|
||||
export type ListGuildsQuery = z.infer<typeof ListGuildsQuery>;
|
||||
|
||||
export const ListGuildMembersQuery = z.object({
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum members to return (1-200, default 50)',
|
||||
),
|
||||
offset: createQueryIntegerType({defaultValue: 0, minValue: 0, maxValue: 2147483647}).describe(
|
||||
'Members to skip before returning results (default 0)',
|
||||
),
|
||||
});
|
||||
|
||||
export type ListGuildMembersQuery = z.infer<typeof ListGuildMembersQuery>;
|
||||
|
||||
export const UpdateGuildRequest = UpdateGuildSettingsRequest.omit({guild_id: true}).extend({
|
||||
name: UpdateGuildNameRequest.shape.name.optional(),
|
||||
vanity_url_code: UpdateGuildVanityRequest.shape.vanity_url_code.optional(),
|
||||
new_owner_id: TransferGuildOwnershipRequest.shape.new_owner_id.optional(),
|
||||
add_features: UpdateGuildFeaturesRequest.shape.add_features.removeDefault().optional(),
|
||||
remove_features: UpdateGuildFeaturesRequest.shape.remove_features.removeDefault().optional(),
|
||||
fields: ClearGuildFieldsRequest.shape.fields.optional(),
|
||||
});
|
||||
|
||||
export type UpdateGuildRequest = z.infer<typeof UpdateGuildRequest>;
|
||||
|
||||
export const BanGuildMemberBody = BanGuildMemberRequest.omit({guild_id: true, user_id: true});
|
||||
|
||||
export type BanGuildMemberBody = z.infer<typeof BanGuildMemberBody>;
|
||||
@@ -1,8 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminMessageSchema} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import {AdminMessageSchema, LookupMessageResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import {MessageResponseSchema} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import {SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {FilenameType} from '@fluxer/schema/src/primitives/FileValidators';
|
||||
import {createQueryIntegerType} from '@fluxer/schema/src/primitives/QueryValidators';
|
||||
import {createStringType, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const BrowseChannelRequest = z.object({
|
||||
@@ -30,10 +32,42 @@ export const SearchChannelMessagesRequest = z.object({
|
||||
|
||||
export type SearchChannelMessagesRequest = z.infer<typeof SearchChannelMessagesRequest>;
|
||||
|
||||
export const SearchChannelMessagesResponse = z.object({
|
||||
const SearchChannelMessagesResponse = z.object({
|
||||
messages: z.array(AdminMessageSchema).max(100),
|
||||
message_responses: z.array(MessageResponseSchema).max(100).optional(),
|
||||
total: z.number().int().min(0),
|
||||
});
|
||||
|
||||
export type SearchChannelMessagesResponse = z.infer<typeof SearchChannelMessagesResponse>;
|
||||
export const AdminChannelMessageListQuery = z.object({
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 100}).describe(
|
||||
'Number of messages to return (1-100, default 50)',
|
||||
),
|
||||
before: SnowflakeType.optional().describe('Return messages older than this message ID'),
|
||||
after: SnowflakeType.optional().describe('Return messages newer than this message ID'),
|
||||
});
|
||||
|
||||
export type AdminChannelMessageListQuery = z.infer<typeof AdminChannelMessageListQuery>;
|
||||
|
||||
export const AdminMessageSearchQuery = z.object({
|
||||
channel_id: SnowflakeType.describe('Return messages sent in this channel'),
|
||||
q: createStringType(1, 200).optional().describe('Free-text query matched against message content'),
|
||||
message_id: SnowflakeType.optional().describe(
|
||||
'Return the single message with this ID together with its surrounding context; ignores every other filter',
|
||||
),
|
||||
attachment_id: SnowflakeType.optional().describe(
|
||||
'Return the single message carrying this attachment together with its surrounding context; requires filename',
|
||||
),
|
||||
filename: FilenameType.optional().describe('The filename of the attachment named by attachment_id'),
|
||||
context_limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 100}).describe(
|
||||
'How many messages surrounding a message resolved by message_id or attachment_id to return as context (1-100, default 50)',
|
||||
),
|
||||
limit: createQueryIntegerType({defaultValue: 25, minValue: 1, maxValue: 100}).describe(
|
||||
'Maximum number of messages to return when searching (1-100, default 25)',
|
||||
),
|
||||
});
|
||||
|
||||
export type AdminMessageSearchQuery = z.infer<typeof AdminMessageSearchQuery>;
|
||||
|
||||
export const AdminMessageSearchResponse = z.union([SearchChannelMessagesResponse, LookupMessageResponse]);
|
||||
|
||||
export type AdminMessageSearchResponse = z.infer<typeof AdminMessageSearchResponse>;
|
||||
@@ -1,9 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {FilenameType} from '@fluxer/schema/src/primitives/FileValidators';
|
||||
import {createQueryIntegerType} from '@fluxer/schema/src/primitives/QueryValidators';
|
||||
import {Int32Type, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
const FALSE_QUERY_VALUES = ['false', 'False', '0'];
|
||||
|
||||
export const LookupMessageRequest = z.object({
|
||||
channel_id: SnowflakeType,
|
||||
message_id: SnowflakeType,
|
||||
@@ -59,12 +62,6 @@ export const MessageShredResponse = z.object({
|
||||
|
||||
export type MessageShredResponse = z.infer<typeof MessageShredResponse>;
|
||||
|
||||
export const MessageShredStatusRequest = z.object({
|
||||
job_id: z.string(),
|
||||
});
|
||||
|
||||
export type MessageShredStatusRequest = z.infer<typeof MessageShredStatusRequest>;
|
||||
|
||||
export const DeleteAllUserMessagesRequest = z.object({
|
||||
user_id: SnowflakeType,
|
||||
dry_run: z.boolean().default(true),
|
||||
@@ -87,3 +84,34 @@ export const BulkDeleteUserMessagesRequest = z.object({
|
||||
});
|
||||
|
||||
export type BulkDeleteUserMessagesRequest = z.infer<typeof BulkDeleteUserMessagesRequest>;
|
||||
export const AdminMessageDetailQuery = z.object({
|
||||
context_limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 100}).describe(
|
||||
'How many messages surrounding the requested message to return as context (1-100, default 50)',
|
||||
),
|
||||
});
|
||||
|
||||
export type AdminMessageDetailQuery = z.infer<typeof AdminMessageDetailQuery>;
|
||||
|
||||
export const AdminUserMessageShredRequest = z.object({
|
||||
entries: z.array(MessageShredEntryType).min(1).max(1000),
|
||||
});
|
||||
|
||||
export type AdminUserMessageShredRequest = z.infer<typeof AdminUserMessageShredRequest>;
|
||||
|
||||
export const AdminUserMessageDeleteQuery = z.object({
|
||||
dry_run: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.default('true')
|
||||
.transform((value) => !FALSE_QUERY_VALUES.includes(value))
|
||||
.describe('Count the messages that would be deleted without deleting anything (default true)'),
|
||||
});
|
||||
|
||||
export type AdminUserMessageDeleteQuery = z.infer<typeof AdminUserMessageDeleteQuery>;
|
||||
|
||||
export const MessageShredJobIdParam = z.object({
|
||||
job_id: SnowflakeType.describe('The ID of the message shred job'),
|
||||
});
|
||||
|
||||
export type MessageShredJobIdParam = z.infer<typeof MessageShredJobIdParam>;
|
||||
@@ -1,6 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
||||
import {
|
||||
GIFT_CODE_DURATION_TYPE_DEFINITIONS,
|
||||
MAX_GIFT_CODES_PER_REQUEST,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
SystemChannelFlagsDescriptions,
|
||||
} from '@fluxer/constants/src/GuildConstants';
|
||||
import {LIMIT_KEYS} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import {ADMIN_ACL_COUNT, AdminAclType} from '@fluxer/schema/src/domains/admin/AdminAclType';
|
||||
import {GuildAdminResponse} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas';
|
||||
import {UserAdminResponseSchema} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
import {
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
NSFWLevelSchema,
|
||||
} from '@fluxer/schema/src/primitives/GuildValidators';
|
||||
import {PermissionStringType} from '@fluxer/schema/src/primitives/PermissionValidators';
|
||||
import {createQueryIntegerType, QueryBooleanType} from '@fluxer/schema/src/primitives/QueryValidators';
|
||||
import {
|
||||
createBitflagInt32Type,
|
||||
createInt32EnumType,
|
||||
@@ -45,8 +46,6 @@ import {
|
||||
import {EmailType} from '@fluxer/schema/src/primitives/UserValidators';
|
||||
import {z} from 'zod';
|
||||
|
||||
const ADMIN_ACL_COUNT = Object.keys(AdminACLs).length;
|
||||
|
||||
const ReportStatusSchema = withOpenApiType(
|
||||
createInt32EnumType(
|
||||
[
|
||||
@@ -113,28 +112,26 @@ const SearchIndexTypeEnum = createNamedStringLiteralUnion(
|
||||
],
|
||||
'Type of search index to refresh',
|
||||
);
|
||||
export const ListAuditLogsRequest = z.object({
|
||||
admin_user_id: SnowflakeType.optional().describe('Filter by admin user who performed the action'),
|
||||
target_type: createStringType(1, 64).optional().describe('Filter by target entity type'),
|
||||
target_id: z.string().optional().describe('Filter by target entity ID (user, channel, role, invite code, etc.)'),
|
||||
limit: z.number().int().min(1).max(200).default(50).describe('Maximum number of entries to return'),
|
||||
offset: z.number().int().min(0).default(0).describe('Number of entries to skip'),
|
||||
export const AuditLogIdParam = z.object({
|
||||
log_id: SnowflakeType.describe('The ID of the audit log entry'),
|
||||
});
|
||||
|
||||
export type ListAuditLogsRequest = z.infer<typeof ListAuditLogsRequest>;
|
||||
export type AuditLogIdParam = z.infer<typeof AuditLogIdParam>;
|
||||
|
||||
export const SearchAuditLogsRequest = z.object({
|
||||
query: createStringType(1, 1024).optional().describe('Search query string'),
|
||||
export const ListAdminAuditLogsQuery = z.object({
|
||||
q: createStringType(1, 1024).optional().describe('Free-text query run against the audit log search index'),
|
||||
admin_user_id: SnowflakeType.optional().describe('Filter by admin user who performed the action'),
|
||||
target_type: createStringType(1, 64).optional().describe('Filter by target entity type'),
|
||||
target_id: z.string().optional().describe('Filter by target entity ID (user, channel, role, invite code, etc.)'),
|
||||
sort_by: AuditLogSortByEnum.default('createdAt'),
|
||||
sort_order: SortOrderEnum.default('desc'),
|
||||
limit: z.number().int().min(1).max(200).default(50).describe('Maximum number of entries to return'),
|
||||
offset: z.number().int().min(0).default(0).describe('Number of entries to skip'),
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum number of entries to return',
|
||||
),
|
||||
offset: createQueryIntegerType({defaultValue: 0, minValue: 0}).describe('Number of entries to skip'),
|
||||
});
|
||||
|
||||
export type SearchAuditLogsRequest = z.infer<typeof SearchAuditLogsRequest>;
|
||||
export type ListAdminAuditLogsQuery = z.infer<typeof ListAdminAuditLogsQuery>;
|
||||
|
||||
export const SearchReportsRequest = z.object({
|
||||
query: createStringType(1, 1024).optional().describe('Search query string'),
|
||||
@@ -155,34 +152,78 @@ export const SearchReportsRequest = z.object({
|
||||
|
||||
export type SearchReportsRequest = z.infer<typeof SearchReportsRequest>;
|
||||
|
||||
export const ListReportsRequest = z.object({
|
||||
status: ReportStatusSchema.optional(),
|
||||
limit: z.number().int().min(1).max(200).optional().describe('Maximum number of reports to return'),
|
||||
offset: z.number().int().min(0).optional().describe('Number of reports to skip'),
|
||||
const ReportStatusFilterEnum = createNamedStringLiteralUnion(
|
||||
[
|
||||
['pending', 'pending', 'Only reports that are waiting to be reviewed'],
|
||||
['resolved', 'resolved', 'Only reports that have been resolved'],
|
||||
],
|
||||
'Only return reports with this status',
|
||||
);
|
||||
|
||||
const ReportTypeFilterEnum = createNamedStringLiteralUnion(
|
||||
[
|
||||
['message', 'message', 'Only reports about a message'],
|
||||
['user', 'user', 'Only reports about a user'],
|
||||
['guild', 'guild', 'Only reports about a community'],
|
||||
],
|
||||
'Only return reports about this kind of entity',
|
||||
);
|
||||
|
||||
const ReportSortByQueryEnum = createNamedStringLiteralUnion(
|
||||
[
|
||||
['created_at', 'created_at', 'Sort by the time the report was created'],
|
||||
['reported_at', 'reported_at', 'Sort by the time the report was submitted'],
|
||||
['resolved_at', 'resolved_at', 'Sort by the time the report was resolved'],
|
||||
],
|
||||
'The field to sort the reports by',
|
||||
);
|
||||
|
||||
export const ListReportsQuery = z.object({
|
||||
q: createStringType(1, 1024).optional().describe('Free-text query matched against the searchable report fields'),
|
||||
status: ReportStatusFilterEnum.optional().describe('Only return reports with this status'),
|
||||
report_type: ReportTypeFilterEnum.optional().describe('Only return reports about this kind of entity'),
|
||||
category: createStringType(1, 128).optional().describe('Only return reports filed under this category'),
|
||||
reporter_id: SnowflakeType.optional().describe('Only return reports submitted by this user'),
|
||||
reported_user_id: SnowflakeType.optional().describe('Only return reports about this user'),
|
||||
reported_guild_id: SnowflakeType.optional().describe('Only return reports about this community'),
|
||||
reported_channel_id: SnowflakeType.optional().describe('Only return reports about content in this channel'),
|
||||
guild_context_id: SnowflakeType.optional().describe('Only return reports filed from within this community'),
|
||||
resolved_by_admin_id: SnowflakeType.optional().describe('Only return reports resolved by this admin'),
|
||||
sort_by: ReportSortByQueryEnum.default('reported_at').describe('The field to sort the reports by'),
|
||||
sort_order: SortOrderEnum.default('desc').describe('The direction to sort the reports in'),
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum number of reports to return (1-200, default 50)',
|
||||
),
|
||||
offset: createQueryIntegerType({defaultValue: 0, minValue: 0, maxValue: 10000}).describe('Number of reports to skip'),
|
||||
});
|
||||
|
||||
export type ListReportsRequest = z.infer<typeof ListReportsRequest>;
|
||||
export type ListReportsQuery = z.infer<typeof ListReportsQuery>;
|
||||
|
||||
export const ResolveReportRequest = z.object({
|
||||
report_id: SnowflakeType.describe('The ID of the report to resolve'),
|
||||
export const UpdateReportRequest = z.object({
|
||||
status: z.literal('resolved').describe('The status to move the report to'),
|
||||
public_comment: createStringType(0, 512).optional().describe('Public comment to include with the resolution'),
|
||||
});
|
||||
|
||||
export type ResolveReportRequest = z.infer<typeof ResolveReportRequest>;
|
||||
export type UpdateReportRequest = z.infer<typeof UpdateReportRequest>;
|
||||
|
||||
export const SearchIndexNameParam = z.object({
|
||||
index_name: SearchIndexTypeEnum.describe('The name of the search index'),
|
||||
});
|
||||
|
||||
export type SearchIndexNameParam = z.infer<typeof SearchIndexNameParam>;
|
||||
|
||||
export const RefreshSearchIndexRequest = z.object({
|
||||
index_type: SearchIndexTypeEnum,
|
||||
guild_id: SnowflakeType.optional().describe('Specific guild ID to reindex'),
|
||||
user_id: SnowflakeType.optional().describe('Specific user ID to reindex'),
|
||||
});
|
||||
|
||||
export type RefreshSearchIndexRequest = z.infer<typeof RefreshSearchIndexRequest>;
|
||||
|
||||
export const GetIndexRefreshStatusRequest = z.object({
|
||||
job_id: createStringType(1, 128).describe('ID of the index refresh job to check'),
|
||||
export const SearchIndexRefreshIdParam = z.object({
|
||||
job_id: createStringType(1, 128).describe('ID of the index refresh to read'),
|
||||
});
|
||||
|
||||
export type GetIndexRefreshStatusRequest = z.infer<typeof GetIndexRefreshStatusRequest>;
|
||||
export type SearchIndexRefreshIdParam = z.infer<typeof SearchIndexRefreshIdParam>;
|
||||
|
||||
export const PurgeGuildAssetsRequest = z.object({
|
||||
ids: z.array(createStringType(1, 64)).max(100).describe('List of asset IDs to purge'),
|
||||
@@ -190,29 +231,33 @@ export const PurgeGuildAssetsRequest = z.object({
|
||||
|
||||
export type PurgeGuildAssetsRequest = z.infer<typeof PurgeGuildAssetsRequest>;
|
||||
|
||||
export const TriggerUserArchiveRequest = z.object({
|
||||
user_id: SnowflakeType.describe('ID of the user to archive'),
|
||||
export const AdminArchiveCreateRequest = z.object({
|
||||
include_attachments: z.boolean().default(false).describe('Whether to include attachment binaries in the archive'),
|
||||
});
|
||||
|
||||
export type TriggerUserArchiveRequest = z.infer<typeof TriggerUserArchiveRequest>;
|
||||
export type AdminArchiveCreateRequest = z.infer<typeof AdminArchiveCreateRequest>;
|
||||
|
||||
export const TriggerGuildArchiveRequest = z.object({
|
||||
guild_id: SnowflakeType.describe('ID of the guild to archive'),
|
||||
include_attachments: z.boolean().default(false).describe('Whether to include attachment binaries in the archive'),
|
||||
});
|
||||
export const ListArchivesQuery = z
|
||||
.object({
|
||||
subject_type: ArchiveListSubjectTypeEnum.default('all'),
|
||||
subject_id: SnowflakeType.optional().describe('Filter by specific subject ID'),
|
||||
requested_by: SnowflakeType.optional().describe('Filter by user who requested the archive'),
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum number of archives to return (1-200, default 50)',
|
||||
),
|
||||
include_expired: QueryBooleanType.describe('Whether to include archives past their expires_at'),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.subject_id !== undefined && value.subject_type === 'all') {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'subject_type must name user or guild when subject_id is supplied',
|
||||
path: ['subject_type'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type TriggerGuildArchiveRequest = z.infer<typeof TriggerGuildArchiveRequest>;
|
||||
|
||||
export const ListArchivesRequest = z.object({
|
||||
subject_type: ArchiveListSubjectTypeEnum.default('all'),
|
||||
subject_id: SnowflakeType.optional().describe('Filter by specific subject ID'),
|
||||
requested_by: SnowflakeType.optional().describe('Filter by user who requested the archive'),
|
||||
limit: z.number().min(1).max(200).default(50).describe('Maximum number of archives to return'),
|
||||
include_expired: z.boolean().default(false).describe('Whether to include expired archives'),
|
||||
});
|
||||
|
||||
export type ListArchivesRequest = z.infer<typeof ListArchivesRequest>;
|
||||
export type ListArchivesQuery = z.infer<typeof ListArchivesQuery>;
|
||||
|
||||
const IP_OR_CIDR_REGEX = /^(?:(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?|(?:[a-fA-F0-9:]+)(?:\/\d{1,3})?)$/;
|
||||
export const BanIpRequest = z.object({
|
||||
@@ -268,12 +313,6 @@ export const BanUrlRequest = z.object({
|
||||
|
||||
export type BanUrlRequest = z.infer<typeof BanUrlRequest>;
|
||||
|
||||
export const UnbanUrlRequest = z.object({
|
||||
url: createStringType(1, 2048).describe('URL to unban (must match the canonicalized form in storage)'),
|
||||
});
|
||||
|
||||
export type UnbanUrlRequest = z.infer<typeof UnbanUrlRequest>;
|
||||
|
||||
export const BanUrlDomainRequest = z.object({
|
||||
domain: createStringType(1, 253)
|
||||
.refine(
|
||||
@@ -296,12 +335,6 @@ export const BanUrlDomainRequest = z.object({
|
||||
|
||||
export type BanUrlDomainRequest = z.infer<typeof BanUrlDomainRequest>;
|
||||
|
||||
export const UnbanUrlDomainRequest = z.object({
|
||||
domain: createStringType(1, 253).describe('Domain to unban'),
|
||||
});
|
||||
|
||||
export type UnbanUrlDomainRequest = z.infer<typeof UnbanUrlDomainRequest>;
|
||||
|
||||
export const BanFileShaRequest = z.object({
|
||||
sha256_hex: createStringType(64, 64)
|
||||
.refine((v) => /^[0-9a-fA-F]{64}$/.test(v), 'Must be a 64-character hex SHA-256')
|
||||
@@ -315,24 +348,6 @@ export const BanFileShaRequest = z.object({
|
||||
|
||||
export type BanFileShaRequest = z.infer<typeof BanFileShaRequest>;
|
||||
|
||||
export const UnbanFileShaRequest = z.object({
|
||||
sha256_hex: createStringType(64, 64).refine((v) => /^[0-9a-fA-F]{64}$/.test(v), 'Must be a 64-character hex SHA-256'),
|
||||
});
|
||||
|
||||
export type UnbanFileShaRequest = z.infer<typeof UnbanFileShaRequest>;
|
||||
|
||||
export const CheckUrlBlocklistRequest = z.object({
|
||||
url: createStringType(1, 2048).describe('URL to check against the blocklist'),
|
||||
});
|
||||
|
||||
export type CheckUrlBlocklistRequest = z.infer<typeof CheckUrlBlocklistRequest>;
|
||||
|
||||
export const CheckFileShaRequest = z.object({
|
||||
sha256_hex: createStringType(64, 64).refine((v) => /^[0-9a-fA-F]{64}$/.test(v), 'Must be a 64-character hex SHA-256'),
|
||||
});
|
||||
|
||||
export type CheckFileShaRequest = z.infer<typeof CheckFileShaRequest>;
|
||||
|
||||
const AvatarHashShortType = createStringType(8, 10).refine(
|
||||
(v) => /^(a_)?[0-9a-fA-F]{8}$/.test(v),
|
||||
'Must be an 8-character MD5 prefix (with optional "a_" prefix)',
|
||||
@@ -732,6 +747,7 @@ export const InstanceConfigUpdateRequest = z.object({
|
||||
single_community_enabled: z.boolean().optional(),
|
||||
single_community_name: z.string().trim().min(1).max(100).optional(),
|
||||
direct_messages_disabled: z.boolean().optional(),
|
||||
direct_messages_locked: z.literal(false).optional(),
|
||||
premium_mode: z.enum(['mirror', 'everyone']).optional(),
|
||||
services: z
|
||||
.object({
|
||||
@@ -794,14 +810,22 @@ export const CreateRegistrationUrlResponse = z.object({
|
||||
|
||||
export type CreateRegistrationUrlResponse = z.infer<typeof CreateRegistrationUrlResponse>;
|
||||
|
||||
export const RegistrationUrlActionRequest = z.object({
|
||||
id: createStringType(1, 128),
|
||||
export const RegistrationUrlIdParam = z.object({
|
||||
registration_url_id: createStringType(1, 128).describe('The ID of the registration URL'),
|
||||
});
|
||||
|
||||
export type RegistrationUrlActionRequest = z.infer<typeof RegistrationUrlActionRequest>;
|
||||
export type RegistrationUrlIdParam = z.infer<typeof RegistrationUrlIdParam>;
|
||||
|
||||
const PendingRegistrationStatusSchema = createNamedStringLiteralUnion(
|
||||
[
|
||||
['approved', 'approved', 'The account may log in'],
|
||||
['rejected', 'rejected', 'The account is blocked from logging in'],
|
||||
],
|
||||
'Pending registration decision',
|
||||
);
|
||||
|
||||
export const PendingRegistrationActionRequest = z.object({
|
||||
user_id: SnowflakeStringType,
|
||||
status: PendingRegistrationStatusSchema,
|
||||
});
|
||||
|
||||
export type PendingRegistrationActionRequest = z.infer<typeof PendingRegistrationActionRequest>;
|
||||
@@ -860,7 +884,7 @@ export const CreateAdminApiKeyRequest = z.object({
|
||||
.refine((value) => value.trim().length > 0, 'Name cannot be empty')
|
||||
.describe('Display name for the API key'),
|
||||
expires_in_days: z.number().int().min(1).max(365).optional().describe('Number of days until the key expires'),
|
||||
acls: z.array(z.string()).max(ADMIN_ACL_COUNT).describe('List of access control permissions for the key'),
|
||||
acls: z.array(AdminAclType).max(ADMIN_ACL_COUNT).describe('List of access control permissions for the key'),
|
||||
});
|
||||
|
||||
export type CreateAdminApiKeyRequest = z.infer<typeof CreateAdminApiKeyRequest>;
|
||||
@@ -888,6 +912,23 @@ export const ListAdminApiKeyResponse = z.object({
|
||||
|
||||
export type ListAdminApiKeyResponse = z.infer<typeof ListAdminApiKeyResponse>;
|
||||
|
||||
export const UpdateAdminApiKeyRequest = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.refine((value) => value.trim().length > 0, 'Name cannot be empty')
|
||||
.optional()
|
||||
.describe('New display name for the API key'),
|
||||
acls: z
|
||||
.array(AdminAclType)
|
||||
.max(ADMIN_ACL_COUNT)
|
||||
.optional()
|
||||
.describe('Replacement list of access control permissions for the key'),
|
||||
});
|
||||
|
||||
export type UpdateAdminApiKeyRequest = z.infer<typeof UpdateAdminApiKeyRequest>;
|
||||
|
||||
export const SearchGuildsResponse = z.object({
|
||||
guilds: z.array(GuildAdminResponse),
|
||||
total: z.number(),
|
||||
@@ -1016,7 +1057,7 @@ const AdminAuditLogChannelSummarySchema = z.object({
|
||||
type: ChannelTypeSchema,
|
||||
guild_id: SnowflakeStringType.nullable(),
|
||||
});
|
||||
const AdminAuditLogResponseSchema = z.object({
|
||||
export const AdminAuditLogResponseSchema = z.object({
|
||||
log_id: SnowflakeStringType,
|
||||
admin_user_id: SnowflakeStringType,
|
||||
admin_user: AdminAuditLogUserSummarySchema.nullable(),
|
||||
@@ -1228,8 +1269,8 @@ const AdminLookupGuildSchema = z.object({
|
||||
rules_channel_id: SnowflakeStringType.nullable(),
|
||||
disabled_operations: Int32Type,
|
||||
member_count: Int32Type,
|
||||
channels: z.array(AdminGuildChannelSummarySchema).max(500),
|
||||
roles: z.array(AdminGuildRoleSummarySchema).max(250),
|
||||
channels: z.array(AdminGuildChannelSummarySchema),
|
||||
roles: z.array(AdminGuildRoleSummarySchema),
|
||||
});
|
||||
export const LookupGuildResponse = z.object({
|
||||
guild: AdminLookupGuildSchema.nullable(),
|
||||
@@ -1250,14 +1291,14 @@ const GuildAssetItemSchema = z.object({
|
||||
|
||||
export const ListGuildEmojisResponse = z.object({
|
||||
guild_id: SnowflakeStringType,
|
||||
emojis: z.array(GuildAssetItemSchema).max(500),
|
||||
emojis: z.array(GuildAssetItemSchema),
|
||||
});
|
||||
|
||||
export type ListGuildEmojisResponse = z.infer<typeof ListGuildEmojisResponse>;
|
||||
|
||||
export const ListGuildStickersResponse = z.object({
|
||||
guild_id: SnowflakeStringType,
|
||||
stickers: z.array(GuildAssetItemSchema).max(500),
|
||||
stickers: z.array(GuildAssetItemSchema),
|
||||
});
|
||||
|
||||
export type ListGuildStickersResponse = z.infer<typeof ListGuildStickersResponse>;
|
||||
@@ -1403,7 +1444,7 @@ export const ReportAdminResponseSchema = z.object({
|
||||
message_context: z.array(ReportMessageContextSchema).optional(),
|
||||
message_responses: z.array(MessageResponseSchema).optional(),
|
||||
});
|
||||
export const ListReportsResponse = z.object({
|
||||
const ListReportsResponse = z.object({
|
||||
reports: z.array(ReportAdminResponseSchema),
|
||||
});
|
||||
export const ResolveReportResponse = z.object({
|
||||
@@ -1412,12 +1453,13 @@ export const ResolveReportResponse = z.object({
|
||||
resolved_at: z.string().nullable(),
|
||||
public_comment: z.string().nullable(),
|
||||
});
|
||||
export const SearchReportsResponse = z.object({
|
||||
const SearchReportsResponse = z.object({
|
||||
reports: z.array(ReportAdminResponseSchema),
|
||||
total: z.number(),
|
||||
offset: z.number(),
|
||||
limit: z.number(),
|
||||
});
|
||||
export const AdminReportListResponse = z.union([SearchReportsResponse, ListReportsResponse]);
|
||||
const LimitKeyMetadataSchema = z.object({
|
||||
key: z.string(),
|
||||
label: z.string(),
|
||||
|
||||
@@ -11,6 +11,11 @@ describe('SetUserAclsRequest', () => {
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
test('rejects an ACL outside the registry', () => {
|
||||
const result = SetUserAclsRequest.safeParse({user_id: '1', acls: ['user:veiw']});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
test('rejects more entries than there are ACLs', () => {
|
||||
const acls = [...Object.values(AdminACLs), 'overflow:one'];
|
||||
const result = SetUserAclsRequest.safeParse({user_id: '1', acls});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
||||
import {DeletionReasons} from '@fluxer/constants/src/Core';
|
||||
import {
|
||||
PremiumFlags,
|
||||
PremiumFlagsDescriptions,
|
||||
@@ -9,21 +9,23 @@ import {
|
||||
UserFlags,
|
||||
UserFlagsDescriptions,
|
||||
} from '@fluxer/constants/src/UserConstants';
|
||||
import {ADMIN_ACL_COUNT, AdminAclType} from '@fluxer/schema/src/domains/admin/AdminAclType';
|
||||
import {NSFWLevelSchema} from '@fluxer/schema/src/primitives/GuildValidators';
|
||||
import {createQueryIntegerType, QueryBooleanType} from '@fluxer/schema/src/primitives/QueryValidators';
|
||||
import {
|
||||
createBitflagInt32Type,
|
||||
createBitflagStringType,
|
||||
createInt32EnumType,
|
||||
createNamedStringLiteralUnion,
|
||||
createStringType,
|
||||
Int32Type,
|
||||
SnowflakeStringType,
|
||||
SnowflakeType,
|
||||
withFieldDescription,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {DiscriminatorType, EmailType, UsernameType} from '@fluxer/schema/src/primitives/UserValidators';
|
||||
import {z} from 'zod';
|
||||
|
||||
const ADMIN_ACL_COUNT = Object.keys(AdminACLs).length;
|
||||
|
||||
export const UserAdminResponseSchema = z.object({
|
||||
id: SnowflakeStringType,
|
||||
username: z.string(),
|
||||
@@ -92,22 +94,6 @@ export const LookupUserRequest = z.union([LookupUserByQueryRequest, LookupUserBy
|
||||
|
||||
export type LookupUserRequest = z.infer<typeof LookupUserRequest>;
|
||||
|
||||
export const SearchUsersRequest = z.object({
|
||||
query: createStringType(1, 1024).optional(),
|
||||
email: createStringType(1, 320).optional(),
|
||||
last_active_ip: createStringType(1, 64).optional(),
|
||||
limit: z.number().int().min(1).max(200).default(50),
|
||||
offset: z.number().int().min(0).default(0),
|
||||
});
|
||||
|
||||
export type SearchUsersRequest = z.infer<typeof SearchUsersRequest>;
|
||||
|
||||
export const ListUserSessionsRequest = z.object({
|
||||
user_id: SnowflakeType,
|
||||
});
|
||||
|
||||
export type ListUserSessionsRequest = z.infer<typeof ListUserSessionsRequest>;
|
||||
|
||||
const UserContactChangeLogEntrySchema = z.object({
|
||||
event_id: z.string(),
|
||||
field: z.string(),
|
||||
@@ -194,7 +180,7 @@ const AdminUserDmChannelSchema = z.object({
|
||||
owner_id: SnowflakeStringType.nullable(),
|
||||
});
|
||||
|
||||
export const ListUserDmChannelsResponse = z.object({
|
||||
const ListUserDmChannelsResponse = z.object({
|
||||
channels: z.array(AdminUserDmChannelSchema).max(200),
|
||||
});
|
||||
export const ListUserGroupDmChannelsRequest = z.object({
|
||||
@@ -203,13 +189,10 @@ export const ListUserGroupDmChannelsRequest = z.object({
|
||||
|
||||
export type ListUserGroupDmChannelsRequest = z.infer<typeof ListUserGroupDmChannelsRequest>;
|
||||
|
||||
export const ListUserGroupDmChannelsResponse = z.object({
|
||||
const ListUserGroupDmChannelsResponse = z.object({
|
||||
channels: z.array(AdminUserDmChannelSchema).max(500),
|
||||
});
|
||||
|
||||
export type ListUserGroupDmChannelsResponse = z.infer<typeof ListUserGroupDmChannelsResponse>;
|
||||
export type ListUserDmChannelsResponse = z.infer<typeof ListUserDmChannelsResponse>;
|
||||
|
||||
export const TerminateSessionsResponse = z.object({
|
||||
terminated_count: Int32Type,
|
||||
});
|
||||
@@ -228,22 +211,18 @@ const PremiumFlagValueType = createBitflagInt32Type(
|
||||
'A single premium flag value to add or remove',
|
||||
'PremiumFlags',
|
||||
);
|
||||
export const UpdateUserFlagsRequest = z.object({
|
||||
const UpdateUserFlagsRequest = z.object({
|
||||
user_id: SnowflakeType.describe('ID of the user to update'),
|
||||
add_flags: z.array(UserFlagValueType).max(64).default([]).describe('User flags to add'),
|
||||
remove_flags: z.array(UserFlagValueType).max(64).default([]).describe('User flags to remove'),
|
||||
});
|
||||
|
||||
export type UpdateUserFlagsRequest = z.infer<typeof UpdateUserFlagsRequest>;
|
||||
|
||||
export const UpdatePremiumFlagsRequest = z.object({
|
||||
const UpdatePremiumFlagsRequest = z.object({
|
||||
user_id: SnowflakeType.describe('ID of the user to update'),
|
||||
add_flags: z.array(PremiumFlagValueType).max(64).default([]).describe('Premium flags to add'),
|
||||
remove_flags: z.array(PremiumFlagValueType).max(64).default([]).describe('Premium flags to remove'),
|
||||
});
|
||||
|
||||
export type UpdatePremiumFlagsRequest = z.infer<typeof UpdatePremiumFlagsRequest>;
|
||||
|
||||
export const DisableMfaRequest = z.object({
|
||||
user_id: SnowflakeType.describe('ID of the user to disable MFA for'),
|
||||
});
|
||||
@@ -339,9 +318,15 @@ export const TempBanUserRequest = z.object({
|
||||
|
||||
export type TempBanUserRequest = z.infer<typeof TempBanUserRequest>;
|
||||
|
||||
const DeletionReasonCodeType = createInt32EnumType(
|
||||
Object.entries(DeletionReasons).map(([name, value]) => [value, name] as const),
|
||||
'Reason the account was scheduled for deletion',
|
||||
'DeletionReasonCode',
|
||||
);
|
||||
|
||||
export const ScheduleAccountDeletionRequest = z.object({
|
||||
user_id: SnowflakeType.describe('ID of the user to schedule deletion for'),
|
||||
reason_code: Int32Type.describe('Code indicating the reason for deletion'),
|
||||
reason_code: withFieldDescription(DeletionReasonCodeType, 'Code indicating the reason for deletion'),
|
||||
public_reason: createStringType(0, 512).optional().describe('Public-facing reason for the deletion'),
|
||||
days_until_deletion: z
|
||||
.number()
|
||||
@@ -356,7 +341,7 @@ export type ScheduleAccountDeletionRequest = z.infer<typeof ScheduleAccountDelet
|
||||
|
||||
export const SetUserAclsRequest = z.object({
|
||||
user_id: SnowflakeType.describe('ID of the user to set ACLs for'),
|
||||
acls: z.array(createStringType(1, 64)).max(ADMIN_ACL_COUNT).describe('List of access control permissions to assign'),
|
||||
acls: z.array(AdminAclType).max(ADMIN_ACL_COUNT).describe('List of access control permissions to assign'),
|
||||
});
|
||||
|
||||
export type SetUserAclsRequest = z.infer<typeof SetUserAclsRequest>;
|
||||
@@ -438,7 +423,7 @@ export type BulkUpdateUserFlagsRequest = z.infer<typeof BulkUpdateUserFlagsReque
|
||||
|
||||
export const BulkScheduleUserDeletionRequest = z.object({
|
||||
user_ids: z.array(SnowflakeType).max(1000).describe('List of user IDs to schedule deletion for'),
|
||||
reason_code: Int32Type.describe('Code indicating the reason for deletion'),
|
||||
reason_code: withFieldDescription(DeletionReasonCodeType, 'Code indicating the reason for deletion'),
|
||||
public_reason: createStringType(0, 512).optional().describe('Public-facing reason for the deletion'),
|
||||
days_until_deletion: z
|
||||
.number()
|
||||
@@ -493,10 +478,10 @@ export const AdminRelationshipEntrySchema = z.object({
|
||||
export type AdminRelationshipEntry = z.infer<typeof AdminRelationshipEntrySchema>;
|
||||
|
||||
export const ListUserRelationshipsResponse = z.object({
|
||||
friends: z.array(AdminRelationshipEntrySchema).max(10000),
|
||||
incoming_requests: z.array(AdminRelationshipEntrySchema).max(10000),
|
||||
outgoing_requests: z.array(AdminRelationshipEntrySchema).max(10000),
|
||||
blocked: z.array(AdminRelationshipEntrySchema).max(10000),
|
||||
friends: z.array(AdminRelationshipEntrySchema),
|
||||
incoming_requests: z.array(AdminRelationshipEntrySchema),
|
||||
outgoing_requests: z.array(AdminRelationshipEntrySchema),
|
||||
blocked: z.array(AdminRelationshipEntrySchema),
|
||||
});
|
||||
|
||||
export type ListUserRelationshipsResponse = z.infer<typeof ListUserRelationshipsResponse>;
|
||||
@@ -523,3 +508,166 @@ export const RemoveUserRelationshipsResponse = z.object({
|
||||
});
|
||||
|
||||
export type RemoveUserRelationshipsResponse = z.infer<typeof RemoveUserRelationshipsResponse>;
|
||||
|
||||
export const AdminAclListResponse = z.object({
|
||||
acls: z
|
||||
.array(createStringType(1, 64))
|
||||
.max(ADMIN_ACL_COUNT)
|
||||
.describe('Every admin access control permission the admin API recognises'),
|
||||
});
|
||||
|
||||
export type AdminAclListResponse = z.infer<typeof AdminAclListResponse>;
|
||||
|
||||
export const AdminUserListQuery = z.object({
|
||||
q: createStringType(1, 1024).optional().describe('Restrict the results to the users matching this free-text query'),
|
||||
user_id: z
|
||||
.union([SnowflakeStringType, z.array(SnowflakeStringType).max(100)])
|
||||
.optional()
|
||||
.describe('Restrict the results to these users. Repeat the parameter to pass more than one.'),
|
||||
resolve: createStringType(1, 1024)
|
||||
.optional()
|
||||
.describe(
|
||||
'Resolve one exact identifier: a username#discriminator tag, a user ID, an email address, or a Stripe subscription ID',
|
||||
),
|
||||
email: createStringType(1, 320).optional().describe('Restrict the results to the user with this exact email address'),
|
||||
last_active_ip: createStringType(1, 64)
|
||||
.optional()
|
||||
.describe('Restrict the results to the users whose last active IP address matches this one exactly'),
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum number of users to return',
|
||||
),
|
||||
offset: createQueryIntegerType({defaultValue: 0, minValue: 0, maxValue: 100000}).describe(
|
||||
'Number of users to skip before returning results',
|
||||
),
|
||||
});
|
||||
|
||||
export type AdminUserListQuery = z.infer<typeof AdminUserListQuery>;
|
||||
|
||||
export const AdminUserGuildListQuery = z.object({
|
||||
before: SnowflakeType.optional().describe('Return guilds with IDs lower than this guild ID'),
|
||||
after: SnowflakeType.optional().describe('Return guilds with IDs higher than this guild ID'),
|
||||
limit: createQueryIntegerType({defaultValue: 200, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum number of guilds to return',
|
||||
),
|
||||
with_counts: QueryBooleanType.describe('Whether to resolve live member and presence counts from the gateway'),
|
||||
});
|
||||
|
||||
export type AdminUserGuildListQuery = z.infer<typeof AdminUserGuildListQuery>;
|
||||
|
||||
export const AdminUserDmChannelType = createNamedStringLiteralUnion(
|
||||
[
|
||||
['dm', 'DM', 'One-to-one direct message channels'],
|
||||
['group_dm', 'GROUP_DM', 'Group direct message channels'],
|
||||
],
|
||||
'Kind of direct message channel to list',
|
||||
);
|
||||
|
||||
export const AdminUserDmChannelListQuery = z
|
||||
.object({
|
||||
type: AdminUserDmChannelType.optional()
|
||||
.default('dm')
|
||||
.describe('The kind of direct message channel to list. Defaults to the one-to-one direct message channels.'),
|
||||
before: SnowflakeType.optional().describe('Return channels with IDs lower than this channel ID'),
|
||||
after: SnowflakeType.optional().describe('Return channels with IDs higher than this channel ID'),
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum number of DM channels to return',
|
||||
),
|
||||
})
|
||||
.refine((value) => value.before === undefined || value.after === undefined, {
|
||||
message: 'before and after cannot both be provided',
|
||||
});
|
||||
|
||||
export type AdminUserDmChannelListQuery = z.infer<typeof AdminUserDmChannelListQuery>;
|
||||
|
||||
export const AdminUserChangeLogQuery = z.object({
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum number of entries to return',
|
||||
),
|
||||
page_token: createStringType(1, 64).optional().describe('Pagination token for the next page of results'),
|
||||
});
|
||||
|
||||
export type AdminUserChangeLogQuery = z.infer<typeof AdminUserChangeLogQuery>;
|
||||
|
||||
export const AdminUserRelationshipCategoryQuery = z.object({
|
||||
category: RelationshipCategoryEnum.describe('Category of relationships the operation applies to'),
|
||||
});
|
||||
|
||||
export type AdminUserRelationshipCategoryQuery = z.infer<typeof AdminUserRelationshipCategoryQuery>;
|
||||
|
||||
export const AdminUserRelationshipParam = z.object({
|
||||
user_id: SnowflakeType.describe('The ID of the user'),
|
||||
target_user_id: SnowflakeType.describe('The ID of the target user'),
|
||||
});
|
||||
|
||||
export type AdminUserRelationshipParam = z.infer<typeof AdminUserRelationshipParam>;
|
||||
|
||||
export const AdminUserWebAuthnCredentialParam = z.object({
|
||||
user_id: SnowflakeType.describe('The ID of the user'),
|
||||
credential_id: createStringType(1, 512).describe('The ID of the WebAuthn credential'),
|
||||
});
|
||||
|
||||
export type AdminUserWebAuthnCredentialParam = z.infer<typeof AdminUserWebAuthnCredentialParam>;
|
||||
|
||||
export const AdminUserClearFieldsRequest = ClearUserFieldsRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserClearFieldsRequest = z.infer<typeof AdminUserClearFieldsRequest>;
|
||||
|
||||
export const AdminUserBotStatusRequest = SetUserBotStatusRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserBotStatusRequest = z.infer<typeof AdminUserBotStatusRequest>;
|
||||
|
||||
export const AdminUserSystemStatusRequest = SetUserSystemStatusRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserSystemStatusRequest = z.infer<typeof AdminUserSystemStatusRequest>;
|
||||
|
||||
export const AdminUserUsernameUpdateRequest = ChangeUsernameRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserUsernameUpdateRequest = z.infer<typeof AdminUserUsernameUpdateRequest>;
|
||||
|
||||
export const AdminUserEmailUpdateRequest = ChangeEmailRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserEmailUpdateRequest = z.infer<typeof AdminUserEmailUpdateRequest>;
|
||||
|
||||
export const AdminUserBanRequest = TempBanUserRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserBanRequest = z.infer<typeof AdminUserBanRequest>;
|
||||
|
||||
export const AdminUserDeletionScheduleRequest = ScheduleAccountDeletionRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserDeletionScheduleRequest = z.infer<typeof AdminUserDeletionScheduleRequest>;
|
||||
|
||||
export const AdminUserAclsRequest = SetUserAclsRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserAclsRequest = z.infer<typeof AdminUserAclsRequest>;
|
||||
|
||||
export const AdminUserTraitsRequest = SetUserTraitsRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserTraitsRequest = z.infer<typeof AdminUserTraitsRequest>;
|
||||
|
||||
export const AdminUserFlagsUpdateRequest = UpdateUserFlagsRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserFlagsUpdateRequest = z.infer<typeof AdminUserFlagsUpdateRequest>;
|
||||
|
||||
export const AdminUserPremiumFlagsUpdateRequest = UpdatePremiumFlagsRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserPremiumFlagsUpdateRequest = z.infer<typeof AdminUserPremiumFlagsUpdateRequest>;
|
||||
|
||||
export const AdminUserPhoneVerificationRequest = UpdateHasVerifiedPhoneRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserPhoneVerificationRequest = z.infer<typeof AdminUserPhoneVerificationRequest>;
|
||||
|
||||
export const AdminUserDobUpdateRequest = ChangeDobRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserDobUpdateRequest = z.infer<typeof AdminUserDobUpdateRequest>;
|
||||
|
||||
export const AdminUserSuspiciousActivityFlagsRequest = UpdateSuspiciousActivityFlagsRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserSuspiciousActivityFlagsRequest = z.infer<typeof AdminUserSuspiciousActivityFlagsRequest>;
|
||||
|
||||
export const AdminUserSuspiciousDisableRequest = DisableForSuspiciousActivityRequest.omit({user_id: true});
|
||||
|
||||
export type AdminUserSuspiciousDisableRequest = z.infer<typeof AdminUserSuspiciousDisableRequest>;
|
||||
|
||||
export const AdminUserDmChannelListResponse = z.union([ListUserDmChannelsResponse, ListUserGroupDmChannelsResponse]);
|
||||
|
||||
export type AdminUserDmChannelListResponse = z.infer<typeof AdminUserDmChannelListResponse>;
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {QueryBooleanType} from '@fluxer/schema/src/primitives/QueryValidators';
|
||||
import {createStringType, SnowflakeStringType, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
@@ -278,3 +279,32 @@ export const DeleteVoiceResponse = z.object({
|
||||
});
|
||||
|
||||
export type DeleteVoiceResponse = z.infer<typeof DeleteVoiceResponse>;
|
||||
|
||||
export const VoiceRegionIdParam = z.object({
|
||||
region_id: createStringType(1, 64).describe('ID of the voice region'),
|
||||
});
|
||||
|
||||
export type VoiceRegionIdParam = z.infer<typeof VoiceRegionIdParam>;
|
||||
|
||||
export const VoiceServerIdParam = z.object({
|
||||
region_id: createStringType(1, 64).describe('ID of the region the server belongs to'),
|
||||
server_id: createStringType(1, 64).describe('ID of the voice server'),
|
||||
});
|
||||
|
||||
export type VoiceServerIdParam = z.infer<typeof VoiceServerIdParam>;
|
||||
|
||||
export const ListVoiceRegionsQuery = z.object({
|
||||
include_servers: QueryBooleanType.optional()
|
||||
.default(false)
|
||||
.describe('Whether to include voice servers in the response'),
|
||||
});
|
||||
|
||||
export type ListVoiceRegionsQuery = z.infer<typeof ListVoiceRegionsQuery>;
|
||||
|
||||
export const GetVoiceRegionQuery = z.object({
|
||||
include_servers: QueryBooleanType.optional()
|
||||
.default(true)
|
||||
.describe('Whether to include voice servers in the response'),
|
||||
});
|
||||
|
||||
export type GetVoiceRegionQuery = z.infer<typeof GetVoiceRegionQuery>;
|
||||
@@ -1,9 +1,11 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {SnowflakeStringType, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {createQueryIntegerType} from '@fluxer/schema/src/primitives/QueryValidators';
|
||||
import {createStringType, SnowflakeStringType, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
const JobStatusEnum = z.enum(['queued', 'running', 'succeeded', 'failed', 'cancelled', 'deadletter']);
|
||||
const JobStatusEnum = z.enum(['queued', 'running', 'succeeded', 'cancelled', 'deadletter']);
|
||||
|
||||
export const JobLedgerEntrySchema = z.object({
|
||||
job_id: SnowflakeStringType,
|
||||
@@ -48,27 +50,60 @@ export const ListJobsRequest = z.object({
|
||||
|
||||
export type ListJobsRequest = z.infer<typeof ListJobsRequest>;
|
||||
|
||||
const CURSOR_FIELD_NAMES = ['cursor_bucket_day', 'cursor_created_at', 'cursor_job_id'] as const;
|
||||
|
||||
const BUCKET_DAY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const ISO_TIMESTAMP_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/;
|
||||
|
||||
function isCalendarDay(value: string): boolean {
|
||||
return BUCKET_DAY_REGEX.test(value) && !Number.isNaN(new Date(`${value}T00:00:00Z`).getTime());
|
||||
}
|
||||
|
||||
function isIsoTimestamp(value: string): boolean {
|
||||
return ISO_TIMESTAMP_REGEX.test(value) && !Number.isNaN(new Date(value).getTime());
|
||||
}
|
||||
|
||||
export const ListJobsQuery = z
|
||||
.object({
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum number of jobs to return (1-200, default 50)',
|
||||
),
|
||||
cursor_bucket_day: createStringType(10, 10)
|
||||
.refine(isCalendarDay, ValidationErrorCodes.INVALID_FORMAT)
|
||||
.optional()
|
||||
.describe('Day bucket to resume from as a YYYY-MM-DD UTC date, taken from next_cursor.bucket_day'),
|
||||
cursor_created_at: createStringType(1, 64)
|
||||
.refine(isIsoTimestamp, ValidationErrorCodes.INVALID_ISO_TIMESTAMP)
|
||||
.optional()
|
||||
.describe('Creation time to resume before as an ISO 8601 timestamp, taken from next_cursor.created_at'),
|
||||
cursor_job_id: SnowflakeStringType.optional().describe('Job to resume from, taken from next_cursor.job_id'),
|
||||
max_lookback_days: createQueryIntegerType({defaultValue: 14, minValue: 1, maxValue: 60}).describe(
|
||||
'How many day buckets to scan back through (1-60, default 14)',
|
||||
),
|
||||
status: JobStatusEnum.optional().describe('Filter by job status'),
|
||||
task_type: createStringType(1, 128).optional().describe('Filter by task type'),
|
||||
requested_by_user_id: SnowflakeType.optional().describe('Filter by admin user who scheduled the job'),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
const supplied = CURSOR_FIELD_NAMES.filter((name) => value[name] !== undefined);
|
||||
if (supplied.length === 0 || supplied.length === CURSOR_FIELD_NAMES.length) return;
|
||||
for (const name of CURSOR_FIELD_NAMES) {
|
||||
if (value[name] !== undefined) continue;
|
||||
ctx.addIssue({code: 'custom', message: ValidationErrorCodes.INVALID_FORMAT, path: [name]});
|
||||
}
|
||||
});
|
||||
|
||||
export type ListJobsQuery = z.infer<typeof ListJobsQuery>;
|
||||
|
||||
export const ListJobsResponseSchema = z.object({
|
||||
jobs: z.array(JobLedgerEntrySchema),
|
||||
next_cursor: ListJobsCursorSchema.nullable(),
|
||||
});
|
||||
|
||||
export const GetJobRequest = z.object({
|
||||
job_id: SnowflakeType,
|
||||
});
|
||||
|
||||
export type GetJobRequest = z.infer<typeof GetJobRequest>;
|
||||
|
||||
export const GetJobResponseSchema = z.object({
|
||||
job: JobLedgerEntrySchema,
|
||||
});
|
||||
|
||||
export const CancelJobRequest = z.object({
|
||||
job_id: SnowflakeType,
|
||||
});
|
||||
|
||||
export type CancelJobRequest = z.infer<typeof CancelJobRequest>;
|
||||
|
||||
export const CancelJobResponseSchema = z.object({
|
||||
cancelled: z.boolean().describe('True if a cancel request was recorded; false if the job was already terminal.'),
|
||||
});
|
||||
|
||||
@@ -191,7 +191,7 @@ export const ReportIdParam = z.object({
|
||||
export type ReportIdParam = z.infer<typeof ReportIdParam>;
|
||||
|
||||
export const KeyIdParam = z.object({
|
||||
keyId: createStringType(1, 64).describe('The ID of the key'),
|
||||
key_id: SnowflakeType.describe('The ID of the key'),
|
||||
});
|
||||
|
||||
export type KeyIdParam = z.infer<typeof KeyIdParam>;
|
||||
@@ -219,13 +219,19 @@ const ArchiveSubjectTypeEnum = z
|
||||
.describe('Type of entity being archived: user for user data archives, guild for guild data archives');
|
||||
|
||||
export const ArchivePathParam = z.object({
|
||||
subjectType: ArchiveSubjectTypeEnum.describe('The type of subject (user or guild)'),
|
||||
subjectId: SnowflakeType.describe('The ID of the subject'),
|
||||
archiveId: SnowflakeType.describe('The ID of the archive'),
|
||||
subject_type: ArchiveSubjectTypeEnum.describe('The type of subject (user or guild)'),
|
||||
subject_id: SnowflakeType.describe('The ID of the subject'),
|
||||
archive_id: SnowflakeType.describe('The ID of the archive'),
|
||||
});
|
||||
|
||||
export type ArchivePathParam = z.infer<typeof ArchivePathParam>;
|
||||
|
||||
export const JobIdParam = z.object({
|
||||
job_id: SnowflakeType.describe('The ID of the job'),
|
||||
});
|
||||
|
||||
export type JobIdParam = z.infer<typeof JobIdParam>;
|
||||
|
||||
export const HarvestIdParam = z.object({
|
||||
harvestId: SnowflakeType.describe('The ID of the harvest request'),
|
||||
});
|
||||
|
||||
@@ -6,14 +6,17 @@ import {
|
||||
DISCOVERY_MAX_TAGS,
|
||||
DISCOVERY_TAG_MAX_LENGTH,
|
||||
DISCOVERY_TAG_MIN_LENGTH,
|
||||
DiscoveryApplicationStatus,
|
||||
DiscoveryCategories,
|
||||
isValidDiscoveryLanguage,
|
||||
isValidDiscoveryTag,
|
||||
normalizeDiscoveryTag,
|
||||
} from '@fluxer/constants/src/DiscoveryConstants';
|
||||
import {NSFWLevelSchema} from '@fluxer/schema/src/primitives/GuildValidators';
|
||||
import {SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {SnowflakeStringType, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
const DISCOVERY_CATEGORY_MAX = Math.max(...Object.values(DiscoveryCategories));
|
||||
const DiscoveryTagSchema = z
|
||||
.string()
|
||||
.min(DISCOVERY_TAG_MIN_LENGTH)
|
||||
@@ -39,7 +42,7 @@ export const DiscoveryApplicationRequest = z.object({
|
||||
.min(DISCOVERY_DESCRIPTION_MIN_LENGTH)
|
||||
.max(DISCOVERY_DESCRIPTION_MAX_LENGTH)
|
||||
.describe('Description for discovery listing'),
|
||||
category_type: z.number().int().min(0).max(8).describe('Discovery category type'),
|
||||
category_type: z.number().int().min(0).max(DISCOVERY_CATEGORY_MAX).describe('Discovery category type'),
|
||||
primary_language: DiscoveryLanguageSchema.optional(),
|
||||
custom_tags: DiscoveryTagsSchema.optional(),
|
||||
});
|
||||
@@ -53,7 +56,13 @@ export const DiscoveryApplicationPatchRequest = z.object({
|
||||
.max(DISCOVERY_DESCRIPTION_MAX_LENGTH)
|
||||
.optional()
|
||||
.describe('Updated description for discovery listing'),
|
||||
category_type: z.number().int().min(0).max(8).optional().describe('Updated discovery category type'),
|
||||
category_type: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(DISCOVERY_CATEGORY_MAX)
|
||||
.optional()
|
||||
.describe('Updated discovery category type'),
|
||||
primary_language: DiscoveryLanguageSchema.optional(),
|
||||
custom_tags: DiscoveryTagsSchema.optional(),
|
||||
});
|
||||
@@ -62,7 +71,7 @@ export type DiscoveryApplicationPatchRequest = z.infer<typeof DiscoveryApplicati
|
||||
|
||||
export const DiscoverySearchQuery = z.object({
|
||||
query: z.string().max(100).optional().describe('Search query'),
|
||||
category: z.coerce.number().int().min(0).max(8).optional().describe('Filter by category'),
|
||||
category: z.coerce.number().int().min(0).max(DISCOVERY_CATEGORY_MAX).optional().describe('Filter by category'),
|
||||
language: z
|
||||
.string()
|
||||
.refine((value) => isValidDiscoveryLanguage(value), {message: 'Unsupported language code'})
|
||||
@@ -183,20 +192,59 @@ export const DiscoveryCategoryListResponse = z.array(DiscoveryCategoryResponse);
|
||||
|
||||
export type DiscoveryCategoryListResponse = z.infer<typeof DiscoveryCategoryListResponse>;
|
||||
|
||||
export const DiscoveryAdminReviewRequest = z.object({
|
||||
const DiscoveryAdminReviewRequest = z.object({
|
||||
reason: z.string().max(500).optional().describe('Review reason'),
|
||||
});
|
||||
|
||||
export type DiscoveryAdminReviewRequest = z.infer<typeof DiscoveryAdminReviewRequest>;
|
||||
|
||||
export const DiscoveryAdminRejectRequest = z.object({
|
||||
const DiscoveryAdminRejectRequest = z.object({
|
||||
reason: z.string().min(1).max(500).describe('Rejection reason'),
|
||||
});
|
||||
|
||||
export type DiscoveryAdminRejectRequest = z.infer<typeof DiscoveryAdminRejectRequest>;
|
||||
|
||||
export const DiscoveryAdminRemoveRequest = z.object({
|
||||
reason: z.string().min(1).max(500).describe('Removal reason'),
|
||||
});
|
||||
|
||||
export type DiscoveryAdminRemoveRequest = z.infer<typeof DiscoveryAdminRemoveRequest>;
|
||||
|
||||
export const DiscoveryAdminApplicationUpdateRequest = z.discriminatedUnion('status', [
|
||||
DiscoveryAdminReviewRequest.extend({
|
||||
status: z.literal(DiscoveryApplicationStatus.APPROVED).describe('Approve the pending application'),
|
||||
}),
|
||||
DiscoveryAdminRejectRequest.extend({
|
||||
status: z.literal(DiscoveryApplicationStatus.REJECTED).describe('Reject the pending application'),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type DiscoveryAdminApplicationUpdateRequest = z.infer<typeof DiscoveryAdminApplicationUpdateRequest>;
|
||||
|
||||
export const DiscoveryCategoryIdParam = z.object({
|
||||
category_id: z.coerce.number().int().min(0).max(DISCOVERY_CATEGORY_MAX).describe('The ID of the discovery category'),
|
||||
});
|
||||
|
||||
export type DiscoveryCategoryIdParam = z.infer<typeof DiscoveryCategoryIdParam>;
|
||||
|
||||
export const DiscoveryAdminCategoryListingQuery = z.object({
|
||||
limit: z.coerce.number().int().min(1).max(100).optional().default(50).describe('Number of listings to return'),
|
||||
offset: z.coerce.number().int().min(0).max(10000).optional().default(0).describe('Pagination offset'),
|
||||
});
|
||||
|
||||
export type DiscoveryAdminCategoryListingQuery = z.infer<typeof DiscoveryAdminCategoryListingQuery>;
|
||||
|
||||
export const DiscoveryAdminListingBulkCategoryRequest = z.object({
|
||||
guild_ids: z.array(SnowflakeType).min(1).max(100).describe('Listed guilds to move'),
|
||||
category_type: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(DISCOVERY_CATEGORY_MAX)
|
||||
.describe('Discovery category to file every named guild under'),
|
||||
});
|
||||
|
||||
export type DiscoveryAdminListingBulkCategoryRequest = z.infer<typeof DiscoveryAdminListingBulkCategoryRequest>;
|
||||
|
||||
export const DiscoveryAdminListingBulkCategoryResponse = z.object({
|
||||
updated: z.number().describe('Number of listings moved'),
|
||||
failed_guild_ids: z.array(SnowflakeStringType).describe('Guilds that could not be moved'),
|
||||
});
|
||||
|
||||
export type DiscoveryAdminListingBulkCategoryResponse = z.infer<typeof DiscoveryAdminListingBulkCategoryResponse>;
|
||||
@@ -0,0 +1,122 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {ListGuildEmojisResponse, LookupGuildResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import {ListUserRelationshipsResponse} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
function snowflakeAt(index: number): string {
|
||||
return String(100000000000000000n + BigInt(index));
|
||||
}
|
||||
|
||||
describe('LookupGuildResponse', () => {
|
||||
const validGuild = {
|
||||
id: '123456789012345678',
|
||||
owner_id: '111111111111111111',
|
||||
owner_username: 'owner',
|
||||
owner_global_name: null,
|
||||
owner_discriminator: '0001',
|
||||
name: 'Test Guild',
|
||||
vanity_url_code: null,
|
||||
icon: null,
|
||||
banner: null,
|
||||
splash: null,
|
||||
embed_splash: null,
|
||||
features: [],
|
||||
verification_level: 0,
|
||||
mfa_level: 0,
|
||||
nsfw_level: 0,
|
||||
explicit_content_filter: 0,
|
||||
default_message_notifications: 0,
|
||||
afk_channel_id: null,
|
||||
afk_timeout: 300,
|
||||
system_channel_id: null,
|
||||
system_channel_flags: 0,
|
||||
rules_channel_id: null,
|
||||
disabled_operations: 0,
|
||||
member_count: 1,
|
||||
channels: [],
|
||||
roles: [],
|
||||
};
|
||||
|
||||
it('accepts more roles than the old response ceiling', () => {
|
||||
const result = LookupGuildResponse.safeParse({
|
||||
guild: {
|
||||
...validGuild,
|
||||
roles: Array.from({length: 251}, (_, index) => ({
|
||||
id: snowflakeAt(index),
|
||||
name: `role-${index}`,
|
||||
color: 0,
|
||||
position: index,
|
||||
permissions: '0',
|
||||
hoist: false,
|
||||
mentionable: false,
|
||||
})),
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.guild?.roles).toHaveLength(251);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts more channels than the old response ceiling', () => {
|
||||
const result = LookupGuildResponse.safeParse({
|
||||
guild: {
|
||||
...validGuild,
|
||||
channels: Array.from({length: 501}, (_, index) => ({
|
||||
id: snowflakeAt(index),
|
||||
name: `channel-${index}`,
|
||||
type: 0,
|
||||
position: index,
|
||||
parent_id: null,
|
||||
nsfw: false,
|
||||
url: null,
|
||||
})),
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.guild?.channels).toHaveLength(501);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ListGuildEmojisResponse', () => {
|
||||
it('accepts more emojis than the old response ceiling', () => {
|
||||
const result = ListGuildEmojisResponse.safeParse({
|
||||
guild_id: '123456789012345678',
|
||||
emojis: Array.from({length: 501}, (_, index) => ({
|
||||
id: snowflakeAt(index),
|
||||
name: `emoji_${index}`,
|
||||
animated: false,
|
||||
creator_id: '111111111111111111',
|
||||
media_url: 'https://example.com/emoji.png',
|
||||
})),
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.emojis).toHaveLength(501);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ListUserRelationshipsResponse', () => {
|
||||
it('accepts more friends than the old response ceiling', () => {
|
||||
const result = ListUserRelationshipsResponse.safeParse({
|
||||
friends: Array.from({length: 10001}, (_, index) => ({
|
||||
target_user_id: snowflakeAt(index),
|
||||
category: 'friend',
|
||||
nickname: null,
|
||||
since: null,
|
||||
target: null,
|
||||
})),
|
||||
incoming_requests: [],
|
||||
outgoing_requests: [],
|
||||
blocked: [],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.friends).toHaveLength(10001);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {describe, expect, test} from 'vitest';
|
||||
import {JobLedgerEntrySchema, ListJobsQuery, ListJobsRequest} from '../admin/JobsSchemas';
|
||||
|
||||
describe('jobs schemas', () => {
|
||||
test('the job status enum rejects failed, which the ledger never writes', () => {
|
||||
expect(ListJobsQuery.safeParse({status: 'failed'}).success).toBe(false);
|
||||
expect(ListJobsRequest.safeParse({status: 'failed'}).success).toBe(false);
|
||||
expect(JobLedgerEntrySchema.shape.status.safeParse('failed').success).toBe(false);
|
||||
});
|
||||
|
||||
test('the job status enum accepts every status the ledger writes', () => {
|
||||
for (const status of ['queued', 'running', 'succeeded', 'cancelled', 'deadletter']) {
|
||||
expect(ListJobsQuery.safeParse({status}).success).toBe(true);
|
||||
expect(JobLedgerEntrySchema.shape.status.safeParse(status).success).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user