import { ChannelFlags, ChannelType, VideoQualityMode, type APIDMChannel, type APIGuildCategoryChannel, type APIGuildForumChannel, type APIGuildMediaChannel, type APIGuildStageVoiceChannel, type APIGuildVoiceChannel, type APINewsChannel, type APITextChannel, type APIThreadChannel, type ThreadAutoArchiveDuration, } from 'discord-api-types/v10'; import { mix } from 'ts-mixer'; import { Embed, resolveAttachment } from '../builders'; import type { BaseClient } from '../client/base'; import type { APIChannelBase, APIGuildChannel, APIGuildForumDefaultReactionEmoji, APIGuildForumTag, EmojiResolvable, MessageCreateBodyRequest, MessageUpdateBodyRequest, MethodContext, ObjectToLower, RESTGetAPIChannelMessageReactionUsersQuery, RESTPatchAPIChannelJSONBody, RESTPatchAPIGuildChannelPositionsJSONBody, RESTPostAPIChannelWebhookJSONBody, RESTPostAPIGuildChannelJSONBody, SortOrderType, StringToNumber, ToClass, } from '../common'; import { ComponentsListener } from '../components'; import type { GuildMember } from './GuildMember'; import type { GuildRole } from './GuildRole'; import { Webhook } from './Webhook'; import { DiscordBase } from './extra/DiscordBase'; import { channelLink } from './extra/functions'; export class BaseChannel extends DiscordBase> { declare type: T; constructor(client: BaseClient, data: APIChannelBase) { super(client, data); } static __intent__(id: '@me'): 'DirectMessages'; static __intent__(id: string): 'DirectMessages' | 'Guilds'; static __intent__(id: string) { return id === '@me' ? 'DirectMessages' : 'Guilds'; } /** The URL to the channel */ get url() { return channelLink(this.id); } fetch(force = false) { return this.client.channels.fetch(this.id, force); } delete(reason?: string) { return this.client.channels.delete(this.id, { reason }); } edit(body: RESTPatchAPIChannelJSONBody, reason?: string) { return this.client.channels.edit(this.id, body, { reason, guildId: 'guildId' in this ? (this.guildId as string) : '@me', }); } toString() { return `<#${this.id}>`; } isStage(): this is StageChannel { return this.type === ChannelType.GuildStageVoice; } isMedia(): this is MediaChannel { return this.type === ChannelType.GuildMedia; } isDM(): this is DMChannel { return [ChannelType.DM, ChannelType.GroupDM].includes(this.type); } isForum(): this is ForumChannel { return this.type === ChannelType.GuildForum; } isThread(): this is ThreadChannel { return [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.AnnouncementThread].includes(this.type); } isDirectory(): this is DirectoryChannel { return this.type === ChannelType.GuildDirectory; } isVoice(): this is VoiceChannel { return this.type === ChannelType.GuildVoice; } isTextGuild(): this is TextGuildChannel { return this.type === ChannelType.GuildText; } isCategory(): this is CategoryChannel { return this.type === ChannelType.GuildCategory; } isNews(): this is NewsChannel { return this.type === ChannelType.GuildAnnouncement; } isTextable(): this is AllTextableChannels { return 'messages' in this; } isGuildTextable(): this is AllGuildTextableChannels { return !this.isDM() && this.isTextable(); } isThreadOnly(): this is ForumChannel | MediaChannel { return this.isForum() || this.isMedia(); } is(channelTypes: T): this is IChannelTypes[T[number]] { return channelTypes.some(x => ChannelType[x] === this.type); } static allMethods(ctx: MethodContext<{ guildId: string }>) { return { list: (force = false) => ctx.client.guilds.channels.list(ctx.guildId, force), fetch: (id: string, force = false) => ctx.client.guilds.channels.fetch(ctx.guildId, id, force), create: (body: RESTPostAPIGuildChannelJSONBody) => ctx.client.guilds.channels.create(ctx.guildId, body), delete: (id: string, reason?: string) => ctx.client.guilds.channels.delete(ctx.guildId, id, reason), edit: (id: string, body: RESTPatchAPIChannelJSONBody, reason?: string) => ctx.client.guilds.channels.edit(ctx.guildId, id, body, reason), editPositions: (body: RESTPatchAPIGuildChannelPositionsJSONBody) => ctx.client.guilds.channels.editPositions(ctx.guildId, body), }; } } interface IChannelTypes { GuildStageVoice: StageChannel; GuildMedia: MediaChannel; DM: DMChannel; GuildForum: ForumChannel; AnnouncementThread: ThreadChannel; PrivateThread: ThreadChannel; PublicThread: ThreadChannel; GuildDirectory: DirectoryChannel; GuildVoice: VoiceChannel; GuildText: TextGuildChannel; GuildCategory: CategoryChannel; GuildAnnouncement: NewsChannel; } export interface BaseGuildChannel extends ObjectToLower, 'permission_overwrites'>> {} export class BaseGuildChannel extends BaseChannel { constructor(client: BaseClient, data: APIGuildChannel) { const { permission_overwrites, ...rest } = data; super(client, rest); } permissionOverwrites = { fetch: () => this.client.cache.overwrites?.get(this.id), values: () => (this.guildId ? this.client.cache.overwrites?.values(this.guildId) ?? [] : []), }; memberPermissions(member: GuildMember, checkAdmin = true) { return this.client.channels.overwrites.memberPermissions(this.id, member, checkAdmin); } rolePermissions(role: GuildRole, checkAdmin = true) { return this.client.channels.overwrites.rolePermissions(this.id, role, checkAdmin); } overwritesFor(member: GuildMember) { return this.client.channels.overwrites.overwritesFor(this.id, member); } guild(force = false) { return this.client.guilds.fetch(this.guildId!, force); } get url() { return channelLink(this.id, this.guildId); } setPosition(position: number, reason?: string) { return this.edit({ position }, reason); } setName(name: string, reason?: string) { return this.edit({ name }, reason); } setParent(parent_id: string | null, reason?: string) { return this.edit({ parent_id }, reason); } } export interface MessagesMethods extends BaseChannel {} export class MessagesMethods extends DiscordBase { typing() { return this.client.channels.typing(this.id); } messages = MessagesMethods.messages({ client: this.client, channelId: this.id }); pins = MessagesMethods.pins({ client: this.client, channelId: this.id }); reactions = MessagesMethods.reactions({ client: this.client, channelId: this.id }); static messages(ctx: MethodContext<{ channelId: string }>) { return { write: (body: MessageCreateBodyRequest) => ctx.client.messages.write(ctx.channelId, body), edit: (messageId: string, body: MessageUpdateBodyRequest) => ctx.client.messages.edit(messageId, ctx.channelId, body), crosspost: (messageId: string, reason?: string) => ctx.client.messages.crosspost(messageId, ctx.channelId, reason), delete: (messageId: string, reason?: string) => ctx.client.messages.delete(messageId, ctx.channelId, reason), fetch: (messageId: string) => ctx.client.messages.fetch(messageId, ctx.channelId), purge: (messages: string[], reason?: string) => ctx.client.messages.purge(messages, ctx.channelId, reason), }; } static reactions(ctx: MethodContext<{ channelId: string }>) { return { add: (messageId: string, emoji: EmojiResolvable) => ctx.client.messages.reactions.add(messageId, ctx.channelId, emoji), delete: (messageId: string, emoji: EmojiResolvable, userId = '@me') => ctx.client.messages.reactions.delete(messageId, ctx.channelId, emoji, userId), fetch: (messageId: string, emoji: EmojiResolvable, query?: RESTGetAPIChannelMessageReactionUsersQuery) => ctx.client.messages.reactions.fetch(messageId, ctx.channelId, emoji, query), purge: (messageId: string, emoji?: EmojiResolvable) => ctx.client.messages.reactions.purge(messageId, ctx.channelId, emoji), }; } static pins(ctx: MethodContext<{ channelId: string }>) { return { fetch: () => ctx.client.channels.pins.fetch(ctx.channelId), set: (messageId: string, reason?: string) => ctx.client.channels.pins.set(messageId, ctx.channelId, reason), delete: (messageId: string, reason?: string) => ctx.client.channels.pins.delete(messageId, ctx.channelId, reason), }; } static transformMessageBody(body: MessageCreateBodyRequest | MessageUpdateBodyRequest) { return { ...body, components: body.components ? (body?.components instanceof ComponentsListener ? body.components.components : body.components).map(x => 'toJSON' in x ? x.toJSON() : x, ) : undefined, embeds: body.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)) ?? undefined, //? attachments: body.attachments?.map((x, i) => ({ id: i, ...resolveAttachment(x) })) ?? undefined, } as T; } } export interface TextBaseGuildChannel extends ObjectToLower>, MessagesMethods {} @mix(MessagesMethods) export class TextBaseGuildChannel extends BaseGuildChannel {} export default function channelFrom(data: APIChannelBase, client: BaseClient): AllChannels { switch (data.type) { case ChannelType.GuildStageVoice: return new StageChannel(client, data); case ChannelType.GuildMedia: return new MediaChannel(client, data); case ChannelType.DM: return new DMChannel(client, data); case ChannelType.GuildForum: return new ForumChannel(client, data); case ChannelType.AnnouncementThread: case ChannelType.PrivateThread: case ChannelType.PublicThread: return new ThreadChannel(client, data); case ChannelType.GuildDirectory: return new DirectoryChannel(client, data); case ChannelType.GuildVoice: return new VoiceChannel(client, data); case ChannelType.GuildText: return new TextGuildChannel(client, data as APIGuildChannel); case ChannelType.GuildCategory: return new CategoryChannel(client, data); case ChannelType.GuildAnnouncement: return new NewsChannel(client, data); default: if ('guild_id' in data) { return new BaseGuildChannel(client, data as APIGuildChannel); } return new BaseChannel(client, data); } } export interface TopicableGuildChannel extends BaseChannel {} export class TopicableGuildChannel extends DiscordBase { setTopic(topic: string | null, reason?: string) { return this.edit({ topic }, reason); } } export interface ThreadOnlyMethods extends BaseChannel, TopicableGuildChannel {} @mix(TopicableGuildChannel) export class ThreadOnlyMethods extends DiscordBase { setTags(tags: APIGuildForumTag[], reason?: string) { return this.edit({ available_tags: tags }, reason); } setAutoArchiveDuration(duration: ThreadAutoArchiveDuration, reason?: string) { return this.edit({ default_auto_archive_duration: duration }, reason); } setReactionEmoji(emoji: APIGuildForumDefaultReactionEmoji, reason?: string) { return this.edit({ default_reaction_emoji: emoji }, reason); } setSortOrder(sort: SortOrderType, reason?: string) { return this.edit({ default_sort_order: sort }, reason); } setThreadRateLimit(rate: number, reason?: string) { return this.edit({ default_thread_rate_limit_per_user: rate }, reason); } } export interface VoiceChannelMethods extends BaseChannel {} export class VoiceChannelMethods extends DiscordBase { setBitrate(bitrate: number | null, reason?: string) { return this.edit({ bitrate }, reason); } setUserLimit(user_limit: number | null, reason?: string) { return this.edit({ user_limit: user_limit ?? 0 }, reason); } setRTC(rtc_region: string | null, reason?: string) { return this.edit({ rtc_region }, reason); } setVideoQuality(quality: keyof typeof VideoQualityMode, reason?: string) { return this.edit({ video_quality_mode: VideoQualityMode[quality] }, reason); } } export class WebhookGuildMethods extends DiscordBase { webhooks = WebhookGuildMethods.guild({ client: this.client, guildId: this.id }); static guild(ctx: MethodContext<{ guildId: string }>) { return { list: async () => { const webhooks = await ctx.client.proxy.guilds(ctx.guildId).webhooks.get(); return webhooks.map(webhook => new Webhook(ctx.client, webhook)); }, }; } } export class WebhookChannelMethods extends DiscordBase { webhooks = WebhookChannelMethods.channel({ client: this.client, channelId: this.id }); static channel(ctx: MethodContext<{ channelId: string }>) { return { list: async () => { const webhooks = await ctx.client.proxy.channels(ctx.channelId).webhooks.get(); return webhooks.map(webhook => new Webhook(ctx.client, webhook)); }, create: async (body: RESTPostAPIChannelWebhookJSONBody) => { const webhook = await ctx.client.proxy.channels(ctx.channelId).webhooks.post({ body, }); return new Webhook(ctx.client, webhook); }, }; } } export interface TextGuildChannel extends ObjectToLower>, BaseGuildChannel, TextBaseGuildChannel, WebhookChannelMethods {} @mix(TextBaseGuildChannel, WebhookChannelMethods) export class TextGuildChannel extends BaseGuildChannel { declare type: ChannelType.GuildText; setRatelimitPerUser(rate_limit_per_user: number | null | undefined) { return this.edit({ rate_limit_per_user }); } setNsfw(nsfw = true, reason?: string) { return this.edit({ nsfw }, reason); } } export interface DMChannel extends ObjectToLower, Omit {} @mix(MessagesMethods) export class DMChannel extends (BaseChannel as unknown as ToClass< Omit, 'edit'>, DMChannel >) { declare type: ChannelType.DM; } export interface VoiceChannel extends ObjectToLower>, Omit, VoiceChannelMethods, WebhookChannelMethods {} @mix(TextGuildChannel, WebhookChannelMethods, VoiceChannelMethods) export class VoiceChannel extends BaseChannel { declare type: ChannelType.GuildVoice; } export interface StageChannel extends ObjectToLower>, TopicableGuildChannel, VoiceChannelMethods {} @mix(TopicableGuildChannel, VoiceChannelMethods) export class StageChannel extends BaseChannel { declare type: ChannelType.GuildStageVoice; } export interface MediaChannel extends ObjectToLower>, ThreadOnlyMethods {} @mix(ThreadOnlyMethods) export class MediaChannel extends BaseChannel { declare type: ChannelType.GuildMedia; } export interface ForumChannel extends ObjectToLower, Omit, WebhookChannelMethods {} @mix(ThreadOnlyMethods, WebhookChannelMethods) export class ForumChannel extends BaseChannel { declare type: ChannelType.GuildForum; } export interface ThreadChannel extends ObjectToLower>, TextBaseGuildChannel {} @mix(TextBaseGuildChannel) export class ThreadChannel extends BaseChannel< ChannelType.PublicThread | ChannelType.AnnouncementThread | ChannelType.PrivateThread > { declare type: ChannelType.PublicThread | ChannelType.AnnouncementThread | ChannelType.PrivateThread; webhooks = WebhookChannelMethods.channel({ client: this.client, channelId: this.parentId!, }); setRatelimitPerUser(rate_limit_per_user: number | null | undefined) { return this.edit({ rate_limit_per_user }); } pin(reason?: string) { return this.edit({ flags: (this.flags ?? 0) | ChannelFlags.Pinned }, reason); } unpin(reason?: string) { return this.edit({ flags: (this.flags ?? 0) & ~ChannelFlags.Pinned }, reason); } setTags(applied_tags: string[], reason?: string) { /** * The available_tags field can be set when creating or updating a channel. * Which determines which tags can be set on individual threads within the thread's applied_tags field. */ return this.edit({ applied_tags }, reason); } setArchived(archived = true, reason?: string) { return this.edit({ archived }, reason); } setAutoArchiveDuration(auto_archive_duration: StringToNumber<`${ThreadAutoArchiveDuration}`>, reason?: string) { return this.edit({ auto_archive_duration }, reason); } setInvitable(invitable = true, reason?: string) { return this.edit({ invitable }, reason); } setLocked(locked = true, reason?: string) { return this.edit({ locked }, reason); } } export interface CategoryChannel extends ObjectToLower> {} export class CategoryChannel extends (BaseGuildChannel as unknown as ToClass< Omit, CategoryChannel >) { declare type: ChannelType.GuildCategory; } export interface NewsChannel extends ObjectToLower, WebhookChannelMethods {} @mix(WebhookChannelMethods) export class NewsChannel extends BaseChannel { declare type: ChannelType.GuildAnnouncement; addFollower(webhook_channel_id: string, reason?: string) { return this.api.channels(this.id).followers.post({ body: { webhook_channel_id, }, reason, }); } } export class DirectoryChannel extends BaseChannel {} export type AllGuildChannels = | TextGuildChannel | VoiceChannel | MediaChannel | ForumChannel | ThreadChannel | CategoryChannel | NewsChannel | DirectoryChannel | StageChannel; export type AllTextableChannels = TextGuildChannel | VoiceChannel | DMChannel | NewsChannel | ThreadChannel; export type AllGuildTextableChannels = TextGuildChannel | VoiceChannel | NewsChannel | ThreadChannel; export type AllChannels = | BaseChannel | BaseGuildChannel | TextGuildChannel | DMChannel | VoiceChannel | MediaChannel | ForumChannel | ThreadChannel | CategoryChannel | NewsChannel | DirectoryChannel | StageChannel;