From 484ed2c0f777c95e09607b39cab6809f880608fc Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sat, 2 Jul 2022 07:50:53 -0500 Subject: [PATCH 1/8] feat: components --- mod.ts | 8 +- structures/Component.ts | 41 ------- structures/Interaction.ts | 3 + structures/Message.ts | 5 + structures/components/ActionRowComponent.ts | 39 ++++++ structures/components/ButtonComponent.ts | 33 +++++ structures/components/Component.ts | 122 +++++++++++++++++++ structures/components/LinkButtonComponent.ts | 33 +++++ structures/components/SelectMenuComponent.ts | 39 ++++++ structures/components/TextInputComponent.ts | 38 ++++++ 10 files changed, 319 insertions(+), 42 deletions(-) delete mode 100644 structures/Component.ts create mode 100644 structures/components/ActionRowComponent.ts create mode 100644 structures/components/ButtonComponent.ts create mode 100644 structures/components/Component.ts create mode 100644 structures/components/LinkButtonComponent.ts create mode 100644 structures/components/SelectMenuComponent.ts create mode 100644 structures/components/TextInputComponent.ts diff --git a/mod.ts b/mod.ts index 8e72101..7262d9a 100644 --- a/mod.ts +++ b/mod.ts @@ -3,7 +3,6 @@ export * from "./structures/Attachment.ts"; export * from "./structures/Base.ts"; export * from "./structures/BaseGuild.ts"; export * from "./structures/BaseChannel.ts"; -export * from "./structures/Component.ts"; export * from "./structures/DMChannel.ts"; export * from "./structures/Embed.ts"; export * from "./structures/Emoji.ts"; @@ -25,6 +24,13 @@ export * from "./structures/VoiceChannel.ts"; export * from "./structures/WelcomeChannel.ts"; export * from "./structures/WelcomeScreen.ts"; +export * from "./structures/components/ActionRowComponent.ts"; +export * from "./structures/components/ButtonComponent.ts"; +export * from "./structures/components/Component.ts"; +export * from "./structures/components/LinkButtonComponent.ts"; +export * from "./structures/components/SelectMenuComponent.ts"; +export * from "./structures/components/TextInputComponent.ts"; + export * from "./session/Session.ts"; export * from "./util/shared/flags.ts"; diff --git a/structures/Component.ts b/structures/Component.ts deleted file mode 100644 index 02daff5..0000000 --- a/structures/Component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Session } from "../session/Session.ts"; -import type { DiscordComponent, MessageComponentTypes } from "../vendor/external.ts"; -import Emoji from "./Emoji.ts"; - -export class Component { - constructor(session: Session, data: DiscordComponent) { - this.session = session; - this.customId = data.custom_id; - this.type = data.type; - this.components = data.components?.map((component) => new Component(session, component)); - this.disabled = !!data.disabled; - - if (data.emoji) { - this.emoji = new Emoji(session, data.emoji); - } - - this.maxValues = data.max_values; - this.minValues = data.min_values; - this.label = data.label; - this.value = data.value; - this.options = data.options ?? []; - this.placeholder = data.placeholder; - } - - readonly session: Session; - - customId?: string; - type: MessageComponentTypes; - components?: Component[]; - disabled: boolean; - emoji?: Emoji; - maxValues?: number; - minValues?: number; - label?: string; - value?: string; - // deno-lint-ignore no-explicit-any - options: any[]; - placeholder?: string; -} - -export default Component; diff --git a/structures/Interaction.ts b/structures/Interaction.ts index f11dc99..820d6aa 100644 --- a/structures/Interaction.ts +++ b/structures/Interaction.ts @@ -38,6 +38,9 @@ export interface ApplicationCommandOptionChoice { value: string | number; } +// TODO: abstract Interaction, CommandInteraction, ComponentInteraction, PingInteraction, etc + + export class Interaction implements Model { constructor(session: Session, data: DiscordInteraction) { this.session = session; diff --git a/structures/Message.ts b/structures/Message.ts index 65f3c35..3d734d8 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -8,11 +8,13 @@ import type { DiscordUser, FileContent, } from "../vendor/external.ts"; +import type { Component } from "./components/Component.ts"; import type { GetReactions } from "../util/Routes.ts"; import { MessageFlags } from "../util/shared/flags.ts"; import User from "./User.ts"; import Member from "./Member.ts"; import Attachment from "./Attachment.ts"; +import BaseComponent from "./components/Component.ts"; import * as Routes from "../util/Routes.ts"; /** @@ -80,6 +82,8 @@ export class Message implements Model { if (data.guild_id && data.member) { this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id); } + + this.components = data.components?.map((component) => BaseComponent.from(session, component)); } readonly session: Session; @@ -95,6 +99,7 @@ export class Message implements Model { attachments: Attachment[]; member?: Member; + components?: Component[]; get url() { return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`; diff --git a/structures/components/ActionRowComponent.ts b/structures/components/ActionRowComponent.ts new file mode 100644 index 0000000..9976333 --- /dev/null +++ b/structures/components/ActionRowComponent.ts @@ -0,0 +1,39 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent } from "../../vendor/external.ts"; +import type { ActionRowComponent, Component } from "./Component.ts"; +import { MessageComponentTypes, ButtonStyles } from "../../vendor/external.ts"; +import BaseComponent from "./Component.ts"; +import Button from "./ButtonComponent.ts"; +import LinkButton from "./LinkButtonComponent.ts"; +import SelectMenu from "./SelectMenuComponent.ts"; +import InputText from "./TextInputComponent.ts"; + +export class ActionRow extends BaseComponent implements ActionRowComponent { + constructor(session: Session, data: DiscordComponent) { + super(data.type); + + this.session = session; + this.type = data.type as MessageComponentTypes.ActionRow; + this.components = data.components!.map((component) => { + switch (component.type) { + case MessageComponentTypes.Button: + if (component.style === ButtonStyles.Link) { + return new LinkButton(session, component); + } + return new Button(session, component); + case MessageComponentTypes.SelectMenu: + return new SelectMenu(session, component); + case MessageComponentTypes.InputText: + return new InputText(session, component); + case MessageComponentTypes.ActionRow: + throw new Error("Cannot have an action row inside an action row"); + } + }); + } + + readonly session: Session; + override type: MessageComponentTypes.ActionRow; + components: Array>; +} + +export default ActionRow; diff --git a/structures/components/ButtonComponent.ts b/structures/components/ButtonComponent.ts new file mode 100644 index 0000000..c0a47ab --- /dev/null +++ b/structures/components/ButtonComponent.ts @@ -0,0 +1,33 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent, ButtonStyles } from "../../vendor/external.ts"; +import type { ButtonComponent } from "./Component.ts"; +import { MessageComponentTypes } from "../../vendor/external.ts"; +import BaseComponent from "./Component.ts"; +import Emoji from "../Emoji.ts"; + +export class Button extends BaseComponent implements ButtonComponent { + constructor(session: Session, data: DiscordComponent) { + super(data.type); + + this.session = session; + this.type = data.type as MessageComponentTypes.Button; + this.customId = data.custom_id; + this.label = data.label; + this.style = data.style as number; + this.disabled = data.disabled; + + if (data.emoji) { + this.emoji = new Emoji(session, data.emoji); + } + } + + readonly session: Session; + override type: MessageComponentTypes.Button; + customId?: string; + label?: string; + style: ButtonStyles.Primary | ButtonStyles.Secondary | ButtonStyles.Success | ButtonStyles.Danger; + disabled?: boolean; + emoji?: Emoji; +} + +export default Button; diff --git a/structures/components/Component.ts b/structures/components/Component.ts new file mode 100644 index 0000000..4bb2cf0 --- /dev/null +++ b/structures/components/Component.ts @@ -0,0 +1,122 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent } from "../../vendor/external.ts"; +import type Emoji from "../Emoji.ts"; +import { ButtonStyles, MessageComponentTypes, TextStyles } from "../../vendor/external.ts"; + +// TODO: fix circular dependencies +import ActionRow from "./ActionRowComponent.ts"; +import Button from "./ButtonComponent.ts"; +import LinkButton from "./ButtonComponent.ts"; +import SelectMenu from "./SelectMenuComponent.ts"; +import TextInput from "./TextInputComponent.ts"; + +export class BaseComponent { + constructor(type: MessageComponentTypes) { + this.type = type; + } + + type: MessageComponentTypes; + + isActionRow(): this is ActionRowComponent { + return this.type === MessageComponentTypes.ActionRow; + } + + isButton(): this is ButtonComponent { + return this.type === MessageComponentTypes.Button; + } + + isSelectMenu(): this is SelectMenuComponent { + return this.type === MessageComponentTypes.SelectMenu; + } + + isTextInput(): this is TextInputComponent { + return this.type === MessageComponentTypes.InputText; + } + + /** + * Component factory + * @internal + * */ + static from(session: Session, component: DiscordComponent): Component { + switch (component.type) { + case MessageComponentTypes.ActionRow: + return new ActionRow(session, component); + case MessageComponentTypes.Button: + if (component.style === ButtonStyles.Link) { + return new LinkButton(session, component); + } + return new Button(session, component); + case MessageComponentTypes.SelectMenu: + return new SelectMenu(session, component); + case MessageComponentTypes.InputText: + return new TextInput(session, component); + } + } +} + +/** Action Row Component */ +export interface ActionRowComponent { + type: MessageComponentTypes.ActionRow; + components: Array>; +} + +/** All Components */ +export type Component = + | ActionRowComponent + | ButtonComponent + | LinkButtonComponent + | SelectMenuComponent + | TextInputComponent; + +/** Button Component */ +export interface ButtonComponent { + type: MessageComponentTypes.Button; + style: ButtonStyles.Primary | ButtonStyles.Secondary | ButtonStyles.Success | ButtonStyles.Danger; + label?: string; + emoji?: Emoji; + customId?: string; + disabled?: boolean; +} + +/** Link Button Component */ +export interface LinkButtonComponent { + type: MessageComponentTypes.Button; + style: ButtonStyles.Link; + label?: string; + url: string; + disabled?: boolean; +} + +/** Select Menu Component */ +export interface SelectMenuComponent { + type: MessageComponentTypes.SelectMenu; + customId: string; + options: SelectMenuOption[]; + placeholder?: string; + minValue?: number; + maxValue?: number; + disabled?: boolean; +} + +/** Text Input Component */ +export interface TextInputComponent { + type: MessageComponentTypes.InputText; + customId: string; + style: TextStyles; + label: string; + minLength?: number; + maxLength?: number; + required?: boolean; + value?: string; + placeholder?: string; +} + +export interface SelectMenuOption { + label: string; + value: string; + description?: string; + emoji?: Emoji; + default?: boolean; +} + +export default BaseComponent; diff --git a/structures/components/LinkButtonComponent.ts b/structures/components/LinkButtonComponent.ts new file mode 100644 index 0000000..65b01cb --- /dev/null +++ b/structures/components/LinkButtonComponent.ts @@ -0,0 +1,33 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent, ButtonStyles } from "../../vendor/external.ts"; +import type { LinkButtonComponent } from "./Component.ts"; +import { MessageComponentTypes } from "../../vendor/external.ts"; +import BaseComponent from "./Component.ts"; +import Emoji from "../Emoji.ts"; + +export class LinkButton extends BaseComponent implements LinkButtonComponent { + constructor(session: Session, data: DiscordComponent) { + super(data.type); + + this.session = session; + this.type = data.type as MessageComponentTypes.Button; + this.url = data.url!; + this.label = data.label; + this.style = data.style as number; + this.disabled = data.disabled; + + if (data.emoji) { + this.emoji = new Emoji(session, data.emoji); + } + } + + readonly session: Session; + override type: MessageComponentTypes.Button; + url: string; + label?: string; + style: ButtonStyles.Link; + disabled?: boolean; + emoji?: Emoji; +} + +export default LinkButton; diff --git a/structures/components/SelectMenuComponent.ts b/structures/components/SelectMenuComponent.ts new file mode 100644 index 0000000..40f61de --- /dev/null +++ b/structures/components/SelectMenuComponent.ts @@ -0,0 +1,39 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent } from "../../vendor/external.ts"; +import type { SelectMenuComponent, SelectMenuOption } from "./Component.ts"; +import { MessageComponentTypes } from "../../vendor/external.ts"; +import BaseComponent from "./Component.ts"; +import Emoji from "../Emoji.ts"; + +export class SelectMenu extends BaseComponent implements SelectMenuComponent { + constructor(session: Session, data: DiscordComponent) { + super(data.type); + + this.session = session; + this.type = data.type as MessageComponentTypes.SelectMenu; + this.customId = data.custom_id!; + this.options = data.options!.map((option) => { + return { + label: option.label, + description: option.description, + emoji: option.emoji || new Emoji(session, option.emoji!), + value: option.value, + }; + }); + this.placeholder = data.placeholder; + this.minValues = data.min_values; + this.maxValues = data.max_values; + this.disabled = data.disabled; + } + + readonly session: Session; + override type: MessageComponentTypes.SelectMenu; + customId: string; + options: SelectMenuOption[]; + placeholder?: string; + minValues?: number; + maxValues?: number; + disabled?: boolean; +} + +export default SelectMenu; diff --git a/structures/components/TextInputComponent.ts b/structures/components/TextInputComponent.ts new file mode 100644 index 0000000..27abfa3 --- /dev/null +++ b/structures/components/TextInputComponent.ts @@ -0,0 +1,38 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent } from "../../vendor/external.ts"; +import type { TextInputComponent } from "./Component.ts"; +import { MessageComponentTypes, TextStyles } from "../../vendor/external.ts"; +import BaseComponent from "./Component.ts"; + +export class TextInput extends BaseComponent implements TextInputComponent { + constructor(session: Session, data: DiscordComponent) { + super(data.type); + + this.session = session; + this.type = data.type as MessageComponentTypes.InputText; + this.customId = data.custom_id!; + this.label = data.label!; + this.style = data.style as TextStyles; + + this.placeholder = data.placeholder; + this.value = data.value; + + // @ts-ignore: vendor bug + this.minLength = data.min_length; + + // @ts-ignore: vendor bug + this.maxLength = data.max_length; + } + + readonly session: Session; + override type: MessageComponentTypes.InputText; + style: TextStyles; + customId: string; + label: string; + placeholder?: string; + value?: string; + minLength?: number; + maxLength?: number; +} + +export default TextInput; From 36acf92ad1182b1a156194c1dc2ec7e58af5b16c Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sat, 2 Jul 2022 08:52:54 -0500 Subject: [PATCH 2/8] refactor: channels --- handlers/Actions.ts | 68 ++++++++++++++++++++++++++++++++++++++ structures/BaseChannel.ts | 41 +++++++++++++++++++---- structures/GuildChannel.ts | 2 +- structures/TextChannel.ts | 20 +++++++++++ 4 files changed, 123 insertions(+), 8 deletions(-) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 032853c..9d97992 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -1,14 +1,24 @@ import type { + DiscordChannel, + DiscordChannelPinsUpdate, DiscordGuildMemberAdd, DiscordGuildMemberRemove, DiscordGuildMemberUpdate, DiscordInteraction, + DiscordMemberWithUser, DiscordMessage, DiscordMessageDelete, DiscordReady, + // DiscordThreadMemberUpdate, + // DiscordThreadMembersUpdate, + DiscordThreadListSync, } from "../vendor/external.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; +import type { Channel } from "../structures/BaseChannel.ts"; +import BaseChannel from "../structures/BaseChannel.ts"; +import GuildChannel from "../structures/GuildChannel.ts"; +import ThreadChannel from "../structures/ThreadChannel.ts"; import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; @@ -56,6 +66,56 @@ export const INTERACTION_CREATE: RawHandler = (session, _sha session.emit("interactionCreate", new Interaction(session, interaction)); }; +export const CHANNEL_CREATE: RawHandler = (session, _shardId, channel) => { + session.emit("channelCreate", BaseChannel.from(session, channel)); +}; + +export const CHANNEL_UPDATE: RawHandler = (session, _shardId, channel) => { + session.emit("channelUpdate", BaseChannel.from(session, channel)); +}; + +export const CHANNEL_DELETE: RawHandler = (session, _shardId, channel) => { + if (!channel.guild_id) return; + + session.emit("channelDelete", new GuildChannel(session, channel, channel.guild_id)); +}; + +export const THREAD_CREATE: RawHandler = (session, _shardId, channel) => { + if (!channel.guild_id) return; + + session.emit("threadCreate", new ThreadChannel(session, channel, channel.guild_id)); +}; + +export const THREAD_UPDATE: RawHandler = (session, _shardId, channel) => { + if (!channel.guild_id) return; + + session.emit("threadUpdate", new ThreadChannel(session, channel, channel.guild_id)); +}; + +export const THREAD_DELETE: RawHandler = (session, _shardId, channel) => { + if (!channel.guild_id) return; + + session.emit("threadDelete", new ThreadChannel(session, channel, channel.guild_id)); +}; + +export const THREAD_LIST_SYNC: RawHandler = (session, _shardId, payload) => { + session.emit("threadListSync", { + guildId: payload.guild_id, + channelIds: payload.channel_ids ?? [], + threads: payload.threads.map((channel) => new ThreadChannel(session, channel, payload.guild_id)), + // @ts-ignore: TODO: thread member structure + members: payload.members.map((member) => new Member(session, member as DiscordMemberWithUser, payload.guild_id)), + }); +}; + +export const CHANNEL_PINS_UPDATE: RawHandler = (session, _shardId, payload) => { + session.emit("channelPinsUpdate", { + guildId: payload.guild_id, + channelId: payload.channel_id, + lastPinTimestamp: payload.last_pin_timestamp ? Date.parse(payload.last_pin_timestamp) : undefined, + }); +}; + export const raw: RawHandler = (session, shardId, data) => { session.emit("raw", data, shardId); }; @@ -73,6 +133,14 @@ export interface Events { "guildMemberAdd": Handler<[Member]>; "guildMemberUpdate": Handler<[Member]>; "guildMemberRemove": Handler<[User, Snowflake]>; + "channelCreate": Handler<[Channel]>; + "channelUpdate": Handler<[Channel]>; + "channelDelete": Handler<[GuildChannel]>; + "channelPinsUpdate": Handler<[{ guildId?: Snowflake, channelId: Snowflake, lastPinTimestamp?: number }]> + "threadCreate": Handler<[ThreadChannel]>; + "threadUpdate": Handler<[ThreadChannel]>; + "threadDelete": Handler<[ThreadChannel]>; + "threadListSync": Handler<[{ guildId: Snowflake, channelIds: Snowflake[], threads: ThreadChannel[], members: Member[] }]> "interactionCreate": Handler<[Interaction]>; "raw": Handler<[unknown, number]>; } diff --git a/structures/BaseChannel.ts b/structures/BaseChannel.ts index a0ed80b..f7814ae 100644 --- a/structures/BaseChannel.ts +++ b/structures/BaseChannel.ts @@ -1,13 +1,21 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { ChannelTypes, DiscordChannel } from "../vendor/external.ts"; -import TextChannel from "./TextChannel.ts"; +import type { DiscordChannel } from "../vendor/external.ts"; +import { ChannelTypes } from "../vendor/external.ts"; +import TextChannel, { textBasedChannels } from "./TextChannel.ts"; import VoiceChannel from "./VoiceChannel.ts"; import DMChannel from "./DMChannel.ts"; import NewsChannel from "./NewsChannel.ts"; import ThreadChannel from "./ThreadChannel.ts"; +export type Channel = + | TextChannel + | VoiceChannel + | DMChannel + | NewsChannel + | ThreadChannel; + export abstract class BaseChannel implements Model { constructor(session: Session, data: DiscordChannel) { this.id = data.id; @@ -22,23 +30,42 @@ export abstract class BaseChannel implements Model { type: ChannelTypes; isText(): this is TextChannel { - return this instanceof TextChannel; + return textBasedChannels.includes(this.type); } isVoice(): this is VoiceChannel { - return this instanceof VoiceChannel; + return this.type === ChannelTypes.GuildVoice; } isDM(): this is DMChannel { - return this instanceof DMChannel; + return this.type === ChannelTypes.DM; } isNews(): this is NewsChannel { - return this instanceof NewsChannel; + return this.type === ChannelTypes.GuildNews; } isThread(): this is ThreadChannel { - return this instanceof ThreadChannel; + return this.type === ChannelTypes.GuildPublicThread || this.type === ChannelTypes.GuildPrivateThread; + } + + static from(session: Session, channel: DiscordChannel): Channel { + switch (channel.type) { + case ChannelTypes.GuildPublicThread: + case ChannelTypes.GuildPrivateThread: + return new ThreadChannel(session, channel, channel.guild_id!); + case ChannelTypes.GuildNews: + return new NewsChannel(session, channel, channel.guild_id!); + case ChannelTypes.DM: + return new DMChannel(session, channel); + case ChannelTypes.GuildVoice: + return new VoiceChannel(session, channel, channel.guild_id!); + default: + if (textBasedChannels.includes(channel.type)) { + return new TextChannel(session, channel, channel.guild_id!); + } + throw new Error("Channel was not implemented"); + } } toString(): string { diff --git a/structures/GuildChannel.ts b/structures/GuildChannel.ts index 73dfd3b..2854425 100644 --- a/structures/GuildChannel.ts +++ b/structures/GuildChannel.ts @@ -6,7 +6,7 @@ import BaseChannel from "./BaseChannel.ts"; import Invite from "./Invite.ts"; import * as Routes from "../util/Routes.ts"; -export abstract class GuildChannel extends BaseChannel implements Model { +export class GuildChannel extends BaseChannel implements Model { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { super(session, data); this.guildId = guildId; diff --git a/structures/TextChannel.ts b/structures/TextChannel.ts index 7ecd39b..2491391 100644 --- a/structures/TextChannel.ts +++ b/structures/TextChannel.ts @@ -3,6 +3,7 @@ import type { Snowflake } from "../util/Snowflake.ts"; import type { GetMessagesOptions, GetReactions } from "../util/Routes.ts"; import type { DiscordChannel, DiscordInvite, DiscordMessage, TargetTypes } from "../vendor/external.ts"; import type { CreateMessage, EditMessage, ReactionResolvable } from "./Message.ts"; +import { ChannelTypes } from "../vendor/external.ts"; import GuildChannel from "./GuildChannel.ts"; import ThreadChannel from "./ThreadChannel.ts"; import Message from "./Message.ts"; @@ -36,15 +37,34 @@ export interface ThreadCreateOptions { reason?: string; } +export const textBasedChannels = [ + ChannelTypes.DM, + ChannelTypes.GroupDm, + ChannelTypes.GuildPrivateThread, + ChannelTypes.GuildPublicThread, + ChannelTypes.GuildNews, + ChannelTypes.GuildText, +]; + +export type TextBasedChannels = + | ChannelTypes.DM + | ChannelTypes.GroupDm + | ChannelTypes.GuildPrivateThread + | ChannelTypes.GuildPublicThread + | ChannelTypes.GuildNews + | ChannelTypes.GuildText; + export class TextChannel extends GuildChannel { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { super(session, data, guildId); data.last_message_id ? this.lastMessageId = data.last_message_id : undefined; data.last_pin_timestamp ? this.lastPinTimestamp = data.last_pin_timestamp : undefined; + this.type = data.type as TextBasedChannels; this.rateLimitPerUser = data.rate_limit_per_user ?? 0; this.nsfw = !!data.nsfw ?? false; } + override type: TextBasedChannels; lastMessageId?: Snowflake; lastPinTimestamp?: string; rateLimitPerUser: number; From a2b53ab51e1a721c8e0692743ad6ca76109f7c23 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sat, 2 Jul 2022 10:19:08 -0500 Subject: [PATCH 3/8] fix: TextChannel no longer extends GuildChannel --- structures/BaseChannel.ts | 2 +- structures/DMChannel.ts | 11 ++++---- structures/GuildChannel.ts | 51 +++++++++++++++++++++++++++++++++++- structures/NewsChannel.ts | 9 ++++--- structures/TextChannel.ts | 43 +++++++++--------------------- structures/Webhook.ts | 47 +++++++++++++++++++++++++++++++++ structures/WelcomeChannel.ts | 1 + util/Routes.ts | 4 +++ 8 files changed, 127 insertions(+), 41 deletions(-) create mode 100644 structures/Webhook.ts diff --git a/structures/BaseChannel.ts b/structures/BaseChannel.ts index f7814ae..7a39ab9 100644 --- a/structures/BaseChannel.ts +++ b/structures/BaseChannel.ts @@ -62,7 +62,7 @@ export abstract class BaseChannel implements Model { return new VoiceChannel(session, channel, channel.guild_id!); default: if (textBasedChannels.includes(channel.type)) { - return new TextChannel(session, channel, channel.guild_id!); + return new TextChannel(session, channel); } throw new Error("Channel was not implemented"); } diff --git a/structures/DMChannel.ts b/structures/DMChannel.ts index d13bd5d..1045c67 100644 --- a/structures/DMChannel.ts +++ b/structures/DMChannel.ts @@ -1,24 +1,23 @@ import type { Model } from "./Base.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { DiscordChannel } from "../vendor/external.ts"; -import BaseChannel from "./BaseChannel.ts"; +import type { ChannelTypes, DiscordChannel } from "../vendor/external.ts"; +import TextChannel from "./TextChannel.ts"; import User from "./User.ts"; import * as Routes from "../util/Routes.ts"; -export class DMChannel extends BaseChannel implements Model { +export class DMChannel extends TextChannel implements Model { constructor(session: Session, data: DiscordChannel) { super(session, data); this.user = new User(this.session, data.recipents!.find((r) => r.id !== this.session.botId)!); - + this.type = data.type as ChannelTypes.DM | ChannelTypes.GroupDm; if (data.last_message_id) { this.lastMessageId = data.last_message_id; } } + override type: ChannelTypes.DM | ChannelTypes.GroupDm; user: User; - lastMessageId?: Snowflake; async close() { const channel = await this.session.rest.runMethod( diff --git a/structures/GuildChannel.ts b/structures/GuildChannel.ts index 2854425..58065bc 100644 --- a/structures/GuildChannel.ts +++ b/structures/GuildChannel.ts @@ -1,20 +1,44 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { DiscordChannel, DiscordInviteMetadata } from "../vendor/external.ts"; +import type { ChannelTypes, DiscordChannel, DiscordWebhook, DiscordInviteMetadata } from "../vendor/external.ts"; +import { urlToBase64 } from "../util/urlToBase64.ts"; import BaseChannel from "./BaseChannel.ts"; import Invite from "./Invite.ts"; +import Webhook from "./Webhook.ts"; +import ThreadChannel from "./ThreadChannel.ts"; import * as Routes from "../util/Routes.ts"; +export interface CreateWebhook { + name: string; + avatar?: string; + reason?: string; +} + + +/** + * Represent the options object to create a Thread Channel + * @link https://discord.com/developers/docs/resources/channel#start-thread-without-message + */ +export interface ThreadCreateOptions { + name: string; + autoArchiveDuration: 60 | 1440 | 4320 | 10080; + type: 10 | 11 | 12; + invitable?: boolean; + reason?: string; +} + export class GuildChannel extends BaseChannel implements Model { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { super(session, data); + this.type = data.type as number this.guildId = guildId; this.position = data.position; data.topic ? this.topic = data.topic : null; data.parent_id ? this.parentId = data.parent_id : undefined; } + override type: Exclude; guildId: Snowflake; topic?: string; position?: number; @@ -40,6 +64,31 @@ export class GuildChannel extends BaseChannel implements Model { }, ); } + + async createThread(options: ThreadCreateOptions): Promise { + const thread = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_CREATE_THREAD(this.id), + options, + ); + return new ThreadChannel(this.session, thread, this.guildId); + } + + async createWebhook(options: CreateWebhook) { + const webhook = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_WEBHOOKS(this.id), + { + name: options.name, + avatar: options.avatar ? urlToBase64(options.avatar) : undefined, + reason: options.reason, + } + ); + + return new Webhook(this.session, webhook); + } } export default GuildChannel; diff --git a/structures/NewsChannel.ts b/structures/NewsChannel.ts index ef76049..e0b7dad 100644 --- a/structures/NewsChannel.ts +++ b/structures/NewsChannel.ts @@ -1,14 +1,17 @@ import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { DiscordChannel } from "../vendor/external.ts"; -import TextChannel from "./TextChannel.ts"; +import type { ChannelTypes, DiscordChannel } from "../vendor/external.ts"; +import GuildChannel from "./GuildChannel.ts"; import Message from "./Message.ts"; -export class NewsChannel extends TextChannel { +export class NewsChannel extends GuildChannel { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { super(session, data, guildId); + this.type = data.type as ChannelTypes.GuildNews; this.defaultAutoArchiveDuration = data.default_auto_archive_duration; } + + override type: ChannelTypes.GuildNews; defaultAutoArchiveDuration?: number; crosspostMessage(messageId: Snowflake): Promise { diff --git a/structures/TextChannel.ts b/structures/TextChannel.ts index 2491391..c2ea25f 100644 --- a/structures/TextChannel.ts +++ b/structures/TextChannel.ts @@ -4,8 +4,7 @@ import type { GetMessagesOptions, GetReactions } from "../util/Routes.ts"; import type { DiscordChannel, DiscordInvite, DiscordMessage, TargetTypes } from "../vendor/external.ts"; import type { CreateMessage, EditMessage, ReactionResolvable } from "./Message.ts"; import { ChannelTypes } from "../vendor/external.ts"; -import GuildChannel from "./GuildChannel.ts"; -import ThreadChannel from "./ThreadChannel.ts"; +import BaseChannel from "./BaseChannel.ts"; import Message from "./Message.ts"; import Invite from "./Invite.ts"; import * as Routes from "../util/Routes.ts"; @@ -25,18 +24,6 @@ export interface DiscordInviteOptions { targetApplicationId?: Snowflake; } -/** - * Represent the options object to create a Thread Channel - * @link https://discord.com/developers/docs/resources/channel#start-thread-without-message - */ -export interface ThreadCreateOptions { - name: string; - autoArchiveDuration: 60 | 1440 | 4320 | 10080; - type: 10 | 11 | 12; - invitable?: boolean; - reason?: string; -} - export const textBasedChannels = [ ChannelTypes.DM, ChannelTypes.GroupDm, @@ -54,14 +41,20 @@ export type TextBasedChannels = | ChannelTypes.GuildNews | ChannelTypes.GuildText; -export class TextChannel extends GuildChannel { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - data.last_message_id ? this.lastMessageId = data.last_message_id : undefined; - data.last_pin_timestamp ? this.lastPinTimestamp = data.last_pin_timestamp : undefined; - this.type = data.type as TextBasedChannels; +export class TextChannel extends BaseChannel { + constructor(session: Session, data: DiscordChannel) { + super(session, data); + this.type = data.type as number; this.rateLimitPerUser = data.rate_limit_per_user ?? 0; this.nsfw = !!data.nsfw ?? false; + + if (data.last_message_id) { + this.lastMessageId = data.last_message_id; + } + + if (data.last_pin_timestamp) { + this.lastPinTimestamp = data.last_pin_timestamp; + } } override type: TextBasedChannels; @@ -100,16 +93,6 @@ export class TextChannel extends GuildChannel { return new Invite(this.session, invite); } - async createThread(options: ThreadCreateOptions): Promise { - const thread = await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_CREATE_THREAD(this.id), - options, - ); - return new ThreadChannel(this.session, thread, this.guildId); - } - async fetchMessages(options?: GetMessagesOptions): Promise { if (options?.limit! > 100) throw Error("Values must be between 0-100"); const messages = await this.session.rest.runMethod( diff --git a/structures/Webhook.ts b/structures/Webhook.ts new file mode 100644 index 0000000..6a85a22 --- /dev/null +++ b/structures/Webhook.ts @@ -0,0 +1,47 @@ +import type { Model } from "./Base.ts"; +import type { Session } from "../session/Session.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { DiscordWebhook, WebhookTypes } from "../vendor/external.ts"; +import { iconHashToBigInt } from "../util/hash.ts"; +import User from "./User.ts"; + +export class Webhook implements Model { + constructor(session: Session, data: DiscordWebhook) { + this.session = session; + this.id = data.id; + this.type = data.type; + this.token = data.token; + + if (data.avatar) { + this.avatar = iconHashToBigInt(data.avatar); + } + + if (data.user) { + this.user = new User(session, data.user); + } + + if (data.guild_id) { + this.guildId = data.guild_id; + } + + if (data.channel_id) { + this.channelId = data.channel_id; + } + + if (data.application_id) { + this.applicationId = data.application_id; + } + } + + readonly session: Session; + readonly id: Snowflake; + type: WebhookTypes; + token?: string; + avatar?: bigint; + applicationId?: Snowflake; + channelId?: Snowflake; + guildId?: Snowflake; + user?: User; +} + +export default Webhook; diff --git a/structures/WelcomeChannel.ts b/structures/WelcomeChannel.ts index e540207..b1bcb5d 100644 --- a/structures/WelcomeChannel.ts +++ b/structures/WelcomeChannel.ts @@ -5,6 +5,7 @@ import type { DiscordWelcomeScreenChannel } from "../vendor/external.ts"; import Emoji from "./Emoji.ts"; /** + * Not a channel * @link https://discord.com/developers/docs/resources/guild#welcome-screen-object-welcome-screen-channel-structure */ export class WelcomeChannel implements Model { diff --git a/util/Routes.ts b/util/Routes.ts index db2654e..247cadd 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -224,3 +224,7 @@ export function CHANNEL_MESSAGE_CROSSPOST(channelId: Snowflake, messageId: Snowf export function GUILD_MEMBER_ROLE(guildId: Snowflake, memberId: Snowflake, roleId: Snowflake) { return `/guilds/${guildId}/members/${memberId}/roles/${roleId}`; } + +export function CHANNEL_WEBHOOKS(channelId: Snowflake) { + return `/channels/${channelId}/webhooks`; +} From 503036188485cd465d241a5e7eb6ac3b6b4d125f Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sat, 2 Jul 2022 11:05:18 -0500 Subject: [PATCH 4/8] refactor: circular imports solved --- handlers/Actions.ts | 6 +-- structures/BaseChannel.ts | 39 +++------------ structures/ChannelFactory.ts | 39 +++++++++++++++ structures/DMChannel.ts | 9 +++- structures/GuildChannel.ts | 50 +++++-------------- structures/NewsChannel.ts | 5 ++ structures/TextChannel.ts | 59 +++++++++++++++++++++-- structures/ThreadChannel.ts | 10 +++- structures/components/Component.ts | 28 ----------- structures/components/ComponentFactory.ts | 33 +++++++++++++ 10 files changed, 172 insertions(+), 106 deletions(-) create mode 100644 structures/ChannelFactory.ts create mode 100644 structures/components/ComponentFactory.ts diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 9d97992..d931398 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -16,7 +16,7 @@ import type { import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { Channel } from "../structures/BaseChannel.ts"; -import BaseChannel from "../structures/BaseChannel.ts"; +import ChannelFactory from "../structures/ChannelFactory.ts"; import GuildChannel from "../structures/GuildChannel.ts"; import ThreadChannel from "../structures/ThreadChannel.ts"; import Member from "../structures/Member.ts"; @@ -67,11 +67,11 @@ export const INTERACTION_CREATE: RawHandler = (session, _sha }; export const CHANNEL_CREATE: RawHandler = (session, _shardId, channel) => { - session.emit("channelCreate", BaseChannel.from(session, channel)); + session.emit("channelCreate", ChannelFactory.from(session, channel)); }; export const CHANNEL_UPDATE: RawHandler = (session, _shardId, channel) => { - session.emit("channelUpdate", BaseChannel.from(session, channel)); + session.emit("channelUpdate", ChannelFactory.from(session, channel)); }; export const CHANNEL_DELETE: RawHandler = (session, _shardId, channel) => { diff --git a/structures/BaseChannel.ts b/structures/BaseChannel.ts index 7a39ab9..7c9602d 100644 --- a/structures/BaseChannel.ts +++ b/structures/BaseChannel.ts @@ -2,19 +2,13 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { DiscordChannel } from "../vendor/external.ts"; +import type TextChannel from "./TextChannel.ts"; +import type VoiceChannel from "./VoiceChannel.ts"; +import type DMChannel from "./DMChannel.ts"; +import type NewsChannel from "./NewsChannel.ts"; +import type ThreadChannel from "./ThreadChannel.ts"; import { ChannelTypes } from "../vendor/external.ts"; -import TextChannel, { textBasedChannels } from "./TextChannel.ts"; -import VoiceChannel from "./VoiceChannel.ts"; -import DMChannel from "./DMChannel.ts"; -import NewsChannel from "./NewsChannel.ts"; -import ThreadChannel from "./ThreadChannel.ts"; - -export type Channel = - | TextChannel - | VoiceChannel - | DMChannel - | NewsChannel - | ThreadChannel; +import { textBasedChannels } from "./TextChannel.ts"; export abstract class BaseChannel implements Model { constructor(session: Session, data: DiscordChannel) { @@ -49,28 +43,11 @@ export abstract class BaseChannel implements Model { return this.type === ChannelTypes.GuildPublicThread || this.type === ChannelTypes.GuildPrivateThread; } - static from(session: Session, channel: DiscordChannel): Channel { - switch (channel.type) { - case ChannelTypes.GuildPublicThread: - case ChannelTypes.GuildPrivateThread: - return new ThreadChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildNews: - return new NewsChannel(session, channel, channel.guild_id!); - case ChannelTypes.DM: - return new DMChannel(session, channel); - case ChannelTypes.GuildVoice: - return new VoiceChannel(session, channel, channel.guild_id!); - default: - if (textBasedChannels.includes(channel.type)) { - return new TextChannel(session, channel); - } - throw new Error("Channel was not implemented"); - } - } - toString(): string { return `<#${this.id}>`; } } + + export default BaseChannel; diff --git a/structures/ChannelFactory.ts b/structures/ChannelFactory.ts new file mode 100644 index 0000000..be073ff --- /dev/null +++ b/structures/ChannelFactory.ts @@ -0,0 +1,39 @@ +import type { Session } from "../session/Session.ts"; +import type { DiscordChannel } from "../vendor/external.ts"; +import { ChannelTypes } from "../vendor/external.ts"; +import { textBasedChannels } from "./TextChannel.ts"; +import TextChannel from "./TextChannel.ts"; +import VoiceChannel from "./VoiceChannel.ts"; +import DMChannel from "./DMChannel.ts"; +import NewsChannel from "./NewsChannel.ts"; +import ThreadChannel from "./ThreadChannel.ts"; + +export type Channel = + | TextChannel + | VoiceChannel + | DMChannel + | NewsChannel + | ThreadChannel; + +export class ChannelFactory { + static from(session: Session, channel: DiscordChannel): Channel { + switch (channel.type) { + case ChannelTypes.GuildPublicThread: + case ChannelTypes.GuildPrivateThread: + return new ThreadChannel(session, channel, channel.guild_id!); + case ChannelTypes.GuildNews: + return new NewsChannel(session, channel, channel.guild_id!); + case ChannelTypes.DM: + return new DMChannel(session, channel); + case ChannelTypes.GuildVoice: + return new VoiceChannel(session, channel, channel.guild_id!); + default: + if (textBasedChannels.includes(channel.type)) { + return new TextChannel(session, channel); + } + throw new Error("Channel was not implemented"); + } + } +} + +export default ChannelFactory; diff --git a/structures/DMChannel.ts b/structures/DMChannel.ts index 1045c67..d8006c0 100644 --- a/structures/DMChannel.ts +++ b/structures/DMChannel.ts @@ -1,11 +1,13 @@ import type { Model } from "./Base.ts"; import type { Session } from "../session/Session.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; import type { ChannelTypes, DiscordChannel } from "../vendor/external.ts"; import TextChannel from "./TextChannel.ts"; +import BaseChannel from "./BaseChannel.ts"; import User from "./User.ts"; import * as Routes from "../util/Routes.ts"; -export class DMChannel extends TextChannel implements Model { +export class DMChannel extends BaseChannel implements Model { constructor(session: Session, data: DiscordChannel) { super(session, data); @@ -18,6 +20,7 @@ export class DMChannel extends TextChannel implements Model { override type: ChannelTypes.DM | ChannelTypes.GroupDm; user: User; + lastMessageId?: Snowflake; async close() { const channel = await this.session.rest.runMethod( @@ -30,4 +33,8 @@ export class DMChannel extends TextChannel implements Model { } } +TextChannel.applyTo(DMChannel); + +export interface DMChannel extends TextChannel, BaseChannel {} + export default DMChannel; diff --git a/structures/GuildChannel.ts b/structures/GuildChannel.ts index 58065bc..19aa931 100644 --- a/structures/GuildChannel.ts +++ b/structures/GuildChannel.ts @@ -1,21 +1,11 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { ChannelTypes, DiscordChannel, DiscordWebhook, DiscordInviteMetadata } from "../vendor/external.ts"; -import { urlToBase64 } from "../util/urlToBase64.ts"; +import type { ChannelTypes, DiscordChannel, DiscordInviteMetadata } from "../vendor/external.ts"; import BaseChannel from "./BaseChannel.ts"; import Invite from "./Invite.ts"; -import Webhook from "./Webhook.ts"; -import ThreadChannel from "./ThreadChannel.ts"; import * as Routes from "../util/Routes.ts"; -export interface CreateWebhook { - name: string; - avatar?: string; - reason?: string; -} - - /** * Represent the options object to create a Thread Channel * @link https://discord.com/developers/docs/resources/channel#start-thread-without-message @@ -31,7 +21,7 @@ export interface ThreadCreateOptions { export class GuildChannel extends BaseChannel implements Model { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { super(session, data); - this.type = data.type as number + this.type = data.type as number; this.guildId = guildId; this.position = data.position; data.topic ? this.topic = data.topic : null; @@ -54,6 +44,17 @@ export class GuildChannel extends BaseChannel implements Model { return invites.map((invite) => new Invite(this.session, invite)); } + /* + async createThread(options: ThreadCreateOptions): Promise { + const thread = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_CREATE_THREAD(this.id), + options, + ); + return new ThreadChannel(this.session, thread, this.guildId); + }*/ + async delete(reason?: string) { await this.session.rest.runMethod( this.session.rest, @@ -64,31 +65,6 @@ export class GuildChannel extends BaseChannel implements Model { }, ); } - - async createThread(options: ThreadCreateOptions): Promise { - const thread = await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_CREATE_THREAD(this.id), - options, - ); - return new ThreadChannel(this.session, thread, this.guildId); - } - - async createWebhook(options: CreateWebhook) { - const webhook = await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_WEBHOOKS(this.id), - { - name: options.name, - avatar: options.avatar ? urlToBase64(options.avatar) : undefined, - reason: options.reason, - } - ); - - return new Webhook(this.session, webhook); - } } export default GuildChannel; diff --git a/structures/NewsChannel.ts b/structures/NewsChannel.ts index e0b7dad..2dbebbb 100644 --- a/structures/NewsChannel.ts +++ b/structures/NewsChannel.ts @@ -3,6 +3,7 @@ import type { Session } from "../session/Session.ts"; import type { ChannelTypes, DiscordChannel } from "../vendor/external.ts"; import GuildChannel from "./GuildChannel.ts"; import Message from "./Message.ts"; +import TextChannel from "./TextChannel.ts"; export class NewsChannel extends GuildChannel { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { @@ -23,4 +24,8 @@ export class NewsChannel extends GuildChannel { } } +TextChannel.applyTo(NewsChannel); + +export interface NewsChannel extends TextChannel, GuildChannel {} + export default NewsChannel; diff --git a/structures/TextChannel.ts b/structures/TextChannel.ts index c2ea25f..3699583 100644 --- a/structures/TextChannel.ts +++ b/structures/TextChannel.ts @@ -1,12 +1,14 @@ +// deno-lint-ignore-file ban-types import type { Session } from "../session/Session.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { GetMessagesOptions, GetReactions } from "../util/Routes.ts"; -import type { DiscordChannel, DiscordInvite, DiscordMessage, TargetTypes } from "../vendor/external.ts"; +import type { DiscordChannel, DiscordInvite, DiscordMessage, DiscordWebhook, TargetTypes } from "../vendor/external.ts"; import type { CreateMessage, EditMessage, ReactionResolvable } from "./Message.ts"; import { ChannelTypes } from "../vendor/external.ts"; -import BaseChannel from "./BaseChannel.ts"; +import { urlToBase64 } from "../util/urlToBase64.ts"; import Message from "./Message.ts"; import Invite from "./Invite.ts"; +import Webhook from "./Webhook.ts"; import * as Routes from "../util/Routes.ts"; /** @@ -24,6 +26,12 @@ export interface DiscordInviteOptions { targetApplicationId?: Snowflake; } +export interface CreateWebhook { + name: string; + avatar?: string; + reason?: string; +} + export const textBasedChannels = [ ChannelTypes.DM, ChannelTypes.GroupDm, @@ -41,9 +49,11 @@ export type TextBasedChannels = | ChannelTypes.GuildNews | ChannelTypes.GuildText; -export class TextChannel extends BaseChannel { +export class TextChannel { constructor(session: Session, data: DiscordChannel) { - super(session, data); + this.session = session; + this.id = data.id; + this.name = data.name; this.type = data.type as number; this.rateLimitPerUser = data.rate_limit_per_user ?? 0; this.nsfw = !!data.nsfw ?? false; @@ -57,12 +67,35 @@ export class TextChannel extends BaseChannel { } } - override type: TextBasedChannels; + readonly session: Session; + readonly id: Snowflake; + name?: string; + type: TextBasedChannels; lastMessageId?: Snowflake; lastPinTimestamp?: string; rateLimitPerUser: number; nsfw: boolean; + /** + * Mixin + * */ + static applyTo(klass: Function) { + klass.prototype.fetchPins = TextChannel.prototype.fetchPins; + klass.prototype.createInvite = TextChannel.prototype.createInvite; + klass.prototype.fetchMessages = TextChannel.prototype.fetchMessages; + klass.prototype.sendTyping = TextChannel.prototype.sendTyping; + klass.prototype.pinMessage = TextChannel.prototype.pinMessage; + klass.prototype.unpinMessage = TextChannel.prototype.unpinMessage; + klass.prototype.addReaction = TextChannel.prototype.addReaction; + klass.prototype.removeReaction = TextChannel.prototype.removeReaction; + klass.prototype.removeReactionEmoji = TextChannel.prototype.removeReactionEmoji; + klass.prototype.nukeReactions = TextChannel.prototype.nukeReactions; + klass.prototype.fetchReactions = TextChannel.prototype.fetchReactions; + klass.prototype.sendMessage = TextChannel.prototype.sendMessage; + klass.prototype.editMessage = TextChannel.prototype.editMessage; + klass.prototype.createWebhook = TextChannel.prototype.createWebhook; + } + async fetchPins(): Promise { const messages = await this.session.rest.runMethod( this.session.rest, @@ -162,6 +195,22 @@ export class TextChannel extends BaseChannel { editMessage(messageId: Snowflake, options: EditMessage) { return Message.prototype.edit.call({ channelId: this.id, id: messageId, session: this.session }, options); } + + + async createWebhook(options: CreateWebhook) { + const webhook = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_WEBHOOKS(this.id), + { + name: options.name, + avatar: options.avatar ? urlToBase64(options.avatar) : undefined, + reason: options.reason, + } + ); + + return new Webhook(this.session, webhook); + } } export default TextChannel; diff --git a/structures/ThreadChannel.ts b/structures/ThreadChannel.ts index b846f93..a6073eb 100644 --- a/structures/ThreadChannel.ts +++ b/structures/ThreadChannel.ts @@ -1,12 +1,14 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { DiscordChannel } from "../vendor/external.ts"; +import type { ChannelTypes, DiscordChannel } from "../vendor/external.ts"; import GuildChannel from "./GuildChannel.ts"; +import TextChannel from "./TextChannel.ts"; export class ThreadChannel extends GuildChannel implements Model { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { super(session, data, guildId); + this.type = data.type as number; this.archived = !!data.thread_metadata?.archived; this.archiveTimestamp = data.thread_metadata?.archive_timestamp; this.autoArchiveDuration = data.thread_metadata?.auto_archive_duration; @@ -15,6 +17,8 @@ export class ThreadChannel extends GuildChannel implements Model { this.memberCount = data.member_count; this.ownerId = data.owner_id; } + + override type: ChannelTypes.GuildNewsThread | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread; archived?: boolean; archiveTimestamp?: string; autoArchiveDuration?: number; @@ -24,4 +28,8 @@ export class ThreadChannel extends GuildChannel implements Model { ownerId?: Snowflake; } +TextChannel.applyTo(ThreadChannel); + +export interface ThreadChannel extends Omit, Omit {} + export default ThreadChannel; diff --git a/structures/components/Component.ts b/structures/components/Component.ts index 4bb2cf0..ffda874 100644 --- a/structures/components/Component.ts +++ b/structures/components/Component.ts @@ -1,15 +1,6 @@ -import type { Session } from "../../session/Session.ts"; -import type { DiscordComponent } from "../../vendor/external.ts"; import type Emoji from "../Emoji.ts"; import { ButtonStyles, MessageComponentTypes, TextStyles } from "../../vendor/external.ts"; -// TODO: fix circular dependencies -import ActionRow from "./ActionRowComponent.ts"; -import Button from "./ButtonComponent.ts"; -import LinkButton from "./ButtonComponent.ts"; -import SelectMenu from "./SelectMenuComponent.ts"; -import TextInput from "./TextInputComponent.ts"; - export class BaseComponent { constructor(type: MessageComponentTypes) { this.type = type; @@ -33,25 +24,6 @@ export class BaseComponent { return this.type === MessageComponentTypes.InputText; } - /** - * Component factory - * @internal - * */ - static from(session: Session, component: DiscordComponent): Component { - switch (component.type) { - case MessageComponentTypes.ActionRow: - return new ActionRow(session, component); - case MessageComponentTypes.Button: - if (component.style === ButtonStyles.Link) { - return new LinkButton(session, component); - } - return new Button(session, component); - case MessageComponentTypes.SelectMenu: - return new SelectMenu(session, component); - case MessageComponentTypes.InputText: - return new TextInput(session, component); - } - } } /** Action Row Component */ diff --git a/structures/components/ComponentFactory.ts b/structures/components/ComponentFactory.ts new file mode 100644 index 0000000..4ef8d2b --- /dev/null +++ b/structures/components/ComponentFactory.ts @@ -0,0 +1,33 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent } from "../../vendor/external.ts"; +import type { Component } from "./Component.ts"; +import { MessageComponentTypes, ButtonStyles } from "../../vendor/external.ts"; +import ActionRow from "./ActionRowComponent.ts"; +import Button from "./ButtonComponent.ts"; +import LinkButton from "./ButtonComponent.ts"; +import SelectMenu from "./SelectMenuComponent.ts"; +import TextInput from "./TextInputComponent.ts"; + +export class ComponentFactory { + /** + * Component factory + * @internal + * */ + static from(session: Session, component: DiscordComponent): Component { + switch (component.type) { + case MessageComponentTypes.ActionRow: + return new ActionRow(session, component); + case MessageComponentTypes.Button: + if (component.style === ButtonStyles.Link) { + return new LinkButton(session, component); + } + return new Button(session, component); + case MessageComponentTypes.SelectMenu: + return new SelectMenu(session, component); + case MessageComponentTypes.InputText: + return new TextInput(session, component); + } + } +} + +export default ComponentFactory; From 177f361590bbb8a95d90279d8186ee37dda3caff Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sat, 2 Jul 2022 11:34:25 -0500 Subject: [PATCH 5/8] fix: fmt --- handlers/Actions.ts | 4 +++- session/Session.ts | 2 +- structures/BaseChannel.ts | 2 -- structures/Guild.ts | 8 +++----- structures/Interaction.ts | 1 - structures/Member.ts | 7 ++++++- structures/TextChannel.ts | 5 ++--- structures/components/ActionRowComponent.ts | 4 ++-- structures/components/ButtonComponent.ts | 2 +- structures/components/Component.ts | 1 - structures/components/ComponentFactory.ts | 4 ++-- structures/components/LinkButtonComponent.ts | 2 +- structures/components/SelectMenuComponent.ts | 2 +- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index d931398..2d44512 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -104,7 +104,9 @@ export const THREAD_LIST_SYNC: RawHandler = (session, _sh channelIds: payload.channel_ids ?? [], threads: payload.threads.map((channel) => new ThreadChannel(session, channel, payload.guild_id)), // @ts-ignore: TODO: thread member structure - members: payload.members.map((member) => new Member(session, member as DiscordMemberWithUser, payload.guild_id)), + members: payload.members.map((member) => + new Member(session, member as DiscordMemberWithUser, payload.guild_id) + ), }); }; diff --git a/session/Session.ts b/session/Session.ts index 10e74e4..6b25993 100644 --- a/session/Session.ts +++ b/session/Session.ts @@ -49,7 +49,7 @@ export class Session extends EventEmitter { } get applicationId() { - return this.#applicationId!; + return this.#applicationId!; } set botId(id: Snowflake) { diff --git a/structures/BaseChannel.ts b/structures/BaseChannel.ts index 7c9602d..602f62c 100644 --- a/structures/BaseChannel.ts +++ b/structures/BaseChannel.ts @@ -48,6 +48,4 @@ export abstract class BaseChannel implements Model { } } - - export default BaseChannel; diff --git a/structures/Guild.ts b/structures/Guild.ts index 1013814..8f8f57f 100644 --- a/structures/Guild.ts +++ b/structures/Guild.ts @@ -82,8 +82,8 @@ export interface BeginGuildPrune { } export interface ModifyRolePositions { - id: Snowflake; - position?: number | null; + id: Snowflake; + position?: number | null; } /** @@ -217,8 +217,6 @@ export class Guild extends BaseGuild implements Model { return new Role(this.session, role, this.id); } - - async addRole(memberId: Snowflake, roleId: Snowflake, { reason }: { reason?: string } = {}) { await this.session.rest.runMethod( this.session.rest, @@ -239,7 +237,7 @@ export class Guild extends BaseGuild implements Model { /** * Returns the roles moved - * */ + */ async moveRoles(options: ModifyRolePositions[]) { const roles = await this.session.rest.runMethod( this.session.rest, diff --git a/structures/Interaction.ts b/structures/Interaction.ts index 820d6aa..762244f 100644 --- a/structures/Interaction.ts +++ b/structures/Interaction.ts @@ -40,7 +40,6 @@ export interface ApplicationCommandOptionChoice { // TODO: abstract Interaction, CommandInteraction, ComponentInteraction, PingInteraction, etc - export class Interaction implements Model { constructor(session: Session, data: DiscordInteraction) { this.session = session; diff --git a/structures/Member.ts b/structures/Member.ts index 6468ce3..8babac0 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -87,7 +87,12 @@ export class Member implements Model { } async removeRole(roleId: Snowflake, options: { reason?: string } = {}) { - await Guild.prototype.removeRole.call({ id: this.guildId, session: this.session }, this.user.id, roleId, options); + await Guild.prototype.removeRole.call( + { id: this.guildId, session: this.session }, + this.user.id, + roleId, + options, + ); } /** gets the user's avatar */ diff --git a/structures/TextChannel.ts b/structures/TextChannel.ts index 3699583..d007807 100644 --- a/structures/TextChannel.ts +++ b/structures/TextChannel.ts @@ -78,7 +78,7 @@ export class TextChannel { /** * Mixin - * */ + */ static applyTo(klass: Function) { klass.prototype.fetchPins = TextChannel.prototype.fetchPins; klass.prototype.createInvite = TextChannel.prototype.createInvite; @@ -196,7 +196,6 @@ export class TextChannel { return Message.prototype.edit.call({ channelId: this.id, id: messageId, session: this.session }, options); } - async createWebhook(options: CreateWebhook) { const webhook = await this.session.rest.runMethod( this.session.rest, @@ -206,7 +205,7 @@ export class TextChannel { name: options.name, avatar: options.avatar ? urlToBase64(options.avatar) : undefined, reason: options.reason, - } + }, ); return new Webhook(this.session, webhook); diff --git a/structures/components/ActionRowComponent.ts b/structures/components/ActionRowComponent.ts index 9976333..8825553 100644 --- a/structures/components/ActionRowComponent.ts +++ b/structures/components/ActionRowComponent.ts @@ -1,7 +1,7 @@ import type { Session } from "../../session/Session.ts"; import type { DiscordComponent } from "../../vendor/external.ts"; import type { ActionRowComponent, Component } from "./Component.ts"; -import { MessageComponentTypes, ButtonStyles } from "../../vendor/external.ts"; +import { ButtonStyles, MessageComponentTypes } from "../../vendor/external.ts"; import BaseComponent from "./Component.ts"; import Button from "./ButtonComponent.ts"; import LinkButton from "./LinkButtonComponent.ts"; @@ -22,7 +22,7 @@ export class ActionRow extends BaseComponent implements ActionRowComponent { } return new Button(session, component); case MessageComponentTypes.SelectMenu: - return new SelectMenu(session, component); + return new SelectMenu(session, component); case MessageComponentTypes.InputText: return new InputText(session, component); case MessageComponentTypes.ActionRow: diff --git a/structures/components/ButtonComponent.ts b/structures/components/ButtonComponent.ts index c0a47ab..903e6ec 100644 --- a/structures/components/ButtonComponent.ts +++ b/structures/components/ButtonComponent.ts @@ -1,5 +1,5 @@ import type { Session } from "../../session/Session.ts"; -import type { DiscordComponent, ButtonStyles } from "../../vendor/external.ts"; +import type { ButtonStyles, DiscordComponent } from "../../vendor/external.ts"; import type { ButtonComponent } from "./Component.ts"; import { MessageComponentTypes } from "../../vendor/external.ts"; import BaseComponent from "./Component.ts"; diff --git a/structures/components/Component.ts b/structures/components/Component.ts index ffda874..74e70d9 100644 --- a/structures/components/Component.ts +++ b/structures/components/Component.ts @@ -23,7 +23,6 @@ export class BaseComponent { isTextInput(): this is TextInputComponent { return this.type === MessageComponentTypes.InputText; } - } /** Action Row Component */ diff --git a/structures/components/ComponentFactory.ts b/structures/components/ComponentFactory.ts index 4ef8d2b..2a5d60a 100644 --- a/structures/components/ComponentFactory.ts +++ b/structures/components/ComponentFactory.ts @@ -1,7 +1,7 @@ import type { Session } from "../../session/Session.ts"; import type { DiscordComponent } from "../../vendor/external.ts"; import type { Component } from "./Component.ts"; -import { MessageComponentTypes, ButtonStyles } from "../../vendor/external.ts"; +import { ButtonStyles, MessageComponentTypes } from "../../vendor/external.ts"; import ActionRow from "./ActionRowComponent.ts"; import Button from "./ButtonComponent.ts"; import LinkButton from "./ButtonComponent.ts"; @@ -12,7 +12,7 @@ export class ComponentFactory { /** * Component factory * @internal - * */ + */ static from(session: Session, component: DiscordComponent): Component { switch (component.type) { case MessageComponentTypes.ActionRow: diff --git a/structures/components/LinkButtonComponent.ts b/structures/components/LinkButtonComponent.ts index 65b01cb..ad3e53b 100644 --- a/structures/components/LinkButtonComponent.ts +++ b/structures/components/LinkButtonComponent.ts @@ -1,5 +1,5 @@ import type { Session } from "../../session/Session.ts"; -import type { DiscordComponent, ButtonStyles } from "../../vendor/external.ts"; +import type { ButtonStyles, DiscordComponent } from "../../vendor/external.ts"; import type { LinkButtonComponent } from "./Component.ts"; import { MessageComponentTypes } from "../../vendor/external.ts"; import BaseComponent from "./Component.ts"; diff --git a/structures/components/SelectMenuComponent.ts b/structures/components/SelectMenuComponent.ts index 40f61de..846104b 100644 --- a/structures/components/SelectMenuComponent.ts +++ b/structures/components/SelectMenuComponent.ts @@ -13,7 +13,7 @@ export class SelectMenu extends BaseComponent implements SelectMenuComponent { this.type = data.type as MessageComponentTypes.SelectMenu; this.customId = data.custom_id!; this.options = data.options!.map((option) => { - return { + return { label: option.label, description: option.description, emoji: option.emoji || new Emoji(session, option.emoji!), From e66356f2c4634963159fb0b44af5c484233e5420 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sat, 2 Jul 2022 11:40:40 -0500 Subject: [PATCH 6/8] fix: tests --- tests/mod.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/mod.ts b/tests/mod.ts index f5e7a96..03fd262 100644 --- a/tests/mod.ts +++ b/tests/mod.ts @@ -1,19 +1,26 @@ +import "https://deno.land/std@0.146.0/dotenv/load.ts"; import { GatewayIntents, Session } from "./deps.ts"; -if (!Deno.args[0]) { +const token = Deno.env.get("TOKEN") ?? Deno.args[0]; + +if (!token) { throw new Error("Please provide a token"); } const intents = GatewayIntents.MessageContent | GatewayIntents.Guilds | GatewayIntents.GuildMessages; -const session = new Session({ token: Deno.args[0], intents }); +const session = new Session({ token, intents }); session.on("ready", (payload) => { console.log("Logged in as:", payload.user.username); }); -const PREFIX = "&"; +const PREFIX = ">"; session.on("messageCreate", (message) => { + if (message.author.bot) { + return; + } + const args = message.content.slice(PREFIX.length).trim().split(/\s+/gm); const name = args.shift()?.toLowerCase(); From d1d0a1ce76458cb974c4df6d91325e04f9d26f39 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sat, 2 Jul 2022 12:15:47 -0500 Subject: [PATCH 7/8] refactor: structures & folders --- handlers/Actions.ts | 12 +- mod.ts | 28 +-- structures/AnonymousGuild.ts | 55 ------ structures/BaseChannel.ts | 51 ----- structures/BaseGuild.ts | 41 ---- structures/ChannelFactory.ts | 39 ---- structures/DMChannel.ts | 40 ---- structures/Guild.ts | 367 ----------------------------------- structures/GuildChannel.ts | 70 ------- structures/GuildEmoji.ts | 4 +- structures/Interaction.ts | 145 -------------- structures/Invite.ts | 4 +- structures/InviteGuild.ts | 19 -- structures/Member.ts | 4 +- structures/NewsChannel.ts | 31 --- structures/Role.ts | 2 +- structures/TextChannel.ts | 215 -------------------- structures/ThreadChannel.ts | 35 ---- structures/VoiceChannel.ts | 60 ------ 19 files changed, 29 insertions(+), 1193 deletions(-) delete mode 100644 structures/AnonymousGuild.ts delete mode 100644 structures/BaseChannel.ts delete mode 100644 structures/BaseGuild.ts delete mode 100644 structures/ChannelFactory.ts delete mode 100644 structures/DMChannel.ts delete mode 100644 structures/Guild.ts delete mode 100644 structures/GuildChannel.ts delete mode 100644 structures/Interaction.ts delete mode 100644 structures/InviteGuild.ts delete mode 100644 structures/NewsChannel.ts delete mode 100644 structures/TextChannel.ts delete mode 100644 structures/ThreadChannel.ts delete mode 100644 structures/VoiceChannel.ts diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 2d44512..5975a4b 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -15,14 +15,14 @@ import type { } from "../vendor/external.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { Channel } from "../structures/BaseChannel.ts"; -import ChannelFactory from "../structures/ChannelFactory.ts"; -import GuildChannel from "../structures/GuildChannel.ts"; -import ThreadChannel from "../structures/ThreadChannel.ts"; +import type { Channel } from "../structures/channels/ChannelFactory.ts"; +import ChannelFactory from "../structures/channels/ChannelFactory.ts"; +import GuildChannel from "../structures/channels/GuildChannel.ts"; +import ThreadChannel from "../structures/channels/ThreadChannel.ts"; import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; -import Interaction from "../structures/Interaction.ts"; +import Interaction from "../structures/interactions/Interaction.ts"; export type RawHandler = (...args: [Session, number, T]) => void; export type Handler = (...args: T) => unknown; @@ -103,8 +103,8 @@ export const THREAD_LIST_SYNC: RawHandler = (session, _sh guildId: payload.guild_id, channelIds: payload.channel_ids ?? [], threads: payload.threads.map((channel) => new ThreadChannel(session, channel, payload.guild_id)), - // @ts-ignore: TODO: thread member structure members: payload.members.map((member) => + // @ts-ignore: TODO: thread member structure new Member(session, member as DiscordMemberWithUser, payload.guild_id) ), }); diff --git a/mod.ts b/mod.ts index 7262d9a..f7d725a 100644 --- a/mod.ts +++ b/mod.ts @@ -1,29 +1,26 @@ -export * from "./structures/AnonymousGuild.ts"; export * from "./structures/Attachment.ts"; export * from "./structures/Base.ts"; -export * from "./structures/BaseGuild.ts"; -export * from "./structures/BaseChannel.ts"; -export * from "./structures/DMChannel.ts"; export * from "./structures/Embed.ts"; export * from "./structures/Emoji.ts"; -export * from "./structures/Guild.ts"; -export * from "./structures/GuildChannel.ts"; export * from "./structures/GuildEmoji.ts"; -export * from "./structures/Interaction.ts"; export * from "./structures/Invite.ts"; -export * from "./structures/InviteGuild.ts"; export * from "./structures/Member.ts"; export * from "./structures/Message.ts"; -export * from "./structures/NewsChannel.ts"; export * from "./structures/Permissions.ts"; export * from "./structures/Role.ts"; -export * from "./structures/TextChannel.ts"; -export * from "./structures/ThreadChannel.ts"; export * from "./structures/User.ts"; -export * from "./structures/VoiceChannel.ts"; export * from "./structures/WelcomeChannel.ts"; export * from "./structures/WelcomeScreen.ts"; +export * from "./structures/channels/BaseChannel.ts"; +export * from "./structures/channels/ChannelFactory.ts"; +export * from "./structures/channels/DMChannel.ts"; +export * from "./structures/channels/GuildChannel.ts"; +export * from "./structures/channels/NewsChannel.ts"; +export * from "./structures/channels/TextChannel.ts"; +export * from "./structures/channels/ThreadChannel.ts"; +export * from "./structures/channels/VoiceChannel.ts"; + export * from "./structures/components/ActionRowComponent.ts"; export * from "./structures/components/ButtonComponent.ts"; export * from "./structures/components/Component.ts"; @@ -31,6 +28,13 @@ export * from "./structures/components/LinkButtonComponent.ts"; export * from "./structures/components/SelectMenuComponent.ts"; export * from "./structures/components/TextInputComponent.ts"; +export * from "./structures/guilds/AnonymousGuild.ts"; +export * from "./structures/guilds/BaseGuild.ts"; +export * from "./structures/guilds/Guild.ts"; +export * from "./structures/guilds/InviteGuild.ts"; + +export * from "./structures/interactions/Interaction.ts"; + export * from "./session/Session.ts"; export * from "./util/shared/flags.ts"; diff --git a/structures/AnonymousGuild.ts b/structures/AnonymousGuild.ts deleted file mode 100644 index fa86532..0000000 --- a/structures/AnonymousGuild.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { Model } from "./Base.ts"; -import type { Session } from "../session/Session.ts"; -import type { DiscordGuild, GuildNsfwLevel, VerificationLevels } from "../vendor/external.ts"; -import type { ImageFormat, ImageSize } from "../util/shared/images.ts"; -import { iconBigintToHash, iconHashToBigInt } from "../util/hash.ts"; -import { formatImageUrl } from "../util/shared/images.ts"; -import BaseGuild from "./BaseGuild.ts"; -import * as Routes from "../util/Routes.ts"; - -export class AnonymousGuild extends BaseGuild implements Model { - constructor(session: Session, data: Partial); // TODO: Improve this type (name and id are required) - constructor(session: Session, data: DiscordGuild) { - super(session, data); - - this.splashHash = data.splash ? iconHashToBigInt(data.splash) : undefined; - this.bannerHash = data.banner ? iconHashToBigInt(data.banner) : undefined; - - this.verificationLevel = data.verification_level; - this.vanityUrlCode = data.vanity_url_code ? data.vanity_url_code : undefined; - this.nsfwLevel = data.nsfw_level; - this.description = data.description ? data.description : undefined; - this.premiumSubscriptionCount = data.premium_subscription_count; - } - - splashHash?: bigint; - bannerHash?: bigint; - - verificationLevel: VerificationLevels; - vanityUrlCode?: string; - nsfwLevel: GuildNsfwLevel; - description?: string; - premiumSubscriptionCount?: number; - - splashUrl(options: { size?: ImageSize; format?: ImageFormat } = { size: 128 }) { - if (this.splashHash) { - return formatImageUrl( - Routes.GUILD_SPLASH(this.id, iconBigintToHash(this.splashHash)), - options.size, - options.format, - ); - } - } - - bannerUrl(options: { size?: ImageSize; format?: ImageFormat } = { size: 128 }) { - if (this.bannerHash) { - return formatImageUrl( - Routes.GUILD_BANNER(this.id, iconBigintToHash(this.bannerHash)), - options.size, - options.format, - ); - } - } -} - -export default AnonymousGuild; diff --git a/structures/BaseChannel.ts b/structures/BaseChannel.ts deleted file mode 100644 index 602f62c..0000000 --- a/structures/BaseChannel.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Model } from "./Base.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; -import type { Session } from "../session/Session.ts"; -import type { DiscordChannel } from "../vendor/external.ts"; -import type TextChannel from "./TextChannel.ts"; -import type VoiceChannel from "./VoiceChannel.ts"; -import type DMChannel from "./DMChannel.ts"; -import type NewsChannel from "./NewsChannel.ts"; -import type ThreadChannel from "./ThreadChannel.ts"; -import { ChannelTypes } from "../vendor/external.ts"; -import { textBasedChannels } from "./TextChannel.ts"; - -export abstract class BaseChannel implements Model { - constructor(session: Session, data: DiscordChannel) { - this.id = data.id; - this.session = session; - this.name = data.name; - this.type = data.type; - } - readonly id: Snowflake; - readonly session: Session; - - name?: string; - type: ChannelTypes; - - isText(): this is TextChannel { - return textBasedChannels.includes(this.type); - } - - isVoice(): this is VoiceChannel { - return this.type === ChannelTypes.GuildVoice; - } - - isDM(): this is DMChannel { - return this.type === ChannelTypes.DM; - } - - isNews(): this is NewsChannel { - return this.type === ChannelTypes.GuildNews; - } - - isThread(): this is ThreadChannel { - return this.type === ChannelTypes.GuildPublicThread || this.type === ChannelTypes.GuildPrivateThread; - } - - toString(): string { - return `<#${this.id}>`; - } -} - -export default BaseChannel; diff --git a/structures/BaseGuild.ts b/structures/BaseGuild.ts deleted file mode 100644 index d233e9c..0000000 --- a/structures/BaseGuild.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Model } from "./Base.ts"; -import type { Session } from "../session/Session.ts"; -import type { DiscordGuild, GuildFeatures } from "../vendor/external.ts"; -import { Snowflake } from "../util/Snowflake.ts"; -import { iconHashToBigInt } from "../util/hash.ts"; - -/** - * Class for {@link Guild} and {@link AnonymousGuild} - */ -export abstract class BaseGuild implements Model { - constructor(session: Session, data: DiscordGuild) { - this.session = session; - this.id = data.id; - - this.name = data.name; - this.iconHash = data.icon ? iconHashToBigInt(data.icon) : undefined; - - this.features = data.features; - } - - readonly session: Session; - readonly id: Snowflake; - - name: string; - iconHash?: bigint; - features: GuildFeatures[]; - - get createdTimestamp() { - return Snowflake.snowflakeToTimestamp(this.id); - } - - get createdAt() { - return new Date(this.createdTimestamp); - } - - toString() { - return this.name; - } -} - -export default BaseGuild; diff --git a/structures/ChannelFactory.ts b/structures/ChannelFactory.ts deleted file mode 100644 index be073ff..0000000 --- a/structures/ChannelFactory.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Session } from "../session/Session.ts"; -import type { DiscordChannel } from "../vendor/external.ts"; -import { ChannelTypes } from "../vendor/external.ts"; -import { textBasedChannels } from "./TextChannel.ts"; -import TextChannel from "./TextChannel.ts"; -import VoiceChannel from "./VoiceChannel.ts"; -import DMChannel from "./DMChannel.ts"; -import NewsChannel from "./NewsChannel.ts"; -import ThreadChannel from "./ThreadChannel.ts"; - -export type Channel = - | TextChannel - | VoiceChannel - | DMChannel - | NewsChannel - | ThreadChannel; - -export class ChannelFactory { - static from(session: Session, channel: DiscordChannel): Channel { - switch (channel.type) { - case ChannelTypes.GuildPublicThread: - case ChannelTypes.GuildPrivateThread: - return new ThreadChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildNews: - return new NewsChannel(session, channel, channel.guild_id!); - case ChannelTypes.DM: - return new DMChannel(session, channel); - case ChannelTypes.GuildVoice: - return new VoiceChannel(session, channel, channel.guild_id!); - default: - if (textBasedChannels.includes(channel.type)) { - return new TextChannel(session, channel); - } - throw new Error("Channel was not implemented"); - } - } -} - -export default ChannelFactory; diff --git a/structures/DMChannel.ts b/structures/DMChannel.ts deleted file mode 100644 index d8006c0..0000000 --- a/structures/DMChannel.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Model } from "./Base.ts"; -import type { Session } from "../session/Session.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; -import type { ChannelTypes, DiscordChannel } from "../vendor/external.ts"; -import TextChannel from "./TextChannel.ts"; -import BaseChannel from "./BaseChannel.ts"; -import User from "./User.ts"; -import * as Routes from "../util/Routes.ts"; - -export class DMChannel extends BaseChannel implements Model { - constructor(session: Session, data: DiscordChannel) { - super(session, data); - - this.user = new User(this.session, data.recipents!.find((r) => r.id !== this.session.botId)!); - this.type = data.type as ChannelTypes.DM | ChannelTypes.GroupDm; - if (data.last_message_id) { - this.lastMessageId = data.last_message_id; - } - } - - override type: ChannelTypes.DM | ChannelTypes.GroupDm; - user: User; - lastMessageId?: Snowflake; - - async close() { - const channel = await this.session.rest.runMethod( - this.session.rest, - "DELETE", - Routes.CHANNEL(this.id), - ); - - return new DMChannel(this.session, channel); - } -} - -TextChannel.applyTo(DMChannel); - -export interface DMChannel extends TextChannel, BaseChannel {} - -export default DMChannel; diff --git a/structures/Guild.ts b/structures/Guild.ts deleted file mode 100644 index 8f8f57f..0000000 --- a/structures/Guild.ts +++ /dev/null @@ -1,367 +0,0 @@ -import type { Model } from "./Base.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; -import type { Session } from "../session/Session.ts"; -import type { - DiscordEmoji, - DiscordGuild, - DiscordInviteMetadata, - DiscordMemberWithUser, - DiscordRole, -} from "../vendor/external.ts"; -import type { GetInvite } from "../util/Routes.ts"; -import { - DefaultMessageNotificationLevels, - ExplicitContentFilterLevels, - VerificationLevels, -} from "../vendor/external.ts"; -import { iconBigintToHash, iconHashToBigInt } from "../util/hash.ts"; -import { urlToBase64 } from "../util/urlToBase64.ts"; -import Member from "./Member.ts"; -import BaseGuild from "./BaseGuild.ts"; -import Role from "./Role.ts"; -import GuildEmoji from "./GuildEmoji.ts"; -import Invite from "./Invite.ts"; -import * as Routes from "../util/Routes.ts"; - -export interface CreateRole { - name?: string; - color?: number; - iconHash?: string | bigint; - unicodeEmoji?: string; - hoist?: boolean; - mentionable?: boolean; -} - -export interface ModifyGuildRole { - name?: string; - color?: number; - hoist?: boolean; - mentionable?: boolean; - unicodeEmoji?: string; -} - -export interface CreateGuildEmoji { - name: string; - image: string; - roles?: Snowflake[]; - reason?: string; -} - -export interface ModifyGuildEmoji { - name?: string; - roles?: Snowflake[]; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#create-guild-ban - */ -export interface CreateGuildBan { - deleteMessageDays?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; - reason?: string; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#modify-guild-member - */ -export interface ModifyGuildMember { - nick?: string; - roles?: Snowflake[]; - mute?: boolean; - deaf?: boolean; - channelId?: Snowflake; - communicationDisabledUntil?: number; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#begin-guild-prune - */ -export interface BeginGuildPrune { - days?: number; - computePruneCount?: boolean; - includeRoles?: Snowflake[]; -} - -export interface ModifyRolePositions { - id: Snowflake; - position?: number | null; -} - -/** - * Represents a guild - * @link https://discord.com/developers/docs/resources/guild#guild-object - */ -export class Guild extends BaseGuild implements Model { - constructor(session: Session, data: DiscordGuild) { - super(session, data); - - this.splashHash = data.splash ? iconHashToBigInt(data.splash) : undefined; - this.discoverySplashHash = data.discovery_splash ? iconHashToBigInt(data.discovery_splash) : undefined; - this.ownerId = data.owner_id; - this.widgetEnabled = !!data.widget_enabled; - this.widgetChannelId = data.widget_channel_id ? data.widget_channel_id : undefined; - this.vefificationLevel = data.verification_level; - this.defaultMessageNotificationLevel = data.default_message_notifications; - this.explicitContentFilterLevel = data.explicit_content_filter; - this.members = data.members?.map((member) => new Member(session, { ...member, user: member.user! }, data.id)) ?? - []; - this.roles = data.roles.map((role) => new Role(session, role, data.id)); - this.emojis = data.emojis.map((guildEmoji) => new GuildEmoji(session, guildEmoji, data.id)); - } - - splashHash?: bigint; - discoverySplashHash?: bigint; - ownerId: Snowflake; - widgetEnabled: boolean; - widgetChannelId?: Snowflake; - vefificationLevel: VerificationLevels; - defaultMessageNotificationLevel: DefaultMessageNotificationLevels; - explicitContentFilterLevel: ExplicitContentFilterLevels; - members: Member[]; - roles: Role[]; - emojis: GuildEmoji[]; - - /** - * 'null' would reset the nickname - */ - async editBotNickname(options: { nick: string | null; reason?: string }) { - const result = await this.session.rest.runMethod<{ nick?: string } | undefined>( - this.session.rest, - "PATCH", - Routes.USER_NICK(this.id), - options, - ); - - return result?.nick; - } - - async createEmoji(options: CreateGuildEmoji): Promise { - if (options.image && !options.image.startsWith("data:image/")) { - options.image = await urlToBase64(options.image); - } - - const emoji = await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.GUILD_EMOJIS(this.id), - options, - ); - - return new GuildEmoji(this.session, emoji, this.id); - } - - async deleteEmoji(id: Snowflake, { reason }: { reason?: string } = {}): Promise { - await this.session.rest.runMethod( - this.session.rest, - "DELETE", - Routes.GUILD_EMOJI(this.id, id), - { reason }, - ); - } - - async editEmoji(id: Snowflake, options: ModifyGuildEmoji): Promise { - const emoji = await this.session.rest.runMethod( - this.session.rest, - "PATCH", - Routes.GUILD_EMOJI(this.id, id), - options, - ); - - return new GuildEmoji(this.session, emoji, this.id); - } - - async createRole(options: CreateRole): Promise { - let icon: string | undefined; - - if (options.iconHash) { - if (typeof options.iconHash === "string") { - icon = options.iconHash; - } else { - icon = iconBigintToHash(options.iconHash); - } - } - - const role = await this.session.rest.runMethod( - this.session.rest, - "PUT", - Routes.GUILD_ROLES(this.id), - { - name: options.name, - color: options.color, - icon, - unicode_emoji: options.unicodeEmoji, - hoist: options.hoist, - mentionable: options.mentionable, - }, - ); - - return new Role(this.session, role, this.id); - } - - async deleteRole(roleId: Snowflake): Promise { - await this.session.rest.runMethod(this.session.rest, "DELETE", Routes.GUILD_ROLE(this.id, roleId)); - } - - async editRole(roleId: Snowflake, options: ModifyGuildRole): Promise { - const role = await this.session.rest.runMethod( - this.session.rest, - "PATCH", - Routes.GUILD_ROLE(this.id, roleId), - { - name: options.name, - color: options.color, - hoist: options.hoist, - mentionable: options.mentionable, - }, - ); - - return new Role(this.session, role, this.id); - } - - async addRole(memberId: Snowflake, roleId: Snowflake, { reason }: { reason?: string } = {}) { - await this.session.rest.runMethod( - this.session.rest, - "PUT", - Routes.GUILD_MEMBER_ROLE(this.id, memberId, roleId), - { reason }, - ); - } - - async removeRole(memberId: Snowflake, roleId: Snowflake, { reason }: { reason?: string } = {}) { - await this.session.rest.runMethod( - this.session.rest, - "DELETE", - Routes.GUILD_MEMBER_ROLE(this.id, memberId, roleId), - { reason }, - ); - } - - /** - * Returns the roles moved - */ - async moveRoles(options: ModifyRolePositions[]) { - const roles = await this.session.rest.runMethod( - this.session.rest, - "PATCH", - Routes.GUILD_ROLES(this.id), - options, - ); - - return roles.map((role) => new Role(this.session, role, this.id)); - } - - async deleteInvite(inviteCode: string): Promise { - await this.session.rest.runMethod( - this.session.rest, - "DELETE", - Routes.INVITE(inviteCode), - {}, - ); - } - - async fetchInvite(inviteCode: string, options: GetInvite): Promise { - const inviteMetadata = await this.session.rest.runMethod( - this.session.rest, - "GET", - Routes.INVITE(inviteCode, options), - ); - - return new Invite(this.session, inviteMetadata); - } - - async fetchInvites(): Promise { - const invites = await this.session.rest.runMethod( - this.session.rest, - "GET", - Routes.GUILD_INVITES(this.id), - ); - - return invites.map((invite) => new Invite(this.session, invite)); - } - - /** - * Bans the member - */ - async banMember(memberId: Snowflake, options: CreateGuildBan) { - await this.session.rest.runMethod( - this.session.rest, - "PUT", - Routes.GUILD_BAN(this.id, memberId), - options - ? { - delete_message_days: options.deleteMessageDays, - reason: options.reason, - } - : {}, - ); - } - - /** - * Kicks the member - */ - async kickMember(memberId: Snowflake, { reason }: { reason?: string }) { - await this.session.rest.runMethod( - this.session.rest, - "DELETE", - Routes.GUILD_MEMBER(this.id, memberId), - { reason }, - ); - } - - /* - * Unbans the member - * */ - async unbanMember(memberId: Snowflake) { - await this.session.rest.runMethod( - this.session.rest, - "DELETE", - Routes.GUILD_BAN(this.id, memberId), - ); - } - - async editMember(memberId: Snowflake, options: ModifyGuildMember) { - const member = await this.session.rest.runMethod( - this.session.rest, - "PATCH", - Routes.GUILD_MEMBER(this.id, memberId), - { - nick: options.nick, - roles: options.roles, - mute: options.mute, - deaf: options.deaf, - channel_id: options.channelId, - communication_disabled_until: options.communicationDisabledUntil - ? new Date(options.communicationDisabledUntil).toISOString() - : undefined, - }, - ); - - return new Member(this.session, member, this.id); - } - - async pruneMembers(options: BeginGuildPrune): Promise { - const result = await this.session.rest.runMethod<{ pruned: number }>( - this.session.rest, - "POST", - Routes.GUILD_PRUNE(this.id), - { - days: options.days, - compute_prune_count: options.computePruneCount, - include_roles: options.includeRoles, - }, - ); - - return result.pruned; - } - - async getPruneCount(): Promise { - const result = await this.session.rest.runMethod<{ pruned: number }>( - this.session.rest, - "GET", - Routes.GUILD_PRUNE(this.id), - ); - - return result.pruned; - } -} - -export default Guild; diff --git a/structures/GuildChannel.ts b/structures/GuildChannel.ts deleted file mode 100644 index 19aa931..0000000 --- a/structures/GuildChannel.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { Model } from "./Base.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; -import type { Session } from "../session/Session.ts"; -import type { ChannelTypes, DiscordChannel, DiscordInviteMetadata } from "../vendor/external.ts"; -import BaseChannel from "./BaseChannel.ts"; -import Invite from "./Invite.ts"; -import * as Routes from "../util/Routes.ts"; - -/** - * Represent the options object to create a Thread Channel - * @link https://discord.com/developers/docs/resources/channel#start-thread-without-message - */ -export interface ThreadCreateOptions { - name: string; - autoArchiveDuration: 60 | 1440 | 4320 | 10080; - type: 10 | 11 | 12; - invitable?: boolean; - reason?: string; -} - -export class GuildChannel extends BaseChannel implements Model { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data); - this.type = data.type as number; - this.guildId = guildId; - this.position = data.position; - data.topic ? this.topic = data.topic : null; - data.parent_id ? this.parentId = data.parent_id : undefined; - } - - override type: Exclude; - guildId: Snowflake; - topic?: string; - position?: number; - parentId?: Snowflake; - - async fetchInvites(): Promise { - const invites = await this.session.rest.runMethod( - this.session.rest, - "GET", - Routes.CHANNEL_INVITES(this.id), - ); - - return invites.map((invite) => new Invite(this.session, invite)); - } - - /* - async createThread(options: ThreadCreateOptions): Promise { - const thread = await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_CREATE_THREAD(this.id), - options, - ); - return new ThreadChannel(this.session, thread, this.guildId); - }*/ - - async delete(reason?: string) { - await this.session.rest.runMethod( - this.session.rest, - "DELETE", - Routes.CHANNEL(this.id), - { - reason, - }, - ); - } -} - -export default GuildChannel; diff --git a/structures/GuildEmoji.ts b/structures/GuildEmoji.ts index 0de968d..6b3d2b3 100644 --- a/structures/GuildEmoji.ts +++ b/structures/GuildEmoji.ts @@ -2,8 +2,8 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { DiscordEmoji } from "../vendor/external.ts"; -import type { ModifyGuildEmoji } from "./Guild.ts"; -import Guild from "./Guild.ts"; +import type { ModifyGuildEmoji } from "./guilds/Guild.ts"; +import Guild from "./guilds/Guild.ts"; import Emoji from "./Emoji.ts"; import User from "./User.ts"; import * as Routes from "../util/Routes.ts"; diff --git a/structures/Interaction.ts b/structures/Interaction.ts deleted file mode 100644 index 762244f..0000000 --- a/structures/Interaction.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { Model } from "./Base.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; -import type { Session } from "../session/Session.ts"; -import type { - DiscordInteraction, - DiscordMessage, - FileContent, - InteractionResponseTypes, - InteractionTypes, -} from "../vendor/external.ts"; -import type { MessageFlags } from "../util/shared/flags.ts"; -import type { AllowedMentions } from "./Message.ts"; -import User from "./User.ts"; -import Message from "./Message.ts"; -import Member from "./Member.ts"; -import * as Routes from "../util/Routes.ts"; - -export interface InteractionResponse { - type: InteractionResponseTypes; - data?: InteractionApplicationCommandCallbackData; -} - -export interface InteractionApplicationCommandCallbackData { - content?: string; - tts?: boolean; - allowedMentions?: AllowedMentions; - files?: FileContent[]; - customId?: string; - title?: string; - // components?: Component[]; - flags?: MessageFlags; - choices?: ApplicationCommandOptionChoice[]; -} - -/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice */ -export interface ApplicationCommandOptionChoice { - name: string; - value: string | number; -} - -// TODO: abstract Interaction, CommandInteraction, ComponentInteraction, PingInteraction, etc - -export class Interaction implements Model { - constructor(session: Session, data: DiscordInteraction) { - this.session = session; - this.id = data.id; - this.token = data.token; - this.type = data.type; - this.guildId = data.guild_id; - this.channelId = data.channel_id; - this.applicationId = data.application_id; - this.locale = data.locale; - this.data = data.data; - - if (!data.guild_id) { - this.user = new User(session, data.user!); - } else { - this.member = new Member(session, data.member!, data.guild_id); - } - } - - readonly session: Session; - readonly id: Snowflake; - readonly token: string; - - type: InteractionTypes; - guildId?: Snowflake; - channelId?: Snowflake; - applicationId?: Snowflake; - locale?: string; - // deno-lint-ignore no-explicit-any - data: any; - user?: User; - member?: Member; - - async respond({ type, data }: InteractionResponse) { - const toSend = { - tts: data?.tts, - title: data?.title, - flags: data?.flags, - content: data?.content, - choices: data?.choices, - custom_id: data?.customId, - allowed_mentions: data?.allowedMentions - ? { - users: data.allowedMentions.users, - roles: data.allowedMentions.roles, - parse: data.allowedMentions.parse, - replied_user: data.allowedMentions.repliedUser, - } - : { parse: [] }, - }; - - if (this.session.unrepliedInteractions.delete(BigInt(this.id))) { - await this.session.rest.sendRequest( - this.session.rest, - { - url: Routes.INTERACTION_ID_TOKEN(this.id, this.token), - method: "POST", - payload: this.session.rest.createRequestBody(this.session.rest, { - method: "POST", - body: { - type: type, - data: toSend, - file: data?.files, - }, - headers: { - // remove authorization header - Authorization: "", - }, - }), - }, - ); - - return; - } - - const result = await this.session.rest.sendRequest( - this.session.rest, - { - url: Routes.WEBHOOK(this.session.applicationId ?? this.session.botId, this.token), - method: "POST", - payload: this.session.rest.createRequestBody(this.session.rest, { - method: "POST", - body: { - ...toSend, - file: data?.files, - }, - headers: { - // remove authorization header - Authorization: "", - }, - }), - }, - ); - - return new Message(this.session, result); - } - - inGuild(): this is Interaction & { user: undefined; guildId: Snowflake; member: Member } { - return !!this.guildId; - } -} - -export default Interaction; diff --git a/structures/Invite.ts b/structures/Invite.ts index 2e12880..5d238e7 100644 --- a/structures/Invite.ts +++ b/structures/Invite.ts @@ -1,9 +1,9 @@ import type { Session } from "../session/Session.ts"; import type { DiscordInvite } from "../vendor/external.ts"; import { TargetTypes } from "../vendor/external.ts"; -import InviteGuild from "./InviteGuild.ts"; +import InviteGuild from "./guilds/InviteGuild.ts"; import User from "./User.ts"; -import Guild from "./Guild.ts"; +import Guild from "./guilds/Guild.ts"; /** * @link https://discord.com/developers/docs/resources/invite#invite-object diff --git a/structures/InviteGuild.ts b/structures/InviteGuild.ts deleted file mode 100644 index 0465165..0000000 --- a/structures/InviteGuild.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Model } from "./Base.ts"; -import type { Session } from "../session/Session.ts"; -import type { DiscordGuild } from "../vendor/external.ts"; -import AnonymousGuild from "./AnonymousGuild.ts"; -import WelcomeScreen from "./WelcomeScreen.ts"; - -export class InviteGuild extends AnonymousGuild implements Model { - constructor(session: Session, data: Partial) { - super(session, data); - - if (data.welcome_screen) { - this.welcomeScreen = new WelcomeScreen(session, data.welcome_screen); - } - } - - welcomeScreen?: WelcomeScreen; -} - -export default InviteGuild; diff --git a/structures/Member.ts b/structures/Member.ts index 8babac0..601d5af 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -3,10 +3,10 @@ import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { DiscordMemberWithUser } from "../vendor/external.ts"; import type { ImageFormat, ImageSize } from "../util/shared/images.ts"; -import type { CreateGuildBan, ModifyGuildMember } from "./Guild.ts"; +import type { CreateGuildBan, ModifyGuildMember } from "./guilds/Guild.ts"; import { iconBigintToHash, iconHashToBigInt } from "../util/hash.ts"; import User from "./User.ts"; -import Guild from "./Guild.ts"; +import Guild from "./guilds/Guild.ts"; import * as Routes from "../util/Routes.ts"; /** diff --git a/structures/NewsChannel.ts b/structures/NewsChannel.ts deleted file mode 100644 index 2dbebbb..0000000 --- a/structures/NewsChannel.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Snowflake } from "../util/Snowflake.ts"; -import type { Session } from "../session/Session.ts"; -import type { ChannelTypes, DiscordChannel } from "../vendor/external.ts"; -import GuildChannel from "./GuildChannel.ts"; -import Message from "./Message.ts"; -import TextChannel from "./TextChannel.ts"; - -export class NewsChannel extends GuildChannel { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.type = data.type as ChannelTypes.GuildNews; - this.defaultAutoArchiveDuration = data.default_auto_archive_duration; - } - - override type: ChannelTypes.GuildNews; - defaultAutoArchiveDuration?: number; - - crosspostMessage(messageId: Snowflake): Promise { - return Message.prototype.crosspost.call({ id: messageId, channelId: this.id, session: this.session }); - } - - get publishMessage() { - return this.crosspostMessage; - } -} - -TextChannel.applyTo(NewsChannel); - -export interface NewsChannel extends TextChannel, GuildChannel {} - -export default NewsChannel; diff --git a/structures/Role.ts b/structures/Role.ts index 41a4c97..43ec61d 100644 --- a/structures/Role.ts +++ b/structures/Role.ts @@ -4,7 +4,7 @@ import type { Session } from "../session/Session.ts"; import { Snowflake } from "../util/Snowflake.ts"; import { iconHashToBigInt } from "../util/hash.ts"; import Permissions from "./Permissions.ts"; -import Guild, { ModifyGuildRole } from "./Guild.ts"; +import Guild, { ModifyGuildRole } from "./guilds/Guild.ts"; export class Role implements Model { constructor(session: Session, data: DiscordRole, guildId: Snowflake) { diff --git a/structures/TextChannel.ts b/structures/TextChannel.ts deleted file mode 100644 index d007807..0000000 --- a/structures/TextChannel.ts +++ /dev/null @@ -1,215 +0,0 @@ -// deno-lint-ignore-file ban-types -import type { Session } from "../session/Session.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; -import type { GetMessagesOptions, GetReactions } from "../util/Routes.ts"; -import type { DiscordChannel, DiscordInvite, DiscordMessage, DiscordWebhook, TargetTypes } from "../vendor/external.ts"; -import type { CreateMessage, EditMessage, ReactionResolvable } from "./Message.ts"; -import { ChannelTypes } from "../vendor/external.ts"; -import { urlToBase64 } from "../util/urlToBase64.ts"; -import Message from "./Message.ts"; -import Invite from "./Invite.ts"; -import Webhook from "./Webhook.ts"; -import * as Routes from "../util/Routes.ts"; - -/** - * Represents the options object to create an invitation - * @link https://discord.com/developers/docs/resources/channel#create-channel-invite-json-params - */ -export interface DiscordInviteOptions { - maxAge?: number; - maxUses?: number; - unique?: boolean; - temporary: boolean; - reason?: string; - targetType?: TargetTypes; - targetUserId?: Snowflake; - targetApplicationId?: Snowflake; -} - -export interface CreateWebhook { - name: string; - avatar?: string; - reason?: string; -} - -export const textBasedChannels = [ - ChannelTypes.DM, - ChannelTypes.GroupDm, - ChannelTypes.GuildPrivateThread, - ChannelTypes.GuildPublicThread, - ChannelTypes.GuildNews, - ChannelTypes.GuildText, -]; - -export type TextBasedChannels = - | ChannelTypes.DM - | ChannelTypes.GroupDm - | ChannelTypes.GuildPrivateThread - | ChannelTypes.GuildPublicThread - | ChannelTypes.GuildNews - | ChannelTypes.GuildText; - -export class TextChannel { - constructor(session: Session, data: DiscordChannel) { - this.session = session; - this.id = data.id; - this.name = data.name; - this.type = data.type as number; - this.rateLimitPerUser = data.rate_limit_per_user ?? 0; - this.nsfw = !!data.nsfw ?? false; - - if (data.last_message_id) { - this.lastMessageId = data.last_message_id; - } - - if (data.last_pin_timestamp) { - this.lastPinTimestamp = data.last_pin_timestamp; - } - } - - readonly session: Session; - readonly id: Snowflake; - name?: string; - type: TextBasedChannels; - lastMessageId?: Snowflake; - lastPinTimestamp?: string; - rateLimitPerUser: number; - nsfw: boolean; - - /** - * Mixin - */ - static applyTo(klass: Function) { - klass.prototype.fetchPins = TextChannel.prototype.fetchPins; - klass.prototype.createInvite = TextChannel.prototype.createInvite; - klass.prototype.fetchMessages = TextChannel.prototype.fetchMessages; - klass.prototype.sendTyping = TextChannel.prototype.sendTyping; - klass.prototype.pinMessage = TextChannel.prototype.pinMessage; - klass.prototype.unpinMessage = TextChannel.prototype.unpinMessage; - klass.prototype.addReaction = TextChannel.prototype.addReaction; - klass.prototype.removeReaction = TextChannel.prototype.removeReaction; - klass.prototype.removeReactionEmoji = TextChannel.prototype.removeReactionEmoji; - klass.prototype.nukeReactions = TextChannel.prototype.nukeReactions; - klass.prototype.fetchReactions = TextChannel.prototype.fetchReactions; - klass.prototype.sendMessage = TextChannel.prototype.sendMessage; - klass.prototype.editMessage = TextChannel.prototype.editMessage; - klass.prototype.createWebhook = TextChannel.prototype.createWebhook; - } - - async fetchPins(): Promise { - const messages = await this.session.rest.runMethod( - this.session.rest, - "GET", - Routes.CHANNEL_PINS(this.id), - ); - return messages[0] ? messages.map((x: DiscordMessage) => new Message(this.session, x)) : []; - } - - async createInvite(options?: DiscordInviteOptions) { - const invite = await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_INVITES(this.id), - options - ? { - max_age: options.maxAge, - max_uses: options.maxUses, - temporary: options.temporary, - unique: options.unique, - target_type: options.targetType, - target_user_id: options.targetUserId, - target_application_id: options.targetApplicationId, - } - : {}, - ); - - return new Invite(this.session, invite); - } - - async fetchMessages(options?: GetMessagesOptions): Promise { - if (options?.limit! > 100) throw Error("Values must be between 0-100"); - const messages = await this.session.rest.runMethod( - this.session.rest, - "GET", - Routes.CHANNEL_MESSAGES(this.id, options), - ); - return messages[0] ? messages.map((x) => new Message(this.session, x)) : []; - } - - async sendTyping() { - await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_TYPING(this.id), - ); - } - - async pinMessage(messageId: Snowflake) { - await Message.prototype.pin.call({ id: messageId, channelId: this.id, session: this.session }); - } - - async unpinMessage(messageId: Snowflake) { - await Message.prototype.unpin.call({ id: messageId, channelId: this.id, session: this.session }); - } - - async addReaction(messageId: Snowflake, reaction: ReactionResolvable) { - await Message.prototype.addReaction.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - ); - } - - async removeReaction(messageId: Snowflake, reaction: ReactionResolvable, options?: { userId: Snowflake }) { - await Message.prototype.removeReaction.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - options, - ); - } - - async removeReactionEmoji(messageId: Snowflake, reaction: ReactionResolvable) { - await Message.prototype.removeReactionEmoji.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - ); - } - - async nukeReactions(messageId: Snowflake) { - await Message.prototype.nukeReactions.call({ channelId: this.id, id: messageId }); - } - - async fetchReactions(messageId: Snowflake, reaction: ReactionResolvable, options?: GetReactions) { - const users = await Message.prototype.fetchReactions.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - options, - ); - - return users; - } - - sendMessage(options: CreateMessage) { - return Message.prototype.reply.call({ channelId: this.id, session: this.session }, options); - } - - editMessage(messageId: Snowflake, options: EditMessage) { - return Message.prototype.edit.call({ channelId: this.id, id: messageId, session: this.session }, options); - } - - async createWebhook(options: CreateWebhook) { - const webhook = await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_WEBHOOKS(this.id), - { - name: options.name, - avatar: options.avatar ? urlToBase64(options.avatar) : undefined, - reason: options.reason, - }, - ); - - return new Webhook(this.session, webhook); - } -} - -export default TextChannel; diff --git a/structures/ThreadChannel.ts b/structures/ThreadChannel.ts deleted file mode 100644 index a6073eb..0000000 --- a/structures/ThreadChannel.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Model } from "./Base.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; -import type { Session } from "../session/Session.ts"; -import type { ChannelTypes, DiscordChannel } from "../vendor/external.ts"; -import GuildChannel from "./GuildChannel.ts"; -import TextChannel from "./TextChannel.ts"; - -export class ThreadChannel extends GuildChannel implements Model { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.type = data.type as number; - this.archived = !!data.thread_metadata?.archived; - this.archiveTimestamp = data.thread_metadata?.archive_timestamp; - this.autoArchiveDuration = data.thread_metadata?.auto_archive_duration; - this.locked = !!data.thread_metadata?.locked; - this.messageCount = data.message_count; - this.memberCount = data.member_count; - this.ownerId = data.owner_id; - } - - override type: ChannelTypes.GuildNewsThread | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread; - archived?: boolean; - archiveTimestamp?: string; - autoArchiveDuration?: number; - locked?: boolean; - messageCount?: number; - memberCount?: number; - ownerId?: Snowflake; -} - -TextChannel.applyTo(ThreadChannel); - -export interface ThreadChannel extends Omit, Omit {} - -export default ThreadChannel; diff --git a/structures/VoiceChannel.ts b/structures/VoiceChannel.ts deleted file mode 100644 index fa26b7e..0000000 --- a/structures/VoiceChannel.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { Snowflake } from "../util/Snowflake.ts"; -import type { Session } from "../session/Session.ts"; -import type { DiscordChannel, VideoQualityModes } from "../vendor/external.ts"; -import { GatewayOpcodes } from "../vendor/external.ts"; -import { calculateShardId } from "../vendor/gateway/calculateShardId.ts"; -import GuildChannel from "./GuildChannel.ts"; - -/** - * @link https://discord.com/developers/docs/topics/gateway#update-voice-state - */ -export interface UpdateVoiceState { - guildId: string; - channelId?: string; - selfMute: boolean; - selfDeaf: boolean; -} - -export class VoiceChannel extends GuildChannel { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.bitRate = data.bitrate; - this.userLimit = data.user_limit ?? 0; - this.videoQuality = data.video_quality_mode; - this.nsfw = !!data.nsfw; - - if (data.rtc_region) { - this.rtcRegion = data.rtc_region; - } - } - bitRate?: number; - userLimit: number; - rtcRegion?: Snowflake; - - videoQuality?: VideoQualityModes; - nsfw: boolean; - - /** - * This function was gathered from Discordeno it may not work - */ - async connect(options?: UpdateVoiceState) { - const shardId = calculateShardId(this.session.gateway, BigInt(super.guildId)); - const shard = this.session.gateway.manager.shards.get(shardId); - - if (!shard) { - throw new Error(`Shard (id: ${shardId} not found`); - } - - await shard.send({ - op: GatewayOpcodes.VoiceStateUpdate, - d: { - guild_id: super.guildId, - channel_id: super.id, - self_mute: Boolean(options?.selfMute), - self_deaf: options?.selfDeaf ?? true, - }, - }); - } -} - -export default VoiceChannel; From cc7a5848ebb28b34a34d9f99762b6b6a65bffd71 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sat, 2 Jul 2022 12:21:36 -0500 Subject: [PATCH 8/8] refactor: structures & folders 2 --- structures/channels/BaseChannel.ts | 51 ++++ structures/channels/ChannelFactory.ts | 39 +++ structures/channels/DMChannel.ts | 39 +++ structures/channels/GuildChannel.ts | 70 +++++ structures/channels/NewsChannel.ts | 31 +++ structures/channels/TextChannel.ts | 221 +++++++++++++++ structures/channels/ThreadChannel.ts | 35 +++ structures/channels/VoiceChannel.ts | 60 ++++ structures/guilds/AnonymousGuild.ts | 55 ++++ structures/guilds/BaseGuild.ts | 41 +++ structures/guilds/Guild.ts | 367 +++++++++++++++++++++++++ structures/guilds/InviteGuild.ts | 19 ++ structures/interactions/Interaction.ts | 145 ++++++++++ 13 files changed, 1173 insertions(+) create mode 100644 structures/channels/BaseChannel.ts create mode 100644 structures/channels/ChannelFactory.ts create mode 100644 structures/channels/DMChannel.ts create mode 100644 structures/channels/GuildChannel.ts create mode 100644 structures/channels/NewsChannel.ts create mode 100644 structures/channels/TextChannel.ts create mode 100644 structures/channels/ThreadChannel.ts create mode 100644 structures/channels/VoiceChannel.ts create mode 100644 structures/guilds/AnonymousGuild.ts create mode 100644 structures/guilds/BaseGuild.ts create mode 100644 structures/guilds/Guild.ts create mode 100644 structures/guilds/InviteGuild.ts create mode 100644 structures/interactions/Interaction.ts diff --git a/structures/channels/BaseChannel.ts b/structures/channels/BaseChannel.ts new file mode 100644 index 0000000..fc88e18 --- /dev/null +++ b/structures/channels/BaseChannel.ts @@ -0,0 +1,51 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordChannel } from "../../vendor/external.ts"; +import type TextChannel from "./TextChannel.ts"; +import type VoiceChannel from "./VoiceChannel.ts"; +import type DMChannel from "./DMChannel.ts"; +import type NewsChannel from "./NewsChannel.ts"; +import type ThreadChannel from "./ThreadChannel.ts"; +import { ChannelTypes } from "../../vendor/external.ts"; +import { textBasedChannels } from "./TextChannel.ts"; + +export abstract class BaseChannel implements Model { + constructor(session: Session, data: DiscordChannel) { + this.id = data.id; + this.session = session; + this.name = data.name; + this.type = data.type; + } + readonly id: Snowflake; + readonly session: Session; + + name?: string; + type: ChannelTypes; + + isText(): this is TextChannel { + return textBasedChannels.includes(this.type); + } + + isVoice(): this is VoiceChannel { + return this.type === ChannelTypes.GuildVoice; + } + + isDM(): this is DMChannel { + return this.type === ChannelTypes.DM; + } + + isNews(): this is NewsChannel { + return this.type === ChannelTypes.GuildNews; + } + + isThread(): this is ThreadChannel { + return this.type === ChannelTypes.GuildPublicThread || this.type === ChannelTypes.GuildPrivateThread; + } + + toString(): string { + return `<#${this.id}>`; + } +} + +export default BaseChannel; diff --git a/structures/channels/ChannelFactory.ts b/structures/channels/ChannelFactory.ts new file mode 100644 index 0000000..2603514 --- /dev/null +++ b/structures/channels/ChannelFactory.ts @@ -0,0 +1,39 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordChannel } from "../../vendor/external.ts"; +import { ChannelTypes } from "../../vendor/external.ts"; +import { textBasedChannels } from "./TextChannel.ts"; +import TextChannel from "./TextChannel.ts"; +import VoiceChannel from "./VoiceChannel.ts"; +import DMChannel from "./DMChannel.ts"; +import NewsChannel from "./NewsChannel.ts"; +import ThreadChannel from "./ThreadChannel.ts"; + +export type Channel = + | TextChannel + | VoiceChannel + | DMChannel + | NewsChannel + | ThreadChannel; + +export class ChannelFactory { + static from(session: Session, channel: DiscordChannel): Channel { + switch (channel.type) { + case ChannelTypes.GuildPublicThread: + case ChannelTypes.GuildPrivateThread: + return new ThreadChannel(session, channel, channel.guild_id!); + case ChannelTypes.GuildNews: + return new NewsChannel(session, channel, channel.guild_id!); + case ChannelTypes.DM: + return new DMChannel(session, channel); + case ChannelTypes.GuildVoice: + return new VoiceChannel(session, channel, channel.guild_id!); + default: + if (textBasedChannels.includes(channel.type)) { + return new TextChannel(session, channel); + } + throw new Error("Channel was not implemented"); + } + } +} + +export default ChannelFactory; diff --git a/structures/channels/DMChannel.ts b/structures/channels/DMChannel.ts new file mode 100644 index 0000000..ad94a05 --- /dev/null +++ b/structures/channels/DMChannel.ts @@ -0,0 +1,39 @@ +import type { Model } from "../Base.ts"; +import type { Session } from "../../session/Session.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { ChannelTypes, DiscordChannel } from "../../vendor/external.ts"; +import TextChannel from "./TextChannel.ts"; +import BaseChannel from "./BaseChannel.ts"; +import User from "../User.ts"; +import * as Routes from "../../util/Routes.ts"; + +export class DMChannel extends BaseChannel implements Model { + constructor(session: Session, data: DiscordChannel) { + super(session, data); + this.user = new User(this.session, data.recipents!.find((r) => r.id !== this.session.botId)!); + this.type = data.type as ChannelTypes.DM | ChannelTypes.GroupDm; + if (data.last_message_id) { + this.lastMessageId = data.last_message_id; + } + } + + override type: ChannelTypes.DM | ChannelTypes.GroupDm; + user: User; + lastMessageId?: Snowflake; + + async close() { + const channel = await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.CHANNEL(this.id), + ); + + return new DMChannel(this.session, channel); + } +} + +TextChannel.applyTo(DMChannel); + +export interface DMChannel extends Omit, Omit {} + +export default DMChannel; diff --git a/structures/channels/GuildChannel.ts b/structures/channels/GuildChannel.ts new file mode 100644 index 0000000..aaa4e53 --- /dev/null +++ b/structures/channels/GuildChannel.ts @@ -0,0 +1,70 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ChannelTypes, DiscordChannel, DiscordInviteMetadata } from "../../vendor/external.ts"; +import BaseChannel from "./BaseChannel.ts"; +import Invite from "../Invite.ts"; +import * as Routes from "../../util/Routes.ts"; + +/** + * Represent the options object to create a Thread Channel + * @link https://discord.com/developers/docs/resources/channel#start-thread-without-message + */ +export interface ThreadCreateOptions { + name: string; + autoArchiveDuration: 60 | 1440 | 4320 | 10080; + type: 10 | 11 | 12; + invitable?: boolean; + reason?: string; +} + +export class GuildChannel extends BaseChannel implements Model { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data); + this.type = data.type as number; + this.guildId = guildId; + this.position = data.position; + data.topic ? this.topic = data.topic : null; + data.parent_id ? this.parentId = data.parent_id : undefined; + } + + override type: Exclude; + guildId: Snowflake; + topic?: string; + position?: number; + parentId?: Snowflake; + + async fetchInvites(): Promise { + const invites = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.CHANNEL_INVITES(this.id), + ); + + return invites.map((invite) => new Invite(this.session, invite)); + } + + /* + async createThread(options: ThreadCreateOptions): Promise { + const thread = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_CREATE_THREAD(this.id), + options, + ); + return new ThreadChannel(this.session, thread, this.guildId); + }*/ + + async delete(reason?: string) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.CHANNEL(this.id), + { + reason, + }, + ); + } +} + +export default GuildChannel; diff --git a/structures/channels/NewsChannel.ts b/structures/channels/NewsChannel.ts new file mode 100644 index 0000000..a21ae61 --- /dev/null +++ b/structures/channels/NewsChannel.ts @@ -0,0 +1,31 @@ +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ChannelTypes, DiscordChannel } from "../../vendor/external.ts"; +import GuildChannel from "./GuildChannel.ts"; +import Message from "../Message.ts"; +import TextChannel from "./TextChannel.ts"; + +export class NewsChannel extends GuildChannel { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + this.type = data.type as ChannelTypes.GuildNews; + this.defaultAutoArchiveDuration = data.default_auto_archive_duration; + } + + override type: ChannelTypes.GuildNews; + defaultAutoArchiveDuration?: number; + + crosspostMessage(messageId: Snowflake): Promise { + return Message.prototype.crosspost.call({ id: messageId, channelId: this.id, session: this.session }); + } + + get publishMessage() { + return this.crosspostMessage; + } +} + +TextChannel.applyTo(NewsChannel); + +export interface NewsChannel extends TextChannel, GuildChannel {} + +export default NewsChannel; diff --git a/structures/channels/TextChannel.ts b/structures/channels/TextChannel.ts new file mode 100644 index 0000000..6f5e324 --- /dev/null +++ b/structures/channels/TextChannel.ts @@ -0,0 +1,221 @@ +// deno-lint-ignore-file ban-types +import type { Session } from "../../session/Session.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { GetMessagesOptions, GetReactions } from "../../util/Routes.ts"; +import type { + DiscordChannel, + DiscordInvite, + DiscordMessage, + DiscordWebhook, + TargetTypes, +} from "../../vendor/external.ts"; +import type { CreateMessage, EditMessage, ReactionResolvable } from "../Message.ts"; +import { ChannelTypes } from "../../vendor/external.ts"; +import { urlToBase64 } from "../../util/urlToBase64.ts"; +import Message from "../Message.ts"; +import Invite from "../Invite.ts"; +import Webhook from "../Webhook.ts"; +import * as Routes from "../../util/Routes.ts"; + +/** + * Represents the options object to create an invitation + * @link https://discord.com/developers/docs/resources/channel#create-channel-invite-json-params + */ +export interface DiscordInviteOptions { + maxAge?: number; + maxUses?: number; + unique?: boolean; + temporary: boolean; + reason?: string; + targetType?: TargetTypes; + targetUserId?: Snowflake; + targetApplicationId?: Snowflake; +} + +export interface CreateWebhook { + name: string; + avatar?: string; + reason?: string; +} + +export const textBasedChannels = [ + ChannelTypes.DM, + ChannelTypes.GroupDm, + ChannelTypes.GuildPrivateThread, + ChannelTypes.GuildPublicThread, + ChannelTypes.GuildNews, + ChannelTypes.GuildText, +]; + +export type TextBasedChannels = + | ChannelTypes.DM + | ChannelTypes.GroupDm + | ChannelTypes.GuildPrivateThread + | ChannelTypes.GuildPublicThread + | ChannelTypes.GuildNews + | ChannelTypes.GuildText; + +export class TextChannel { + constructor(session: Session, data: DiscordChannel) { + this.session = session; + this.id = data.id; + this.name = data.name; + this.type = data.type as number; + this.rateLimitPerUser = data.rate_limit_per_user ?? 0; + this.nsfw = !!data.nsfw ?? false; + + if (data.last_message_id) { + this.lastMessageId = data.last_message_id; + } + + if (data.last_pin_timestamp) { + this.lastPinTimestamp = data.last_pin_timestamp; + } + } + + readonly session: Session; + readonly id: Snowflake; + name?: string; + type: TextBasedChannels; + lastMessageId?: Snowflake; + lastPinTimestamp?: string; + rateLimitPerUser: number; + nsfw: boolean; + + /** + * Mixin + */ + static applyTo(klass: Function) { + klass.prototype.fetchPins = TextChannel.prototype.fetchPins; + klass.prototype.createInvite = TextChannel.prototype.createInvite; + klass.prototype.fetchMessages = TextChannel.prototype.fetchMessages; + klass.prototype.sendTyping = TextChannel.prototype.sendTyping; + klass.prototype.pinMessage = TextChannel.prototype.pinMessage; + klass.prototype.unpinMessage = TextChannel.prototype.unpinMessage; + klass.prototype.addReaction = TextChannel.prototype.addReaction; + klass.prototype.removeReaction = TextChannel.prototype.removeReaction; + klass.prototype.removeReactionEmoji = TextChannel.prototype.removeReactionEmoji; + klass.prototype.nukeReactions = TextChannel.prototype.nukeReactions; + klass.prototype.fetchReactions = TextChannel.prototype.fetchReactions; + klass.prototype.sendMessage = TextChannel.prototype.sendMessage; + klass.prototype.editMessage = TextChannel.prototype.editMessage; + klass.prototype.createWebhook = TextChannel.prototype.createWebhook; + } + + async fetchPins(): Promise { + const messages = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.CHANNEL_PINS(this.id), + ); + return messages[0] ? messages.map((x: DiscordMessage) => new Message(this.session, x)) : []; + } + + async createInvite(options?: DiscordInviteOptions) { + const invite = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_INVITES(this.id), + options + ? { + max_age: options.maxAge, + max_uses: options.maxUses, + temporary: options.temporary, + unique: options.unique, + target_type: options.targetType, + target_user_id: options.targetUserId, + target_application_id: options.targetApplicationId, + } + : {}, + ); + + return new Invite(this.session, invite); + } + + async fetchMessages(options?: GetMessagesOptions): Promise { + if (options?.limit! > 100) throw Error("Values must be between 0-100"); + const messages = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.CHANNEL_MESSAGES(this.id, options), + ); + return messages[0] ? messages.map((x) => new Message(this.session, x)) : []; + } + + async sendTyping() { + await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_TYPING(this.id), + ); + } + + async pinMessage(messageId: Snowflake) { + await Message.prototype.pin.call({ id: messageId, channelId: this.id, session: this.session }); + } + + async unpinMessage(messageId: Snowflake) { + await Message.prototype.unpin.call({ id: messageId, channelId: this.id, session: this.session }); + } + + async addReaction(messageId: Snowflake, reaction: ReactionResolvable) { + await Message.prototype.addReaction.call( + { channelId: this.id, id: messageId, session: this.session }, + reaction, + ); + } + + async removeReaction(messageId: Snowflake, reaction: ReactionResolvable, options?: { userId: Snowflake }) { + await Message.prototype.removeReaction.call( + { channelId: this.id, id: messageId, session: this.session }, + reaction, + options, + ); + } + + async removeReactionEmoji(messageId: Snowflake, reaction: ReactionResolvable) { + await Message.prototype.removeReactionEmoji.call( + { channelId: this.id, id: messageId, session: this.session }, + reaction, + ); + } + + async nukeReactions(messageId: Snowflake) { + await Message.prototype.nukeReactions.call({ channelId: this.id, id: messageId }); + } + + async fetchReactions(messageId: Snowflake, reaction: ReactionResolvable, options?: GetReactions) { + const users = await Message.prototype.fetchReactions.call( + { channelId: this.id, id: messageId, session: this.session }, + reaction, + options, + ); + + return users; + } + + sendMessage(options: CreateMessage) { + return Message.prototype.reply.call({ channelId: this.id, session: this.session }, options); + } + + editMessage(messageId: Snowflake, options: EditMessage) { + return Message.prototype.edit.call({ channelId: this.id, id: messageId, session: this.session }, options); + } + + async createWebhook(options: CreateWebhook) { + const webhook = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_WEBHOOKS(this.id), + { + name: options.name, + avatar: options.avatar ? urlToBase64(options.avatar) : undefined, + reason: options.reason, + }, + ); + + return new Webhook(this.session, webhook); + } +} + +export default TextChannel; diff --git a/structures/channels/ThreadChannel.ts b/structures/channels/ThreadChannel.ts new file mode 100644 index 0000000..459ba6e --- /dev/null +++ b/structures/channels/ThreadChannel.ts @@ -0,0 +1,35 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ChannelTypes, DiscordChannel } from "../../vendor/external.ts"; +import GuildChannel from "./GuildChannel.ts"; +import TextChannel from "./TextChannel.ts"; + +export class ThreadChannel extends GuildChannel implements Model { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + this.type = data.type as number; + this.archived = !!data.thread_metadata?.archived; + this.archiveTimestamp = data.thread_metadata?.archive_timestamp; + this.autoArchiveDuration = data.thread_metadata?.auto_archive_duration; + this.locked = !!data.thread_metadata?.locked; + this.messageCount = data.message_count; + this.memberCount = data.member_count; + this.ownerId = data.owner_id; + } + + override type: ChannelTypes.GuildNewsThread | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread; + archived?: boolean; + archiveTimestamp?: string; + autoArchiveDuration?: number; + locked?: boolean; + messageCount?: number; + memberCount?: number; + ownerId?: Snowflake; +} + +TextChannel.applyTo(ThreadChannel); + +export interface ThreadChannel extends Omit, Omit {} + +export default ThreadChannel; diff --git a/structures/channels/VoiceChannel.ts b/structures/channels/VoiceChannel.ts new file mode 100644 index 0000000..00da784 --- /dev/null +++ b/structures/channels/VoiceChannel.ts @@ -0,0 +1,60 @@ +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordChannel, VideoQualityModes } from "../../vendor/external.ts"; +import { GatewayOpcodes } from "../../vendor/external.ts"; +import { calculateShardId } from "../../vendor/gateway/calculateShardId.ts"; +import GuildChannel from "./GuildChannel.ts"; + +/** + * @link https://discord.com/developers/docs/topics/gateway#update-voice-state + */ +export interface UpdateVoiceState { + guildId: string; + channelId?: string; + selfMute: boolean; + selfDeaf: boolean; +} + +export class VoiceChannel extends GuildChannel { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + this.bitRate = data.bitrate; + this.userLimit = data.user_limit ?? 0; + this.videoQuality = data.video_quality_mode; + this.nsfw = !!data.nsfw; + + if (data.rtc_region) { + this.rtcRegion = data.rtc_region; + } + } + bitRate?: number; + userLimit: number; + rtcRegion?: Snowflake; + + videoQuality?: VideoQualityModes; + nsfw: boolean; + + /** + * This function was gathered from Discordeno it may not work + */ + async connect(options?: UpdateVoiceState) { + const shardId = calculateShardId(this.session.gateway, BigInt(super.guildId)); + const shard = this.session.gateway.manager.shards.get(shardId); + + if (!shard) { + throw new Error(`Shard (id: ${shardId} not found`); + } + + await shard.send({ + op: GatewayOpcodes.VoiceStateUpdate, + d: { + guild_id: super.guildId, + channel_id: super.id, + self_mute: Boolean(options?.selfMute), + self_deaf: options?.selfDeaf ?? true, + }, + }); + } +} + +export default VoiceChannel; diff --git a/structures/guilds/AnonymousGuild.ts b/structures/guilds/AnonymousGuild.ts new file mode 100644 index 0000000..c53308e --- /dev/null +++ b/structures/guilds/AnonymousGuild.ts @@ -0,0 +1,55 @@ +import type { Model } from "../Base.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordGuild, GuildNsfwLevel, VerificationLevels } from "../../vendor/external.ts"; +import type { ImageFormat, ImageSize } from "../../util/shared/images.ts"; +import { iconBigintToHash, iconHashToBigInt } from "../../util/hash.ts"; +import { formatImageUrl } from "../../util/shared/images.ts"; +import BaseGuild from "./BaseGuild.ts"; +import * as Routes from "../../util/Routes.ts"; + +export class AnonymousGuild extends BaseGuild implements Model { + constructor(session: Session, data: Partial); // TODO: Improve this type (name and id are required) + constructor(session: Session, data: DiscordGuild) { + super(session, data); + + this.splashHash = data.splash ? iconHashToBigInt(data.splash) : undefined; + this.bannerHash = data.banner ? iconHashToBigInt(data.banner) : undefined; + + this.verificationLevel = data.verification_level; + this.vanityUrlCode = data.vanity_url_code ? data.vanity_url_code : undefined; + this.nsfwLevel = data.nsfw_level; + this.description = data.description ? data.description : undefined; + this.premiumSubscriptionCount = data.premium_subscription_count; + } + + splashHash?: bigint; + bannerHash?: bigint; + + verificationLevel: VerificationLevels; + vanityUrlCode?: string; + nsfwLevel: GuildNsfwLevel; + description?: string; + premiumSubscriptionCount?: number; + + splashUrl(options: { size?: ImageSize; format?: ImageFormat } = { size: 128 }) { + if (this.splashHash) { + return formatImageUrl( + Routes.GUILD_SPLASH(this.id, iconBigintToHash(this.splashHash)), + options.size, + options.format, + ); + } + } + + bannerUrl(options: { size?: ImageSize; format?: ImageFormat } = { size: 128 }) { + if (this.bannerHash) { + return formatImageUrl( + Routes.GUILD_BANNER(this.id, iconBigintToHash(this.bannerHash)), + options.size, + options.format, + ); + } + } +} + +export default AnonymousGuild; diff --git a/structures/guilds/BaseGuild.ts b/structures/guilds/BaseGuild.ts new file mode 100644 index 0000000..0b50c61 --- /dev/null +++ b/structures/guilds/BaseGuild.ts @@ -0,0 +1,41 @@ +import type { Model } from "../Base.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordGuild, GuildFeatures } from "../../vendor/external.ts"; +import { Snowflake } from "../../util/Snowflake.ts"; +import { iconHashToBigInt } from "../../util/hash.ts"; + +/** + * Class for {@link Guild} and {@link AnonymousGuild} + */ +export abstract class BaseGuild implements Model { + constructor(session: Session, data: DiscordGuild) { + this.session = session; + this.id = data.id; + + this.name = data.name; + this.iconHash = data.icon ? iconHashToBigInt(data.icon) : undefined; + + this.features = data.features; + } + + readonly session: Session; + readonly id: Snowflake; + + name: string; + iconHash?: bigint; + features: GuildFeatures[]; + + get createdTimestamp() { + return Snowflake.snowflakeToTimestamp(this.id); + } + + get createdAt() { + return new Date(this.createdTimestamp); + } + + toString() { + return this.name; + } +} + +export default BaseGuild; diff --git a/structures/guilds/Guild.ts b/structures/guilds/Guild.ts new file mode 100644 index 0000000..fe39151 --- /dev/null +++ b/structures/guilds/Guild.ts @@ -0,0 +1,367 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { + DiscordEmoji, + DiscordGuild, + DiscordInviteMetadata, + DiscordMemberWithUser, + DiscordRole, +} from "../../vendor/external.ts"; +import type { GetInvite } from "../../util/Routes.ts"; +import { + DefaultMessageNotificationLevels, + ExplicitContentFilterLevels, + VerificationLevels, +} from "../../vendor/external.ts"; +import { iconBigintToHash, iconHashToBigInt } from "../../util/hash.ts"; +import { urlToBase64 } from "../../util/urlToBase64.ts"; +import Member from "../Member.ts"; +import BaseGuild from "./BaseGuild.ts"; +import Role from "../Role.ts"; +import GuildEmoji from "../GuildEmoji.ts"; +import Invite from "../Invite.ts"; +import * as Routes from "../../util/Routes.ts"; + +export interface CreateRole { + name?: string; + color?: number; + iconHash?: string | bigint; + unicodeEmoji?: string; + hoist?: boolean; + mentionable?: boolean; +} + +export interface ModifyGuildRole { + name?: string; + color?: number; + hoist?: boolean; + mentionable?: boolean; + unicodeEmoji?: string; +} + +export interface CreateGuildEmoji { + name: string; + image: string; + roles?: Snowflake[]; + reason?: string; +} + +export interface ModifyGuildEmoji { + name?: string; + roles?: Snowflake[]; +} + +/** + * @link https://discord.com/developers/docs/resources/guild#create-guild-ban + */ +export interface CreateGuildBan { + deleteMessageDays?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; + reason?: string; +} + +/** + * @link https://discord.com/developers/docs/resources/guild#modify-guild-member + */ +export interface ModifyGuildMember { + nick?: string; + roles?: Snowflake[]; + mute?: boolean; + deaf?: boolean; + channelId?: Snowflake; + communicationDisabledUntil?: number; +} + +/** + * @link https://discord.com/developers/docs/resources/guild#begin-guild-prune + */ +export interface BeginGuildPrune { + days?: number; + computePruneCount?: boolean; + includeRoles?: Snowflake[]; +} + +export interface ModifyRolePositions { + id: Snowflake; + position?: number | null; +} + +/** + * Represents a guild + * @link https://discord.com/developers/docs/resources/guild#guild-object + */ +export class Guild extends BaseGuild implements Model { + constructor(session: Session, data: DiscordGuild) { + super(session, data); + + this.splashHash = data.splash ? iconHashToBigInt(data.splash) : undefined; + this.discoverySplashHash = data.discovery_splash ? iconHashToBigInt(data.discovery_splash) : undefined; + this.ownerId = data.owner_id; + this.widgetEnabled = !!data.widget_enabled; + this.widgetChannelId = data.widget_channel_id ? data.widget_channel_id : undefined; + this.vefificationLevel = data.verification_level; + this.defaultMessageNotificationLevel = data.default_message_notifications; + this.explicitContentFilterLevel = data.explicit_content_filter; + this.members = data.members?.map((member) => new Member(session, { ...member, user: member.user! }, data.id)) ?? + []; + this.roles = data.roles.map((role) => new Role(session, role, data.id)); + this.emojis = data.emojis.map((guildEmoji) => new GuildEmoji(session, guildEmoji, data.id)); + } + + splashHash?: bigint; + discoverySplashHash?: bigint; + ownerId: Snowflake; + widgetEnabled: boolean; + widgetChannelId?: Snowflake; + vefificationLevel: VerificationLevels; + defaultMessageNotificationLevel: DefaultMessageNotificationLevels; + explicitContentFilterLevel: ExplicitContentFilterLevels; + members: Member[]; + roles: Role[]; + emojis: GuildEmoji[]; + + /** + * 'null' would reset the nickname + */ + async editBotNickname(options: { nick: string | null; reason?: string }) { + const result = await this.session.rest.runMethod<{ nick?: string } | undefined>( + this.session.rest, + "PATCH", + Routes.USER_NICK(this.id), + options, + ); + + return result?.nick; + } + + async createEmoji(options: CreateGuildEmoji): Promise { + if (options.image && !options.image.startsWith("data:image/")) { + options.image = await urlToBase64(options.image); + } + + const emoji = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.GUILD_EMOJIS(this.id), + options, + ); + + return new GuildEmoji(this.session, emoji, this.id); + } + + async deleteEmoji(id: Snowflake, { reason }: { reason?: string } = {}): Promise { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.GUILD_EMOJI(this.id, id), + { reason }, + ); + } + + async editEmoji(id: Snowflake, options: ModifyGuildEmoji): Promise { + const emoji = await this.session.rest.runMethod( + this.session.rest, + "PATCH", + Routes.GUILD_EMOJI(this.id, id), + options, + ); + + return new GuildEmoji(this.session, emoji, this.id); + } + + async createRole(options: CreateRole): Promise { + let icon: string | undefined; + + if (options.iconHash) { + if (typeof options.iconHash === "string") { + icon = options.iconHash; + } else { + icon = iconBigintToHash(options.iconHash); + } + } + + const role = await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.GUILD_ROLES(this.id), + { + name: options.name, + color: options.color, + icon, + unicode_emoji: options.unicodeEmoji, + hoist: options.hoist, + mentionable: options.mentionable, + }, + ); + + return new Role(this.session, role, this.id); + } + + async deleteRole(roleId: Snowflake): Promise { + await this.session.rest.runMethod(this.session.rest, "DELETE", Routes.GUILD_ROLE(this.id, roleId)); + } + + async editRole(roleId: Snowflake, options: ModifyGuildRole): Promise { + const role = await this.session.rest.runMethod( + this.session.rest, + "PATCH", + Routes.GUILD_ROLE(this.id, roleId), + { + name: options.name, + color: options.color, + hoist: options.hoist, + mentionable: options.mentionable, + }, + ); + + return new Role(this.session, role, this.id); + } + + async addRole(memberId: Snowflake, roleId: Snowflake, { reason }: { reason?: string } = {}) { + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.GUILD_MEMBER_ROLE(this.id, memberId, roleId), + { reason }, + ); + } + + async removeRole(memberId: Snowflake, roleId: Snowflake, { reason }: { reason?: string } = {}) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.GUILD_MEMBER_ROLE(this.id, memberId, roleId), + { reason }, + ); + } + + /** + * Returns the roles moved + */ + async moveRoles(options: ModifyRolePositions[]) { + const roles = await this.session.rest.runMethod( + this.session.rest, + "PATCH", + Routes.GUILD_ROLES(this.id), + options, + ); + + return roles.map((role) => new Role(this.session, role, this.id)); + } + + async deleteInvite(inviteCode: string): Promise { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.INVITE(inviteCode), + {}, + ); + } + + async fetchInvite(inviteCode: string, options: GetInvite): Promise { + const inviteMetadata = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.INVITE(inviteCode, options), + ); + + return new Invite(this.session, inviteMetadata); + } + + async fetchInvites(): Promise { + const invites = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.GUILD_INVITES(this.id), + ); + + return invites.map((invite) => new Invite(this.session, invite)); + } + + /** + * Bans the member + */ + async banMember(memberId: Snowflake, options: CreateGuildBan) { + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.GUILD_BAN(this.id, memberId), + options + ? { + delete_message_days: options.deleteMessageDays, + reason: options.reason, + } + : {}, + ); + } + + /** + * Kicks the member + */ + async kickMember(memberId: Snowflake, { reason }: { reason?: string }) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.GUILD_MEMBER(this.id, memberId), + { reason }, + ); + } + + /* + * Unbans the member + * */ + async unbanMember(memberId: Snowflake) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.GUILD_BAN(this.id, memberId), + ); + } + + async editMember(memberId: Snowflake, options: ModifyGuildMember) { + const member = await this.session.rest.runMethod( + this.session.rest, + "PATCH", + Routes.GUILD_MEMBER(this.id, memberId), + { + nick: options.nick, + roles: options.roles, + mute: options.mute, + deaf: options.deaf, + channel_id: options.channelId, + communication_disabled_until: options.communicationDisabledUntil + ? new Date(options.communicationDisabledUntil).toISOString() + : undefined, + }, + ); + + return new Member(this.session, member, this.id); + } + + async pruneMembers(options: BeginGuildPrune): Promise { + const result = await this.session.rest.runMethod<{ pruned: number }>( + this.session.rest, + "POST", + Routes.GUILD_PRUNE(this.id), + { + days: options.days, + compute_prune_count: options.computePruneCount, + include_roles: options.includeRoles, + }, + ); + + return result.pruned; + } + + async getPruneCount(): Promise { + const result = await this.session.rest.runMethod<{ pruned: number }>( + this.session.rest, + "GET", + Routes.GUILD_PRUNE(this.id), + ); + + return result.pruned; + } +} + +export default Guild; diff --git a/structures/guilds/InviteGuild.ts b/structures/guilds/InviteGuild.ts new file mode 100644 index 0000000..85e3d50 --- /dev/null +++ b/structures/guilds/InviteGuild.ts @@ -0,0 +1,19 @@ +import type { Model } from "../Base.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordGuild } from "../../vendor/external.ts"; +import AnonymousGuild from "./AnonymousGuild.ts"; +import WelcomeScreen from "../WelcomeScreen.ts"; + +export class InviteGuild extends AnonymousGuild implements Model { + constructor(session: Session, data: Partial) { + super(session, data); + + if (data.welcome_screen) { + this.welcomeScreen = new WelcomeScreen(session, data.welcome_screen); + } + } + + welcomeScreen?: WelcomeScreen; +} + +export default InviteGuild; diff --git a/structures/interactions/Interaction.ts b/structures/interactions/Interaction.ts new file mode 100644 index 0000000..5ed2b94 --- /dev/null +++ b/structures/interactions/Interaction.ts @@ -0,0 +1,145 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { + DiscordInteraction, + DiscordMessage, + FileContent, + InteractionResponseTypes, + InteractionTypes, +} from "../../vendor/external.ts"; +import type { MessageFlags } from "../../util/shared/flags.ts"; +import type { AllowedMentions } from "../Message.ts"; +import User from "../User.ts"; +import Message from "../Message.ts"; +import Member from "../Member.ts"; +import * as Routes from "../../util/Routes.ts"; + +export interface InteractionResponse { + type: InteractionResponseTypes; + data?: InteractionApplicationCommandCallbackData; +} + +export interface InteractionApplicationCommandCallbackData { + content?: string; + tts?: boolean; + allowedMentions?: AllowedMentions; + files?: FileContent[]; + customId?: string; + title?: string; + // components?: Component[]; + flags?: MessageFlags; + choices?: ApplicationCommandOptionChoice[]; +} + +/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice */ +export interface ApplicationCommandOptionChoice { + name: string; + value: string | number; +} + +// TODO: abstract Interaction, CommandInteraction, ComponentInteraction, PingInteraction, etc + +export class Interaction implements Model { + constructor(session: Session, data: DiscordInteraction) { + this.session = session; + this.id = data.id; + this.token = data.token; + this.type = data.type; + this.guildId = data.guild_id; + this.channelId = data.channel_id; + this.applicationId = data.application_id; + this.locale = data.locale; + this.data = data.data; + + if (!data.guild_id) { + this.user = new User(session, data.user!); + } else { + this.member = new Member(session, data.member!, data.guild_id); + } + } + + readonly session: Session; + readonly id: Snowflake; + readonly token: string; + + type: InteractionTypes; + guildId?: Snowflake; + channelId?: Snowflake; + applicationId?: Snowflake; + locale?: string; + // deno-lint-ignore no-explicit-any + data: any; + user?: User; + member?: Member; + + async respond({ type, data }: InteractionResponse) { + const toSend = { + tts: data?.tts, + title: data?.title, + flags: data?.flags, + content: data?.content, + choices: data?.choices, + custom_id: data?.customId, + allowed_mentions: data?.allowedMentions + ? { + users: data.allowedMentions.users, + roles: data.allowedMentions.roles, + parse: data.allowedMentions.parse, + replied_user: data.allowedMentions.repliedUser, + } + : { parse: [] }, + }; + + if (this.session.unrepliedInteractions.delete(BigInt(this.id))) { + await this.session.rest.sendRequest( + this.session.rest, + { + url: Routes.INTERACTION_ID_TOKEN(this.id, this.token), + method: "POST", + payload: this.session.rest.createRequestBody(this.session.rest, { + method: "POST", + body: { + type: type, + data: toSend, + file: data?.files, + }, + headers: { + // remove authorization header + Authorization: "", + }, + }), + }, + ); + + return; + } + + const result = await this.session.rest.sendRequest( + this.session.rest, + { + url: Routes.WEBHOOK(this.session.applicationId ?? this.session.botId, this.token), + method: "POST", + payload: this.session.rest.createRequestBody(this.session.rest, { + method: "POST", + body: { + ...toSend, + file: data?.files, + }, + headers: { + // remove authorization header + Authorization: "", + }, + }), + }, + ); + + return new Message(this.session, result); + } + + inGuild(): this is Interaction & { user: undefined; guildId: Snowflake; member: Member } { + return !!this.guildId; + } +} + +export default Interaction;