mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat: add announcement channels, publishing and following (#3090)
This commit is contained in:
@@ -51,6 +51,7 @@ message SyncedPreferences {
|
||||
optional bool save_camera_uploads_to_device = 113;
|
||||
ReactionEmoji double_tap_reaction = 114;
|
||||
ChannelFrecencyState channel_frecency = 115;
|
||||
AnnouncementPromptsState announcement_prompts = 116;
|
||||
|
||||
}
|
||||
|
||||
@@ -222,6 +223,10 @@ message VoicePromptsState {
|
||||
bool skip_hide_own_screenshare_confirm = 2;
|
||||
}
|
||||
|
||||
message AnnouncementPromptsState {
|
||||
bool hide_publish_nudge = 1;
|
||||
}
|
||||
|
||||
message SudoPromptState {
|
||||
optional MfaMethod last_used_mfa_method = 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Int32Type, SnowflakeStringType, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const ChannelFollowRequest = z.object({
|
||||
webhook_channel_id: SnowflakeType.describe('The ID of the text channel that receives published messages'),
|
||||
});
|
||||
|
||||
export type ChannelFollowRequest = z.infer<typeof ChannelFollowRequest>;
|
||||
|
||||
export const FollowedChannelResponse = z.object({
|
||||
channel_id: SnowflakeStringType.describe('The ID of the followed announcement channel'),
|
||||
webhook_id: SnowflakeStringType.describe('The ID of the channel follower webhook created in the target channel'),
|
||||
});
|
||||
|
||||
export type FollowedChannelResponse = z.infer<typeof FollowedChannelResponse>;
|
||||
|
||||
export const ChannelFollowerStatsResponse = z.object({
|
||||
channel_count: Int32Type.describe('The number of channels following this announcement channel'),
|
||||
guild_count: Int32Type.describe('The number of distinct guilds following this announcement channel'),
|
||||
});
|
||||
|
||||
export type ChannelFollowerStatsResponse = z.infer<typeof ChannelFollowerStatsResponse>;
|
||||
@@ -137,6 +137,15 @@ const ChannelCreateTextRequest = ChannelCreateCommon.extend({
|
||||
name: GeneralChannelNameType.describe('The name of the channel'),
|
||||
});
|
||||
|
||||
const ChannelCreateAnnouncementRequest = ChannelCreateCommon.extend({
|
||||
type: createNamedLiteral(
|
||||
ChannelTypes.GUILD_ANNOUNCEMENT,
|
||||
'GUILD_ANNOUNCEMENT',
|
||||
'Channel type (announcement channel)',
|
||||
),
|
||||
name: GeneralChannelNameType.describe('The name of the channel'),
|
||||
});
|
||||
|
||||
const ChannelCreateVoiceRequest = ChannelCreateCommon.extend({
|
||||
type: createNamedLiteral(ChannelTypes.GUILD_VOICE, 'GUILD_VOICE', 'Channel type (voice channel)'),
|
||||
name: GeneralChannelNameType.describe('The name of the channel'),
|
||||
@@ -154,6 +163,7 @@ const ChannelCreateLinkRequest = ChannelCreateCommon.extend({
|
||||
|
||||
export const ChannelCreateRequest = z.discriminatedUnion('type', [
|
||||
ChannelCreateTextRequest,
|
||||
ChannelCreateAnnouncementRequest,
|
||||
ChannelCreateVoiceRequest,
|
||||
ChannelCreateCategoryRequest,
|
||||
ChannelCreateLinkRequest,
|
||||
@@ -166,6 +176,15 @@ const ChannelUpdateTextRequest = ChannelUpdateCommon.extend({
|
||||
name: GeneralChannelNameType.nullish().describe('The name of the channel'),
|
||||
});
|
||||
|
||||
const ChannelUpdateAnnouncementRequest = ChannelUpdateCommon.extend({
|
||||
type: createNamedLiteral(
|
||||
ChannelTypes.GUILD_ANNOUNCEMENT,
|
||||
'GUILD_ANNOUNCEMENT',
|
||||
'Channel type (announcement channel)',
|
||||
),
|
||||
name: GeneralChannelNameType.nullish().describe('The name of the channel'),
|
||||
});
|
||||
|
||||
const ChannelUpdateVoiceRequest = ChannelUpdateCommon.extend({
|
||||
type: createNamedLiteral(ChannelTypes.GUILD_VOICE, 'GUILD_VOICE', 'Channel type (voice channel)'),
|
||||
name: GeneralChannelNameType.nullish().describe('The name of the channel'),
|
||||
@@ -193,6 +212,7 @@ const ChannelUpdateGroupDmRequest = z.object({
|
||||
|
||||
export const ChannelUpdateRequest = z.discriminatedUnion('type', [
|
||||
ChannelUpdateTextRequest,
|
||||
ChannelUpdateAnnouncementRequest,
|
||||
ChannelUpdateVoiceRequest,
|
||||
ChannelUpdateCategoryRequest,
|
||||
ChannelUpdateLinkRequest,
|
||||
@@ -201,8 +221,20 @@ export const ChannelUpdateRequest = z.discriminatedUnion('type', [
|
||||
|
||||
export type ChannelUpdateRequest = z.infer<typeof ChannelUpdateRequest>;
|
||||
|
||||
const ChannelTypeConversionField = z
|
||||
.union([z.literal(ChannelTypes.GUILD_TEXT), z.literal(ChannelTypes.GUILD_ANNOUNCEMENT)])
|
||||
.optional()
|
||||
.describe('Convert between text (0) and announcement (5). Other conversions are rejected.');
|
||||
|
||||
const CONVERTIBLE_UPDATE_REQUESTS = new Set<z.ZodObject>([ChannelUpdateTextRequest, ChannelUpdateAnnouncementRequest]);
|
||||
|
||||
export const ChannelUpdateRequestBody = z.union(
|
||||
ChannelUpdateRequest.options.map(({shape: {type, ...shape}}) => z.object(shape)),
|
||||
ChannelUpdateRequest.options.map((option) => {
|
||||
const {type, ...shape} = option.shape;
|
||||
return CONVERTIBLE_UPDATE_REQUESTS.has(option)
|
||||
? z.object({...shape, type: ChannelTypeConversionField})
|
||||
: z.object(shape);
|
||||
}),
|
||||
);
|
||||
|
||||
export const PermissionOverwriteCreateRequest = z.object({
|
||||
|
||||
@@ -48,7 +48,12 @@ export function compareChannelOrdering<Id extends string | bigint>(
|
||||
}
|
||||
|
||||
function channelLayoutRank(channel: {type: number}): number {
|
||||
if (channel.type === ChannelTypes.GUILD_TEXT || channel.type === ChannelTypes.GUILD_LINK) return 0;
|
||||
if (
|
||||
channel.type === ChannelTypes.GUILD_TEXT ||
|
||||
channel.type === ChannelTypes.GUILD_ANNOUNCEMENT ||
|
||||
channel.type === ChannelTypes.GUILD_LINK
|
||||
)
|
||||
return 0;
|
||||
if (channel.type === ChannelTypes.GUILD_VOICE) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -108,6 +108,11 @@ export const GuildFeatureSchema = withOpenApiType(
|
||||
[GuildFeatures.VISIONARY, 'VISIONARY', 'Guild is a visionary guild'],
|
||||
[GuildFeatures.LARGE_GUILD_OVERRIDE, 'LARGE_GUILD_OVERRIDE', 'Guild has large guild overrides enabled'],
|
||||
[GuildFeatures.VERY_LARGE_GUILD, 'VERY_LARGE_GUILD', 'Guild has increased member capacity enabled'],
|
||||
[
|
||||
GuildFeatures.ANNOUNCEMENT_CHANNELS_DISABLED,
|
||||
'ANNOUNCEMENT_CHANNELS_DISABLED',
|
||||
'Guild cannot publish announcement messages or gain new followers',
|
||||
],
|
||||
],
|
||||
'A guild feature flag',
|
||||
),
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {GuildFeatureSchema} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
||||
import {Int32Type, SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const CrosspostSourceGuildResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The ID of the source community'),
|
||||
name: z.string().describe('The name of the source community'),
|
||||
icon: z.string().nullish().describe('The hash of the source community icon'),
|
||||
banner: z.string().nullish().describe('The hash of the source community banner'),
|
||||
description: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe('The discovery description of the source community, or null when it is not listed in discovery'),
|
||||
features: z
|
||||
.array(GuildFeatureSchema)
|
||||
.describe('The public badge features of the source community: VERIFIED, PARTNERED and DISCOVERABLE only'),
|
||||
approximate_member_count: Int32Type.nullable().describe(
|
||||
'Approximate number of members in the source community, or null when unavailable',
|
||||
),
|
||||
approximate_presence_count: Int32Type.nullable().describe(
|
||||
'Approximate number of online members in the source community, or null when unavailable',
|
||||
),
|
||||
discoverable: z.boolean().describe('Whether the source community can be joined through discovery'),
|
||||
});
|
||||
export type CrosspostSourceGuildResponse = z.infer<typeof CrosspostSourceGuildResponse>;
|
||||
|
||||
export const CrosspostSourceResponse = z.object({
|
||||
guild: CrosspostSourceGuildResponse.describe('The community the message was published from'),
|
||||
});
|
||||
export type CrosspostSourceResponse = z.infer<typeof CrosspostSourceResponse>;
|
||||
@@ -49,7 +49,9 @@ export type MessageAttachmentResponse = z.infer<typeof MessageAttachmentResponse
|
||||
|
||||
const MessageReferenceResponse = z.object({
|
||||
channel_id: SnowflakeStringType.describe('The ID of the channel containing the referenced message'),
|
||||
message_id: SnowflakeStringType.describe('The ID of the referenced message'),
|
||||
message_id: SnowflakeStringType.nullish().describe(
|
||||
'The ID of the referenced message, absent on a channel follow system message',
|
||||
),
|
||||
guild_id: SnowflakeStringType.nullish().describe('The ID of the guild containing the referenced message'),
|
||||
type: MessageReferenceTypeSchema,
|
||||
});
|
||||
@@ -235,7 +237,7 @@ export const BulkMessageFetchResponse = z.object({
|
||||
export type BulkMessageFetchResponse = z.infer<typeof BulkMessageFetchResponse>;
|
||||
|
||||
export interface MessageReference {
|
||||
readonly message_id: string;
|
||||
readonly message_id?: string;
|
||||
readonly channel_id: string;
|
||||
readonly guild_id?: string;
|
||||
readonly type?: number;
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {ChannelTypes, MessageFlags, MessageTypes, WebhookTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {
|
||||
ChannelFollowerStatsResponse,
|
||||
ChannelFollowRequest,
|
||||
FollowedChannelResponse,
|
||||
} from '@fluxer/schema/src/domains/channel/ChannelFollowSchemas';
|
||||
import {ChannelCreateRequest, ChannelUpdateRequest} from '@fluxer/schema/src/domains/channel/ChannelRequestSchemas';
|
||||
import {MessageResponseSchema} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import {
|
||||
WebhookCreateResponse,
|
||||
WebhookResponse,
|
||||
WebhookTokenResponse,
|
||||
} from '@fluxer/schema/src/domains/webhook/WebhookSchemas';
|
||||
import {ChannelTypeSchema} from '@fluxer/schema/src/primitives/ChannelValidators';
|
||||
import {MessageTypeSchema} from '@fluxer/schema/src/primitives/MessageValidators';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const creator = {
|
||||
id: '100',
|
||||
username: 'owner',
|
||||
discriminator: '0001',
|
||||
global_name: null,
|
||||
avatar: null,
|
||||
avatar_color: null,
|
||||
flags: 0,
|
||||
mention_flags: 0,
|
||||
};
|
||||
|
||||
const incomingWebhook = {
|
||||
id: '1',
|
||||
guild_id: '2',
|
||||
channel_id: '3',
|
||||
name: 'Hook',
|
||||
avatar: null,
|
||||
type: WebhookTypes.INCOMING,
|
||||
token: 'secret',
|
||||
};
|
||||
|
||||
describe('announcement channel type', () => {
|
||||
it('accepts type 5 as a channel type', () => {
|
||||
expect(ChannelTypeSchema.parse(ChannelTypes.GUILD_ANNOUNCEMENT)).toBe(5);
|
||||
});
|
||||
|
||||
it('creates an announcement channel with the text channel fields', () => {
|
||||
const parsed = ChannelCreateRequest.parse({
|
||||
type: ChannelTypes.GUILD_ANNOUNCEMENT,
|
||||
name: 'news',
|
||||
topic: 'Updates',
|
||||
rate_limit_per_user: 10,
|
||||
});
|
||||
expect(parsed).toMatchObject({type: 5, name: 'news', topic: 'Updates', rate_limit_per_user: 10, nsfw: false});
|
||||
});
|
||||
|
||||
it('updates an announcement channel through its own variant', () => {
|
||||
expect(ChannelUpdateRequest.parse({type: ChannelTypes.GUILD_ANNOUNCEMENT, topic: 'New'})).toMatchObject({
|
||||
type: 5,
|
||||
topic: 'New',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('channel follow add message type', () => {
|
||||
it('accepts type 12 as a message type', () => {
|
||||
expect(MessageTypeSchema.parse(MessageTypes.CHANNEL_FOLLOW_ADD)).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('channel follow schemas', () => {
|
||||
it('parses the target channel of a follow request', () => {
|
||||
expect(ChannelFollowRequest.parse({webhook_channel_id: '123'})).toEqual({webhook_channel_id: 123n});
|
||||
expect(ChannelFollowRequest.safeParse({webhook_channel_id: 'abc'}).success).toBe(false);
|
||||
expect(ChannelFollowRequest.safeParse({}).success).toBe(false);
|
||||
});
|
||||
|
||||
it('describes the followed channel and its webhook', () => {
|
||||
expect(FollowedChannelResponse.parse({channel_id: '1', webhook_id: '2'})).toEqual({
|
||||
channel_id: '1',
|
||||
webhook_id: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it('reports follower counts as integers', () => {
|
||||
expect(ChannelFollowerStatsResponse.parse({channel_count: 3, guild_count: 2})).toEqual({
|
||||
channel_count: 3,
|
||||
guild_count: 2,
|
||||
});
|
||||
expect(ChannelFollowerStatsResponse.safeParse({channel_count: 1.5, guild_count: 1}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('webhook responses', () => {
|
||||
it('keeps the token required on token responses', () => {
|
||||
expect(WebhookTokenResponse.parse(incomingWebhook)).toEqual(incomingWebhook);
|
||||
const {token: _token, ...withoutToken} = incomingWebhook;
|
||||
expect(WebhookTokenResponse.safeParse(withoutToken).success).toBe(false);
|
||||
expect(WebhookCreateResponse.safeParse({...withoutToken, user: creator}).success).toBe(false);
|
||||
});
|
||||
|
||||
it('omits the token and carries the source on follower webhooks', () => {
|
||||
const {token: _token, ...withoutToken} = incomingWebhook;
|
||||
const follower = {
|
||||
...withoutToken,
|
||||
type: WebhookTypes.CHANNEL_FOLLOWER,
|
||||
user: creator,
|
||||
source_guild: {id: '7', name: 'Source', icon: null},
|
||||
source_channel: {id: '8', name: 'news'},
|
||||
};
|
||||
const parsed = WebhookResponse.parse(follower);
|
||||
expect(parsed).not.toHaveProperty('token');
|
||||
expect(parsed.source_guild).toEqual({id: '7', name: 'Source', icon: null});
|
||||
expect(parsed.source_channel).toEqual({id: '8', name: 'news'});
|
||||
});
|
||||
|
||||
it('rejects unknown webhook types', () => {
|
||||
expect(WebhookResponse.safeParse({...incomingWebhook, type: 3, user: creator}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('message responses', () => {
|
||||
it('allows a message reference without a message id', () => {
|
||||
const shape = MessageResponseSchema.shape.message_reference;
|
||||
const reference = {type: 0, guild_id: '1', channel_id: '2'};
|
||||
expect(shape.parse(reference)).toEqual(reference);
|
||||
});
|
||||
|
||||
it('accepts the crosspost flags', () => {
|
||||
const flags = MessageFlags.CROSSPOSTED | MessageFlags.IS_CROSSPOST | MessageFlags.SOURCE_MESSAGE_DELETED;
|
||||
expect(MessageResponseSchema.shape.flags.parse(flags)).toBe(flags);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
computeGuildChannelReorderPlan,
|
||||
computePositionFromPrecedingSiblingId,
|
||||
computePrecedingSiblingIdFromPosition,
|
||||
sortChannelsForOrdering,
|
||||
} from '@fluxer/schema/src/domains/channel/GuildChannelOrdering';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
@@ -125,4 +126,18 @@ describe('GuildChannelOrdering', () => {
|
||||
if (result.ok) return;
|
||||
expect(result.code).toBe('PRECEDING_PARENT_MISMATCH');
|
||||
});
|
||||
it('ranks announcement channels with text channels ahead of voice channels', () => {
|
||||
const channels: Array<Ch> = [
|
||||
{id: 'cat', parentId: null, type: ChannelTypes.GUILD_CATEGORY, position: 0},
|
||||
{id: 'voice', parentId: 'cat', type: ChannelTypes.GUILD_VOICE, position: 1},
|
||||
{id: 'announcements', parentId: 'cat', type: ChannelTypes.GUILD_ANNOUNCEMENT, position: 3},
|
||||
{id: 'text', parentId: 'cat', type: ChannelTypes.GUILD_TEXT, position: 2},
|
||||
];
|
||||
expect(sortChannelsForOrdering<string, Ch>(channels).map((c) => c.id)).toEqual([
|
||||
'cat',
|
||||
'text',
|
||||
'announcements',
|
||||
'voice',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,6 +103,7 @@ describe('channel update request body', () => {
|
||||
|
||||
it.each([
|
||||
ChannelTypes.GUILD_TEXT,
|
||||
ChannelTypes.GUILD_ANNOUNCEMENT,
|
||||
ChannelTypes.GUILD_VOICE,
|
||||
ChannelTypes.GUILD_CATEGORY,
|
||||
ChannelTypes.GUILD_LINK,
|
||||
@@ -119,7 +120,11 @@ describe('channel update request body', () => {
|
||||
expect(ChannelUpdateRequestBody.parse(input)).toEqual({...input, topic: 'Topic', parent_id: 123n});
|
||||
});
|
||||
|
||||
it('does not accept the injected discriminator as a body property', () => {
|
||||
it('keeps only text and announcement conversions as a body type', () => {
|
||||
expect(ChannelUpdateRequestBody.parse({type: ChannelTypes.GUILD_TEXT})).toEqual({type: ChannelTypes.GUILD_TEXT});
|
||||
expect(ChannelUpdateRequestBody.parse({type: ChannelTypes.GUILD_ANNOUNCEMENT})).toEqual({
|
||||
type: ChannelTypes.GUILD_ANNOUNCEMENT,
|
||||
});
|
||||
expect(ChannelUpdateRequestBody.parse({type: ChannelTypes.GUILD_VOICE})).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,26 +2,69 @@
|
||||
|
||||
import {UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import {SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {WebhookTypeSchema} from '@fluxer/schema/src/primitives/WebhookValidators';
|
||||
import {z} from 'zod';
|
||||
|
||||
const WebhookBaseResponse = {
|
||||
const WebhookCommonResponse = {
|
||||
id: SnowflakeStringType.describe('The unique identifier (snowflake) for the webhook'),
|
||||
guild_id: SnowflakeStringType.describe('The ID of the guild this webhook belongs to'),
|
||||
channel_id: SnowflakeStringType.describe('The ID of the channel this webhook posts to'),
|
||||
name: z.string().describe('The display name of the webhook'),
|
||||
avatar: z.string().nullish().describe('The hash of the webhook avatar image'),
|
||||
token: z.string().describe('The secure token used to execute the webhook'),
|
||||
type: WebhookTypeSchema,
|
||||
};
|
||||
export const WebhookTokenResponse = z.object(WebhookBaseResponse);
|
||||
export const WebhookTokenResponse = z.object({
|
||||
...WebhookCommonResponse,
|
||||
token: z.string().describe('The secure token used to execute the webhook'),
|
||||
});
|
||||
|
||||
export type WebhookTokenResponse = z.infer<typeof WebhookTokenResponse>;
|
||||
|
||||
export const WebhookResponse = WebhookTokenResponse.extend({
|
||||
export const WebhookCreateResponse = WebhookTokenResponse.extend({
|
||||
user: z.lazy(() => UserPartialResponse).describe('The user who created the webhook'),
|
||||
});
|
||||
|
||||
export type WebhookCreateResponse = z.infer<typeof WebhookCreateResponse>;
|
||||
|
||||
const WebhookSourceGuildResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The ID of the guild that owns the followed announcement channel'),
|
||||
name: z.string().describe('The name of the guild that owns the followed announcement channel'),
|
||||
icon: z.string().nullish().describe('The icon hash of the guild that owns the followed announcement channel'),
|
||||
});
|
||||
|
||||
const WebhookSourceChannelResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The ID of the followed announcement channel'),
|
||||
name: z.string().describe('The name of the followed announcement channel'),
|
||||
});
|
||||
|
||||
export const WebhookResponse = z.object({
|
||||
...WebhookCommonResponse,
|
||||
token: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('The secure token used to execute the webhook, omitted for channel follower webhooks'),
|
||||
user: z.lazy(() => UserPartialResponse).describe('The user who created the webhook'),
|
||||
source_guild: WebhookSourceGuildResponse.optional().describe(
|
||||
'The guild of the followed announcement channel, present on channel follower webhooks while the creator can view it',
|
||||
),
|
||||
source_channel: WebhookSourceChannelResponse.optional().describe(
|
||||
'The followed announcement channel, present on channel follower webhooks while the creator can view it',
|
||||
),
|
||||
});
|
||||
|
||||
export type WebhookResponse = z.infer<typeof WebhookResponse>;
|
||||
|
||||
export interface WebhookSourceGuild {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly icon?: string | null;
|
||||
}
|
||||
|
||||
export interface WebhookSourceChannel {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface Webhook {
|
||||
readonly id: string;
|
||||
readonly guild_id: string;
|
||||
@@ -29,7 +72,10 @@ export interface Webhook {
|
||||
readonly user: UserPartialResponse;
|
||||
readonly name: string;
|
||||
readonly avatar: string | null;
|
||||
readonly token: string;
|
||||
readonly type: number;
|
||||
readonly token?: string;
|
||||
readonly source_guild?: WebhookSourceGuild;
|
||||
readonly source_channel?: WebhookSourceChannel;
|
||||
}
|
||||
|
||||
export const WebhookListResponse = z.array(WebhookResponse);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -25,6 +25,11 @@ export const ChannelTypeSchema = createInt32EnumType(
|
||||
[ChannelTypes.GUILD_VOICE, 'GUILD_VOICE', 'A voice channel within a guild'],
|
||||
[ChannelTypes.GROUP_DM, 'GROUP_DM', 'A group direct message between users'],
|
||||
[ChannelTypes.GUILD_CATEGORY, 'GUILD_CATEGORY', 'A category that contains channels'],
|
||||
[
|
||||
ChannelTypes.GUILD_ANNOUNCEMENT,
|
||||
'GUILD_ANNOUNCEMENT',
|
||||
'A guild channel whose messages can be published to channels that follow it',
|
||||
],
|
||||
[ChannelTypes.GUILD_LINK, 'GUILD_LINK', 'A link channel for external resources'],
|
||||
[ChannelTypes.DM_PERSONAL_NOTES, 'DM_PERSONAL_NOTES', 'Personal notes DM channel'],
|
||||
],
|
||||
|
||||
@@ -23,6 +23,11 @@ export const MessageTypeSchema = createInt32EnumType(
|
||||
[MessageTypes.CHANNEL_ICON_CHANGE, 'CHANNEL_ICON_CHANGE', 'A system message indicating the channel icon changed'],
|
||||
[MessageTypes.CHANNEL_PINNED_MESSAGE, 'CHANNEL_PINNED_MESSAGE', 'A system message indicating a message was pinned'],
|
||||
[MessageTypes.USER_JOIN, 'USER_JOIN', 'A system message indicating a user joined'],
|
||||
[
|
||||
MessageTypes.CHANNEL_FOLLOW_ADD,
|
||||
'CHANNEL_FOLLOW_ADD',
|
||||
'System message posted when a channel starts following an announcement channel',
|
||||
],
|
||||
[MessageTypes.REPLY, 'REPLY', 'A reply message'],
|
||||
],
|
||||
'The type of message',
|
||||
|
||||
Reference in New Issue
Block a user