feat: add announcement channels, publishing and following (#3090)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-01 16:57:21 +02:00
1 parent 376afd2ad6
commit 7e4d5137f8
381 files changed
+45650 -14076

No files matched your search

@@ -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);
@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file fluxer/user/preferences/v1/preferences.proto.
*/
export const file_fluxer_user_preferences_v1_preferences: GenFile = /*@__PURE__*/
fileDesc("CixmbHV4ZXIvdXNlci9wcmVmZXJlbmNlcy92MS9wcmVmZXJlbmNlcy5wcm90bxIaZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEipRUKEVN5bmNlZFByZWZlcmVuY2VzEkgKDWFjY2Vzc2liaWxpdHkYASABKAsyMS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5U2V0dGluZ3MSUwoXYWNjZXNzaWJpbGl0eV9vdmVycmlkZXMYAiABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5T3ZlcnJpZGVzEksKD3RleHR1YWxfcHJldmlldxgDIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlRleHR1YWxQcmV2aWV3U2V0dGluZ3MSQgoMZW1vamlfcGlja2VyGBQgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlQaWNrZXJTdGF0ZRJGCg5zdGlja2VyX3BpY2tlchgVIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN0aWNrZXJQaWNrZXJTdGF0ZRJCCgxtZW1lc19waWNrZXIYFiABKAsyLC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW1lc1BpY2tlclN0YXRlEjUKBWVtb2ppGBcgASgLMiYuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlTdGF0ZRJUChRlbW9qaV9zdGlja2VyX2xheW91dBgYIAEoCzI2LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkVtb2ppU3RpY2tlckxheW91dFNldHRpbmdzEkYKDWZhdm9yaXRlX2dpZnMYGSABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5GYXZvcml0ZUdpZlNldHRpbmdzEj0KCWZhdm9yaXRlcxgoIAEoCzIqLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkZhdm9yaXRlc1N0YXRlEksKD3JlY2VudF9tZW50aW9ucxgpIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlJlY2VudE1lbnRpb25zU2V0dGluZ3MSPwoHc2lkZWJhchgqIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlNpZGViYXJQcmVmZXJlbmNlcxJACgttZW1iZXJfbGlzdBgrIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLk1lbWJlckxpc3RTdGF0ZRJICg91bnJlYWRfY2hhbm5lbHMYLCABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5VbnJlYWRDaGFubmVsc1N0YXRlEkoKEG1lbnRpb25fZnJlY2VuY3kYLSABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW50aW9uRnJlY2VuY3lTdGF0ZRI9CgduYWdiYXJzGDwgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2FscxJHChFkaXNtaXNzZWRfdXBzZWxscxg9IAEoCzIsLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkRpc21pc3NlZFVwc2VsbHMSTgoVZ3VpbGRfbnNmd19hZ3JlZW1lbnRzGD4gASgLMi8uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGROc2Z3QWdyZWVtZW50cxI8Cgl3aGF0c19uZXcYPyABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5XaGF0c05ld1N0YXRlEj8KB3ByaXZhY3kYUCABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Qcml2YWN5UHJlZmVyZW5jZXMSUAoUbG9jYWxfc3BhbV9vdmVycmlkZXMYUSABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Mb2NhbFVzZXJTcGFtT3ZlcnJpZGVzEhUKDXNhbml0aXplX3VybHMYUiABKAgSOAoFc291bmQYZCABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Tb3VuZFNldHRpbmdzEkIKCnNwZWxsY2hlY2sYZSABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TcGVsbGNoZWNrU2V0dGluZ3MSSAoOc2VhcmNoX2VuZ2luZXMYZiABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TZWFyY2hFbmdpbmVTZXR0aW5ncxJPChFwZXJtaXNzaW9uX2xheW91dBhnIAEoCzI0LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlBlcm1pc3Npb25MYXlvdXRTZXR0aW5ncxJSChNndWlsZF9tZW1iZXJfbGF5b3V0GGggASgLMjUuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRNZW1iZXJMYXlvdXRTZXR0aW5ncxJLCg1ndWlsZF9mb2xkZXJzGGkgASgLMjQuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRGb2xkZXJFeHBhbmRlZFN0YXRlElAKFGhpZGRlbl9ndWlsZF9idXR0b25zGGogASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSGlkZGVuR3VpbGRMaXN0QnV0dG9ucxJPChNrZXlib2FyZF9tb2RlX2ludHJvGGsgASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuS2V5Ym9hcmRNb2RlSW50cm9TdGF0ZRJRChBpbnB1dF9tb25pdG9yaW5nGGwgASgLMjcuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSW5wdXRNb25pdG9yaW5nUHJvbXB0c1N0YXRlEkQKDXZvaWNlX3Byb21wdHMYbSABKAsyLS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Wb2ljZVByb21wdHNTdGF0ZRJACgtzdWRvX3Byb21wdBhuIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN1ZG9Qcm9tcHRTdGF0ZRI9CghrZXliaW5kcxhvIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLktleWJpbmRTZXR0aW5ncxJBCgpjaGF0X2lucHV0GHAgASgLMi0uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhdElucHV0U2V0dGluZ3MSKgodc2F2ZV9jYW1lcmFfdXBsb2Fkc190b19kZXZpY2UYcSABKAhIAIgBARJGChNkb3VibGVfdGFwX3JlYWN0aW9uGHIgASgLMikuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuUmVhY3Rpb25FbW9qaRJKChBjaGFubmVsX2ZyZWNlbmN5GHMgASgLMjAuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhbm5lbEZyZWNlbmN5U3RhdGVCIAoeX3NhdmVfY2FtZXJhX3VwbG9hZHNfdG9fZGV2aWNlIrABChJTcGVsbGNoZWNrU2V0dGluZ3MSFAoHZW5hYmxlZBgBIAEoCEgAiAEBEhEKCWxhbmd1YWdlcxgCIAMoCRIbChNwZXJzb25hbF9kaWN0aW9uYXJ5GAMgAygJEhgKC2F1dG9fZGV0ZWN0GAQgASgISAGIAQESEwoGZW5naW5lGAUgASgJSAKIAQFCCgoIX2VuYWJsZWRCDgoMX2F1dG9fZGV0ZWN0QgkKB19lbmdpbmUi5gEKFFNlYXJjaEVuZ2luZVNldHRpbmdzEiIKFXRleHRfc2VhcmNoX2VuZ2luZV9pZBgBIAEoCUgAiAEBEisKHnJldmVyc2VfaW1hZ2Vfc2VhcmNoX2VuZ2luZV9pZBgCIAEoCUgBiAEBEiQKF3RyYW5zbGF0aW9uX3Byb3ZpZGVyX2lkGAMgASgJSAKIAQFCGAoWX3RleHRfc2VhcmNoX2VuZ2luZV9pZEIhCh9fcmV2ZXJzZV9pbWFnZV9zZWFyY2hfZW5naW5lX2lkQhoKGF90cmFuc2xhdGlvbl9wcm92aWRlcl9pZCK1AQoSUHJpdmFjeVByZWZlcmVuY2VzEh8KF2Rpc2FibGVfc3RyZWFtX3ByZXZpZXdzGAEgASgIEhwKD3Nob3dfYWN0aXZlX25vdxgCIAEoCEgAiAEBEioKHXByZXVwbG9hZF9tZXNzYWdlX2F0dGFjaG1lbnRzGAMgASgISAGIAQFCEgoQX3Nob3dfYWN0aXZlX25vd0IgCh5fcHJldXBsb2FkX21lc3NhZ2VfYXR0YWNobWVudHMiUAoWTG9jYWxVc2VyU3BhbU92ZXJyaWRlcxIYChBzcGFtbWVyX3VzZXJfaWRzGAEgAygJEhwKFG5vdF9zcGFtbWVyX3VzZXJfaWRzGAIgAygJIisKFlRleHR1YWxQcmV2aWV3U2V0dGluZ3MSEQoJd3JhcF90ZXh0GAEgASgIItwBChJTaWRlYmFyUHJlZmVyZW5jZXMSHAoUaW5saW5lX2Rtc19jb2xsYXBzZWQYASABKAgSLAofc2hvd19jb2xsYXBzZWRfdW5yZWFkX2Rtc19iYWRnZRgCIAEoCEgAiAEBEi8KInNob3dfaW5jb21pbmdfZnJpZW5kX3JlcXVlc3RfYmFkZ2UYAyABKAhIAYgBAUIiCiBfc2hvd19jb2xsYXBzZWRfdW5yZWFkX2Rtc19iYWRnZUIlCiNfc2hvd19pbmNvbWluZ19mcmllbmRfcmVxdWVLine truncated
fileDesc("CixmbHV4ZXIvdXNlci9wcmVmZXJlbmNlcy92MS9wcmVmZXJlbmNlcy5wcm90bxIaZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEi+RUKEVN5bmNlZFByZWZlcmVuY2VzEkgKDWFjY2Vzc2liaWxpdHkYASABKAsyMS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5U2V0dGluZ3MSUwoXYWNjZXNzaWJpbGl0eV9vdmVycmlkZXMYAiABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5T3ZlcnJpZGVzEksKD3RleHR1YWxfcHJldmlldxgDIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlRleHR1YWxQcmV2aWV3U2V0dGluZ3MSQgoMZW1vamlfcGlja2VyGBQgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlQaWNrZXJTdGF0ZRJGCg5zdGlja2VyX3BpY2tlchgVIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN0aWNrZXJQaWNrZXJTdGF0ZRJCCgxtZW1lc19waWNrZXIYFiABKAsyLC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW1lc1BpY2tlclN0YXRlEjUKBWVtb2ppGBcgASgLMiYuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlTdGF0ZRJUChRlbW9qaV9zdGlja2VyX2xheW91dBgYIAEoCzI2LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkVtb2ppU3RpY2tlckxheW91dFNldHRpbmdzEkYKDWZhdm9yaXRlX2dpZnMYGSABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5GYXZvcml0ZUdpZlNldHRpbmdzEj0KCWZhdm9yaXRlcxgoIAEoCzIqLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkZhdm9yaXRlc1N0YXRlEksKD3JlY2VudF9tZW50aW9ucxgpIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlJlY2VudE1lbnRpb25zU2V0dGluZ3MSPwoHc2lkZWJhchgqIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlNpZGViYXJQcmVmZXJlbmNlcxJACgttZW1iZXJfbGlzdBgrIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLk1lbWJlckxpc3RTdGF0ZRJICg91bnJlYWRfY2hhbm5lbHMYLCABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5VbnJlYWRDaGFubmVsc1N0YXRlEkoKEG1lbnRpb25fZnJlY2VuY3kYLSABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW50aW9uRnJlY2VuY3lTdGF0ZRI9CgduYWdiYXJzGDwgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2FscxJHChFkaXNtaXNzZWRfdXBzZWxscxg9IAEoCzIsLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkRpc21pc3NlZFVwc2VsbHMSTgoVZ3VpbGRfbnNmd19hZ3JlZW1lbnRzGD4gASgLMi8uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGROc2Z3QWdyZWVtZW50cxI8Cgl3aGF0c19uZXcYPyABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5XaGF0c05ld1N0YXRlEj8KB3ByaXZhY3kYUCABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Qcml2YWN5UHJlZmVyZW5jZXMSUAoUbG9jYWxfc3BhbV9vdmVycmlkZXMYUSABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Mb2NhbFVzZXJTcGFtT3ZlcnJpZGVzEhUKDXNhbml0aXplX3VybHMYUiABKAgSOAoFc291bmQYZCABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Tb3VuZFNldHRpbmdzEkIKCnNwZWxsY2hlY2sYZSABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TcGVsbGNoZWNrU2V0dGluZ3MSSAoOc2VhcmNoX2VuZ2luZXMYZiABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TZWFyY2hFbmdpbmVTZXR0aW5ncxJPChFwZXJtaXNzaW9uX2xheW91dBhnIAEoCzI0LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlBlcm1pc3Npb25MYXlvdXRTZXR0aW5ncxJSChNndWlsZF9tZW1iZXJfbGF5b3V0GGggASgLMjUuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRNZW1iZXJMYXlvdXRTZXR0aW5ncxJLCg1ndWlsZF9mb2xkZXJzGGkgASgLMjQuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRGb2xkZXJFeHBhbmRlZFN0YXRlElAKFGhpZGRlbl9ndWlsZF9idXR0b25zGGogASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSGlkZGVuR3VpbGRMaXN0QnV0dG9ucxJPChNrZXlib2FyZF9tb2RlX2ludHJvGGsgASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuS2V5Ym9hcmRNb2RlSW50cm9TdGF0ZRJRChBpbnB1dF9tb25pdG9yaW5nGGwgASgLMjcuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSW5wdXRNb25pdG9yaW5nUHJvbXB0c1N0YXRlEkQKDXZvaWNlX3Byb21wdHMYbSABKAsyLS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Wb2ljZVByb21wdHNTdGF0ZRJACgtzdWRvX3Byb21wdBhuIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN1ZG9Qcm9tcHRTdGF0ZRI9CghrZXliaW5kcxhvIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLktleWJpbmRTZXR0aW5ncxJBCgpjaGF0X2lucHV0GHAgASgLMi0uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhdElucHV0U2V0dGluZ3MSKgodc2F2ZV9jYW1lcmFfdXBsb2Fkc190b19kZXZpY2UYcSABKAhIAIgBARJGChNkb3VibGVfdGFwX3JlYWN0aW9uGHIgASgLMikuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuUmVhY3Rpb25FbW9qaRJKChBjaGFubmVsX2ZyZWNlbmN5GHMgASgLMjAuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhbm5lbEZyZWNlbmN5U3RhdGUSUgoUYW5ub3VuY2VtZW50X3Byb21wdHMYdCABKAsyNC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Bbm5vdW5jZW1lbnRQcm9tcHRzU3RhdGVCIAoeX3NhdmVfY2FtZXJhX3VwbG9hZHNfdG9fZGV2aWNlIrABChJTcGVsbGNoZWNrU2V0dGluZ3MSFAoHZW5hYmxlZBgBIAEoCEgAiAEBEhEKCWxhbmd1YWdlcxgCIAMoCRIbChNwZXJzb25hbF9kaWN0aW9uYXJ5GAMgAygJEhgKC2F1dG9fZGV0ZWN0GAQgASgISAGIAQESEwoGZW5naW5lGAUgASgJSAKIAQFCCgoIX2VuYWJsZWRCDgoMX2F1dG9fZGV0ZWN0QgkKB19lbmdpbmUi5gEKFFNlYXJjaEVuZ2luZVNldHRpbmdzEiIKFXRleHRfc2VhcmNoX2VuZ2luZV9pZBgBIAEoCUgAiAEBEisKHnJldmVyc2VfaW1hZ2Vfc2VhcmNoX2VuZ2luZV9pZBgCIAEoCUgBiAEBEiQKF3RyYW5zbGF0aW9uX3Byb3ZpZGVyX2lkGAMgASgJSAKIAQFCGAoWX3RleHRfc2VhcmNoX2VuZ2luZV9pZEIhCh9fcmV2ZXJzZV9pbWFnZV9zZWFyY2hfZW5naW5lX2lkQhoKGF90cmFuc2xhdGlvbl9wcm92aWRlcl9pZCK1AQoSUHJpdmFjeVByZWZlcmVuY2VzEh8KF2Rpc2FibGVfc3RyZWFtX3ByZXZpZXdzGAEgASgIEhwKD3Nob3dfYWN0aXZlX25vdxgCIAEoCEgAiAEBEioKHXByZXVwbG9hZF9tZXNzYWdlX2F0dGFjaG1lbnRzGAMgASgISAGIAQFCEgoQX3Nob3dfYWN0aXZlX25vd0IgCh5fcHJldXBsb2FkX21lc3NhZ2VfYXR0YWNobWVudHMiUAoWTG9jYWxVc2VyU3BhbU92ZXJyaWRlcxIYChBzcGFtbWVyX3VzZXJfaWRzGAEgAygJEhwKFG5vdF9zcGFtbWVyX3VzZXJfaWRzGAIgAygJIisKFlRleHR1YWxQcmV2aWV3U2V0dGluZ3MSEQoJd3JhcF90ZXh0GAEgASgIItwBChJTaWRlYmFyUHJlZmVyZW5jZXMSHAoUaW5saW5lX2Rtc19jb2xsYXBzZWQYASABKAgSLAofc2hvd19jb2xsYXBzZWRfdW5yZWFkX2Rtc19iYWRnZRgCIAEoCEgAiAEBEi8KInNob3dfaW5jb21pbmdfZnJpZW5kX3JlcXVlc3RLine truncated
/**
* @generated from message fluxer.user.preferences.v1.SyncedPreferences
@@ -205,6 +205,11 @@ export type SyncedPreferences = Message<"fluxer.user.preferences.v1.SyncedPrefer
* @generated from field: fluxer.user.preferences.v1.ChannelFrecencyState channel_frecency = 115;
*/
channelFrecency?: ChannelFrecencyState | undefined;
/**
* @generated from field: fluxer.user.preferences.v1.AnnouncementPromptsState announcement_prompts = 116;
*/
announcementPrompts?: AnnouncementPromptsState | undefined;
};
/**
@@ -896,6 +901,23 @@ export type VoicePromptsState = Message<"fluxer.user.preferences.v1.VoicePrompts
export const VoicePromptsStateSchema: GenMessage<VoicePromptsState> = /*@__PURE__*/
messageDesc(file_fluxer_user_preferences_v1_preferences, 24);
/**
* @generated from message fluxer.user.preferences.v1.AnnouncementPromptsState
*/
export type AnnouncementPromptsState = Message<"fluxer.user.preferences.v1.AnnouncementPromptsState"> & {
/**
* @generated from field: bool hide_publish_nudge = 1;
*/
hidePublishNudge: boolean;
};
/**
* Describes the message fluxer.user.preferences.v1.AnnouncementPromptsState.
* Use `create(AnnouncementPromptsStateSchema)` to create a new message.
*/
export const AnnouncementPromptsStateSchema: GenMessage<AnnouncementPromptsState> = /*@__PURE__*/
messageDesc(file_fluxer_user_preferences_v1_preferences, 25);
/**
* @generated from message fluxer.user.preferences.v1.SudoPromptState
*/
@@ -911,7 +933,7 @@ export type SudoPromptState = Message<"fluxer.user.preferences.v1.SudoPromptStat
* Use `create(SudoPromptStateSchema)` to create a new message.
*/
export const SudoPromptStateSchema: GenMessage<SudoPromptState> = /*@__PURE__*/
messageDesc(file_fluxer_user_preferences_v1_preferences, 25);
messageDesc(file_fluxer_user_preferences_v1_preferences, 26);
/**
* @generated from message fluxer.user.preferences.v1.KeybindSettings
@@ -938,7 +960,7 @@ export type KeybindSettings = Message<"fluxer.user.preferences.v1.KeybindSetting
* Use `create(KeybindSettingsSchema)` to create a new message.
*/
export const KeybindSettingsSchema: GenMessage<KeybindSettings> = /*@__PURE__*/
messageDesc(file_fluxer_user_preferences_v1_preferences, 26);
messageDesc(file_fluxer_user_preferences_v1_preferences, 27);
/**
* @generated from message fluxer.user.preferences.v1.CustomKeybind
@@ -970,7 +992,7 @@ export type CustomKeybind = Message<"fluxer.user.preferences.v1.CustomKeybind">
* Use `create(CustomKeybindSchema)` to create a new message.
*/
export const CustomKeybindSchema: GenMessage<CustomKeybind> = /*@__PURE__*/
messageDesc(file_fluxer_user_preferences_v1_preferences, 27);
messageDesc(file_fluxer_user_preferences_v1_preferences, 28);
/**
* @generated from message fluxer.user.preferences.v1.KeybindCombo
@@ -1047,7 +1069,7 @@ export type KeybindCombo = Message<"fluxer.user.preferences.v1.KeybindCombo"> &
* Use `create(KeybindComboSchema)` to create a new message.
*/
export const KeybindComboSchema: GenMessage<KeybindCombo> = /*@__PURE__*/
messageDesc(file_fluxer_user_preferences_v1_preferences, 28);
messageDesc(file_fluxer_user_preferences_v1_preferences, 29);
/**
* @generated from message fluxer.user.preferences.v1.ChannelFrecencyEntry
@@ -1069,7 +1091,7 @@ export type ChannelFrecencyEntry = Message<"fluxer.user.preferences.v1.ChannelFr
* Use `create(ChannelFrecencyEntrySchema)` to create a new message.
*/
export const ChannelFrecencyEntrySchema: GenMessage<ChannelFrecencyEntry> = /*@__PURE__*/
messageDesc(file_fluxer_user_preferences_v1_preferences, 29);
messageDesc(file_fluxer_user_preferences_v1_preferences, 30);
/**
* @generated from message fluxer.user.preferences.v1.ChannelFrecencyState
@@ -1086,7 +1108,7 @@ export type ChannelFrecencyState = Message<"fluxer.user.preferences.v1.ChannelFr
* Use `create(ChannelFrecencyStateSchema)` to create a new message.
*/
export const ChannelFrecencyStateSchema: GenMessage<ChannelFrecencyState> = /*@__PURE__*/
messageDesc(file_fluxer_user_preferences_v1_preferences, 30);
messageDesc(file_fluxer_user_preferences_v1_preferences, 31);
/**
* @generated from message fluxer.user.preferences.v1.ChatInputSettings
@@ -1103,7 +1125,7 @@ export type ChatInputSettings = Message<"fluxer.user.preferences.v1.ChatInputSet
* Use `create(ChatInputSettingsSchema)` to create a new message.
*/
export const ChatInputSettingsSchema: GenMessage<ChatInputSettings> = /*@__PURE__*/
messageDesc(file_fluxer_user_preferences_v1_preferences, 31);
messageDesc(file_fluxer_user_preferences_v1_preferences, 32);
/**
* @generated from message fluxer.user.preferences.v1.ReactionEmoji
@@ -1125,7 +1147,7 @@ export type ReactionEmoji = Message<"fluxer.user.preferences.v1.ReactionEmoji">
* Use `create(ReactionEmojiSchema)` to create a new message.
*/
export const ReactionEmojiSchema: GenMessage<ReactionEmoji> = /*@__PURE__*/
messageDesc(file_fluxer_user_preferences_v1_preferences, 32);
messageDesc(file_fluxer_user_preferences_v1_preferences, 33);
/**
* @generated from enum fluxer.user.preferences.v1.PermissionLayoutMode
@@ -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',