From fbfeecf8c533ece9f6a8d3460d9cbae9cf9c0c1e Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sat, 2 Jul 2022 14:06:04 -0500 Subject: [PATCH 01/45] feat: embeds --- mod.ts | 2 + structures/Message.ts | 4 +- structures/builders/EmbedBuilder.ts | 113 ++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 structures/builders/EmbedBuilder.ts diff --git a/mod.ts b/mod.ts index f7d725a..d5cbe11 100644 --- a/mod.ts +++ b/mod.ts @@ -33,6 +33,8 @@ export * from "./structures/guilds/BaseGuild.ts"; export * from "./structures/guilds/Guild.ts"; export * from "./structures/guilds/InviteGuild.ts"; +export * from "./structures/builders/EmbedBuilder.ts"; + export * from "./structures/interactions/Interaction.ts"; export * from "./session/Session.ts"; diff --git a/structures/Message.ts b/structures/Message.ts index 3d734d8..f620ccd 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -14,7 +14,7 @@ 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 ComponentFactory from "./components/ComponentFactory.ts"; import * as Routes from "../util/Routes.ts"; /** @@ -83,7 +83,7 @@ export class Message implements Model { this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id); } - this.components = data.components?.map((component) => BaseComponent.from(session, component)); + this.components = data.components?.map((component) => ComponentFactory.from(session, component)); } readonly session: Session; diff --git a/structures/builders/EmbedBuilder.ts b/structures/builders/EmbedBuilder.ts new file mode 100644 index 0000000..2359c43 --- /dev/null +++ b/structures/builders/EmbedBuilder.ts @@ -0,0 +1,113 @@ +import type { + DiscordEmbedField, + DiscordEmbed, + DiscordEmbedProvider +} from '../../vendor/external.ts'; + +export interface EmbedFooter { + text: string; + iconUrl?: string; + proxyIconUrl?: string +} + +export interface EmbedAuthor { + name: string + text?: string; + url?: string; + iconUrl?: string; + proxyIconUrl?: string +} + +export interface EmbedVideo { + height?: number; + proxyUrl?: string; + url?: string; + width?: number; +} + +export class EmbedBuilder { + #data: DiscordEmbed + constructor(data: DiscordEmbed = {}) { + this.#data = data; + if (!this.#data.fields) this.#data.fields = [] + } + + setAuthor(author: EmbedAuthor) { + this.#data.author = { + name: author.name, + icon_url: author.iconUrl, + proxy_icon_url: author.proxyIconUrl, + url: author.url + }; + return this; + } + + setColor(color: number) { + this.#data.color = color; + return this; + } + + setDescription(description: string) { + this.#data.description = description; + return this; + } + + addField(field: DiscordEmbedField) { + this.#data.fields!.push(field); + return this; + } + + setFooter(footer: EmbedFooter) { + this.#data.footer = { + text: footer.text, + icon_url: footer.iconUrl, + proxy_icon_url: footer.proxyIconUrl, + }; + return this; + } + + setImage(image: string) { + this.#data.image = { url: image }; + return this; + } + + setProvider(provider: DiscordEmbedProvider) { + this.#data.provider = provider; + return this; + } + + setThumbnail(thumbnail: string) { + this.#data.thumbnail = { url: thumbnail }; + return this; + } + + setTimestamp(timestamp: string | Date) { + this.#data.timestamp = timestamp instanceof Date ? timestamp.toISOString() : timestamp; + return this; + } + + setTitle(title: string, url?: string) { + this.#data.title = title; + if (url) this.setUrl(url); + return this; + } + + setUrl(url: string) { + this.#data.url = url; + return this; + } + + setVideo(video: EmbedVideo) { + this.#data.video = { + height: video.height, + proxy_url: video.proxyUrl, + url: video.url, + width: video.width + }; + return this; + } + + toJSON(): DiscordEmbed { + return this.#data; + } +} From 2b0a930d37a75cf3babba5544a713c41ce4b7001 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 2 Jul 2022 16:09:45 -0300 Subject: [PATCH 02/45] Push add-n128 (#6) * Update User.ts method: avatarUrl -> avatarURL * Url -> URL * Fix endpoint for BaseGuild->iconURL * formatting --- structures/User.ts | 6 +++--- structures/guilds/AnonymousGuild.ts | 10 +++++----- structures/guilds/BaseGuild.ts | 8 ++++---- util/shared/images.ts | 2 +- vendor/types/shared.ts | 8 ++++---- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/structures/User.ts b/structures/User.ts index abd6c22..da8c5ee 100644 --- a/structures/User.ts +++ b/structures/User.ts @@ -4,7 +4,7 @@ import type { Session } from "../session/Session.ts"; import type { DiscordUser } 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 { formatImageURL } from "../util/shared/images.ts"; import * as Routes from "../util/Routes.ts"; /** @@ -42,7 +42,7 @@ export class User implements Model { } /** gets the user's avatar */ - avatarUrl(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }) { + avatarURL(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }) { let url: string; if (!this.avatarHash) { @@ -51,7 +51,7 @@ export class User implements Model { url = Routes.USER_AVATAR(this.id, iconBigintToHash(this.avatarHash)); } - return formatImageUrl(url, options.size, options.format); + return formatImageURL(url, options.size, options.format); } toString() { diff --git a/structures/guilds/AnonymousGuild.ts b/structures/guilds/AnonymousGuild.ts index c53308e..995078b 100644 --- a/structures/guilds/AnonymousGuild.ts +++ b/structures/guilds/AnonymousGuild.ts @@ -3,7 +3,7 @@ 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 { formatImageURL } from "../../util/shared/images.ts"; import BaseGuild from "./BaseGuild.ts"; import * as Routes from "../../util/Routes.ts"; @@ -31,9 +31,9 @@ export class AnonymousGuild extends BaseGuild implements Model { description?: string; premiumSubscriptionCount?: number; - splashUrl(options: { size?: ImageSize; format?: ImageFormat } = { size: 128 }) { + splashURL(options: { size?: ImageSize; format?: ImageFormat } = { size: 128 }) { if (this.splashHash) { - return formatImageUrl( + return formatImageURL( Routes.GUILD_SPLASH(this.id, iconBigintToHash(this.splashHash)), options.size, options.format, @@ -41,9 +41,9 @@ export class AnonymousGuild extends BaseGuild implements Model { } } - bannerUrl(options: { size?: ImageSize; format?: ImageFormat } = { size: 128 }) { + bannerURL(options: { size?: ImageSize; format?: ImageFormat } = { size: 128 }) { if (this.bannerHash) { - return formatImageUrl( + return formatImageURL( Routes.GUILD_BANNER(this.id, iconBigintToHash(this.bannerHash)), options.size, options.format, diff --git a/structures/guilds/BaseGuild.ts b/structures/guilds/BaseGuild.ts index c396516..31dca44 100644 --- a/structures/guilds/BaseGuild.ts +++ b/structures/guilds/BaseGuild.ts @@ -2,7 +2,7 @@ import type { Model } from "../Base.ts"; import type { Session } from "../../session/Session.ts"; import type { DiscordGuild } from "../../vendor/external.ts"; import type { ImageFormat, ImageSize } from "../../util/shared/images.ts"; -import { formatImageUrl } from "../../util/shared/images.ts"; +import { formatImageURL } from "../../util/shared/images.ts"; import { iconBigintToHash, iconHashToBigInt } from "../../util/hash.ts"; import { GuildFeatures } from "../../vendor/external.ts"; import { Snowflake } from "../../util/Snowflake.ts"; @@ -45,10 +45,10 @@ export abstract class BaseGuild implements Model { return this.features.includes(GuildFeatures.Verified); } - iconUrl(options: { size?: ImageSize; format?: ImageFormat } = { size: 128 }) { + iconURL(options: { size?: ImageSize; format?: ImageFormat } = { size: 128 }) { if (this.iconHash) { - return formatImageUrl( - Routes.GUILD_BANNER(this.id, iconBigintToHash(this.iconHash)), + return formatImageURL( + Routes.GUILD_ICON(this.id, iconBigintToHash(this.iconHash)), options.size, options.format, ); diff --git a/util/shared/images.ts b/util/shared/images.ts index 06ba7aa..f5a0524 100644 --- a/util/shared/images.ts +++ b/util/shared/images.ts @@ -9,6 +9,6 @@ export type ImageFormat = "jpg" | "jpeg" | "png" | "webp" | "gif" | "json"; export type ImageSize = 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096; /** Help format an image url */ -export function formatImageUrl(url: string, size: ImageSize = 128, format?: ImageFormat) { +export function formatImageURL(url: string, size: ImageSize = 128, format?: ImageFormat) { return `${url}.${format || (url.includes("/a_") ? "gif" : "jpg")}?size=${size}`; } diff --git a/vendor/types/shared.ts b/vendor/types/shared.ts index e2b205d..6c90cdf 100644 --- a/vendor/types/shared.ts +++ b/vendor/types/shared.ts @@ -1238,7 +1238,7 @@ export type CamelCase = S extends `${infer P1}_${infer P2}${in : Lowercase; export type Camelize = { [K in keyof T as CamelCase]: T[K] extends Array ? U extends {} ? Array> - : T[K] + : T[K] : T[K] extends {} ? Camelize : never; }; @@ -1293,8 +1293,8 @@ export type AnythingBut = Exclude< * object identity type */ export type Id = T extends infer U ? { - [K in keyof U]: U[K]; - } + [K in keyof U]: U[K]; +} : never; export type KeysWithUndefined = { @@ -1319,7 +1319,7 @@ type OptionalizeAux = Id< export type Optionalize = T extends object ? T extends Array ? number extends T["length"] ? T[number] extends object ? Array> - : T + : T : Partial : OptionalizeAux : T; From e60deb6cd963e6337eee9a2653459ab2a7cfe4a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Sat, 2 Jul 2022 16:48:09 -0300 Subject: [PATCH 03/45] Update Member: avatarUrl -> avatarURL & using formatImageURL --- structures/Member.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/structures/Member.ts b/structures/Member.ts index 601d5af..028edf7 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -5,6 +5,7 @@ import type { DiscordMemberWithUser } from "../vendor/external.ts"; import type { ImageFormat, ImageSize } from "../util/shared/images.ts"; import type { CreateGuildBan, ModifyGuildMember } from "./guilds/Guild.ts"; import { iconBigintToHash, iconHashToBigInt } from "../util/hash.ts"; +import { formatImageURL } from "../util/shared/images.ts"; import User from "./User.ts"; import Guild from "./guilds/Guild.ts"; import * as Routes from "../util/Routes.ts"; @@ -96,7 +97,7 @@ export class Member implements Model { } /** gets the user's avatar */ - avatarUrl(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }) { + avatarURL(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }) { let url: string; if (!this.avatarHash) { @@ -105,7 +106,7 @@ export class Member implements Model { url = Routes.USER_AVATAR(this.user.id, iconBigintToHash(this.avatarHash)); } - return `${url}.${options.format ?? (url.includes("/a_") ? "gif" : "jpg")}?size=${options.size}`; + return formatImageURL(url, options.size, options.format) } toString() { From aa3c2724d36a08c14074012d2372cb2c403a59d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Sat, 2 Jul 2022 17:04:03 -0300 Subject: [PATCH 04/45] Fix Member: avatarURL() allowed for bots --- structures/Member.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/structures/Member.ts b/structures/Member.ts index 028edf7..0152526 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -100,6 +100,10 @@ export class Member implements Model { avatarURL(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }) { let url: string; + if (this.user.bot) { + return this.user.avatarURL() + } + if (!this.avatarHash) { url = Routes.USER_DEFAULT_AVATAR(Number(this.user.discriminator) % 5); } else { From 482893a9ead93e995947d56bf68d8a62f34e5ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Sat, 2 Jul 2022 18:04:24 -0300 Subject: [PATCH 05/45] Add webhook handling for Message --- structures/Message.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/structures/Message.ts b/structures/Message.ts index f620ccd..483516b 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -15,6 +15,7 @@ import User from "./User.ts"; import Member from "./Member.ts"; import Attachment from "./Attachment.ts"; import ComponentFactory from "./components/ComponentFactory.ts"; +import { iconHashToBigInt } from "../util/hash.ts"; import * as Routes from "../util/Routes.ts"; /** @@ -57,6 +58,13 @@ export type ReactionResolvable = string | { id: Snowflake; }; +export interface WebhookAuthor { + id: string; + username: string; + discriminator: string; + avatar?: bigint; +} + /** * Represents a message * @link https://discord.com/developers/docs/resources/channel#message-object @@ -77,9 +85,19 @@ export class Message implements Model { this.attachments = data.attachments.map((attachment) => new Attachment(session, attachment)); + // webhook handling + if (data.author && data.author.discriminator === "0000") { + this.webhook = { + id: data.author.id, + username: data.author.username, + discriminator: data.author.discriminator, + avatar: data.author.avatar ? iconHashToBigInt(data.author.avatar) : undefined, + }; + } + // user is always null on MessageCreate and its replaced with author - if (data.guild_id && data.member) { + if (data.guild_id && data.member && data.author && !this.isWebhookMessage()) { this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id); } @@ -101,6 +119,8 @@ export class Message implements Model { member?: Member; components?: Component[]; + webhook?: WebhookAuthor; + get url() { return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`; } @@ -284,6 +304,11 @@ export class Message implements Model { inGuild(): this is { guildId: Snowflake } & Message { return !!this.guildId; } + + /** isWebhookMessage if the messages comes from a Webhook */ + isWebhookMessage(): this is User & { author: Partial; webhook: WebhookAuthor; member: undefined } { + return !!this.webhook; + } } export default Message; From c13d9b239fa30d59c9d0610841cac9217cdcad67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Sat, 2 Jul 2022 18:04:40 -0300 Subject: [PATCH 06/45] formatting --- structures/Member.ts | 4 ++-- structures/builders/EmbedBuilder.ts | 20 ++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/structures/Member.ts b/structures/Member.ts index 0152526..d0b0d92 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -101,7 +101,7 @@ export class Member implements Model { let url: string; if (this.user.bot) { - return this.user.avatarURL() + return this.user.avatarURL(); } if (!this.avatarHash) { @@ -110,7 +110,7 @@ export class Member implements Model { url = Routes.USER_AVATAR(this.user.id, iconBigintToHash(this.avatarHash)); } - return formatImageURL(url, options.size, options.format) + return formatImageURL(url, options.size, options.format); } toString() { diff --git a/structures/builders/EmbedBuilder.ts b/structures/builders/EmbedBuilder.ts index 2359c43..8e48e78 100644 --- a/structures/builders/EmbedBuilder.ts +++ b/structures/builders/EmbedBuilder.ts @@ -1,21 +1,17 @@ -import type { - DiscordEmbedField, - DiscordEmbed, - DiscordEmbedProvider -} from '../../vendor/external.ts'; +import type { DiscordEmbed, DiscordEmbedField, DiscordEmbedProvider } from "../../vendor/external.ts"; export interface EmbedFooter { text: string; iconUrl?: string; - proxyIconUrl?: string + proxyIconUrl?: string; } export interface EmbedAuthor { - name: string + name: string; text?: string; url?: string; iconUrl?: string; - proxyIconUrl?: string + proxyIconUrl?: string; } export interface EmbedVideo { @@ -26,10 +22,10 @@ export interface EmbedVideo { } export class EmbedBuilder { - #data: DiscordEmbed + #data: DiscordEmbed; constructor(data: DiscordEmbed = {}) { this.#data = data; - if (!this.#data.fields) this.#data.fields = [] + if (!this.#data.fields) this.#data.fields = []; } setAuthor(author: EmbedAuthor) { @@ -37,7 +33,7 @@ export class EmbedBuilder { name: author.name, icon_url: author.iconUrl, proxy_icon_url: author.proxyIconUrl, - url: author.url + url: author.url, }; return this; } @@ -102,7 +98,7 @@ export class EmbedBuilder { height: video.height, proxy_url: video.proxyUrl, url: video.url, - width: video.width + width: video.width, }; return this; } From 78b8cf9da0b1b127ce908a52f8d877afeced37f8 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 2 Jul 2022 18:08:37 -0300 Subject: [PATCH 07/45] Webhook addition for Message (#7) * Add webhook handling for Message * formatting --- structures/Member.ts | 4 ++-- structures/Message.ts | 27 ++++++++++++++++++++++++++- structures/builders/EmbedBuilder.ts | 20 ++++++++------------ 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/structures/Member.ts b/structures/Member.ts index 0152526..d0b0d92 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -101,7 +101,7 @@ export class Member implements Model { let url: string; if (this.user.bot) { - return this.user.avatarURL() + return this.user.avatarURL(); } if (!this.avatarHash) { @@ -110,7 +110,7 @@ export class Member implements Model { url = Routes.USER_AVATAR(this.user.id, iconBigintToHash(this.avatarHash)); } - return formatImageURL(url, options.size, options.format) + return formatImageURL(url, options.size, options.format); } toString() { diff --git a/structures/Message.ts b/structures/Message.ts index f620ccd..483516b 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -15,6 +15,7 @@ import User from "./User.ts"; import Member from "./Member.ts"; import Attachment from "./Attachment.ts"; import ComponentFactory from "./components/ComponentFactory.ts"; +import { iconHashToBigInt } from "../util/hash.ts"; import * as Routes from "../util/Routes.ts"; /** @@ -57,6 +58,13 @@ export type ReactionResolvable = string | { id: Snowflake; }; +export interface WebhookAuthor { + id: string; + username: string; + discriminator: string; + avatar?: bigint; +} + /** * Represents a message * @link https://discord.com/developers/docs/resources/channel#message-object @@ -77,9 +85,19 @@ export class Message implements Model { this.attachments = data.attachments.map((attachment) => new Attachment(session, attachment)); + // webhook handling + if (data.author && data.author.discriminator === "0000") { + this.webhook = { + id: data.author.id, + username: data.author.username, + discriminator: data.author.discriminator, + avatar: data.author.avatar ? iconHashToBigInt(data.author.avatar) : undefined, + }; + } + // user is always null on MessageCreate and its replaced with author - if (data.guild_id && data.member) { + if (data.guild_id && data.member && data.author && !this.isWebhookMessage()) { this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id); } @@ -101,6 +119,8 @@ export class Message implements Model { member?: Member; components?: Component[]; + webhook?: WebhookAuthor; + get url() { return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`; } @@ -284,6 +304,11 @@ export class Message implements Model { inGuild(): this is { guildId: Snowflake } & Message { return !!this.guildId; } + + /** isWebhookMessage if the messages comes from a Webhook */ + isWebhookMessage(): this is User & { author: Partial; webhook: WebhookAuthor; member: undefined } { + return !!this.webhook; + } } export default Message; diff --git a/structures/builders/EmbedBuilder.ts b/structures/builders/EmbedBuilder.ts index 2359c43..8e48e78 100644 --- a/structures/builders/EmbedBuilder.ts +++ b/structures/builders/EmbedBuilder.ts @@ -1,21 +1,17 @@ -import type { - DiscordEmbedField, - DiscordEmbed, - DiscordEmbedProvider -} from '../../vendor/external.ts'; +import type { DiscordEmbed, DiscordEmbedField, DiscordEmbedProvider } from "../../vendor/external.ts"; export interface EmbedFooter { text: string; iconUrl?: string; - proxyIconUrl?: string + proxyIconUrl?: string; } export interface EmbedAuthor { - name: string + name: string; text?: string; url?: string; iconUrl?: string; - proxyIconUrl?: string + proxyIconUrl?: string; } export interface EmbedVideo { @@ -26,10 +22,10 @@ export interface EmbedVideo { } export class EmbedBuilder { - #data: DiscordEmbed + #data: DiscordEmbed; constructor(data: DiscordEmbed = {}) { this.#data = data; - if (!this.#data.fields) this.#data.fields = [] + if (!this.#data.fields) this.#data.fields = []; } setAuthor(author: EmbedAuthor) { @@ -37,7 +33,7 @@ export class EmbedBuilder { name: author.name, icon_url: author.iconUrl, proxy_icon_url: author.proxyIconUrl, - url: author.url + url: author.url, }; return this; } @@ -102,7 +98,7 @@ export class EmbedBuilder { height: video.height, proxy_url: video.proxyUrl, url: video.url, - width: video.width + width: video.width, }; return this; } From 1cd41c67a7d3b98cbfb0ddfb5f7e797711e79e22 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 3 Jul 2022 13:14:47 -0500 Subject: [PATCH 08/45] feat: threads --- handlers/Actions.ts | 69 ++++++++++++++++-------- structures/Message.ts | 80 ++++++++++++++++++++++++---- structures/MessageReaction.ts | 23 ++++++++ structures/ThreadMember.ts | 47 ++++++++++++++++ structures/channels/GuildChannel.ts | 39 +++++++++++++- structures/channels/ThreadChannel.ts | 51 +++++++++++++++++- structures/guilds/Guild.ts | 21 ++++++++ util/Routes.ts | 71 ++++++++++++++++++++++++ 8 files changed, 367 insertions(+), 34 deletions(-) create mode 100644 structures/MessageReaction.ts create mode 100644 structures/ThreadMember.ts diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 5975a4b..986e8f3 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -8,6 +8,10 @@ import type { DiscordMemberWithUser, DiscordMessage, DiscordMessageDelete, + DiscordMessageReactionAdd, + DiscordMessageReactionRemove, + DiscordMessageReactionRemoveAll, + DiscordMessageReactionRemoveEmoji, DiscordReady, // DiscordThreadMemberUpdate, // DiscordThreadMembersUpdate, @@ -19,6 +23,7 @@ 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 ThreadMember from "../structures/ThreadMember.ts"; import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; @@ -103,10 +108,7 @@ 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)), - members: payload.members.map((member) => - // @ts-ignore: TODO: thread member structure - new Member(session, member as DiscordMemberWithUser, payload.guild_id) - ), + members: payload.members.map((member) => new ThreadMember(session, member)), }); }; @@ -118,6 +120,24 @@ export const CHANNEL_PINS_UPDATE: RawHandler = (sessio }); }; +/* +export const MESSAGE_REACTION_ADD: RawHandler = (session, _shardId, reaction) => { + session.emit("messageReactionAdd", null); +}; + +export const MESSAGE_REACTION_REMOVE: RawHandler = (session, _shardId, reaction) => { + session.emit("messageReactionRemove", null); +}; + +export const MESSAGE_REACTION_REMOVE_ALL: RawHandler = (session, _shardId, reaction) => { + session.emit("messageReactionRemoveAll", null); +}; + +export const MESSAGE_REACTION_REMOVE_EMOJI: RawHandler = (session, _shardId, reaction) => { + session.emit("messageReactionRemoveEmoji", null); +}; +*/ + export const raw: RawHandler = (session, shardId, data) => { session.emit("raw", data, shardId); }; @@ -126,23 +146,30 @@ export interface Ready extends Omit { user: User; } +// TODO: add partial reactions or something +type MessageReaction = any; + // deno-fmt-ignore-file export interface Events { - "ready": Handler<[Ready, number]>; - "messageCreate": Handler<[Message]>; - "messageUpdate": Handler<[Message]>; - "messageDelete": Handler<[{ id: Snowflake, channelId: Snowflake, guildId?: Snowflake }]>; - "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]>; + "ready": Handler<[Ready, number]>; + "messageCreate": Handler<[Message]>; + "messageUpdate": Handler<[Message]>; + "messageDelete": Handler<[{ id: Snowflake, channelId: Snowflake, guildId?: Snowflake }]>; + "messageReactionAdd": Handler<[MessageReaction]>; + "messageReactionRemove": Handler<[MessageReaction]>; + "messageReactionRemoveAll": Handler<[MessageReaction]>; + "messageReactionRemoveEmoji": Handler<[MessageReaction]>; + "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: ThreadMember[] }]> + "interactionCreate": Handler<[Interaction]>; + "raw": Handler<[unknown, number]>; } diff --git a/structures/Message.ts b/structures/Message.ts index 483516b..c1287bd 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -1,5 +1,4 @@ import type { Model } from "./Base.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { AllowedMentionsTypes, @@ -7,15 +6,20 @@ import type { DiscordMessage, DiscordUser, FileContent, + MessageTypes, + MessageActivityTypes, } 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 { iconHashToBigInt } from "../util/hash.ts"; +import { Snowflake } from "../util/Snowflake.ts"; import User from "./User.ts"; import Member from "./Member.ts"; import Attachment from "./Attachment.ts"; import ComponentFactory from "./components/ComponentFactory.ts"; -import { iconHashToBigInt } from "../util/hash.ts"; +import MessageReaction from "./MessageReaction.ts"; +import ThreadChannel from "./channels/ThreadChannel.ts"; import * as Routes from "../util/Routes.ts"; /** @@ -39,11 +43,11 @@ export interface CreateMessageReference { * @link https://discord.com/developers/docs/resources/channel#create-message-json-params */ export interface CreateMessage { + embeds?: DiscordEmbed[]; content?: string; allowedMentions?: AllowedMentions; files?: FileContent[]; messageReference?: CreateMessageReference; - embeds?: DiscordEmbed[]; } /** @@ -74,19 +78,32 @@ export class Message implements Model { this.session = session; this.id = data.id; + this.type = data.type; this.channelId = data.channel_id; this.guildId = data.guild_id; + this.applicationId = data.application_id; this.author = new User(session, data.author); this.flags = data.flags; this.pinned = !!data.pinned; this.tts = !!data.tts; this.content = data.content!; + this.nonce = data.nonce; + this.mentionEveryone = data.mention_everyone; + this.timestamp = Date.parse(data.timestamp); + this.editedTimestamp = data.edited_timestamp ? Date.parse(data.edited_timestamp) : undefined; + + this.reactions = data.reactions?.map((react) => new MessageReaction(session, react)) ?? []; this.attachments = data.attachments.map((attachment) => new Attachment(session, attachment)); + this.embeds = data.embeds; + + if (data.thread && data.guild_id) { + this.thread = new ThreadChannel(session, data.thread, data.guild_id); + } // webhook handling - if (data.author && data.author.discriminator === "0000") { + if (data.author.discriminator === "0000") { this.webhook = { id: data.author.id, username: data.author.username, @@ -96,30 +113,70 @@ export class Message implements Model { } // user is always null on MessageCreate and its replaced with author - - if (data.guild_id && data.member && data.author && !this.isWebhookMessage()) { + if (data.guild_id && data.member && !this.isWebhookMessage()) { this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id); } - this.components = data.components?.map((component) => ComponentFactory.from(session, component)); + this.components = data.components?.map((component) => ComponentFactory.from(session, component)) ?? []; + + if (data.activity) { + this.activity = { + partyId: data.activity.party_id, + type: data.activity.type, + }; + } } readonly session: Session; readonly id: Snowflake; + type: MessageTypes; channelId: Snowflake; guildId?: Snowflake; + applicationId?: Snowflake; author: User; flags?: MessageFlags; pinned: boolean; tts: boolean; content: string; + nonce?: string | number; + mentionEveryone: boolean; + timestamp: number; + editedTimestamp?: number; + + reactions: MessageReaction[]; attachments: Attachment[]; + embeds: DiscordEmbed[]; member?: Member; - components?: Component[]; + thread?: ThreadChannel; + components: Component[]; webhook?: WebhookAuthor; + activity?: { + partyId?: Snowflake; + type: MessageActivityTypes; + }; + + get createdTimestamp() { + return Snowflake.snowflakeToTimestamp(this.id); + } + + get createdAt() { + return new Date(this.createdTimestamp); + } + + get sentAt() { + return new Date(this.timestamp); + } + + get editedAt() { + return this.editedTimestamp ? new Date(this.editedTimestamp) : undefined; + } + + get edited() { + return this.editedTimestamp; + } get url() { return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`; @@ -301,12 +358,13 @@ export class Message implements Model { return this.crosspost; } - inGuild(): this is { guildId: Snowflake } & Message { + /** wheter the message comes from a guild **/ + inGuild(): this is Message & { guildId: Snowflake } { return !!this.guildId; } - /** isWebhookMessage if the messages comes from a Webhook */ - isWebhookMessage(): this is User & { author: Partial; webhook: WebhookAuthor; member: undefined } { + /** wheter the messages comes from a Webhook */ + isWebhookMessage(): this is Message & { author: Partial; webhook: WebhookAuthor; member: undefined } { return !!this.webhook; } } diff --git a/structures/MessageReaction.ts b/structures/MessageReaction.ts new file mode 100644 index 0000000..d819d2c --- /dev/null +++ b/structures/MessageReaction.ts @@ -0,0 +1,23 @@ +import type { Session } from "../session/Session.ts"; +import type { DiscordReaction } from "../vendor/external.ts"; +import Emoji from "./Emoji.ts"; + +/** + * Represents a reaction + * @link https://discord.com/developers/docs/resources/channel#reaction-object + * */ +export class MessageReaction { + constructor(session: Session, data: DiscordReaction) { + this.session = session; + this.me = data.me; + this.count = data.count; + this.emoji = new Emoji(session, data.emoji); + } + + readonly session: Session; + me: boolean; + count: number; + emoji: Emoji; +} + +export default MessageReaction; diff --git a/structures/ThreadMember.ts b/structures/ThreadMember.ts new file mode 100644 index 0000000..7679d88 --- /dev/null +++ b/structures/ThreadMember.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 { DiscordThreadMember } from "../vendor/external.ts"; +import * as Routes from "../util/Routes.ts"; + +/** + * A member that comes from a thread + * @link https://discord.com/developers/docs/resources/channel#thread-member-object + * **/ +export class ThreadMember implements Model { + constructor(session: Session, data: DiscordThreadMember) { + this.session = session; + this.id = data.id; + this.flags = data.flags; + this.timestamp = Date.parse(data.join_timestamp); + } + + readonly session: Session; + readonly id: Snowflake; + flags: number; + timestamp: number; + + get threadId() { + return this.id; + } + + async quitThread(memberId: Snowflake = this.session.botId) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.THREAD_USER(this.id, memberId), + ); + } + + async fetchMember(memberId: Snowflake = this.session.botId) { + const member = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.THREAD_USER(this.id, memberId), + ); + + return new ThreadMember(this.session, member); + } +} + +export default ThreadMember; diff --git a/structures/channels/GuildChannel.ts b/structures/channels/GuildChannel.ts index aaa4e53..b07ae95 100644 --- a/structures/channels/GuildChannel.ts +++ b/structures/channels/GuildChannel.ts @@ -1,8 +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, DiscordInviteMetadata } from "../../vendor/external.ts"; +import type { ChannelTypes, DiscordChannel, DiscordInviteMetadata, DiscordListArchivedThreads } from "../../vendor/external.ts"; +import type { ListArchivedThreads } from "../../util/Routes.ts"; import BaseChannel from "./BaseChannel.ts"; +import ThreadChannel from "./ThreadChannel.ts"; +import ThreadMember from "../ThreadMember.ts"; import Invite from "../Invite.ts"; import * as Routes from "../../util/Routes.ts"; @@ -44,7 +47,41 @@ export class GuildChannel extends BaseChannel implements Model { return invites.map((invite) => new Invite(this.session, invite)); } + + async getArchivedThreads(options: ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }) { + let func: (channelId: Snowflake, options: ListArchivedThreads) => string; + + switch (options.type) { + case "public": + func = Routes.THREAD_ARCHIVED_PUBLIC; + break; + case "private": + func = Routes.THREAD_START_PRIVATE; + break; + case "privateJoinedThreads": + func = Routes.THREAD_ARCHIVED_PRIVATE_JOINED; + break; + } + + const { threads, members, has_more } = await this.session.rest.runMethod( + this.session.rest, + "GET", + func(this.id, options) + ); + + return { + threads: Object.fromEntries( + threads.map((thread) => [thread.id, new ThreadChannel(this.session, thread, this.id)]), + ) as Record, + members: Object.fromEntries( + members.map((threadMember) => [threadMember.id, new ThreadMember(this.session, threadMember)]), + ) as Record, + hasMore: has_more, + }; + } + /* + * TODO: should be in TextChannel async createThread(options: ThreadCreateOptions): Promise { const thread = await this.session.rest.runMethod( this.session.rest, diff --git a/structures/channels/ThreadChannel.ts b/structures/channels/ThreadChannel.ts index 459ba6e..0bb036d 100644 --- a/structures/channels/ThreadChannel.ts +++ b/structures/channels/ThreadChannel.ts @@ -1,9 +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 } from "../../vendor/external.ts"; +import type { ChannelTypes, DiscordChannel, DiscordThreadMember } from "../../vendor/external.ts"; import GuildChannel from "./GuildChannel.ts"; import TextChannel from "./TextChannel.ts"; +import ThreadMember from "../ThreadMember.ts"; +import * as Routes from "../../util/Routes.ts"; export class ThreadChannel extends GuildChannel implements Model { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { @@ -16,6 +18,10 @@ export class ThreadChannel extends GuildChannel implements Model { this.messageCount = data.message_count; this.memberCount = data.member_count; this.ownerId = data.owner_id; + + if (data.member) { + this.member = new ThreadMember(session, data.member); + } } override type: ChannelTypes.GuildNewsThread | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread; @@ -25,7 +31,50 @@ export class ThreadChannel extends GuildChannel implements Model { locked?: boolean; messageCount?: number; memberCount?: number; + member?: ThreadMember; ownerId?: Snowflake; + + async joinThread() { + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.THREAD_ME(this.id), + ); + } + + async addToThread(guildMemberId: Snowflake) { + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.THREAD_USER(this.id, guildMemberId), + ); + } + + async leaveToThread(guildMemberId: Snowflake) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.THREAD_USER(this.id, guildMemberId), + ); + } + + removeMember(memberId: Snowflake = this.session.botId) { + return ThreadMember.prototype.quitThread.call({ id: this.id, session: this.session }, memberId); + } + + fetchMember(memberId: Snowflake = this.session.botId) { + return ThreadMember.prototype.fetchMember.call({ id: this.id, session: this.session }, memberId); + } + + async fetchMembers(): Promise { + const members = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.THREAD_MEMBERS(this.id), + ); + + return members.map((threadMember) => new ThreadMember(this.session, threadMember)); + } } TextChannel.applyTo(ThreadChannel); diff --git a/structures/guilds/Guild.ts b/structures/guilds/Guild.ts index fe39151..727ee39 100644 --- a/structures/guilds/Guild.ts +++ b/structures/guilds/Guild.ts @@ -7,6 +7,8 @@ import type { DiscordInviteMetadata, DiscordMemberWithUser, DiscordRole, + DiscordListActiveThreads, + DiscordListArchivedThreads, } from "../../vendor/external.ts"; import type { GetInvite } from "../../util/Routes.ts"; import { @@ -21,6 +23,8 @@ import BaseGuild from "./BaseGuild.ts"; import Role from "../Role.ts"; import GuildEmoji from "../GuildEmoji.ts"; import Invite from "../Invite.ts"; +import ThreadMember from "../ThreadMember.ts"; +import ThreadChannel from "../channels/ThreadChannel.ts"; import * as Routes from "../../util/Routes.ts"; export interface CreateRole { @@ -362,6 +366,23 @@ export class Guild extends BaseGuild implements Model { return result.pruned; } + + async getActiveThreads() { + const { threads, members } = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.THREAD_ACTIVE(this.id) + ); + + return { + threads: Object.fromEntries( + threads.map((thread) => [thread.id, new ThreadChannel(this.session, thread, this.id)]), + ) as Record, + members: Object.fromEntries( + members.map((threadMember) => [threadMember.id, new ThreadMember(this.session, threadMember)]), + ) as Record, + }; + } } export default Guild; diff --git a/util/Routes.ts b/util/Routes.ts index 247cadd..916e319 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -228,3 +228,74 @@ export function GUILD_MEMBER_ROLE(guildId: Snowflake, memberId: Snowflake, roleI export function CHANNEL_WEBHOOKS(channelId: Snowflake) { return `/channels/${channelId}/webhooks`; } + + +export function THREAD_START_PUBLIC(channelId: Snowflake, messageId: Snowflake) { + return `/channels/${channelId}/messages/${messageId}/threads`; +} + +export function THREAD_START_PRIVATE(channelId: Snowflake) { + return `/channels/${channelId}/threads`; +} + +export function THREAD_ACTIVE(guildId: Snowflake) { + return `/guilds/${guildId}/threads/active`; +} + +export interface ListArchivedThreads { + before?: number; + limit?: number; +} + +export function THREAD_ME(channelId: Snowflake) { + return `/channels/${channelId}/thread-members/@me`; +} + +export function THREAD_MEMBERS(channelId: Snowflake) { + return `/channels/${channelId}/thread-members`; +} + +export function THREAD_USER(channelId: Snowflake, userId: Snowflake) { + return `/channels/${channelId}/thread-members/${userId}`; +} + +export function THREAD_ARCHIVED(channelId: Snowflake) { + return `/channels/${channelId}/threads/archived`; +} + +export function THREAD_ARCHIVED_PUBLIC(channelId: Snowflake, options?: ListArchivedThreads) { + let url = `/channels/${channelId}/threads/archived/public?`; + + if (options) { + if (options.before) url += `before=${new Date(options.before).toISOString()}`; + if (options.limit) url += `&limit=${options.limit}`; + } + + return url; +} + +export function THREAD_ARCHIVED_PRIVATE(channelId: Snowflake, options?: ListArchivedThreads) { + let url = `/channels/${channelId}/threads/archived/private?`; + + if (options) { + if (options.before) url += `before=${new Date(options.before).toISOString()}`; + if (options.limit) url += `&limit=${options.limit}`; + } + + return url; +} + +export function THREAD_ARCHIVED_PRIVATE_JOINED(channelId: Snowflake, options?: ListArchivedThreads) { + let url = `/channels/${channelId}/users/@me/threads/archived/private?`; + + if (options) { + if (options.before) url += `before=${new Date(options.before).toISOString()}`; + if (options.limit) url += `&limit=${options.limit}`; + } + + return url; +} + +export function FORUM_START(channelId: Snowflake) { + return `/channels/${channelId}/threads?has_message=true`; +} From b7ff327a7c4a02db8e317bdff2f6e0178e6ed33c Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 3 Jul 2022 15:17:17 -0500 Subject: [PATCH 09/45] feat: Guild.create Guild.delete Guild.edit --- structures/channels/GuildChannel.ts | 14 +- structures/guilds/Guild.ts | 204 +++++++++++++++++++++++++++- util/Routes.ts | 4 + 3 files changed, 207 insertions(+), 15 deletions(-) diff --git a/structures/channels/GuildChannel.ts b/structures/channels/GuildChannel.ts index b07ae95..0f595d7 100644 --- a/structures/channels/GuildChannel.ts +++ b/structures/channels/GuildChannel.ts @@ -80,8 +80,6 @@ export class GuildChannel extends BaseChannel implements Model { }; } - /* - * TODO: should be in TextChannel async createThread(options: ThreadCreateOptions): Promise { const thread = await this.session.rest.runMethod( this.session.rest, @@ -89,18 +87,8 @@ export class GuildChannel extends BaseChannel implements Model { 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, - }, - ); + return new ThreadChannel(this.session, thread, this.guildId); } } diff --git a/structures/guilds/Guild.ts b/structures/guilds/Guild.ts index 727ee39..f0440a6 100644 --- a/structures/guilds/Guild.ts +++ b/structures/guilds/Guild.ts @@ -2,13 +2,18 @@ import type { Model } from "../Base.ts"; import type { Snowflake } from "../../util/Snowflake.ts"; import type { Session } from "../../session/Session.ts"; import type { + ChannelTypes, DiscordEmoji, DiscordGuild, DiscordInviteMetadata, DiscordMemberWithUser, DiscordRole, DiscordListActiveThreads, - DiscordListArchivedThreads, + GuildFeatures, + SystemChannelFlags, + MakeRequired, + VideoQualityModes, + DiscordOverwrite, } from "../../vendor/external.ts"; import type { GetInvite } from "../../util/Routes.ts"; import { @@ -17,7 +22,7 @@ import { VerificationLevels, } from "../../vendor/external.ts"; import { iconBigintToHash, iconHashToBigInt } from "../../util/hash.ts"; -import { urlToBase64 } from "../../util/urlToBase64.ts"; +import { encode as _encode, urlToBase64 } from "../../util/urlToBase64.ts"; import Member from "../Member.ts"; import BaseGuild from "./BaseGuild.ts"; import Role from "../Role.ts"; @@ -90,6 +95,103 @@ export interface ModifyRolePositions { position?: number | null; } +export interface GuildCreateOptionsRole { + id: Snowflake; + name?: string; + color?: number; + hoist?: boolean; + position?: number; + permissions?: bigint; + mentionable?: boolean; + iconURL?: string; + unicodeEmoji?: string | null; +} + +export interface GuildCreateOptionsRole { + id: Snowflake; + name?: string; + color?: number; + hoist?: boolean; + position?: number; + permissions?: bigint; + mentionable?: boolean; + iconHash?: bigint; + unicodeEmoji?: string | null; +} + +export interface GuildCreateOptionsChannel { + id?: Snowflake; + parentId?: Snowflake; + type?: ChannelTypes.GuildText | ChannelTypes.GuildVoice | ChannelTypes.GuildCategory; + name: string; + topic?: string | null; + nsfw?: boolean; + bitrate?: number; + userLimit?: number; + region?: string | null; + videoQualityMode?: VideoQualityModes; + permissionOverwrites?: MakeRequired, "id">[]; + rateLimitPerUser?: number; +} + +/** + * @link https://discord.com/developers/docs/resources/guild#create-guild + * */ +export interface GuildCreateOptions { + name: string; + afkChannelId?: Snowflake; + afkTimeout?: number; + channels?: GuildCreateOptionsChannel[]; + defaultMessageNotifications?: DefaultMessageNotificationLevels; + explicitContentFilter?: ExplicitContentFilterLevels; + iconURL?: string; + roles?: GuildCreateOptionsRole[]; + systemChannelFlags?: SystemChannelFlags; + systemChannelId?: Snowflake; + verificationLevel?: VerificationLevels; +} + +export interface GuildCreateOptions { + name: string; + afkChannelId?: Snowflake; + afkTimeout?: number; + channels?: GuildCreateOptionsChannel[]; + defaultMessageNotifications?: DefaultMessageNotificationLevels; + explicitContentFilter?: ExplicitContentFilterLevels; + iconHash?: bigint; + roles?: GuildCreateOptionsRole[]; + systemChannelFlags?: SystemChannelFlags; + systemChannelId?: Snowflake; + verificationLevel?: VerificationLevels; +} + +/** + * @link https://discord.com/developers/docs/resources/guild#modify-guild-json-params + * */ +export interface GuildEditOptions extends Omit { + ownerId?: Snowflake; + splashURL?: string; + bannerURL?: string; + discoverySplashURL?: string; + features?: GuildFeatures[]; + rulesChannelId?: Snowflake; + description?: string; + premiumProgressBarEnabled?: boolean; +} + +export interface GuildEditOptions extends Omit { + ownerId?: Snowflake; + splashHash?: bigint; + bannerHash?: bigint; + discoverySplashHash?: bigint; + features?: GuildFeatures[]; + rulesChannelId?: Snowflake; + publicUpdatesChannelId?: Snowflake; + preferredLocale?: string | null; + description?: string; + premiumProgressBarEnabled?: boolean; +} + /** * Represents a guild * @link https://discord.com/developers/docs/resources/guild#guild-object @@ -383,6 +485,104 @@ export class Guild extends BaseGuild implements Model { ) as Record, }; } + + /*** + * Makes the bot leave the guild + * */ + async leave() { + } + + /*** + * Deletes a guild + * */ + async delete() { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.GUILDS(), + ); + } + + /** + * Creates a guild and returns its data, the bot joins the guild + * This was modified from discord.js to make it compatible + * precondition: Bot should be in less than 10 servers + * */ + static async create(session: Session, options: GuildCreateOptions) { + const guild = await session.rest.runMethod(session.rest, "POST", Routes.GUILDS(), { + name: options.name, + afk_channel_id: options.afkChannelId, + afk_timeout: options.afkTimeout, + default_message_notifications: options.defaultMessageNotifications, + explicit_content_filter: options.explicitContentFilter, + system_channel_flags: options.systemChannelFlags, + verification_level: options.verificationLevel, + icon: "iconURL" in options + ? options.iconURL || urlToBase64(options.iconURL!) + : options.iconHash || iconBigintToHash(options.iconHash!), + channels: options.channels?.map((channel) => ({ + name: channel.name, + nsfw: channel.nsfw, + id: channel.id, + bitrate: channel.bitrate, + parent_id: channel.parentId, + permission_overwrites: channel.permissionOverwrites, + region: channel.region, + user_limit: channel.userLimit, + video_quality_mode: channel.videoQualityMode, + rate_limit_per_user: channel.rateLimitPerUser, + })), + roles: options.roles?.map((role) => ({ + name: role.name, + id: role.id, + color: role.color, + mentionable: role.mentionable, + hoist: role.hoist, + position: role.position, + unicode_emoji: role.unicodeEmoji, + icon: options.iconURL || urlToBase64(options.iconURL!), + })), + }); + + return new Guild(session, guild); + } + + /** + * Edits a guild and returns its data + * */ + async edit(session: Session, options: GuildEditOptions) { + const guild = await session.rest.runMethod(session.rest, "PATCH", Routes.GUILDS(), { + name: options.name, + afk_channel_id: options.afkChannelId, + afk_timeout: options.afkTimeout, + default_message_notifications: options.defaultMessageNotifications, + explicit_content_filter: options.explicitContentFilter, + system_channel_flags: options.systemChannelFlags, + verification_level: options.verificationLevel, + icon: "iconURL" in options + ? options.iconURL || urlToBase64(options.iconURL!) + : options.iconHash || iconBigintToHash(options.iconHash!), + // extra props + splash: "splashURL" in options + ? options.splashURL || urlToBase64(options.splashURL!) + : options.splashHash || iconBigintToHash(options.iconHash!), + banner: "bannerURL" in options + ? options.bannerURL || urlToBase64(options.bannerURL!) + : options.bannerHash || iconBigintToHash(options.bannerHash!), + discovery_splash: "discoverySplashURL" in options + ? options.discoverySplashURL || urlToBase64(options.discoverySplashURL!) + : options.discoverySplashHash || iconBigintToHash(options.discoverySplashHash!), + owner_id: options.ownerId, + rules_channel_id: options.rulesChannelId, + public_updates_channel_id: options.publicUpdatesChannelId, + preferred_locale: options.preferredLocale, + features: options.features, + description: options.description, + premiumProgressBarEnabled: options.premiumProgressBarEnabled, + }); + + return new Guild(session, guild); + } } export default Guild; diff --git a/util/Routes.ts b/util/Routes.ts index 916e319..650b5a2 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -120,6 +120,10 @@ export interface GetInvite { scheduledEventId?: Snowflake; } +export function GUILDS() { + return `/guilds`; +} + export function INVITE(inviteCode: string, options?: GetInvite) { let url = `/invites/${inviteCode}?`; From 202c02017144c41efe85efde64884877ae672f4e Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 3 Jul 2022 17:27:56 -0300 Subject: [PATCH 10/45] Resolving merge conflict (#8) stuff --- structures/Message.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/structures/Message.ts b/structures/Message.ts index c1287bd..e265be7 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -112,7 +112,18 @@ export class Message implements Model { }; } + // webhook handling + if (data.author && data.author.discriminator === "0000") { + this.webhook = { + id: data.author.id, + username: data.author.username, + discriminator: data.author.discriminator, + avatar: data.author.avatar ? iconHashToBigInt(data.author.avatar) : undefined, + }; + } + // user is always null on MessageCreate and its replaced with author + if (data.guild_id && data.member && !this.isWebhookMessage()) { this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id); } @@ -178,6 +189,7 @@ export class Message implements Model { return this.editedTimestamp; } + get url() { return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`; } From bd737828870f34997bb8ce17f4a11715de52c9fc Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 3 Jul 2022 15:47:26 -0500 Subject: [PATCH 11/45] feat: GuildChannel.createThread --- structures/channels/GuildChannel.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/structures/channels/GuildChannel.ts b/structures/channels/GuildChannel.ts index 0f595d7..3609b15 100644 --- a/structures/channels/GuildChannel.ts +++ b/structures/channels/GuildChannel.ts @@ -10,17 +10,29 @@ import Invite from "../Invite.ts"; import * as Routes from "../../util/Routes.ts"; /** - * Represent the options object to create a Thread Channel + * 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; + autoArchiveDuration?: 60 | 1440 | 4320 | 10080; type: 10 | 11 | 12; invitable?: boolean; + rateLimitPerUser?: number; reason?: string; } +/** + * Represents the option object to create a thread channel from a message + * @link https://discord.com/developers/docs/resources/channel#start-thread-from-message + * */ +export interface ThreadCreateOptions { + name: string; + autoArchiveDuration?: 60 | 1440 | 4320 | 10080; + rateLimitPerUser?: number; + messageId: Snowflake; +} + export class GuildChannel extends BaseChannel implements Model { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { super(session, data); @@ -84,11 +96,16 @@ export class GuildChannel extends BaseChannel implements Model { const thread = await this.session.rest.runMethod( this.session.rest, "POST", - Routes.CHANNEL_CREATE_THREAD(this.id), - options, + "messageId" in options + ? Routes.THREAD_START_PUBLIC(this.id, options.messageId) + : Routes.THREAD_START_PRIVATE(this.id), + { + name: options.name, + auto_archive_duration: options.autoArchiveDuration, + }, ); - return new ThreadChannel(this.session, thread, this.guildId); + return new ThreadChannel(this.session, thread, thread.guild_id ?? this.guildId); } } From cd517f2048302f0e4a28b242a79fd42391bd8b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Sun, 3 Jul 2022 17:53:36 -0300 Subject: [PATCH 12/45] Add guildCreate and guildDelete events --- handlers/Actions.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 986e8f3..ecfe1dd 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -1,6 +1,7 @@ import type { DiscordChannel, DiscordChannelPinsUpdate, + DiscordGuild, DiscordGuildMemberAdd, DiscordGuildMemberRemove, DiscordGuildMemberUpdate, @@ -27,6 +28,7 @@ import ThreadMember from "../structures/ThreadMember.ts"; import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; +import Guild from "../structures/guilds/Guild.ts"; import Interaction from "../structures/interactions/Interaction.ts"; export type RawHandler = (...args: [Session, number, T]) => void; @@ -50,6 +52,14 @@ export const MESSAGE_DELETE: RawHandler = (session, _shard session.emit("messageDelete", { id, channelId: channel_id, guildId: guild_id }); }; +export const GUILD_CREATE: RawHandler = (session, _shardId, guild) => { + session.emit("guildCreate", new Guild(session, guild)); +}; + +export const GUILD_DELETE: RawHandler = (session, _shardId, guild) => { + session.emit("guildDelete", { id: guild.id, unavailable: true }); +}; + export const GUILD_MEMBER_ADD: RawHandler = (session, _shardId, member) => { session.emit("guildMemberAdd", new Member(session, member, member.guild_id)); }; @@ -159,6 +169,8 @@ export interface Events { "messageReactionRemove": Handler<[MessageReaction]>; "messageReactionRemoveAll": Handler<[MessageReaction]>; "messageReactionRemoveEmoji": Handler<[MessageReaction]>; + "guildCreate": Handler<[Guild]>; + "guildDelete": Handler<[{ id: Snowflake, unavailable: boolean }]>; "guildMemberAdd": Handler<[Member]>; "guildMemberUpdate": Handler<[Member]>; "guildMemberRemove": Handler<[User, Snowflake]>; From 04d9088e64a0f075e5361987b664e7af1519a9d9 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 3 Jul 2022 16:19:09 -0500 Subject: [PATCH 13/45] chore: add script runner --- deno.json | 3 +++ scripts.ts | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100644 scripts.ts diff --git a/deno.json b/deno.json index d010978..25bec05 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,8 @@ { "fmt": { + "files": { + "exclude": "vendor" + }, "options": { "indentWidth": 4, "lineWidth": 120 diff --git a/scripts.ts b/scripts.ts new file mode 100644 index 0000000..eed003e --- /dev/null +++ b/scripts.ts @@ -0,0 +1,6 @@ +export default { + scripts: { + fmt: "deno fmt", + check: "deno check mod.ts", + }, +}; From 8fd209fab7c3703c5679078480102ee6327ee490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Mon, 4 Jul 2022 00:00:19 -0300 Subject: [PATCH 14/45] Add guildBanAdd and guildBanRemove actions --- handlers/Actions.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index ecfe1dd..919d6ea 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -5,6 +5,8 @@ import type { DiscordGuildMemberAdd, DiscordGuildMemberRemove, DiscordGuildMemberUpdate, + DiscordUser, + DiscordGuildBanAddRemove, DiscordInteraction, DiscordMemberWithUser, DiscordMessage, @@ -72,6 +74,14 @@ export const GUILD_MEMBER_REMOVE: RawHandler = (sessio session.emit("guildMemberRemove", new User(session, member.user), member.guild_id); }; +export const GUILD_BAN_ADD: RawHandler = (session, _shardId, data) => { + session.emit("guildBanAdd", { guildId: data.guild_id, user: data.user }); +}; + +export const GUILD_BAN_REMOVE: RawHandler = (session, _shardId, data) => { + session.emit("guildBanRemove", { guildId: data.guild_id, user: data.user }); +}; + export const INTERACTION_CREATE: RawHandler = (session, _shardId, interaction) => { session.unrepliedInteractions.add(BigInt(interaction.id)); @@ -174,6 +184,8 @@ export interface Events { "guildMemberAdd": Handler<[Member]>; "guildMemberUpdate": Handler<[Member]>; "guildMemberRemove": Handler<[User, Snowflake]>; + "guildBanAdd": Handler<[{ guildId: Snowflake, user: DiscordUser}]>; + "guildBanRemove": Handler<[{ guildId: Snowflake, user: DiscordUser }]> "channelCreate": Handler<[Channel]>; "channelUpdate": Handler<[Channel]>; "channelDelete": Handler<[GuildChannel]>; From e4e7d6d9f790a9931d1978e22c91b9939214bd68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Mon, 4 Jul 2022 00:00:45 -0300 Subject: [PATCH 15/45] Update tests/mod.ts intents --- tests/mod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mod.ts b/tests/mod.ts index 03fd262..d1f7a69 100644 --- a/tests/mod.ts +++ b/tests/mod.ts @@ -7,7 +7,7 @@ if (!token) { throw new Error("Please provide a token"); } -const intents = GatewayIntents.MessageContent | GatewayIntents.Guilds | GatewayIntents.GuildMessages; +const intents = GatewayIntents.MessageContent | GatewayIntents.Guilds | GatewayIntents.GuildMessages | GatewayIntents.GuildMembers | GatewayIntents.GuildBans const session = new Session({ token, intents }); session.on("ready", (payload) => { From 0a83ee788f149931b9675215b7c690cb7502e68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Mon, 4 Jul 2022 00:20:56 -0300 Subject: [PATCH 16/45] Add guildEmojisUpdate action --- handlers/Actions.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 919d6ea..65a68e7 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -7,6 +7,8 @@ import type { DiscordGuildMemberUpdate, DiscordUser, DiscordGuildBanAddRemove, + DiscordEmoji, + DiscordGuildEmojisUpdate, DiscordInteraction, DiscordMemberWithUser, DiscordMessage, @@ -82,6 +84,10 @@ export const GUILD_BAN_REMOVE: RawHandler = (session, session.emit("guildBanRemove", { guildId: data.guild_id, user: data.user }); }; +export const GUILD_EMOJIS_UPDATE: RawHandler = (session, _shardId, data) => { + session.emit("guildEmojisUpdate", { guildId: data.guild_id, emojis: data.emojis}) +}; + export const INTERACTION_CREATE: RawHandler = (session, _shardId, interaction) => { session.unrepliedInteractions.add(BigInt(interaction.id)); @@ -186,6 +192,7 @@ export interface Events { "guildMemberRemove": Handler<[User, Snowflake]>; "guildBanAdd": Handler<[{ guildId: Snowflake, user: DiscordUser}]>; "guildBanRemove": Handler<[{ guildId: Snowflake, user: DiscordUser }]> + "guildEmojisUpdate": Handler<[{ guildId: Snowflake, emojis: DiscordEmoji[] }]> "channelCreate": Handler<[Channel]>; "channelUpdate": Handler<[Channel]>; "channelDelete": Handler<[GuildChannel]>; From f449358bd803ae3c8246ef4084121662853971c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Mon, 4 Jul 2022 00:39:34 -0300 Subject: [PATCH 17/45] Add webhooksUpdate action --- handlers/Actions.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 65a68e7..bb7d912 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -21,6 +21,7 @@ import type { // DiscordThreadMemberUpdate, // DiscordThreadMembersUpdate, DiscordThreadListSync, + DiscordWebhookUpdate } from "../vendor/external.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; @@ -146,6 +147,10 @@ export const CHANNEL_PINS_UPDATE: RawHandler = (sessio }); }; +export const WEBHOOKS_UPDATE: RawHandler = (session, _shardId, webhook) => { + session.emit("webhooksUpdate", { guildId: webhook.guild_id, channelId: webhook.channel_id }) +}; + /* export const MESSAGE_REACTION_ADD: RawHandler = (session, _shardId, reaction) => { session.emit("messageReactionAdd", null); @@ -203,4 +208,5 @@ export interface Events { "threadListSync": Handler<[{ guildId: Snowflake, channelIds: Snowflake[], threads: ThreadChannel[], members: ThreadMember[] }]> "interactionCreate": Handler<[Interaction]>; "raw": Handler<[unknown, number]>; + "webhooksUpdate": Handler<[{ guildId: Snowflake, channelId: Snowflake }]>; } From 37128ec2839eaf3adb94cc46c760fdf72f6e3a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Mon, 4 Jul 2022 00:56:28 -0300 Subject: [PATCH 18/45] Add guildRole-Create-Update-Delete actions --- handlers/Actions.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index bb7d912..e1dd524 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -5,10 +5,13 @@ import type { DiscordGuildMemberAdd, DiscordGuildMemberRemove, DiscordGuildMemberUpdate, - DiscordUser, DiscordGuildBanAddRemove, - DiscordEmoji, DiscordGuildEmojisUpdate, + DiscordGuildRoleCreate, + DiscordGuildRoleUpdate, + DiscordGuildRoleDelete, + DiscordUser, + DiscordEmoji, DiscordInteraction, DiscordMemberWithUser, DiscordMessage, @@ -18,6 +21,7 @@ import type { DiscordMessageReactionRemoveAll, DiscordMessageReactionRemoveEmoji, DiscordReady, + DiscordRole, // DiscordThreadMemberUpdate, // DiscordThreadMembersUpdate, DiscordThreadListSync, @@ -89,6 +93,18 @@ export const GUILD_EMOJIS_UPDATE: RawHandler = (sessio session.emit("guildEmojisUpdate", { guildId: data.guild_id, emojis: data.emojis}) }; +export const GUILD_ROLE_CREATE: RawHandler = (session, _shardId, data) => { + session.emit("guildRoleCreate", { guildId: data.guild_id, role: data.role }); +} + +export const GUILD_ROLE_UPDATE: RawHandler = (session, _shardId, data) => { + session.emit("guildRoleUpdate", { guildId: data.guild_id, role: data.role }); +} + +export const GUILD_ROLE_DELETE: RawHandler = (session, _shardId, data) => { + session.emit("guildRoleDelete", { guildId: data.guild_id, roleId: data.role_id }); +} + export const INTERACTION_CREATE: RawHandler = (session, _shardId, interaction) => { session.unrepliedInteractions.add(BigInt(interaction.id)); @@ -198,6 +214,9 @@ export interface Events { "guildBanAdd": Handler<[{ guildId: Snowflake, user: DiscordUser}]>; "guildBanRemove": Handler<[{ guildId: Snowflake, user: DiscordUser }]> "guildEmojisUpdate": Handler<[{ guildId: Snowflake, emojis: DiscordEmoji[] }]> + "guildRoleCreate": Handler<[{ guildId: Snowflake, role: DiscordRole }]>; + "guildRoleUpdate": Handler<[{ guildId: Snowflake, role: DiscordRole }]>; + "guildRoleDelete": Handler<[{ guildId: Snowflake, roleId: Snowflake }]>; "channelCreate": Handler<[Channel]>; "channelUpdate": Handler<[Channel]>; "channelDelete": Handler<[GuildChannel]>; From adc41c88f7532a76182bab2d34d8a853203367a0 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 3 Jul 2022 23:10:50 -0500 Subject: [PATCH 19/45] feat: StageInstance --- mod.ts | 2 + structures/StageInstance.ts | 61 ++++++++++++++++++++++++ structures/channels/BaseVoiceChannel.ts | 62 +++++++++++++++++++++++++ structures/channels/ChannelFactory.ts | 6 ++- structures/channels/StageChannel.ts | 16 +++++++ structures/channels/TextChannel.ts | 39 ++++++++++------ structures/channels/VoiceChannel.ts | 61 ++++-------------------- structures/guilds/Guild.ts | 4 +- util/Routes.ts | 8 ++++ 9 files changed, 190 insertions(+), 69 deletions(-) create mode 100644 structures/StageInstance.ts create mode 100644 structures/channels/BaseVoiceChannel.ts create mode 100644 structures/channels/StageChannel.ts diff --git a/mod.ts b/mod.ts index d5cbe11..ab987f5 100644 --- a/mod.ts +++ b/mod.ts @@ -13,12 +13,14 @@ export * from "./structures/WelcomeChannel.ts"; export * from "./structures/WelcomeScreen.ts"; export * from "./structures/channels/BaseChannel.ts"; +export * from "./structures/channels/BaseVoiceChannel.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/StageChannel.ts"; export * from "./structures/channels/VoiceChannel.ts"; export * from "./structures/components/ActionRowComponent.ts"; diff --git a/structures/StageInstance.ts b/structures/StageInstance.ts new file mode 100644 index 0000000..c82bb56 --- /dev/null +++ b/structures/StageInstance.ts @@ -0,0 +1,61 @@ +import type { Model } from "./Base.ts"; +import type { Session } from "../session/Session.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { DiscordStageInstance as DiscordAutoClosingStageInstance } from "../vendor/external.ts"; +import * as Routes from "../util/Routes.ts"; + +interface DiscordStageInstance extends DiscordAutoClosingStageInstance { + privacy_level: PrivacyLevels; + discoverable_disabled: boolean; + guild_scheduled_event_id: Snowflake; +} + +export enum PrivacyLevels { + Public = 1, + GuildOnly = 2, +} + +export class StageInstance implements Model { + constructor(session: Session, data: DiscordStageInstance) { + this.session = session; + this.id = data.id; + this.channelId = data.channel_id; + this.guildId = data.guild_id; + this.topic = data.topic; + this.privacyLevel = data.privacy_level; + this.discoverableDisabled = data.discoverable_disabled; + this.guildScheduledEventId = data.guild_scheduled_event_id; + } + + readonly session: Session; + readonly id: Snowflake; + + channelId: Snowflake; + guildId: Snowflake; + topic: string; + + // TODO: see if this works + privacyLevel: PrivacyLevels; + discoverableDisabled: boolean; + guildScheduledEventId: Snowflake; + + async edit(options: { topic?: string, privacyLevel?: PrivacyLevels }) { + const stageInstance = await this.session.rest.runMethod( + this.session.rest, + "PATCH", + Routes.STAGE_INSTANCE(this.id), + { + topic: options.topic, + privacy_level: options.privacyLevel + } + ); + + return new StageInstance(this.session, stageInstance); + } + + async delete() { + await this.session.rest.runMethod(this.session.rest, "DELETE", Routes.STAGE_INSTANCE(this.id)); + } +} + +export default StageInstance; diff --git a/structures/channels/BaseVoiceChannel.ts b/structures/channels/BaseVoiceChannel.ts new file mode 100644 index 0000000..7f7cfbe --- /dev/null +++ b/structures/channels/BaseVoiceChannel.ts @@ -0,0 +1,62 @@ +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ChannelTypes, 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 abstract class BaseVoiceChannel 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; + this.type = data.type as number; + + if (data.rtc_region) { + this.rtcRegion = data.rtc_region; + } + } + override type: ChannelTypes.GuildVoice | ChannelTypes.GuildStageVoice; + 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 BaseVoiceChannel; diff --git a/structures/channels/ChannelFactory.ts b/structures/channels/ChannelFactory.ts index 2603514..31a1578 100644 --- a/structures/channels/ChannelFactory.ts +++ b/structures/channels/ChannelFactory.ts @@ -7,13 +7,15 @@ import VoiceChannel from "./VoiceChannel.ts"; import DMChannel from "./DMChannel.ts"; import NewsChannel from "./NewsChannel.ts"; import ThreadChannel from "./ThreadChannel.ts"; +import StageChannel from "./StageChannel.ts"; export type Channel = | TextChannel | VoiceChannel | DMChannel | NewsChannel - | ThreadChannel; + | ThreadChannel + | StageChannel; export class ChannelFactory { static from(session: Session, channel: DiscordChannel): Channel { @@ -27,6 +29,8 @@ export class ChannelFactory { return new DMChannel(session, channel); case ChannelTypes.GuildVoice: return new VoiceChannel(session, channel, channel.guild_id!); + case ChannelTypes.GuildStageVoice: + return new StageChannel(session, channel, channel.guild_id!); default: if (textBasedChannels.includes(channel.type)) { return new TextChannel(session, channel); diff --git a/structures/channels/StageChannel.ts b/structures/channels/StageChannel.ts new file mode 100644 index 0000000..41b49f7 --- /dev/null +++ b/structures/channels/StageChannel.ts @@ -0,0 +1,16 @@ +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ChannelTypes, DiscordChannel } from "../../vendor/external.ts"; +import BaseVoiceChannel from "./BaseVoiceChannel.ts"; + +export class StageChannel extends BaseVoiceChannel { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + this.type = data.type as number; + this.topic = data.topic ? data.topic : undefined; + } + override type: ChannelTypes.GuildStageVoice; + topic?: string; +} + +export default StageChannel; diff --git a/structures/channels/TextChannel.ts b/structures/channels/TextChannel.ts index 6f5e324..5b5bd7d 100644 --- a/structures/channels/TextChannel.ts +++ b/structures/channels/TextChannel.ts @@ -44,6 +44,7 @@ export const textBasedChannels = [ ChannelTypes.GuildPrivateThread, ChannelTypes.GuildPublicThread, ChannelTypes.GuildNews, + ChannelTypes.GuildVoice, ChannelTypes.GuildText, ]; @@ -53,6 +54,7 @@ export type TextBasedChannels = | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread | ChannelTypes.GuildNews + | ChannelTypes.GuildVoice | ChannelTypes.GuildText; export class TextChannel { @@ -85,21 +87,28 @@ export class TextChannel { /** * 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; + static applyTo(klass: Function, ignore: Array = []) { + const methods: Array = [ + "fetchPins", + "createInvite", + "fetchMessages", + "sendTyping", + "pinMessage", + "unpinMessage", + "addReaction", + "removeReaction", + "nukeReactions", + "fetchPins", + "sendMessage", + "editMessage", + "createWebhook", + ]; + + for (const method of methods) { + if (ignore.includes(method)) continue; + + klass.prototype[method] = TextChannel.prototype[method]; + } } async fetchPins(): Promise { diff --git a/structures/channels/VoiceChannel.ts b/structures/channels/VoiceChannel.ts index 00da784..670e5fd 100644 --- a/structures/channels/VoiceChannel.ts +++ b/structures/channels/VoiceChannel.ts @@ -1,60 +1,19 @@ 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"; +import type { ChannelTypes, DiscordChannel } from "../../vendor/external.ts"; +import BaseVoiceChannel from "./BaseVoiceChannel.ts"; +import TextChannel from "./TextChannel.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 { +export class VoiceChannel extends BaseVoiceChannel { 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, - }, - }); + this.type = data.type as number; } + override type: ChannelTypes.GuildVoice; } +export interface VoiceChannel extends TextChannel, BaseVoiceChannel {} + +TextChannel.applyTo(VoiceChannel); + export default VoiceChannel; diff --git a/structures/guilds/Guild.ts b/structures/guilds/Guild.ts index f0440a6..2254e0a 100644 --- a/structures/guilds/Guild.ts +++ b/structures/guilds/Guild.ts @@ -128,7 +128,7 @@ export interface GuildCreateOptionsChannel { nsfw?: boolean; bitrate?: number; userLimit?: number; - region?: string | null; + rtcRegion?: string | null; videoQualityMode?: VideoQualityModes; permissionOverwrites?: MakeRequired, "id">[]; rateLimitPerUser?: number; @@ -527,7 +527,7 @@ export class Guild extends BaseGuild implements Model { bitrate: channel.bitrate, parent_id: channel.parentId, permission_overwrites: channel.permissionOverwrites, - region: channel.region, + rtc_region: channel.rtcRegion, user_limit: channel.userLimit, video_quality_mode: channel.videoQualityMode, rate_limit_per_user: channel.rateLimitPerUser, diff --git a/util/Routes.ts b/util/Routes.ts index 650b5a2..146f944 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -303,3 +303,11 @@ export function THREAD_ARCHIVED_PRIVATE_JOINED(channelId: Snowflake, options?: L export function FORUM_START(channelId: Snowflake) { return `/channels/${channelId}/threads?has_message=true`; } + +export function STAGE_INSTANCES() { + return `/stage-instances`; +} + +export function STAGE_INSTANCE(channelId: Snowflake) { + return `/stage-instances/${channelId}`; +} From 9a5a25c2d58e75db478eeb2a41afa8728ffb2a8d Mon Sep 17 00:00:00 2001 From: Yuzu Date: Mon, 4 Jul 2022 10:40:14 -0500 Subject: [PATCH 20/45] fix(webhook handling) closes #24 --- structures/Message.ts | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/structures/Message.ts b/structures/Message.ts index e265be7..e193d73 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -83,7 +83,10 @@ export class Message implements Model { this.guildId = data.guild_id; this.applicationId = data.application_id; - this.author = new User(session, data.author); + if (!data.webhook_id) { + this.author = new User(session, data.author); + } + this.flags = data.flags; this.pinned = !!data.pinned; this.tts = !!data.tts; @@ -103,19 +106,9 @@ export class Message implements Model { } // webhook handling - if (data.author.discriminator === "0000") { + if (data.webhook_id && data.author.discriminator === "0000") { this.webhook = { - id: data.author.id, - username: data.author.username, - discriminator: data.author.discriminator, - avatar: data.author.avatar ? iconHashToBigInt(data.author.avatar) : undefined, - }; - } - - // webhook handling - if (data.author && data.author.discriminator === "0000") { - this.webhook = { - id: data.author.id, + id: data.webhook_id!, username: data.author.username, discriminator: data.author.discriminator, avatar: data.author.avatar ? iconHashToBigInt(data.author.avatar) : undefined, @@ -123,7 +116,6 @@ export class Message implements Model { } // user is always null on MessageCreate and its replaced with author - if (data.guild_id && data.member && !this.isWebhookMessage()) { this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id); } @@ -145,7 +137,7 @@ export class Message implements Model { channelId: Snowflake; guildId?: Snowflake; applicationId?: Snowflake; - author: User; + author!: User; flags?: MessageFlags; pinned: boolean; tts: boolean; From 948cd2f717d316502288c85d51e35180b1a6e519 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Mon, 4 Jul 2022 12:35:17 -0500 Subject: [PATCH 21/45] feat: BaseChannel.isStage --- deno.json | 3 --- structures/channels/BaseChannel.ts | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/deno.json b/deno.json index 25bec05..d010978 100644 --- a/deno.json +++ b/deno.json @@ -1,8 +1,5 @@ { "fmt": { - "files": { - "exclude": "vendor" - }, "options": { "indentWidth": 4, "lineWidth": 120 diff --git a/structures/channels/BaseChannel.ts b/structures/channels/BaseChannel.ts index fc88e18..deb1809 100644 --- a/structures/channels/BaseChannel.ts +++ b/structures/channels/BaseChannel.ts @@ -7,6 +7,7 @@ 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 type StageChannel from "./StageChannel.ts"; import { ChannelTypes } from "../../vendor/external.ts"; import { textBasedChannels } from "./TextChannel.ts"; @@ -43,6 +44,10 @@ export abstract class BaseChannel implements Model { return this.type === ChannelTypes.GuildPublicThread || this.type === ChannelTypes.GuildPrivateThread; } + isStage(): this is StageChannel { + return this.type === ChannelTypes.GuildStageVoice; + } + toString(): string { return `<#${this.id}>`; } From 35ec425916295da00aa021a1d7999270fc0f1cc9 Mon Sep 17 00:00:00 2001 From: socram03 Date: Mon, 4 Jul 2022 19:34:55 -0400 Subject: [PATCH 22/45] feacture: Builders fix: fmt --- deno.json | 2 +- handlers/Actions.ts | 24 ++++----- mod.ts | 5 ++ structures/Message.ts | 3 +- structures/MessageReaction.ts | 2 +- structures/StageInstance.ts | 6 +-- .../builders/InputTextComponentBuilder.ts | 49 +++++++++++++++++ structures/builders/MessageActionRow.ts | 29 ++++++++++ structures/builders/MessageButton.ts | 44 +++++++++++++++ structures/builders/MessageSelectMenu.ts | 53 +++++++++++++++++++ .../builders/SelectMenuOptionBuilder.ts | 38 +++++++++++++ structures/channels/GuildChannel.ts | 14 +++-- structures/channels/TextChannel.ts | 38 ++++++------- structures/guilds/Guild.ts | 30 +++++------ tests/mod.ts | 3 +- util/Routes.ts | 9 ++-- util/builders.ts | 9 ++++ vendor/types/discord.ts | 2 + 18 files changed, 296 insertions(+), 64 deletions(-) create mode 100644 structures/builders/InputTextComponentBuilder.ts create mode 100644 structures/builders/MessageActionRow.ts create mode 100644 structures/builders/MessageButton.ts create mode 100644 structures/builders/MessageSelectMenu.ts create mode 100644 structures/builders/SelectMenuOptionBuilder.ts create mode 100644 util/builders.ts diff --git a/deno.json b/deno.json index 25bec05..b115d2b 100644 --- a/deno.json +++ b/deno.json @@ -1,7 +1,7 @@ { "fmt": { "files": { - "exclude": "vendor" + "exclude": ["vendor"] }, "options": { "indentWidth": 4, diff --git a/handlers/Actions.ts b/handlers/Actions.ts index e1dd524..a5cf1e8 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -1,17 +1,16 @@ import type { DiscordChannel, DiscordChannelPinsUpdate, + DiscordEmoji, DiscordGuild, + DiscordGuildBanAddRemove, + DiscordGuildEmojisUpdate, DiscordGuildMemberAdd, DiscordGuildMemberRemove, DiscordGuildMemberUpdate, - DiscordGuildBanAddRemove, - DiscordGuildEmojisUpdate, DiscordGuildRoleCreate, - DiscordGuildRoleUpdate, DiscordGuildRoleDelete, - DiscordUser, - DiscordEmoji, + DiscordGuildRoleUpdate, DiscordInteraction, DiscordMemberWithUser, DiscordMessage, @@ -25,7 +24,8 @@ import type { // DiscordThreadMemberUpdate, // DiscordThreadMembersUpdate, DiscordThreadListSync, - DiscordWebhookUpdate + DiscordUser, + DiscordWebhookUpdate, } from "../vendor/external.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; @@ -37,7 +37,7 @@ import ThreadMember from "../structures/ThreadMember.ts"; import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; -import Guild from "../structures/guilds/Guild.ts"; +import Guild from "../structures/guilds/Guild.ts"; import Interaction from "../structures/interactions/Interaction.ts"; export type RawHandler = (...args: [Session, number, T]) => void; @@ -90,20 +90,20 @@ export const GUILD_BAN_REMOVE: RawHandler = (session, }; export const GUILD_EMOJIS_UPDATE: RawHandler = (session, _shardId, data) => { - session.emit("guildEmojisUpdate", { guildId: data.guild_id, emojis: data.emojis}) + session.emit("guildEmojisUpdate", { guildId: data.guild_id, emojis: data.emojis }); }; export const GUILD_ROLE_CREATE: RawHandler = (session, _shardId, data) => { session.emit("guildRoleCreate", { guildId: data.guild_id, role: data.role }); -} +}; export const GUILD_ROLE_UPDATE: RawHandler = (session, _shardId, data) => { session.emit("guildRoleUpdate", { guildId: data.guild_id, role: data.role }); -} +}; export const GUILD_ROLE_DELETE: RawHandler = (session, _shardId, data) => { session.emit("guildRoleDelete", { guildId: data.guild_id, roleId: data.role_id }); -} +}; export const INTERACTION_CREATE: RawHandler = (session, _shardId, interaction) => { session.unrepliedInteractions.add(BigInt(interaction.id)); @@ -164,7 +164,7 @@ export const CHANNEL_PINS_UPDATE: RawHandler = (sessio }; export const WEBHOOKS_UPDATE: RawHandler = (session, _shardId, webhook) => { - session.emit("webhooksUpdate", { guildId: webhook.guild_id, channelId: webhook.channel_id }) + session.emit("webhooksUpdate", { guildId: webhook.guild_id, channelId: webhook.channel_id }); }; /* diff --git a/mod.ts b/mod.ts index ab987f5..93499a6 100644 --- a/mod.ts +++ b/mod.ts @@ -36,6 +36,11 @@ export * from "./structures/guilds/Guild.ts"; export * from "./structures/guilds/InviteGuild.ts"; export * from "./structures/builders/EmbedBuilder.ts"; +export * from "./structures/builders/InputTextComponentBuilder.ts"; +export * from "./structures/builders/MessageActionRow.ts"; +export * from "./structures/builders/MessageButton.ts"; +export * from "./structures/builders/MessageSelectMenu.ts"; +export * from "./structures/builders/SelectMenuOptionBuilder.ts"; export * from "./structures/interactions/Interaction.ts"; diff --git a/structures/Message.ts b/structures/Message.ts index e193d73..d13e095 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -6,8 +6,8 @@ import type { DiscordMessage, DiscordUser, FileContent, - MessageTypes, MessageActivityTypes, + MessageTypes, } from "../vendor/external.ts"; import type { Component } from "./components/Component.ts"; import type { GetReactions } from "../util/Routes.ts"; @@ -181,7 +181,6 @@ export class Message implements Model { return this.editedTimestamp; } - get url() { return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`; } diff --git a/structures/MessageReaction.ts b/structures/MessageReaction.ts index d819d2c..5decdf4 100644 --- a/structures/MessageReaction.ts +++ b/structures/MessageReaction.ts @@ -5,7 +5,7 @@ import Emoji from "./Emoji.ts"; /** * Represents a reaction * @link https://discord.com/developers/docs/resources/channel#reaction-object - * */ + */ export class MessageReaction { constructor(session: Session, data: DiscordReaction) { this.session = session; diff --git a/structures/StageInstance.ts b/structures/StageInstance.ts index c82bb56..5c2b91e 100644 --- a/structures/StageInstance.ts +++ b/structures/StageInstance.ts @@ -39,15 +39,15 @@ export class StageInstance implements Model { discoverableDisabled: boolean; guildScheduledEventId: Snowflake; - async edit(options: { topic?: string, privacyLevel?: PrivacyLevels }) { + async edit(options: { topic?: string; privacyLevel?: PrivacyLevels }) { const stageInstance = await this.session.rest.runMethod( this.session.rest, "PATCH", Routes.STAGE_INSTANCE(this.id), { topic: options.topic, - privacy_level: options.privacyLevel - } + privacy_level: options.privacyLevel, + }, ); return new StageInstance(this.session, stageInstance); diff --git a/structures/builders/InputTextComponentBuilder.ts b/structures/builders/InputTextComponentBuilder.ts new file mode 100644 index 0000000..3bba80d --- /dev/null +++ b/structures/builders/InputTextComponentBuilder.ts @@ -0,0 +1,49 @@ +import { DiscordInputTextComponent, MessageComponentTypes, TextStyles } from "../../vendor/external.ts"; + +export class InputTextBuilder { + constructor() { + this.#data = {} as DiscordInputTextComponent; + this.type = 4; + } + #data: DiscordInputTextComponent; + type: MessageComponentTypes.InputText; + + setStyle(style: TextStyles) { + this.#data.style = style; + return this; + } + + setLabel(label: string) { + this.#data.label = label; + return this; + } + + setPlaceholder(placeholder: string) { + this.#data.placeholder = placeholder; + return this; + } + + setLength(max?: number, min?: number) { + this.#data.max_length = max; + this.#data.min_length = min; + return this; + } + + setCustomId(id: string) { + this.#data.custom_id = id; + return this; + } + + setValue(value: string) { + this.#data.value = value; + return this; + } + + setRequired(required = true) { + this.#data.required = required; + return this; + } + toJSON() { + return { ...this.#data }; + } +} diff --git a/structures/builders/MessageActionRow.ts b/structures/builders/MessageActionRow.ts new file mode 100644 index 0000000..856929a --- /dev/null +++ b/structures/builders/MessageActionRow.ts @@ -0,0 +1,29 @@ +import { MessageComponentTypes } from "../../vendor/external.ts"; +import { AnyComponentBuilder } from "../../util/builders.ts"; + +export class ActionRowBuilder { + constructor() { + this.components = [] as T[]; + this.type = 1; + } + components: T[]; + type: MessageComponentTypes.ActionRow; + + addComponents(...components: T[]) { + this.components.push(...components); + return this; + } + + setComponents(...components: T[]) { + this.components.splice( + 0, + this.components.length, + ...components, + ); + return this; + } + + toJSON() { + return { type: this.type, components: this.components.map((c) => c.toJSON()) }; + } +} diff --git a/structures/builders/MessageButton.ts b/structures/builders/MessageButton.ts new file mode 100644 index 0000000..4ef0910 --- /dev/null +++ b/structures/builders/MessageButton.ts @@ -0,0 +1,44 @@ +import { ButtonStyles, type DiscordButtonComponent, MessageComponentTypes } from "../../vendor/external.ts"; +import { ComponentEmoji } from "../../util/builders.ts"; + +export class ButtonBuilder { + constructor() { + this.#data = {} as DiscordButtonComponent; + this.type = 2; + } + #data: DiscordButtonComponent; + type: MessageComponentTypes.Button; + setStyle(style: ButtonStyles) { + this.#data.style = style; + return this; + } + + setLabel(label: string) { + this.#data.label = label; + return this; + } + + setCustomId(id: string) { + this.#data.custom_id = id; + return this; + } + + setEmoji(emoji: ComponentEmoji) { + this.#data.emoji = emoji; + return this; + } + + setDisabled(disabled = true) { + this.#data.disabled = disabled; + return this; + } + + setURL(url: string) { + this.#data.url = url; + return this; + } + + toJSON(): DiscordButtonComponent { + return { ...this.#data }; + } +} diff --git a/structures/builders/MessageSelectMenu.ts b/structures/builders/MessageSelectMenu.ts new file mode 100644 index 0000000..a932131 --- /dev/null +++ b/structures/builders/MessageSelectMenu.ts @@ -0,0 +1,53 @@ +import { type DiscordSelectMenuComponent, MessageComponentTypes } from "../../vendor/external.ts"; +import { SelectMenuOptionBuilder } from "./SelectMenuOptionBuilder.ts"; + +export class SelectMenuBuilder { + constructor() { + this.#data = {} as DiscordSelectMenuComponent; + this.type = 3; + this.options = []; + } + #data: DiscordSelectMenuComponent; + type: MessageComponentTypes.SelectMenu; + options: SelectMenuOptionBuilder[]; + + setPlaceholder(placeholder: string) { + this.#data.placeholder = placeholder; + return this; + } + + setValues(max?: number, min?: number) { + this.#data.max_values = max; + this.#data.min_values = min; + return this; + } + + setDisabled(disabled = true) { + this.#data.disabled = disabled; + return this; + } + + setCustomId(id: string) { + this.#data.custom_id = id; + return this; + } + + setOptions(...options: SelectMenuOptionBuilder[]) { + this.options.splice( + 0, + this.options.length, + ...options, + ); + return this; + } + + addOptions(...options: SelectMenuOptionBuilder[]) { + this.options.push( + ...options, + ); + } + + toJSON() { + return { ...this.#data, options: this.options.map((option) => option.toJSON()) }; + } +} diff --git a/structures/builders/SelectMenuOptionBuilder.ts b/structures/builders/SelectMenuOptionBuilder.ts new file mode 100644 index 0000000..ad84ff6 --- /dev/null +++ b/structures/builders/SelectMenuOptionBuilder.ts @@ -0,0 +1,38 @@ +import type { DiscordSelectOption } from "../../vendor/external.ts"; +import type { ComponentEmoji } from "../../util/builders.ts"; + +export class SelectMenuOptionBuilder { + constructor() { + this.#data = {} as DiscordSelectOption; + } + #data: DiscordSelectOption; + + setLabel(label: string) { + this.#data.label = label; + return this; + } + + setValue(value: string) { + this.#data.value = value; + return this; + } + + setDescription(description: string) { + this.#data.description = description; + return this; + } + + setDefault(Default = true) { + this.#data.default = Default; + return this; + } + + setEmoji(emoji: ComponentEmoji) { + this.#data.emoji = emoji; + return this; + } + + toJSON() { + return { ...this.#data }; + } +} diff --git a/structures/channels/GuildChannel.ts b/structures/channels/GuildChannel.ts index 3609b15..6dde8ff 100644 --- a/structures/channels/GuildChannel.ts +++ b/structures/channels/GuildChannel.ts @@ -1,7 +1,12 @@ 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, DiscordListArchivedThreads } from "../../vendor/external.ts"; +import type { + ChannelTypes, + DiscordChannel, + DiscordInviteMetadata, + DiscordListArchivedThreads, +} from "../../vendor/external.ts"; import type { ListArchivedThreads } from "../../util/Routes.ts"; import BaseChannel from "./BaseChannel.ts"; import ThreadChannel from "./ThreadChannel.ts"; @@ -25,7 +30,7 @@ export interface ThreadCreateOptions { /** * Represents the option object to create a thread channel from a message * @link https://discord.com/developers/docs/resources/channel#start-thread-from-message - * */ + */ export interface ThreadCreateOptions { name: string; autoArchiveDuration?: 60 | 1440 | 4320 | 10080; @@ -59,7 +64,6 @@ export class GuildChannel extends BaseChannel implements Model { return invites.map((invite) => new Invite(this.session, invite)); } - async getArchivedThreads(options: ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }) { let func: (channelId: Snowflake, options: ListArchivedThreads) => string; @@ -78,10 +82,10 @@ export class GuildChannel extends BaseChannel implements Model { const { threads, members, has_more } = await this.session.rest.runMethod( this.session.rest, "GET", - func(this.id, options) + func(this.id, options), ); - return { + return { threads: Object.fromEntries( threads.map((thread) => [thread.id, new ThreadChannel(this.session, thread, this.id)]), ) as Record, diff --git a/structures/channels/TextChannel.ts b/structures/channels/TextChannel.ts index 5b5bd7d..e2ae955 100644 --- a/structures/channels/TextChannel.ts +++ b/structures/channels/TextChannel.ts @@ -88,27 +88,27 @@ export class TextChannel { * Mixin */ static applyTo(klass: Function, ignore: Array = []) { - const methods: Array = [ - "fetchPins", - "createInvite", - "fetchMessages", - "sendTyping", - "pinMessage", - "unpinMessage", - "addReaction", - "removeReaction", - "nukeReactions", - "fetchPins", - "sendMessage", - "editMessage", - "createWebhook", - ]; + const methods: Array = [ + "fetchPins", + "createInvite", + "fetchMessages", + "sendTyping", + "pinMessage", + "unpinMessage", + "addReaction", + "removeReaction", + "nukeReactions", + "fetchPins", + "sendMessage", + "editMessage", + "createWebhook", + ]; - for (const method of methods) { - if (ignore.includes(method)) continue; + for (const method of methods) { + if (ignore.includes(method)) continue; - klass.prototype[method] = TextChannel.prototype[method]; - } + klass.prototype[method] = TextChannel.prototype[method]; + } } async fetchPins(): Promise { diff --git a/structures/guilds/Guild.ts b/structures/guilds/Guild.ts index 2254e0a..bae102c 100644 --- a/structures/guilds/Guild.ts +++ b/structures/guilds/Guild.ts @@ -6,14 +6,14 @@ import type { DiscordEmoji, DiscordGuild, DiscordInviteMetadata, - DiscordMemberWithUser, - DiscordRole, DiscordListActiveThreads, - GuildFeatures, - SystemChannelFlags, - MakeRequired, - VideoQualityModes, + DiscordMemberWithUser, DiscordOverwrite, + DiscordRole, + GuildFeatures, + MakeRequired, + SystemChannelFlags, + VideoQualityModes, } from "../../vendor/external.ts"; import type { GetInvite } from "../../util/Routes.ts"; import { @@ -136,7 +136,7 @@ export interface GuildCreateOptionsChannel { /** * @link https://discord.com/developers/docs/resources/guild#create-guild - * */ + */ export interface GuildCreateOptions { name: string; afkChannelId?: Snowflake; @@ -167,7 +167,7 @@ export interface GuildCreateOptions { /** * @link https://discord.com/developers/docs/resources/guild#modify-guild-json-params - * */ + */ export interface GuildEditOptions extends Omit { ownerId?: Snowflake; splashURL?: string; @@ -473,10 +473,10 @@ export class Guild extends BaseGuild implements Model { const { threads, members } = await this.session.rest.runMethod( this.session.rest, "GET", - Routes.THREAD_ACTIVE(this.id) + Routes.THREAD_ACTIVE(this.id), ); - return { + return { threads: Object.fromEntries( threads.map((thread) => [thread.id, new ThreadChannel(this.session, thread, this.id)]), ) as Record, @@ -488,13 +488,13 @@ export class Guild extends BaseGuild implements Model { /*** * Makes the bot leave the guild - * */ + */ async leave() { } /*** * Deletes a guild - * */ + */ async delete() { await this.session.rest.runMethod( this.session.rest, @@ -507,7 +507,7 @@ export class Guild extends BaseGuild implements Model { * Creates a guild and returns its data, the bot joins the guild * This was modified from discord.js to make it compatible * precondition: Bot should be in less than 10 servers - * */ + */ static async create(session: Session, options: GuildCreateOptions) { const guild = await session.rest.runMethod(session.rest, "POST", Routes.GUILDS(), { name: options.name, @@ -540,7 +540,7 @@ export class Guild extends BaseGuild implements Model { hoist: role.hoist, position: role.position, unicode_emoji: role.unicodeEmoji, - icon: options.iconURL || urlToBase64(options.iconURL!), + icon: options.iconURL || urlToBase64(options.iconURL!), })), }); @@ -549,7 +549,7 @@ export class Guild extends BaseGuild implements Model { /** * Edits a guild and returns its data - * */ + */ async edit(session: Session, options: GuildEditOptions) { const guild = await session.rest.runMethod(session.rest, "PATCH", Routes.GUILDS(), { name: options.name, diff --git a/tests/mod.ts b/tests/mod.ts index d1f7a69..1cef8bc 100644 --- a/tests/mod.ts +++ b/tests/mod.ts @@ -7,7 +7,8 @@ if (!token) { throw new Error("Please provide a token"); } -const intents = GatewayIntents.MessageContent | GatewayIntents.Guilds | GatewayIntents.GuildMessages | GatewayIntents.GuildMembers | GatewayIntents.GuildBans +const intents = GatewayIntents.MessageContent | GatewayIntents.Guilds | GatewayIntents.GuildMessages | + GatewayIntents.GuildMembers | GatewayIntents.GuildBans; const session = new Session({ token, intents }); session.on("ready", (payload) => { diff --git a/util/Routes.ts b/util/Routes.ts index 146f944..2748cbc 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -233,7 +233,6 @@ export function CHANNEL_WEBHOOKS(channelId: Snowflake) { return `/channels/${channelId}/webhooks`; } - export function THREAD_START_PUBLIC(channelId: Snowflake, messageId: Snowflake) { return `/channels/${channelId}/messages/${messageId}/threads`; } @@ -271,8 +270,8 @@ export function THREAD_ARCHIVED_PUBLIC(channelId: Snowflake, options?: ListArchi let url = `/channels/${channelId}/threads/archived/public?`; if (options) { - if (options.before) url += `before=${new Date(options.before).toISOString()}`; - if (options.limit) url += `&limit=${options.limit}`; + if (options.before) url += `before=${new Date(options.before).toISOString()}`; + if (options.limit) url += `&limit=${options.limit}`; } return url; @@ -293,8 +292,8 @@ export function THREAD_ARCHIVED_PRIVATE_JOINED(channelId: Snowflake, options?: L let url = `/channels/${channelId}/users/@me/threads/archived/private?`; if (options) { - if (options.before) url += `before=${new Date(options.before).toISOString()}`; - if (options.limit) url += `&limit=${options.limit}`; + if (options.before) url += `before=${new Date(options.before).toISOString()}`; + if (options.limit) url += `&limit=${options.limit}`; } return url; diff --git a/util/builders.ts b/util/builders.ts new file mode 100644 index 0000000..19c9e8c --- /dev/null +++ b/util/builders.ts @@ -0,0 +1,9 @@ +import { ButtonBuilder, InputTextBuilder, SelectMenuBuilder } from "../mod.ts"; +import { Snowflake } from "./Snowflake.ts"; + +export type AnyComponentBuilder = InputTextBuilder | SelectMenuBuilder | ButtonBuilder; +export type ComponentEmoji = { + id: Snowflake; + name: string; + animated?: boolean; +}; diff --git a/vendor/types/discord.ts b/vendor/types/discord.ts index 5123cbb..496e294 100644 --- a/vendor/types/discord.ts +++ b/vendor/types/discord.ts @@ -1171,6 +1171,8 @@ export interface DiscordSelectMenuComponent { max_values?: number; /** The choices! Maximum of 25 items. */ options: DiscordSelectOption[]; + /** Whether or not this select menu is disabled */ + disabled?: boolean; } export interface DiscordSelectOption { From 1e527cd24ca87320e9d76d8ccdac4e55e34956cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Mon, 4 Jul 2022 20:54:05 -0300 Subject: [PATCH 23/45] Add Integration structure --- structures/Integration.ts | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 structures/Integration.ts diff --git a/structures/Integration.ts b/structures/Integration.ts new file mode 100644 index 0000000..852cfb3 --- /dev/null +++ b/structures/Integration.ts @@ -0,0 +1,55 @@ +import type { Model } from "./Base.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { Session } from "../session/Session.ts"; +import type { + DiscordIntegration, + DiscordIntegrationAccount, + DiscordIntegrationApplication, + DiscordUser, + IntegrationExpireBehaviors +} from "../vendor/external.ts"; + +export class Integration implements Model { + constructor(session: Session, data: DiscordIntegration & { guild_id?: string }) { + this.id = data.id; + this.session = session; + + data.guild_id ? this.guildId = data.guild_id : null; + + this.name = data.name; + this.type = data.type; + this.enabled = !!data.enabled; + this.syncing = !!data.syncing; + this.roleId = data.role_id; + this.enableEmoticons = !!data.enable_emoticons; + this.expireBehavior = data.expire_behavior; + this.expireGracePeriod = data.expire_grace_period; + this.syncedAt = data.synced_at; + this.subscriberCount = data.subscriber_count; + this.revoked = !!data.revoked; + + this.user = data.user; + this.account = data.account; + this.application = data.application; + } + + id: Snowflake; + session: Session; + guildId?: string; + + name: string + type: "twitch" | "youtube" | "discord"; + enabled?: boolean; + syncing?: boolean; + roleId?: string; + enableEmoticons?: boolean; + expireBehavior?: IntegrationExpireBehaviors; + expireGracePeriod?: number; + syncedAt?: string; + subscriberCount?: number; + revoked?: boolean; + + user?: DiscordUser; + account?: DiscordIntegrationAccount; + application?: DiscordIntegrationApplication; +} \ No newline at end of file From cb20e1f04dee76d4937d315fcafc76c235541819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Mon, 4 Jul 2022 21:02:50 -0300 Subject: [PATCH 24/45] Fix Integration account property --- structures/Integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structures/Integration.ts b/structures/Integration.ts index 852cfb3..4d21059 100644 --- a/structures/Integration.ts +++ b/structures/Integration.ts @@ -50,6 +50,6 @@ export class Integration implements Model { revoked?: boolean; user?: DiscordUser; - account?: DiscordIntegrationAccount; + account: DiscordIntegrationAccount; application?: DiscordIntegrationApplication; } \ No newline at end of file From a36e0915e55dfb866376e8818d9375363cebde97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Mon, 4 Jul 2022 21:11:16 -0300 Subject: [PATCH 25/45] Add integrationCreate action --- handlers/Actions.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index e1dd524..575f312 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -25,7 +25,8 @@ import type { // DiscordThreadMemberUpdate, // DiscordThreadMembersUpdate, DiscordThreadListSync, - DiscordWebhookUpdate + DiscordWebhookUpdate, + DiscordIntegration } from "../vendor/external.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; @@ -39,6 +40,7 @@ import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; import Guild from "../structures/guilds/Guild.ts"; import Interaction from "../structures/interactions/Interaction.ts"; +import { Integration } from "../structures/Integration.ts" export type RawHandler = (...args: [Session, number, T]) => void; export type Handler = (...args: T) => unknown; @@ -167,6 +169,10 @@ export const WEBHOOKS_UPDATE: RawHandler = (session, _shar session.emit("webhooksUpdate", { guildId: webhook.guild_id, channelId: webhook.channel_id }) }; +export const INTEGRATION_CREATE: RawHandler = (session, _shardId, payload) => { + session.emit("integrationCreate", new Integration(session, payload)); +}; + /* export const MESSAGE_REACTION_ADD: RawHandler = (session, _shardId, reaction) => { session.emit("messageReactionAdd", null); @@ -226,6 +232,7 @@ export interface Events { "threadDelete": Handler<[ThreadChannel]>; "threadListSync": Handler<[{ guildId: Snowflake, channelIds: Snowflake[], threads: ThreadChannel[], members: ThreadMember[] }]> "interactionCreate": Handler<[Interaction]>; + "integrationCreate": Handler<[DiscordIntegration]>; "raw": Handler<[unknown, number]>; "webhooksUpdate": Handler<[{ guildId: Snowflake, channelId: Snowflake }]>; } From c05d8113bd23d6c91d9bcee6b19e00c03cdcdd97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Mon, 4 Jul 2022 21:12:39 -0300 Subject: [PATCH 26/45] Update Integration structure --- structures/Integration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structures/Integration.ts b/structures/Integration.ts index 4d21059..3514250 100644 --- a/structures/Integration.ts +++ b/structures/Integration.ts @@ -10,7 +10,7 @@ import type { } from "../vendor/external.ts"; export class Integration implements Model { - constructor(session: Session, data: DiscordIntegration & { guild_id?: string }) { + constructor(session: Session, data: DiscordIntegration & { guild_id?: Snowflake }) { this.id = data.id; this.session = session; @@ -35,7 +35,7 @@ export class Integration implements Model { id: Snowflake; session: Session; - guildId?: string; + guildId?: Snowflake; name: string type: "twitch" | "youtube" | "discord"; From 59755f2f6eeff0f21a53304d3471d4ab1b4d5740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Mon, 4 Jul 2022 21:53:31 -0300 Subject: [PATCH 27/45] Add integration-Update/Delete action --- handlers/Actions.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 575f312..40afec9 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -26,7 +26,8 @@ import type { // DiscordThreadMembersUpdate, DiscordThreadListSync, DiscordWebhookUpdate, - DiscordIntegration + DiscordIntegration, + DiscordIntegrationDelete } from "../vendor/external.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; @@ -173,6 +174,14 @@ export const INTEGRATION_CREATE: RawHandler = (session, _sha session.emit("integrationCreate", new Integration(session, payload)); }; +export const INTEGRATION_UPDATE: RawHandler = (session, _shardId, payload) => { + session.emit("integrationCreate", new Integration(session, payload)); +}; + +export const INTEGRATION_DELETE: RawHandler = (session, _shardId, payload) => { + session.emit("integrationDelete", { id: payload.id, guildId: payload.guild_id, applicationId: payload.application_id }); +}; + /* export const MESSAGE_REACTION_ADD: RawHandler = (session, _shardId, reaction) => { session.emit("messageReactionAdd", null); @@ -233,6 +242,8 @@ export interface Events { "threadListSync": Handler<[{ guildId: Snowflake, channelIds: Snowflake[], threads: ThreadChannel[], members: ThreadMember[] }]> "interactionCreate": Handler<[Interaction]>; "integrationCreate": Handler<[DiscordIntegration]>; + "integrationUpdate": Handler<[DiscordIntegration]>; + "integrationDelete": Handler<[{ id: Snowflake, guildId?: Snowflake, applicationId?: Snowflake }]>; "raw": Handler<[unknown, number]>; "webhooksUpdate": Handler<[{ guildId: Snowflake, channelId: Snowflake }]>; } From ac32d41330939a754578741c4f04c171677b1b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Tue, 5 Jul 2022 13:57:21 -0300 Subject: [PATCH 28/45] Update User structure --- structures/User.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/structures/User.ts b/structures/User.ts index da8c5ee..82b38ac 100644 --- a/structures/User.ts +++ b/structures/User.ts @@ -18,6 +18,7 @@ export class User implements Model { this.username = data.username; this.discriminator = data.discriminator; + this.avatar = data.avatar ? data.avatar : undefined; this.avatarHash = data.avatar ? iconHashToBigInt(data.avatar) : undefined; this.accentColor = data.accent_color; this.bot = !!data.bot; @@ -30,6 +31,7 @@ export class User implements Model { username: string; discriminator: string; + avatar?: string; avatarHash?: bigint; accentColor?: number; bot: boolean; From 4b89e81839e369dd15bf8dc9d3501429b5bca064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Tue, 5 Jul 2022 14:05:23 -0300 Subject: [PATCH 29/45] Update Integration structure --- structures/Integration.ts | 41 ++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/structures/Integration.ts b/structures/Integration.ts index 3514250..45a0de7 100644 --- a/structures/Integration.ts +++ b/structures/Integration.ts @@ -3,11 +3,22 @@ import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { DiscordIntegration, - DiscordIntegrationAccount, - DiscordIntegrationApplication, - DiscordUser, IntegrationExpireBehaviors } from "../vendor/external.ts"; +import User from "./User.ts" + +export interface IntegrationAccount { + id: Snowflake; + name: string; +} + +export interface IntegrationApplication { + id: Snowflake; + name: string; + icon?: string; + description: string; + bot?: User; +} export class Integration implements Model { constructor(session: Session, data: DiscordIntegration & { guild_id?: Snowflake }) { @@ -28,9 +39,21 @@ export class Integration implements Model { this.subscriberCount = data.subscriber_count; this.revoked = !!data.revoked; - this.user = data.user; - this.account = data.account; - this.application = data.application; + this.user = data.user ? new User(session, data.user) : undefined; + this.account = { + id: data.account.id, + name: data.account.name + } + + if (data.application) { + this.application = { + id: data.application.id, + name: data.application.name, + icon: data.application.icon ? data.application.icon : undefined, + description: data.application.description, + bot: data.application.bot ? new User(session, data.application.bot) : undefined + }; + } } id: Snowflake; @@ -49,7 +72,7 @@ export class Integration implements Model { subscriberCount?: number; revoked?: boolean; - user?: DiscordUser; - account: DiscordIntegrationAccount; - application?: DiscordIntegrationApplication; + user?: User; + account: IntegrationAccount; + application?: IntegrationApplication; } \ No newline at end of file From 58e078a2e67d52c13ec51b6ec59495125ff7556f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Tue, 5 Jul 2022 14:44:08 -0300 Subject: [PATCH 30/45] Revert "Update User structure" This reverts commit ac32d41330939a754578741c4f04c171677b1b0f. --- structures/User.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/structures/User.ts b/structures/User.ts index 82b38ac..da8c5ee 100644 --- a/structures/User.ts +++ b/structures/User.ts @@ -18,7 +18,6 @@ export class User implements Model { this.username = data.username; this.discriminator = data.discriminator; - this.avatar = data.avatar ? data.avatar : undefined; this.avatarHash = data.avatar ? iconHashToBigInt(data.avatar) : undefined; this.accentColor = data.accent_color; this.bot = !!data.bot; @@ -31,7 +30,6 @@ export class User implements Model { username: string; discriminator: string; - avatar?: string; avatarHash?: bigint; accentColor?: number; bot: boolean; From 712d816fadeed60cd7445924fc2e882d51fea111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Tue, 5 Jul 2022 15:10:11 -0300 Subject: [PATCH 31/45] Fix type for Integration event --- handlers/Actions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 8d302e5..c7f1580 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -171,11 +171,11 @@ export const WEBHOOKS_UPDATE: RawHandler = (session, _shar session.emit("webhooksUpdate", { guildId: webhook.guild_id, channelId: webhook.channel_id }); }; -export const INTEGRATION_CREATE: RawHandler = (session, _shardId, payload) => { +export const INTEGRATION_CREATE: RawHandler = (session, _shardId, payload) => { session.emit("integrationCreate", new Integration(session, payload)); }; -export const INTEGRATION_UPDATE: RawHandler = (session, _shardId, payload) => { +export const INTEGRATION_UPDATE: RawHandler = (session, _shardId, payload) => { session.emit("integrationCreate", new Integration(session, payload)); }; @@ -242,8 +242,8 @@ export interface Events { "threadDelete": Handler<[ThreadChannel]>; "threadListSync": Handler<[{ guildId: Snowflake, channelIds: Snowflake[], threads: ThreadChannel[], members: ThreadMember[] }]> "interactionCreate": Handler<[Interaction]>; - "integrationCreate": Handler<[DiscordIntegration]>; - "integrationUpdate": Handler<[DiscordIntegration]>; + "integrationCreate": Handler<[Integration]>; + "integrationUpdate": Handler<[Integration]>; "integrationDelete": Handler<[{ id: Snowflake, guildId?: Snowflake, applicationId?: Snowflake }]>; "raw": Handler<[unknown, number]>; "webhooksUpdate": Handler<[{ guildId: Snowflake, channelId: Snowflake }]>; From 22f05c814519065b18450b59b1f005948b3ed374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Tue, 5 Jul 2022 15:52:32 -0300 Subject: [PATCH 32/45] Update Invite structure --- structures/Invite.ts | 90 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 8 deletions(-) diff --git a/structures/Invite.ts b/structures/Invite.ts index 5d238e7..5b4eaa6 100644 --- a/structures/Invite.ts +++ b/structures/Invite.ts @@ -1,9 +1,50 @@ import type { Session } from "../session/Session.ts"; -import type { DiscordInvite } from "../vendor/external.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { + DiscordChannel, + DiscordMemberWithUser, + DiscordInvite, + DiscordScheduledEventEntityMetadata, + ScheduledEventPrivacyLevel, + ScheduledEventStatus, + ScheduledEventEntityType +} from "../vendor/external.ts"; import { TargetTypes } from "../vendor/external.ts"; import InviteGuild from "./guilds/InviteGuild.ts"; import User from "./User.ts"; import Guild from "./guilds/Guild.ts"; +import { GuildChannel } from "./channels/GuildChannel.ts"; +import { Member } from "./Member.ts"; + +export interface InviteStageInstance { + /** The members speaking in the Stage */ + members: Partial[]; + /** The number of users in the Stage */ + participantCount: number; + /** The number of users speaking in the Stage */ + speakerCount: number; + /** The topic of the Stage instance (1-120 characters) */ + topic: string; +} + +export interface InviteScheduledEvent { + id: Snowflake; + guildId: string; + channelId?: string; + creatorId?: string; + name: string; + description?: string; + scheduledStartTime: string; + scheduledEndTime?: string; + privacyLevel: ScheduledEventPrivacyLevel; + status: ScheduledEventStatus; + entityType: ScheduledEventEntityType; + entityId?: string; + entityMetadata?: DiscordScheduledEventEntityMetadata; + creator?: User; + userCount?: number; + image?: string; +} /** * @link https://discord.com/developers/docs/resources/invite#invite-object @@ -24,16 +65,37 @@ export class Invite { this.approximatePresenceCount = data.approximate_presence_count; } - // TODO: fix this - // this.channel = data.channel; + if (data.channel) { + const guildId = (data.guild && data.guild?.id) ? data.guild.id : ""; + this.channel = new GuildChannel(session, (data.channel as DiscordChannel), guildId); + } + this.code = data.code; if (data.expires_at) { this.expiresAt = Number.parseInt(data.expires_at); } - // TODO: fix this - // this.xd = data.guild_scheduled_event + if (data.guild_scheduled_event) { + this.guildScheduledEvent = { + id: data.guild_scheduled_event.id, + guildId: data.guild_scheduled_event.guild_id, + channelId: data.guild_scheduled_event.channel_id ? data.guild_scheduled_event.channel_id : undefined, + creatorId: data.guild_scheduled_event.creator_id ? data.guild_scheduled_event.creator_id : undefined, + name: data.guild_scheduled_event.name, + description: data.guild_scheduled_event.description ? data.guild_scheduled_event.description : undefined, + scheduledStartTime: data.guild_scheduled_event.scheduled_start_time, + scheduledEndTime: data.guild_scheduled_event.scheduled_end_time ? data.guild_scheduled_event.scheduled_end_time : undefined, + privacyLevel: data.guild_scheduled_event.privacy_level, + status: data.guild_scheduled_event.status, + entityType: data.guild_scheduled_event.entity_type, + entityId: data.guild ? data.guild.id : undefined, + entityMetadata: data.guild_scheduled_event.entity_metadata ? data.guild_scheduled_event.entity_metadata : undefined, + creator: data.guild_scheduled_event.creator ? new User(session, data.guild_scheduled_event.creator) : undefined, + userCount: data.guild_scheduled_event.user_count ? data.guild_scheduled_event.user_count : undefined, + image: data.guild_scheduled_event.image ? data.guild_scheduled_event.image : undefined + }; + } if (data.inviter) { this.inviter = new User(session, data.inviter); @@ -43,10 +105,17 @@ export class Invite { this.targetUser = new User(session, data.target_user); } - // TODO: fix this - // this.stageInstance = data.stage_instance + if (data.stage_instance) { + const guildId = (data.guild && data.guild?.id) ? data.guild.id : ""; + this.stageInstance = { + members: data.stage_instance.members.map(m => new Member(session, (m as DiscordMemberWithUser), guildId)), + participantCount: data.stage_instance.participant_count, + speakerCount: data.stage_instance.speaker_count, + topic: data.stage_instance.topic + }; + } - // TODO: fix this + // TODO: create Application structure // this.targetApplication = data.target_application if (data.target_type) { @@ -63,6 +132,11 @@ export class Invite { inviter?: User; targetUser?: User; targetType?: TargetTypes; + channel?: Partial; + stageInstance?: InviteStageInstance; + guildScheduledEvent?: InviteScheduledEvent; + // TODO: create Application structure + // targetApplication?: Partial async delete(): Promise { await Guild.prototype.deleteInvite.call(this.guild, this.code); From 971f541f830d09906b65c1be4da48e4e6a0b6ca8 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Mon, 4 Jul 2022 21:11:35 -0500 Subject: [PATCH 33/45] wip: interactions --- :x | 36 +++ handlers/Actions.ts | 9 +- mod.ts | 9 +- session/Session.ts | 2 - .../interactions/AutoCompleteInteraction.ts | 40 +++ structures/interactions/BaseInteraction.ts | 84 +++++++ structures/interactions/CommandInteraction.ts | 108 ++++++++ .../CommandInteractionOptionResolver.ts | 233 ++++++++++++++++++ .../interactions/ComponentInteraction.ts | 45 ++++ structures/interactions/Interaction.ts | 145 ----------- structures/interactions/InteractionFactory.ts | 32 +++ .../interactions/ModalSubmitInteraction.ts | 50 ++++ structures/interactions/PingInteraction.ts | 38 +++ 13 files changed, 676 insertions(+), 155 deletions(-) create mode 100644 :x create mode 100644 structures/interactions/AutoCompleteInteraction.ts create mode 100644 structures/interactions/BaseInteraction.ts create mode 100644 structures/interactions/CommandInteraction.ts create mode 100644 structures/interactions/CommandInteractionOptionResolver.ts create mode 100644 structures/interactions/ComponentInteraction.ts delete mode 100644 structures/interactions/Interaction.ts create mode 100644 structures/interactions/InteractionFactory.ts create mode 100644 structures/interactions/ModalSubmitInteraction.ts create mode 100644 structures/interactions/PingInteraction.ts diff --git a/:x b/:x new file mode 100644 index 0000000..43b5f29 --- /dev/null +++ b/:x @@ -0,0 +1,36 @@ + +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ApplicationCommandTypes, DiscordInteraction } from "../../vendor/external.ts"; +import { InteractionResponseTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import * as Routes from "../../util/Routes.ts"; + +export class PingInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.commandId = data.data!.id; + this.commandName = data.data!.name; + this.commandType = data.data!.type; + this.commandGuildId = data.data!.guild_id; + } + + commandId: Snowflake; + commandName: string; + commandType: ApplicationCommandTypes; + commandGuildId?: Snowflake; + + async pong() { + await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.INTERACTION_ID_TOKEN(this.id, this.token), + { + type: InteractionResponseTypes.Pong, + } + ); + } +} + +export default PingInteraction; diff --git a/handlers/Actions.ts b/handlers/Actions.ts index e1dd524..371a37b 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -38,7 +38,7 @@ import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; import Guild from "../structures/guilds/Guild.ts"; -import Interaction from "../structures/interactions/Interaction.ts"; +import InteractionFactory from "../structures/interactions/interactions/InteractionFactory.ts"; export type RawHandler = (...args: [Session, number, T]) => void; export type Handler = (...args: T) => unknown; @@ -106,12 +106,7 @@ export const GUILD_ROLE_DELETE: RawHandler = (session, _ } export const INTERACTION_CREATE: RawHandler = (session, _shardId, interaction) => { - session.unrepliedInteractions.add(BigInt(interaction.id)); - - // could be improved - setTimeout(() => session.unrepliedInteractions.delete(BigInt(interaction.id)), 15 * 60 * 1000); - - session.emit("interactionCreate", new Interaction(session, interaction)); + session.emit("interactionCreate", InteractionFactory.from(session, interaction)); }; export const CHANNEL_CREATE: RawHandler = (session, _shardId, channel) => { diff --git a/mod.ts b/mod.ts index ab987f5..f43a323 100644 --- a/mod.ts +++ b/mod.ts @@ -37,7 +37,14 @@ export * from "./structures/guilds/InviteGuild.ts"; export * from "./structures/builders/EmbedBuilder.ts"; -export * from "./structures/interactions/Interaction.ts"; +export * from "./structures/interactions/AutoCompleteInteraction.ts"; +export * from "./structures/interactions/BaseInteraction.ts"; +export * from "./structures/interactions/CommandInteraction.ts"; +export * from "./structures/interactions/CommandInteractionOptionResolver.ts"; +export * from "./structures/interactions/ComponentInteraction.ts"; +export * from "./structures/interactions/InteractionFactory.ts"; +export * from "./structures/interactions/ModalSubmitInteraction.ts"; +export * from "./structures/interactions/PingInteraction.ts"; export * from "./session/Session.ts"; diff --git a/session/Session.ts b/session/Session.ts index 6b25993..50b9e61 100644 --- a/session/Session.ts +++ b/session/Session.ts @@ -39,8 +39,6 @@ export class Session extends EventEmitter { rest: ReturnType; gateway: ReturnType; - unrepliedInteractions: Set = new Set(); - #botId: Snowflake; #applicationId?: Snowflake; diff --git a/structures/interactions/AutoCompleteInteraction.ts b/structures/interactions/AutoCompleteInteraction.ts new file mode 100644 index 0000000..877d87e --- /dev/null +++ b/structures/interactions/AutoCompleteInteraction.ts @@ -0,0 +1,40 @@ + +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ApplicationCommandTypes, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import type { ApplicationCommandOptionChoice } from "./BaseInteraction.ts"; +import { InteractionResponseTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import * as Routes from "../../util/Routes.ts"; + +export class AutoCompleteInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.type = data.type as number; + this.commandId = data.data!.id; + this.commandName = data.data!.name; + this.commandType = data.data!.type; + this.commandGuildId = data.data!.guild_id; + } + + override type: InteractionTypes.ApplicationCommandAutocomplete; + commandId: Snowflake; + commandName: string; + commandType: ApplicationCommandTypes; + commandGuildId?: Snowflake; + + async respond(choices: ApplicationCommandOptionChoice[]) { + await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.INTERACTION_ID_TOKEN(this.id, this.token), + { + data: { choices }, + type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, + } + ); + } +} + +export default AutoCompleteInteraction; diff --git a/structures/interactions/BaseInteraction.ts b/structures/interactions/BaseInteraction.ts new file mode 100644 index 0000000..a93ca8f --- /dev/null +++ b/structures/interactions/BaseInteraction.ts @@ -0,0 +1,84 @@ +import type { Model } from "../Base.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordInteraction } from "../../vendor/external.ts"; +import type CommandInteraction from "./CommandInteraction.ts"; +import type PingInteraction from "./PingInteraction.ts"; +import { InteractionTypes } from "../../vendor/external.ts"; +import { Snowflake } from "../../util/Snowflake.ts"; +import User from "../User.ts"; +import Member from "../Member.ts"; +import Permsisions from "../Permissions.ts"; + +export abstract class BaseInteraction 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.version = data.version; + + // @ts-expect-error: vendor error + const perms = data.app_permissions as string; + + if (perms) { + this.appPermissions = new Permsisions(BigInt(perms)); + } + + 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; + user?: User; + member?: Member; + appPermissions?: Permsisions; + + readonly version: 1; + + get createdTimestamp() { + return Snowflake.snowflakeToTimestamp(this.id); + } + + get createdAt() { + return new Date(this.createdTimestamp); + } + + isCommand(): this is CommandInteraction { + return this.type === InteractionTypes.ApplicationCommand; + } + + isAutoComplete() { + return this.type === InteractionTypes.ApplicationCommandAutocomplete; + } + + isComponent() { + return this.type === InteractionTypes.MessageComponent; + } + + isPing(): this is PingInteraction { + return this.type === InteractionTypes.Ping; + } + + isModalSubmit() { + return this.type === InteractionTypes.ModalSubmit; + } + + inGuild() { + return !!this.guildId; + } +} + +export default BaseInteraction; diff --git a/structures/interactions/CommandInteraction.ts b/structures/interactions/CommandInteraction.ts new file mode 100644 index 0000000..ef1294a --- /dev/null +++ b/structures/interactions/CommandInteraction.ts @@ -0,0 +1,108 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ApplicationCommandTypes, DiscordMemberWithUser, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import type { CreateMessage } from "../Message.ts"; +import { InteractionResponseTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import CommandInteractionOptionResolver from "./CommandInteractionOptionResolver.ts"; +import Attachment from "../Attachment.ts"; +import User from "../User.ts"; +import Member from "../Member.ts"; +import Message from "../Message.ts"; +import Role from "../Role.ts"; + +/** + * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response + * */ +export interface InteractionResponse { + type: InteractionResponseTypes; + data?: InteractionApplicationCommandCallbackData; +} + +/** + * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionapplicationcommandcallbackdata + * */ +export interface InteractionApplicationCommandCallbackData extends Omit { + customId?: string; + title?: string; + // TODO: use builder + // components?: MessageComponents; + flags?: number; + choices?: ApplicationCommandOptionChoice[]; +} + +/** + * @link https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice + * */ +export interface ApplicationCommandOptionChoice { + name: string; + value: string | number; +} + +export class CommandInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.type = data.type as number; + this.commandId = data.data!.id; + this.commandName = data.data!.name; + this.commandType = data.data!.type; + this.commandGuildId = data.data!.guild_id; + this.options = new CommandInteractionOptionResolver(data.data!.options ?? []); + + this.resolved = { + users: new Map(), + members: new Map(), + roles: new Map(), + attachments: new Map(), + messages: new Map(), + }; + + if (data.data!.resolved?.users) { + for (const [id, u] of Object.entries(data.data!.resolved.users)) { + this.resolved.users.set(id, new User(session, u)); + } + } + + if (data.data!.resolved?.members && !!super.guildId) { + for (const [id, m] of Object.entries(data.data!.resolved.members)) { + this.resolved.members.set(id, new Member(session, m as DiscordMemberWithUser, super.guildId!)); + } + } + + if (data.data!.resolved?.roles && !!super.guildId) { + for (const [id, r] of Object.entries(data.data!.resolved.roles)) { + this.resolved.roles.set(id, new Role(session, r, super.guildId!)); + } + } + + if (data.data!.resolved?.attachments) { + for (const [id, a] of Object.entries(data.data!.resolved.attachments)) { + this.resolved.attachments.set(id, new Attachment(session, a)); + } + } + + if (data.data!.resolved?.messages) { + for (const [id, m] of Object.entries(data.data!.resolved.messages)) { + this.resolved.messages.set(id, new Message(session, m)); + } + } + } + + override type: InteractionTypes.ApplicationCommand; + commandId: Snowflake; + commandName: string; + commandType: ApplicationCommandTypes; + commandGuildId?: Snowflake; + resolved: { + users: Map; + members: Map; + roles: Map; + attachments: Map; + messages: Map; + }; + options: CommandInteractionOptionResolver; + responded = false; +} + +export default CommandInteraction; diff --git a/structures/interactions/CommandInteractionOptionResolver.ts b/structures/interactions/CommandInteractionOptionResolver.ts new file mode 100644 index 0000000..a654566 --- /dev/null +++ b/structures/interactions/CommandInteractionOptionResolver.ts @@ -0,0 +1,233 @@ +import type { DiscordInteractionDataOption, DiscordInteractionDataResolved } from '../../vendor/external.ts'; +import { ApplicationCommandOptionTypes } from "../../vendor/external.ts"; + +export function transformOasisInteractionDataOption(o: DiscordInteractionDataOption): CommandInteractionOption { + const output: CommandInteractionOption = { ...o, Otherwise: o.value as string | boolean | number | undefined }; + + switch (o.type) { + case ApplicationCommandOptionTypes.String: + output.String = o.value as string; + break; + case ApplicationCommandOptionTypes.Number: + output.Number = o.value as number; + break; + case ApplicationCommandOptionTypes.Integer: + output.Integer = o.value as number; + break; + case ApplicationCommandOptionTypes.Boolean: + output.Boolean = o.value as boolean; + break; + case ApplicationCommandOptionTypes.Role: + output.Role = BigInt(o.value as string); + break; + case ApplicationCommandOptionTypes.User: + output.User = BigInt(o.value as string); + break; + case ApplicationCommandOptionTypes.Channel: + output.Channel = BigInt(o.value as string); + break; + + case ApplicationCommandOptionTypes.Mentionable: + case ApplicationCommandOptionTypes.SubCommand: + case ApplicationCommandOptionTypes.SubCommandGroup: + default: + output.Otherwise = o.value as string | boolean | number | undefined; + } + + return output; +} + +export interface CommandInteractionOption extends Omit { + Attachment?: string; + Boolean?: boolean; + User?: bigint; + Role?: bigint; + Number?: number; + Integer?: number; + Channel?: bigint; + String?: string; + Mentionable?: string; + Otherwise: string | number | boolean | bigint | undefined; +} + +/** + * Utility class to get the resolved options for a command + * It is really typesafe + * @example const option = ctx.options.getStringOption("name"); + */ +export class CommandInteractionOptionResolver { + #subcommand?: string; + #group?: string; + + hoistedOptions: CommandInteractionOption[]; + resolved?: DiscordInteractionDataResolved; + + constructor(options?: DiscordInteractionDataOption[], resolved?: DiscordInteractionDataResolved) { + this.hoistedOptions = options?.map(transformOasisInteractionDataOption) ?? []; + + // warning: black magic do not edit and thank djs authors + + if (this.hoistedOptions[0]?.type === ApplicationCommandOptionTypes.SubCommandGroup) { + this.#group = this.hoistedOptions[0].name; + this.hoistedOptions = (this.hoistedOptions[0].options ?? []).map(transformOasisInteractionDataOption); + } + + if (this.hoistedOptions[0]?.type === ApplicationCommandOptionTypes.SubCommand) { + this.#subcommand = this.hoistedOptions[0].name; + this.hoistedOptions = (this.hoistedOptions[0].options ?? []).map(transformOasisInteractionDataOption); + } + + this.resolved = resolved; + } + + private getTypedOption( + name: string | number, + type: ApplicationCommandOptionTypes, + properties: Array, + required: boolean, + ) { + const option = this.get(name, required); + + if (!option) { + return; + } + + if (option.type !== type) { + // pass + } + + if (required === true && properties.every((prop) => typeof option[prop] === "undefined")) { + throw new TypeError(`Properties ${properties.join(', ')} are missing in option ${name}`); + } + + return option; + } + + get(name: string | number, required: true): CommandInteractionOption; + get(name: string | number, required: boolean): CommandInteractionOption | undefined; + get(name: string | number, required?: boolean) { + const option = this.hoistedOptions.find((o) => + typeof name === 'number' ? o.name === name.toString() : o.name === name + ); + + if (!option) { + if (required && name in this.hoistedOptions.map((o) => o.name)) { + throw new TypeError('Option marked as required was undefined'); + } + + return; + } + + return option; + } + + /** searches for a string option */ + getString(name: string | number, required: true): string; + getString(name: string | number, required?: boolean): string | undefined; + getString(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.String, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a number option */ + getNumber(name: string | number, required: true): number; + getNumber(name: string | number, required?: boolean): number | undefined; + getNumber(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Number, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searhces for an integer option */ + getInteger(name: string | number, required: true): number; + getInteger(name: string | number, required?: boolean): number | undefined; + getInteger(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Integer, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a boolean option */ + getBoolean(name: string | number, required: true): boolean; + getBoolean(name: string | number, required?: boolean): boolean | undefined; + getBoolean(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Boolean, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a user option */ + getUser(name: string | number, required: true): bigint; + getUser(name: string | number, required?: boolean): bigint | undefined; + getUser(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.User, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a channel option */ + getChannel(name: string | number, required: true): bigint; + getChannel(name: string | number, required?: boolean): bigint | undefined; + getChannel(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Channel, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a mentionable-based option */ + getMentionable(name: string | number, required: true): string; + getMentionable(name: string | number, required?: boolean): string | undefined; + getMentionable(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Mentionable, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a mentionable-based option */ + getRole(name: string | number, required: true): bigint; + getRole(name: string | number, required?: boolean): bigint | undefined; + getRole(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Role, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for an attachment option */ + getAttachment(name: string | number, required: true): string; + getAttachment(name: string | number, required?: boolean): string | undefined; + getAttachment(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Attachment, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for the focused option */ + getFocused(full = false) { + const focusedOption = this.hoistedOptions.find((option) => option.focused); + + if (!focusedOption) { + throw new TypeError('No option found'); + } + + return full ? focusedOption : focusedOption.Otherwise; + } + + getSubCommand(required = true) { + if (required && !this.#subcommand) { + throw new TypeError('Option marked as required was undefined'); + } + + return [this.#subcommand, this.hoistedOptions]; + } + + getSubCommandGroup(required = false) { + if (required && !this.#group) { + throw new TypeError('Option marked as required was undefined'); + } + + return [this.#group, this.hoistedOptions]; + } +} + +export default CommandInteractionOptionResolver; diff --git a/structures/interactions/ComponentInteraction.ts b/structures/interactions/ComponentInteraction.ts new file mode 100644 index 0000000..a85524f --- /dev/null +++ b/structures/interactions/ComponentInteraction.ts @@ -0,0 +1,45 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import { MessageComponentTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import Message from "../Message.ts"; + +export class ComponentInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.type = data.type as number; + this.componentType = data.data!.component_type!; + this.customId = data.data!.custom_id; + this.targetId = data.data!.target_id; + this.values = data.data!.values; + this.message = new Message(session, data.message!); + } + + override type: InteractionTypes.MessageComponent; + componentType: MessageComponentTypes; + customId?: string; + targetId?: Snowflake; + values?: string[]; + message: Message; + responded = false; + + isButton() { + return this.componentType === MessageComponentTypes.Button; + } + + isActionRow() { + return this.componentType === MessageComponentTypes.ActionRow; + } + + isTextInput() { + return this.componentType === MessageComponentTypes.InputText; + } + + isSelectMenu() { + return this.componentType === MessageComponentTypes.SelectMenu; + } +} + +export default ComponentInteraction; diff --git a/structures/interactions/Interaction.ts b/structures/interactions/Interaction.ts deleted file mode 100644 index 5ed2b94..0000000 --- a/structures/interactions/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/interactions/InteractionFactory.ts b/structures/interactions/InteractionFactory.ts new file mode 100644 index 0000000..1ed1f5a --- /dev/null +++ b/structures/interactions/InteractionFactory.ts @@ -0,0 +1,32 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordInteraction } from "../../vendor/external.ts"; +import { InteractionTypes } from "../../vendor/external.ts"; +import CommandInteraction from "./CommandInteraction.ts"; +import ComponentInteraction from "./ComponentInteraction.ts"; +import PingInteraction from "./PingInteraction.ts"; +import AutoCompleteInteraction from "./AutoCompleteInteraction.ts"; +import ModalSubmitInteraction from "./ModalSubmitInteraction.ts"; + +export type Interaction = + | CommandInteraction + | ComponentInteraction + | PingInteraction + | AutoCompleteInteraction + | ModalSubmitInteraction; + +export class InteractionFactory { + static from(session: Session, interaction: DiscordInteraction): Interaction { + switch (interaction.type) { + case InteractionTypes.Ping: + return new PingInteraction(session, interaction); + case InteractionTypes.ApplicationCommand: + return new CommandInteraction(session, interaction); + case InteractionTypes.MessageComponent: + return new ComponentInteraction(session, interaction); + case InteractionTypes.ApplicationCommandAutocomplete: + return new AutoCompleteInteraction(session, interaction); + case InteractionTypes.ModalSubmit: + return new ModalSubmitInteraction(session, interaction); + } + } +} diff --git a/structures/interactions/ModalSubmitInteraction.ts b/structures/interactions/ModalSubmitInteraction.ts new file mode 100644 index 0000000..ff14a18 --- /dev/null +++ b/structures/interactions/ModalSubmitInteraction.ts @@ -0,0 +1,50 @@ + +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordInteraction, InteractionTypes, MessageComponentTypes, DiscordMessageComponents } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import Message from "../Message.ts"; + +export class ModalSubmitInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.type = data.type as number; + this.componentType = data.data!.component_type!; + this.customId = data.data!.custom_id; + this.targetId = data.data!.target_id; + this.values = data.data!.values; + + this.components = data.data?.components?.map(ModalSubmitInteraction.transformComponent); + + if (data.message) { + this.message = new Message(session, data.message); + } + } + + override type: InteractionTypes.MessageComponent; + componentType: MessageComponentTypes; + customId?: string; + targetId?: Snowflake; + values?: string[]; + message?: Message; + components; + + static transformComponent(component: DiscordMessageComponents[number]) { + return { + type: component.type, + components: component.components.map((component) => { + return { + customId: component.custom_id, + value: (component as typeof component & { value: string }).value, + }; + }), + }; + } + + inMessage(): this is ModalSubmitInteraction & { message: Message } { + return !!this.message; + } +} + +export default ModalSubmitInteraction; diff --git a/structures/interactions/PingInteraction.ts b/structures/interactions/PingInteraction.ts new file mode 100644 index 0000000..438ac01 --- /dev/null +++ b/structures/interactions/PingInteraction.ts @@ -0,0 +1,38 @@ + +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ApplicationCommandTypes, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import { InteractionResponseTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import * as Routes from "../../util/Routes.ts"; + +export class PingInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.type = data.type as number; + this.commandId = data.data!.id; + this.commandName = data.data!.name; + this.commandType = data.data!.type; + this.commandGuildId = data.data!.guild_id; + } + + override type: InteractionTypes.Ping; + commandId: Snowflake; + commandName: string; + commandType: ApplicationCommandTypes; + commandGuildId?: Snowflake; + + async pong() { + await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.INTERACTION_ID_TOKEN(this.id, this.token), + { + type: InteractionResponseTypes.Pong, + } + ); + } +} + +export default PingInteraction; From 7faca2af688c98271dd50e01a94aa163f47d4a55 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Tue, 5 Jul 2022 14:30:04 -0500 Subject: [PATCH 34/45] fix: type errors --- handlers/Actions.ts | 3 ++- structures/interactions/AutoCompleteInteraction.ts | 2 +- structures/interactions/InteractionFactory.ts | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 371a37b..3589e88 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -30,6 +30,7 @@ import type { import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { Channel } from "../structures/channels/ChannelFactory.ts"; +import type { Interaction } from "../structures/interactions/InteractionFactory.ts"; import ChannelFactory from "../structures/channels/ChannelFactory.ts"; import GuildChannel from "../structures/channels/GuildChannel.ts"; import ThreadChannel from "../structures/channels/ThreadChannel.ts"; @@ -38,7 +39,7 @@ import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; import Guild from "../structures/guilds/Guild.ts"; -import InteractionFactory from "../structures/interactions/interactions/InteractionFactory.ts"; +import InteractionFactory from "../structures/interactions/InteractionFactory.ts"; export type RawHandler = (...args: [Session, number, T]) => void; export type Handler = (...args: T) => unknown; diff --git a/structures/interactions/AutoCompleteInteraction.ts b/structures/interactions/AutoCompleteInteraction.ts index 877d87e..063b407 100644 --- a/structures/interactions/AutoCompleteInteraction.ts +++ b/structures/interactions/AutoCompleteInteraction.ts @@ -3,7 +3,7 @@ import type { Model } from "../Base.ts"; import type { Snowflake } from "../../util/Snowflake.ts"; import type { Session } from "../../session/Session.ts"; import type { ApplicationCommandTypes, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; -import type { ApplicationCommandOptionChoice } from "./BaseInteraction.ts"; +import type { ApplicationCommandOptionChoice } from "./CommandInteraction.ts"; import { InteractionResponseTypes } from "../../vendor/external.ts"; import BaseInteraction from "./BaseInteraction.ts"; import * as Routes from "../../util/Routes.ts"; diff --git a/structures/interactions/InteractionFactory.ts b/structures/interactions/InteractionFactory.ts index 1ed1f5a..386d23d 100644 --- a/structures/interactions/InteractionFactory.ts +++ b/structures/interactions/InteractionFactory.ts @@ -30,3 +30,5 @@ export class InteractionFactory { } } } + +export default InteractionFactory; From fbe3a6402c8376f3e8a03d8ad308cada52a9fecd Mon Sep 17 00:00:00 2001 From: Yuzu Date: Tue, 5 Jul 2022 14:43:15 -0500 Subject: [PATCH 35/45] oops --- :x | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 :x diff --git a/:x b/:x deleted file mode 100644 index 43b5f29..0000000 --- a/:x +++ /dev/null @@ -1,36 +0,0 @@ - -import type { Model } from "../Base.ts"; -import type { Snowflake } from "../../util/Snowflake.ts"; -import type { Session } from "../../session/Session.ts"; -import type { ApplicationCommandTypes, DiscordInteraction } from "../../vendor/external.ts"; -import { InteractionResponseTypes } from "../../vendor/external.ts"; -import BaseInteraction from "./BaseInteraction.ts"; -import * as Routes from "../../util/Routes.ts"; - -export class PingInteraction extends BaseInteraction implements Model { - constructor(session: Session, data: DiscordInteraction) { - super(session, data); - this.commandId = data.data!.id; - this.commandName = data.data!.name; - this.commandType = data.data!.type; - this.commandGuildId = data.data!.guild_id; - } - - commandId: Snowflake; - commandName: string; - commandType: ApplicationCommandTypes; - commandGuildId?: Snowflake; - - async pong() { - await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.INTERACTION_ID_TOKEN(this.id, this.token), - { - type: InteractionResponseTypes.Pong, - } - ); - } -} - -export default PingInteraction; From 7203eb01ebef666d205e0d75c67f750ee13698d0 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Tue, 5 Jul 2022 20:26:21 -0500 Subject: [PATCH 36/45] wip: CommandInteraction.respond --- structures/Message.ts | 2 + structures/Webhook.ts | 62 ++++++++++++++++++- structures/interactions/CommandInteraction.ts | 48 +++++++++++++- util/Routes.ts | 23 +++++-- 4 files changed, 127 insertions(+), 8 deletions(-) diff --git a/structures/Message.ts b/structures/Message.ts index e193d73..52be7c5 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -48,6 +48,7 @@ export interface CreateMessage { allowedMentions?: AllowedMentions; files?: FileContent[]; messageReference?: CreateMessageReference; + tts?: boolean; } /** @@ -271,6 +272,7 @@ export class Message implements Model { } : undefined, embeds: options.embeds, + tts: options.tts, }, ); diff --git a/structures/Webhook.ts b/structures/Webhook.ts index 6a85a22..c17a7ff 100644 --- a/structures/Webhook.ts +++ b/structures/Webhook.ts @@ -1,9 +1,13 @@ 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 type { DiscordMessage, DiscordWebhook, WebhookTypes } from "../vendor/external.ts"; +import type { WebhookOptions } from "../util/Routes.ts"; +import type { CreateMessage } from "./Message.ts"; import { iconHashToBigInt } from "../util/hash.ts"; import User from "./User.ts"; +import Message from "./Message.ts"; +import * as Routes from "../util/Routes.ts"; export class Webhook implements Model { constructor(session: Session, data: DiscordWebhook) { @@ -42,6 +46,62 @@ export class Webhook implements Model { channelId?: Snowflake; guildId?: Snowflake; user?: User; + + async execute(options?: WebhookOptions & CreateMessage & { avatarUrl?: string, username?: string }) { + if (!this.token) { + return; + } + + const data = { + content: options?.content, + embeds: options?.embeds, + tts: options?.tts, + allowed_mentions: options?.allowedMentions, + // @ts-ignore: TODO: component builder or something + components: options?.components, + file: options?.files, + }; + + const message = await this.session.rest.sendRequest(this.session.rest, { + url: Routes.WEBHOOK(this.id, this.token!, { + wait: options?.wait, + threadId: options?.threadId, + }), + method: "POST", + payload: this.session.rest.createRequestBody(this.session.rest, { + method: "POST", + body: { + ...data, + }, + }), + }); + + return (options?.wait ?? true) ? new Message(this.session, message) : undefined; + } + + async fetch() { + const message = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.WEBHOOK_TOKEN(this.id, this.token), + ); + + return new Webhook(this.session, message); + } + + async fetchMessage(messageId: Snowflake) { + if (!this.token) { + return; + } + + const message = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.WEBHOOK_MESSAGE(this.id, this.token, messageId), + ); + + return new Message(this.session, message); + } } export default Webhook; diff --git a/structures/interactions/CommandInteraction.ts b/structures/interactions/CommandInteraction.ts index ef1294a..0d25e91 100644 --- a/structures/interactions/CommandInteraction.ts +++ b/structures/interactions/CommandInteraction.ts @@ -3,6 +3,7 @@ import type { Snowflake } from "../../util/Snowflake.ts"; import type { Session } from "../../session/Session.ts"; import type { ApplicationCommandTypes, DiscordMemberWithUser, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; import type { CreateMessage } from "../Message.ts"; +import type { MessageFlags } from "../../util/shared/flags.ts"; import { InteractionResponseTypes } from "../../vendor/external.ts"; import BaseInteraction from "./BaseInteraction.ts"; import CommandInteractionOptionResolver from "./CommandInteractionOptionResolver.ts"; @@ -11,6 +12,8 @@ import User from "../User.ts"; import Member from "../Member.ts"; import Message from "../Message.ts"; import Role from "../Role.ts"; +import Webhook from "../Webhook.ts"; +import * as Routes from "../../util/Routes.ts"; /** * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response @@ -23,12 +26,11 @@ export interface InteractionResponse { /** * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionapplicationcommandcallbackdata * */ -export interface InteractionApplicationCommandCallbackData extends Omit { +export interface InteractionApplicationCommandCallbackData extends Pick { customId?: string; title?: string; - // TODO: use builder // components?: MessageComponents; - flags?: number; + flags?: MessageFlags; choices?: ApplicationCommandOptionChoice[]; } @@ -103,6 +105,46 @@ export class CommandInteraction extends BaseInteraction implements Model { }; options: CommandInteractionOptionResolver; responded = false; + + async sendFollowUp(options: InteractionApplicationCommandCallbackData): Promise { + const message = await Webhook.prototype.execute.call({ + id: this.applicationId!, + token: this.token, + session: this.session, + }, options); + + return message!; + } + + async respond({ type, data: options }: InteractionResponse): Promise { + const data = { + content: options?.content, + custom_id: options?.customId, + file: options?.files, + allowed_mentions: options?.allowedMentions, + flags: options?.flags, + chocies: options?.choices, + embeds: options?.embeds, + title: options?.title, + }; + + if (!this.respond) { + 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, data, file: options?.files }, + headers: { "Authorization": "" }, + }), + }); + + this.responded = true; + return; + } + + return this.sendFollowUp(data); + } } export default CommandInteraction; diff --git a/util/Routes.ts b/util/Routes.ts index 146f944..51e74e4 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -144,11 +144,26 @@ export function INTERACTION_ID_TOKEN(interactionId: Snowflake, token: string) { return `/interactions/${interactionId}/${token}/callback`; } -export function WEBHOOK(webhookId: Snowflake, token: string, options?: { wait?: boolean; threadId?: Snowflake }) { - let url = `/webhooks/${webhookId}/${token}?`; +export function WEBHOOK_MESSAGE(webhookId: Snowflake, token: string, messageId: Snowflake) { + return `/webhooks/${webhookId}/${token}/messages/${messageId}`; +} - if (options?.wait !== undefined) url += `wait=${options.wait}`; - if (options?.threadId) url += `threadId=${options.threadId}`; +export function WEBHOOK_TOKEN(webhookId: Snowflake, token?: string) { + if (!token) return `/webhooks/${webhookId}`; + return `/webhooks/${webhookId}/${token}`; +} + +export interface WebhookOptions { + wait?: boolean; + threadId?: Snowflake; +} + +export function WEBHOOK(webhookId: Snowflake, token: string, options?: WebhookOptions) { + let url = `/webhooks/${webhookId}/${token}`; + + if (options?.wait) url += `?wait=${options.wait}`; + if (options?.threadId) url += `?threadId=${options.threadId}`; + if (options?.wait && options.threadId) url += `?wait=${options.wait}&threadId=${options.threadId}`; return url; } From ea91bd6230fbf52f9b6e7a8ddeae7055aa1f87ca Mon Sep 17 00:00:00 2001 From: Yuzu Date: Tue, 5 Jul 2022 20:47:20 -0500 Subject: [PATCH 37/45] hotfix: circular dependencies --- structures/Message.ts | 4 ++-- structures/channels/GuildChannel.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/structures/Message.ts b/structures/Message.ts index ecf94b0..3e42786 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -19,7 +19,7 @@ import Member from "./Member.ts"; import Attachment from "./Attachment.ts"; import ComponentFactory from "./components/ComponentFactory.ts"; import MessageReaction from "./MessageReaction.ts"; -import ThreadChannel from "./channels/ThreadChannel.ts"; +// import ThreadChannel from "./channels/ThreadChannel.ts"; import * as Routes from "../util/Routes.ts"; /** @@ -103,7 +103,7 @@ export class Message implements Model { this.embeds = data.embeds; if (data.thread && data.guild_id) { - this.thread = new ThreadChannel(session, data.thread, data.guild_id); + // this.thread = new ThreadChannel(session, data.thread, data.guild_id); } // webhook handling diff --git a/structures/channels/GuildChannel.ts b/structures/channels/GuildChannel.ts index 6dde8ff..4a3600a 100644 --- a/structures/channels/GuildChannel.ts +++ b/structures/channels/GuildChannel.ts @@ -9,7 +9,6 @@ import type { } from "../../vendor/external.ts"; import type { ListArchivedThreads } from "../../util/Routes.ts"; import BaseChannel from "./BaseChannel.ts"; -import ThreadChannel from "./ThreadChannel.ts"; import ThreadMember from "../ThreadMember.ts"; import Invite from "../Invite.ts"; import * as Routes from "../../util/Routes.ts"; @@ -64,6 +63,7 @@ export class GuildChannel extends BaseChannel implements Model { return invites.map((invite) => new Invite(this.session, invite)); } + /* async getArchivedThreads(options: ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }) { let func: (channelId: Snowflake, options: ListArchivedThreads) => string; @@ -110,7 +110,8 @@ export class GuildChannel extends BaseChannel implements Model { ); return new ThreadChannel(this.session, thread, thread.guild_id ?? this.guildId); - } + }*/ } + export default GuildChannel; From 3a5dbdfc7779ec7a0a5702a23949992d3233efab Mon Sep 17 00:00:00 2001 From: Yuzu Date: Wed, 6 Jul 2022 13:44:12 -0500 Subject: [PATCH 38/45] fix: dirty trick on prototypes --- handlers/Actions.ts | 20 +++++++++++++++++++- session/Session.ts | 2 +- structures/Message.ts | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index bc7c813..74961b4 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -59,6 +59,24 @@ export const MESSAGE_CREATE: RawHandler = (session, _shardId, me }; export const MESSAGE_UPDATE: RawHandler = (session, _shardId, new_message) => { + // message is partial + if (!new_message.edited_timestamp) { + const message = { + // TODO: improve this + // ...new_message, + id: new_message.id, + guildId: new_message.guild_id, + channelId: new_message.channel_id, + }; + + // all methods of Message can run on partial messages + // we aknowledge people that their callback could be partial but giving them all functions of Message + Object.setPrototypeOf(message, Message.prototype); + + session.emit("messageUpdate", message); + return; + } + session.emit("messageUpdate", new Message(session, new_message)); }; @@ -212,7 +230,7 @@ type MessageReaction = any; export interface Events { "ready": Handler<[Ready, number]>; "messageCreate": Handler<[Message]>; - "messageUpdate": Handler<[Message]>; + "messageUpdate": Handler<[Partial]>; "messageDelete": Handler<[{ id: Snowflake, channelId: Snowflake, guildId?: Snowflake }]>; "messageReactionAdd": Handler<[MessageReaction]>; "messageReactionRemove": Handler<[MessageReaction]>; diff --git a/session/Session.ts b/session/Session.ts index 50b9e61..cb82058 100644 --- a/session/Session.ts +++ b/session/Session.ts @@ -65,7 +65,7 @@ export class Session extends EventEmitter { const defHandler: DiscordRawEventHandler = (shard, data) => { Actions.raw(this, shard.id, data); - if (!data.t) { + if (!data.t || !data.d) { return; } diff --git a/structures/Message.ts b/structures/Message.ts index 3e42786..9c9dd6d 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -153,7 +153,7 @@ export class Message implements Model { attachments: Attachment[]; embeds: DiscordEmbed[]; member?: Member; - thread?: ThreadChannel; + // thread?: ThreadChannel; components: Component[]; webhook?: WebhookAuthor; From 5471fbb0e119e9688a85b5e1ee03e8212705b87c Mon Sep 17 00:00:00 2001 From: Yuzu Date: Wed, 6 Jul 2022 14:53:08 -0500 Subject: [PATCH 39/45] feat: ideas --- handlers/Actions.ts | 3 +- vendor/gateway/shard/deps.ts | 2 +- vendor/zlib.js | 1023 ++++++++++++++++++++++++++++++++++ 3 files changed, 1025 insertions(+), 3 deletions(-) create mode 100644 vendor/zlib.js diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 74961b4..e64c58d 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -64,6 +64,7 @@ export const MESSAGE_UPDATE: RawHandler = (session, _shardId, ne const message = { // TODO: improve this // ...new_message, + session, id: new_message.id, guildId: new_message.guild_id, channelId: new_message.channel_id, @@ -197,7 +198,6 @@ export const INTEGRATION_DELETE: RawHandler = (session session.emit("integrationDelete", { id: payload.id, guildId: payload.guild_id, applicationId: payload.application_id }); }; -/* export const MESSAGE_REACTION_ADD: RawHandler = (session, _shardId, reaction) => { session.emit("messageReactionAdd", null); }; @@ -213,7 +213,6 @@ export const MESSAGE_REACTION_REMOVE_ALL: RawHandler = (session, _shardId, reaction) => { session.emit("messageReactionRemoveEmoji", null); }; -*/ export const raw: RawHandler = (session, shardId, data) => { session.emit("raw", data, shardId); diff --git a/vendor/gateway/shard/deps.ts b/vendor/gateway/shard/deps.ts index 32a9eb3..af7e5c4 100644 --- a/vendor/gateway/shard/deps.ts +++ b/vendor/gateway/shard/deps.ts @@ -1 +1 @@ -export { decompress_with as decompressWith } from "https://unpkg.com/@evan/wasm@0.0.94/target/zlib/deno.js"; +export { deflate as decompressWith } from "../../zlib.js"; diff --git a/vendor/zlib.js b/vendor/zlib.js new file mode 100644 index 0000000..636af00 --- /dev/null +++ b/vendor/zlib.js @@ -0,0 +1,1023 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file +// This code was bundled using `deno bundle` and it's not recommended to edit it manually + +function calcAdler32(input) { + let s1 = 1; + let s2 = 0; + const inputLen = input.length; + for(let i = 0; i < inputLen; i++){ + s1 = (s1 + input[i]) % 65521; + s2 = (s1 + s2) % 65521; + } + return (s2 << 16) + s1; +} +const BTYPE = Object.freeze({ + UNCOMPRESSED: 0, + FIXED: 1, + DYNAMIC: 2 +}); +const BLOCK_MAX_BUFFER_LEN = 131072; +const LENGTH_EXTRA_BIT_LEN = [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 3, + 3, + 3, + 3, + 4, + 4, + 4, + 4, + 5, + 5, + 5, + 5, + 0, +]; +const LENGTH_EXTRA_BIT_BASE = [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 15, + 17, + 19, + 23, + 27, + 31, + 35, + 43, + 51, + 59, + 67, + 83, + 99, + 115, + 131, + 163, + 195, + 227, + 258, +]; +const DISTANCE_EXTRA_BIT_BASE = [ + 1, + 2, + 3, + 4, + 5, + 7, + 9, + 13, + 17, + 25, + 33, + 49, + 65, + 97, + 129, + 193, + 257, + 385, + 513, + 769, + 1025, + 1537, + 2049, + 3073, + 4097, + 6145, + 8193, + 12289, + 16385, + 24577, +]; +const DISTANCE_EXTRA_BIT_LEN = [ + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 2, + 3, + 3, + 4, + 4, + 5, + 5, + 6, + 6, + 7, + 7, + 8, + 8, + 9, + 9, + 10, + 10, + 11, + 11, + 12, + 12, + 13, + 13, +]; +const CODELEN_VALUES = [ + 16, + 17, + 18, + 0, + 8, + 7, + 9, + 6, + 10, + 5, + 11, + 4, + 12, + 3, + 13, + 2, + 14, + 1, + 15, +]; +function generateHuffmanTable(codelenValues) { + const codelens = Object.keys(codelenValues); + let codelen = 0; + let codelenMax = 0; + let codelenMin = Number.MAX_SAFE_INTEGER; + codelens.forEach((key)=>{ + codelen = Number(key); + if (codelenMax < codelen) codelenMax = codelen; + if (codelenMin > codelen) codelenMin = codelen; + }); + let code = 0; + let values; + const bitlenTables = {}; + for(let bitlen = codelenMin; bitlen <= codelenMax; bitlen++){ + values = codelenValues[bitlen]; + if (values === undefined) values = []; + values.sort((a, b)=>{ + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + const table = {}; + values.forEach((value)=>{ + table[code] = value; + code++; + }); + bitlenTables[bitlen] = table; + code <<= 1; + } + return bitlenTables; +} +function makeFixedHuffmanCodelenValues() { + const codelenValues = {}; + codelenValues[7] = []; + codelenValues[8] = []; + codelenValues[9] = []; + for(let i = 0; i <= 287; i++){ + i <= 143 ? codelenValues[8].push(i) : i <= 255 ? codelenValues[9].push(i) : i <= 279 ? codelenValues[7].push(i) : codelenValues[8].push(i); + } + return codelenValues; +} +function generateDeflateHuffmanTable(values, maxLength = 15) { + const valuesCount = {}; + for (const value of values){ + if (!valuesCount[value]) { + valuesCount[value] = 1; + } else { + valuesCount[value]++; + } + } + const valuesCountKeys = Object.keys(valuesCount); + let tmpPackages = []; + let tmpPackageIndex = 0; + let packages = []; + if (valuesCountKeys.length === 1) { + packages.push({ + count: valuesCount[0], + simbles: [ + Number(valuesCountKeys[0]) + ] + }); + } else { + for(let i = 0; i < maxLength; i++){ + packages = []; + valuesCountKeys.forEach((value)=>{ + const pack = { + count: valuesCount[Number(value)], + simbles: [ + Number(value) + ] + }; + packages.push(pack); + }); + tmpPackageIndex = 0; + while(tmpPackageIndex + 2 <= tmpPackages.length){ + const pack = { + count: tmpPackages[tmpPackageIndex].count + tmpPackages[tmpPackageIndex + 1].count, + simbles: tmpPackages[tmpPackageIndex].simbles.concat(tmpPackages[tmpPackageIndex + 1].simbles) + }; + packages.push(pack); + tmpPackageIndex += 2; + } + packages = packages.sort((a, b)=>{ + if (a.count < b.count) return -1; + if (a.count > b.count) return 1; + return 0; + }); + if (packages.length % 2 !== 0) { + packages.pop(); + } + tmpPackages = packages; + } + } + const valuesCodelen = {}; + packages.forEach((pack)=>{ + pack.simbles.forEach((symble)=>{ + if (!valuesCodelen[symble]) { + valuesCodelen[symble] = 1; + } else { + valuesCodelen[symble]++; + } + }); + }); + let group; + const valuesCodelenKeys = Object.keys(valuesCodelen); + const codelenGroup = {}; + let code = 0; + let codelen = 3; + let codelenValueMin = Number.MAX_SAFE_INTEGER; + let codelenValueMax = 0; + valuesCodelenKeys.forEach((valuesCodelenKey)=>{ + codelen = valuesCodelen[Number(valuesCodelenKey)]; + if (!codelenGroup[codelen]) { + codelenGroup[codelen] = []; + if (codelenValueMin > codelen) codelenValueMin = codelen; + if (codelenValueMax < codelen) codelenValueMax = codelen; + } + codelenGroup[codelen].push(Number(valuesCodelenKey)); + }); + code = 0; + const table = new Map(); + for(let i1 = codelenValueMin; i1 <= codelenValueMax; i1++){ + group = codelenGroup[i1]; + if (group) { + group = group.sort((a, b)=>{ + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + group.forEach((value)=>{ + table.set(value, { + code, + bitlen: i1 + }); + code++; + }); + } + code <<= 1; + } + return table; +} +function generateLZ77IndexMap(input, startIndex, targetLength) { + const end = startIndex + targetLength - 3; + const indexMap = {}; + for(let i = startIndex; i <= end; i++){ + const indexKey = input[i] << 16 | input[i + 1] << 8 | input[i + 2]; + if (indexMap[indexKey] === undefined) { + indexMap[indexKey] = []; + } + indexMap[indexKey].push(i); + } + return indexMap; +} +function generateLZ77Codes(input, startIndex, targetLength) { + let nowIndex = startIndex; + const endIndex = startIndex + targetLength - 3; + let slideIndexBase = 0; + let repeatLength = 0; + let repeatLengthMax = 0; + let repeatLengthMaxIndex = 0; + let distance = 0; + let repeatLengthCodeValue = 0; + let repeatDistanceCodeValue = 0; + const codeTargetValues = []; + const startIndexMap = {}; + const endIndexMap = {}; + const indexMap = generateLZ77IndexMap(input, startIndex, targetLength); + while(nowIndex <= endIndex){ + const indexKey = input[nowIndex] << 16 | input[nowIndex + 1] << 8 | input[nowIndex + 2]; + const indexes = indexMap[indexKey]; + if (indexes === undefined || indexes.length <= 1) { + codeTargetValues.push([ + input[nowIndex] + ]); + nowIndex++; + continue; + } + slideIndexBase = nowIndex > 0x8000 ? nowIndex - 0x8000 : 0; + repeatLengthMax = 0; + repeatLengthMaxIndex = 0; + let skipindexes = startIndexMap[indexKey] || 0; + while(indexes[skipindexes] < slideIndexBase){ + skipindexes = skipindexes + 1 | 0; + } + startIndexMap[indexKey] = skipindexes; + skipindexes = endIndexMap[indexKey] || 0; + while(indexes[skipindexes] < nowIndex){ + skipindexes = skipindexes + 1 | 0; + } + endIndexMap[indexKey] = skipindexes; + let checkCount = 0; + indexMapLoop: for(let i = endIndexMap[indexKey] - 1, iMin = startIndexMap[indexKey]; iMin <= i; i--){ + if (checkCount >= 128 || repeatLengthMax >= 8 && checkCount >= 16) { + break; + } + checkCount++; + const index = indexes[i]; + for(let j = repeatLengthMax - 1; 0 < j; j--){ + if (input[index + j] !== input[nowIndex + j]) { + continue indexMapLoop; + } + } + repeatLength = 258; + for(let j1 = repeatLengthMax; j1 <= 258; j1++){ + if (input[index + j1] !== input[nowIndex + j1]) { + repeatLength = j1; + break; + } + } + if (repeatLengthMax < repeatLength) { + repeatLengthMax = repeatLength; + repeatLengthMaxIndex = index; + if (258 <= repeatLength) { + break; + } + } + } + if (repeatLengthMax >= 3 && nowIndex + repeatLengthMax <= endIndex) { + distance = nowIndex - repeatLengthMaxIndex; + for(let i1 = 0; i1 < LENGTH_EXTRA_BIT_BASE.length; i1++){ + if (LENGTH_EXTRA_BIT_BASE[i1] > repeatLengthMax) { + break; + } + repeatLengthCodeValue = i1; + } + for(let i2 = 0; i2 < DISTANCE_EXTRA_BIT_BASE.length; i2++){ + if (DISTANCE_EXTRA_BIT_BASE[i2] > distance) { + break; + } + repeatDistanceCodeValue = i2; + } + codeTargetValues.push([ + repeatLengthCodeValue, + repeatDistanceCodeValue, + repeatLengthMax, + distance, + ]); + nowIndex += repeatLengthMax; + } else { + codeTargetValues.push([ + input[nowIndex] + ]); + nowIndex++; + } + } + codeTargetValues.push([ + input[nowIndex] + ]); + codeTargetValues.push([ + input[nowIndex + 1] + ]); + return codeTargetValues; +} +class BitWriteStream { + buffer; + bufferIndex; + nowBits; + nowBitsIndex = 0; + isEnd = false; + constructor(buffer, bufferOffset = 0, bitsOffset = 0){ + this.buffer = buffer; + this.bufferIndex = bufferOffset; + this.nowBits = buffer[bufferOffset]; + this.nowBitsIndex = bitsOffset; + } + write(bit) { + if (this.isEnd) throw new Error("Lack of data length"); + bit <<= this.nowBitsIndex; + this.nowBits += bit; + this.nowBitsIndex++; + if (this.nowBitsIndex >= 8) { + this.buffer[this.bufferIndex] = this.nowBits; + this.bufferIndex++; + this.nowBits = 0; + this.nowBitsIndex = 0; + if (this.buffer.length <= this.bufferIndex) { + this.isEnd = true; + } + } + } + writeRange(value, length) { + let mask = 1; + let bit = 0; + for(let i = 0; i < length; i++){ + bit = value & mask ? 1 : 0; + this.write(bit); + mask <<= 1; + } + } + writeRangeCoded(value, length) { + let mask = 1 << length - 1; + let bit = 0; + for(let i = 0; i < length; i++){ + bit = value & mask ? 1 : 0; + this.write(bit); + mask >>>= 1; + } + } +} +function deflate(input) { + const inputLength = input.length; + const streamHeap = inputLength < 131072 / 2 ? 131072 : inputLength * 2; + const stream = new BitWriteStream(new Uint8Array(streamHeap)); + let processedLength = 0; + let targetLength = 0; + while(true){ + if (processedLength + 131072 >= inputLength) { + targetLength = inputLength - processedLength; + stream.writeRange(1, 1); + } else { + targetLength = BLOCK_MAX_BUFFER_LEN; + stream.writeRange(0, 1); + } + stream.writeRange(BTYPE.DYNAMIC, 2); + deflateDynamicBlock(stream, input, processedLength, targetLength); + processedLength += BLOCK_MAX_BUFFER_LEN; + if (processedLength >= inputLength) { + break; + } + } + if (stream.nowBitsIndex !== 0) { + stream.writeRange(0, 8 - stream.nowBitsIndex); + } + return stream.buffer.subarray(0, stream.bufferIndex); +} +function deflateDynamicBlock(stream, input, startIndex, targetLength) { + const lz77Codes = generateLZ77Codes(input, startIndex, targetLength); + const clCodeValues = [ + 256 + ]; + const distanceCodeValues = []; + let clCodeValueMax = 256; + let distanceCodeValueMax = 0; + for(let i = 0, iMax = lz77Codes.length; i < iMax; i++){ + const values = lz77Codes[i]; + let cl = values[0]; + const distance = values[1]; + if (distance !== undefined) { + cl += 257; + distanceCodeValues.push(distance); + if (distanceCodeValueMax < distance) { + distanceCodeValueMax = distance; + } + } + clCodeValues.push(cl); + if (clCodeValueMax < cl) { + clCodeValueMax = cl; + } + } + const dataHuffmanTables = generateDeflateHuffmanTable(clCodeValues); + const distanceHuffmanTables = generateDeflateHuffmanTable(distanceCodeValues); + const codelens = []; + for(let i1 = 0; i1 <= clCodeValueMax; i1++){ + if (dataHuffmanTables.has(i1)) { + codelens.push(dataHuffmanTables.get(i1).bitlen); + } else { + codelens.push(0); + } + } + const HLIT = codelens.length; + for(let i2 = 0; i2 <= distanceCodeValueMax; i2++){ + if (distanceHuffmanTables.has(i2)) { + codelens.push(distanceHuffmanTables.get(i2).bitlen); + } else { + codelens.push(0); + } + } + const HDIST = codelens.length - HLIT; + const runLengthCodes = []; + const runLengthRepeatCount = []; + let codelen = 0; + let repeatLength = 0; + for(let i3 = 0; i3 < codelens.length; i3++){ + codelen = codelens[i3]; + repeatLength = 1; + while(codelen === codelens[i3 + 1]){ + repeatLength++; + i3++; + if (codelen === 0) { + if (138 <= repeatLength) { + break; + } + } else { + if (6 <= repeatLength) { + break; + } + } + } + if (4 <= repeatLength) { + if (codelen === 0) { + if (11 <= repeatLength) { + runLengthCodes.push(18); + } else { + runLengthCodes.push(17); + } + } else { + runLengthCodes.push(codelen); + runLengthRepeatCount.push(1); + repeatLength--; + runLengthCodes.push(16); + } + runLengthRepeatCount.push(repeatLength); + } else { + for(let j = 0; j < repeatLength; j++){ + runLengthCodes.push(codelen); + runLengthRepeatCount.push(1); + } + } + } + const codelenHuffmanTable = generateDeflateHuffmanTable(runLengthCodes, 7); + let HCLEN = 0; + CODELEN_VALUES.forEach((value, index)=>{ + if (codelenHuffmanTable.has(value)) { + HCLEN = index + 1; + } + }); + stream.writeRange(HLIT - 257, 5); + stream.writeRange(HDIST - 1, 5); + stream.writeRange(HCLEN - 4, 4); + let codelenTableObj; + for(let i4 = 0; i4 < HCLEN; i4++){ + codelenTableObj = codelenHuffmanTable.get(CODELEN_VALUES[i4]); + if (codelenTableObj !== undefined) { + stream.writeRange(codelenTableObj.bitlen, 3); + } else { + stream.writeRange(0, 3); + } + } + runLengthCodes.forEach((value, index)=>{ + codelenTableObj = codelenHuffmanTable.get(value); + if (codelenTableObj !== undefined) { + stream.writeRangeCoded(codelenTableObj.code, codelenTableObj.bitlen); + } else { + throw new Error("Data is corrupted"); + } + if (value === 18) { + stream.writeRange(runLengthRepeatCount[index] - 11, 7); + } else if (value === 17) { + stream.writeRange(runLengthRepeatCount[index] - 3, 3); + } else if (value === 16) { + stream.writeRange(runLengthRepeatCount[index] - 3, 2); + } + }); + for(let i5 = 0, iMax1 = lz77Codes.length; i5 < iMax1; i5++){ + const values1 = lz77Codes[i5]; + const clCodeValue = values1[0]; + const distanceCodeValue = values1[1]; + if (distanceCodeValue !== undefined) { + codelenTableObj = dataHuffmanTables.get(clCodeValue + 257); + if (codelenTableObj === undefined) { + throw new Error("Data is corrupted"); + } + stream.writeRangeCoded(codelenTableObj.code, codelenTableObj.bitlen); + if (0 < LENGTH_EXTRA_BIT_LEN[clCodeValue]) { + repeatLength = values1[2]; + stream.writeRange(repeatLength - LENGTH_EXTRA_BIT_BASE[clCodeValue], LENGTH_EXTRA_BIT_LEN[clCodeValue]); + } + const distanceTableObj = distanceHuffmanTables.get(distanceCodeValue); + if (distanceTableObj === undefined) { + throw new Error("Data is corrupted"); + } + stream.writeRangeCoded(distanceTableObj.code, distanceTableObj.bitlen); + if (0 < DISTANCE_EXTRA_BIT_LEN[distanceCodeValue]) { + const distance1 = values1[3]; + stream.writeRange(distance1 - DISTANCE_EXTRA_BIT_BASE[distanceCodeValue], DISTANCE_EXTRA_BIT_LEN[distanceCodeValue]); + } + } else { + codelenTableObj = dataHuffmanTables.get(clCodeValue); + if (codelenTableObj === undefined) { + throw new Error("Data is corrupted"); + } + stream.writeRangeCoded(codelenTableObj.code, codelenTableObj.bitlen); + } + } + codelenTableObj = dataHuffmanTables.get(256); + if (codelenTableObj === undefined) { + throw new Error("Data is corrupted"); + } + stream.writeRangeCoded(codelenTableObj.code, codelenTableObj.bitlen); +} +class BitReadStream { + buffer; + bufferIndex; + nowBits; + nowBitsLength = 0; + isEnd = false; + constructor(buffer, offset = 0){ + this.buffer = buffer; + this.bufferIndex = offset; + this.nowBits = buffer[offset]; + this.nowBitsLength = 8; + } + read() { + if (this.isEnd) throw new Error("Lack of data length"); + const bit = this.nowBits & 1; + if (this.nowBitsLength > 1) { + this.nowBitsLength--; + this.nowBits >>= 1; + } else { + this.bufferIndex++; + if (this.bufferIndex < this.buffer.length) { + this.nowBits = this.buffer[this.bufferIndex]; + this.nowBitsLength = 8; + } else { + this.nowBitsLength = 0; + this.isEnd = true; + } + } + return bit; + } + readRange(length) { + while(this.nowBitsLength <= length){ + this.nowBits |= this.buffer[++this.bufferIndex] << this.nowBitsLength; + this.nowBitsLength += 8; + } + const bits = this.nowBits & (1 << length) - 1; + this.nowBits >>>= length; + this.nowBitsLength -= length; + return bits; + } + readRangeCoded(length) { + let bits = 0; + for(let i = 0; i < length; i++){ + bits <<= 1; + bits |= this.read(); + } + return bits; + } +} +class Uint8WriteStream { + index = 0; + buffer; + length; + _extendedSize; + constructor(extendedSize){ + this.buffer = new Uint8Array(extendedSize); + this.length = extendedSize; + this._extendedSize = extendedSize; + } + write(value) { + if (this.length <= this.index) { + this.length += this._extendedSize; + const newBuffer = new Uint8Array(this.length); + const nowSize = this.buffer.length; + for(let i = 0; i < nowSize; i++){ + newBuffer[i] = this.buffer[i]; + } + this.buffer = newBuffer; + } + this.buffer[this.index] = value; + this.index++; + } +} +const FIXED_HUFFMAN_TABLE = generateHuffmanTable(makeFixedHuffmanCodelenValues()); +function inflate(input, offset = 0) { + const buffer = new Uint8WriteStream(input.length * 10); + const stream = new BitReadStream(input, offset); + let bFinal = 0; + let bType = 0; + while(bFinal !== 1){ + bFinal = stream.readRange(1); + bType = stream.readRange(2); + if (bType === BTYPE.UNCOMPRESSED) { + inflateUncompressedBlock(stream, buffer); + } else if (bType === BTYPE.FIXED) { + inflateFixedBlock(stream, buffer); + } else if (bType === BTYPE.DYNAMIC) { + inflateDynamicBlock(stream, buffer); + } else { + throw new Error("Not supported BTYPE : " + bType); + } + if (bFinal === 0 && stream.isEnd) { + throw new Error("Data length is insufficient"); + } + } + return buffer.buffer.subarray(0, buffer.index); +} +function inflateUncompressedBlock(stream, buffer) { + if (stream.nowBitsLength < 8) { + stream.readRange(stream.nowBitsLength); + } + const LEN = stream.readRange(8) | stream.readRange(8) << 8; + const NLEN = stream.readRange(8) | stream.readRange(8) << 8; + if (LEN + NLEN !== 65535) { + throw new Error("Data is corrupted"); + } + for(let i = 0; i < LEN; i++){ + buffer.write(stream.readRange(8)); + } +} +function inflateFixedBlock(stream, buffer) { + const tables = FIXED_HUFFMAN_TABLE; + const codelens = Object.keys(tables); + let codelen = 0; + let codelenMax = 0; + let codelenMin = Number.MAX_SAFE_INTEGER; + codelens.forEach((key)=>{ + codelen = Number(key); + if (codelenMax < codelen) codelenMax = codelen; + if (codelenMin > codelen) codelenMin = codelen; + }); + let code = 0; + let value; + let repeatLengthCode; + let repeatLengthValue; + let repeatLengthExt; + let repeatDistanceCode; + let repeatDistanceValue; + let repeatDistanceExt; + let repeatStartIndex; + while(!stream.isEnd){ + value = undefined; + codelen = codelenMin; + code = stream.readRangeCoded(codelenMin); + while(true){ + value = tables[codelen][code]; + if (value !== undefined) { + break; + } + if (codelenMax <= codelen) { + throw new Error("Data is corrupted"); + } + codelen++; + code <<= 1; + code |= stream.read(); + } + if (value < 256) { + buffer.write(value); + continue; + } + if (value === 256) { + break; + } + repeatLengthCode = value - 257; + repeatLengthValue = LENGTH_EXTRA_BIT_BASE[repeatLengthCode]; + repeatLengthExt = LENGTH_EXTRA_BIT_LEN[repeatLengthCode]; + if (0 < repeatLengthExt) { + repeatLengthValue += stream.readRange(repeatLengthExt); + } + repeatDistanceCode = stream.readRangeCoded(5); + repeatDistanceValue = DISTANCE_EXTRA_BIT_BASE[repeatDistanceCode]; + repeatDistanceExt = DISTANCE_EXTRA_BIT_LEN[repeatDistanceCode]; + if (0 < repeatDistanceExt) { + repeatDistanceValue += stream.readRange(repeatDistanceExt); + } + repeatStartIndex = buffer.index - repeatDistanceValue; + for(let i = 0; i < repeatLengthValue; i++){ + buffer.write(buffer.buffer[repeatStartIndex + i]); + } + } +} +function inflateDynamicBlock(stream, buffer) { + const HLIT = stream.readRange(5) + 257; + const HDIST = stream.readRange(5) + 1; + const HCLEN = stream.readRange(4) + 4; + let codelenCodelen = 0; + const codelenCodelenValues = {}; + for(let i = 0; i < HCLEN; i++){ + codelenCodelen = stream.readRange(3); + if (codelenCodelen === 0) { + continue; + } + if (!codelenCodelenValues[codelenCodelen]) { + codelenCodelenValues[codelenCodelen] = []; + } + codelenCodelenValues[codelenCodelen].push(CODELEN_VALUES[i]); + } + const codelenHuffmanTables = generateHuffmanTable(codelenCodelenValues); + const codelenCodelens = Object.keys(codelenHuffmanTables); + let codelenCodelenMax = 0; + let codelenCodelenMin = Number.MAX_SAFE_INTEGER; + codelenCodelens.forEach((key)=>{ + codelenCodelen = Number(key); + if (codelenCodelenMax < codelenCodelen) codelenCodelenMax = codelenCodelen; + if (codelenCodelenMin > codelenCodelen) codelenCodelenMin = codelenCodelen; + }); + const dataCodelenValues = {}; + const distanceCodelenValues = {}; + let codelenCode = 0; + let runlengthCode; + let repeat = 0; + let codelen = 0; + const codesNumber = HLIT + HDIST; + for(let i1 = 0; i1 < codesNumber;){ + runlengthCode = undefined; + codelenCodelen = codelenCodelenMin; + codelenCode = stream.readRangeCoded(codelenCodelenMin); + while(true){ + runlengthCode = codelenHuffmanTables[codelenCodelen][codelenCode]; + if (runlengthCode !== undefined) { + break; + } + if (codelenCodelenMax <= codelenCodelen) { + throw new Error("Data is corrupted"); + } + codelenCodelen++; + codelenCode <<= 1; + codelenCode |= stream.read(); + } + if (runlengthCode === 16) { + repeat = 3 + stream.readRange(2); + } else if (runlengthCode === 17) { + repeat = 3 + stream.readRange(3); + codelen = 0; + } else if (runlengthCode === 18) { + repeat = 11 + stream.readRange(7); + codelen = 0; + } else { + repeat = 1; + codelen = runlengthCode; + } + if (codelen <= 0) { + i1 += repeat; + } else { + while(repeat){ + if (i1 < HLIT) { + if (!dataCodelenValues[codelen]) { + dataCodelenValues[codelen] = []; + } + dataCodelenValues[codelen].push(i1++); + } else { + if (!distanceCodelenValues[codelen]) { + distanceCodelenValues[codelen] = []; + } + distanceCodelenValues[codelen].push((i1++) - HLIT); + } + repeat--; + } + } + } + const dataHuffmanTables = generateHuffmanTable(dataCodelenValues); + const distanceHuffmanTables = generateHuffmanTable(distanceCodelenValues); + const dataCodelens = Object.keys(dataHuffmanTables); + let dataCodelen = 0; + let dataCodelenMax = 0; + let dataCodelenMin = Number.MAX_SAFE_INTEGER; + dataCodelens.forEach((key)=>{ + dataCodelen = Number(key); + if (dataCodelenMax < dataCodelen) dataCodelenMax = dataCodelen; + if (dataCodelenMin > dataCodelen) dataCodelenMin = dataCodelen; + }); + const distanceCodelens = Object.keys(distanceHuffmanTables); + let distanceCodelen = 0; + let distanceCodelenMax = 0; + let distanceCodelenMin = Number.MAX_SAFE_INTEGER; + distanceCodelens.forEach((key)=>{ + distanceCodelen = Number(key); + if (distanceCodelenMax < distanceCodelen) { + distanceCodelenMax = distanceCodelen; + } + if (distanceCodelenMin > distanceCodelen) { + distanceCodelenMin = distanceCodelen; + } + }); + let dataCode = 0; + let data; + let repeatLengthCode; + let repeatLengthValue; + let repeatLengthExt; + let repeatDistanceCode; + let repeatDistanceValue; + let repeatDistanceExt; + let repeatDistanceCodeCodelen; + let repeatDistanceCodeCode; + let repeatStartIndex; + while(!stream.isEnd){ + data = undefined; + dataCodelen = dataCodelenMin; + dataCode = stream.readRangeCoded(dataCodelenMin); + while(true){ + data = dataHuffmanTables[dataCodelen][dataCode]; + if (data !== undefined) { + break; + } + if (dataCodelenMax <= dataCodelen) { + throw new Error("Data is corrupted"); + } + dataCodelen++; + dataCode <<= 1; + dataCode |= stream.read(); + } + if (data < 256) { + buffer.write(data); + continue; + } + if (data === 256) { + break; + } + repeatLengthCode = data - 257; + repeatLengthValue = LENGTH_EXTRA_BIT_BASE[repeatLengthCode]; + repeatLengthExt = LENGTH_EXTRA_BIT_LEN[repeatLengthCode]; + if (0 < repeatLengthExt) { + repeatLengthValue += stream.readRange(repeatLengthExt); + } + repeatDistanceCode = undefined; + repeatDistanceCodeCodelen = distanceCodelenMin; + repeatDistanceCodeCode = stream.readRangeCoded(distanceCodelenMin); + while(true){ + repeatDistanceCode = distanceHuffmanTables[repeatDistanceCodeCodelen][repeatDistanceCodeCode]; + if (repeatDistanceCode !== undefined) { + break; + } + if (distanceCodelenMax <= repeatDistanceCodeCodelen) { + throw new Error("Data is corrupted"); + } + repeatDistanceCodeCodelen++; + repeatDistanceCodeCode <<= 1; + repeatDistanceCodeCode |= stream.read(); + } + repeatDistanceValue = DISTANCE_EXTRA_BIT_BASE[repeatDistanceCode]; + repeatDistanceExt = DISTANCE_EXTRA_BIT_LEN[repeatDistanceCode]; + if (0 < repeatDistanceExt) { + repeatDistanceValue += stream.readRange(repeatDistanceExt); + } + repeatStartIndex = buffer.index - repeatDistanceValue; + for(let i2 = 0; i2 < repeatLengthValue; i2++){ + buffer.write(buffer.buffer[repeatStartIndex + i2]); + } + } +} +function inflate1(input) { + const stream = new BitReadStream(input); + const CM = stream.readRange(4); + if (CM !== 8) { + throw new Error("Not compressed by deflate"); + } + stream.readRange(4); + stream.readRange(5); + stream.readRange(1); + stream.readRange(2); + return inflate(input, 2); +} +function deflate1(input) { + const data = deflate(input); + const CMF = new BitWriteStream(new Uint8Array(1)); + CMF.writeRange(8, 4); + CMF.writeRange(7, 4); + const FLG = new BitWriteStream(new Uint8Array(1)); + FLG.writeRange(28, 5); + FLG.writeRange(0, 1); + FLG.writeRange(2, 2); + const ADLER32 = new BitWriteStream(new Uint8Array(4)); + const adler32 = calcAdler32(input); + ADLER32.writeRange(adler32 >>> 24, 8); + ADLER32.writeRange(adler32 >>> 16 & 0xff, 8); + ADLER32.writeRange(adler32 >>> 8 & 0xff, 8); + ADLER32.writeRange(adler32 & 0xff, 8); + const output = new Uint8Array(data.length + 6); + output.set(CMF.buffer); + output.set(FLG.buffer, 1); + output.set(data, 2); + output.set(ADLER32.buffer, output.length - 4); + return output; +} +export { inflate1 as inflate }; +export { deflate1 as deflate }; From 54b0cb2326ccfa78ff7b8cc6638d82b573aadf28 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Wed, 6 Jul 2022 20:02:06 -0500 Subject: [PATCH 40/45] fix: wrong function call --- vendor/gateway/shard/deps.ts | 2 +- vendor/gateway/shard/handleMessage.ts | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/vendor/gateway/shard/deps.ts b/vendor/gateway/shard/deps.ts index af7e5c4..e2cb039 100644 --- a/vendor/gateway/shard/deps.ts +++ b/vendor/gateway/shard/deps.ts @@ -1 +1 @@ -export { deflate as decompressWith } from "../../zlib.js"; +export { inflate as decompressWith } from "../../zlib.js"; diff --git a/vendor/gateway/shard/handleMessage.ts b/vendor/gateway/shard/handleMessage.ts index 1bb3e89..cae9839 100644 --- a/vendor/gateway/shard/handleMessage.ts +++ b/vendor/gateway/shard/handleMessage.ts @@ -7,17 +7,14 @@ import { GATEWAY_RATE_LIMIT_RESET_INTERVAL, Shard, ShardState } from "./types.ts const decoder = new TextDecoder(); -export async function handleMessage(shard: Shard, message: MessageEvent): Promise { - message = message.data; +export async function handleMessage(shard: Shard, message_: MessageEvent): Promise { + let message = message_.data; // If message compression is enabled, // Discord might send zlib compressed payloads. if (shard.gatewayConfig.compress && message instanceof Blob) { - message = decompressWith( - new Uint8Array(await message.arrayBuffer()), - 0, - (slice: Uint8Array) => decoder.decode(slice), - ); + message = decoder.decode(decompressWith(new Uint8Array(await message.arrayBuffer()))); + console.log(message); } // Safeguard incase decompression failed to make a string. From 040aec95178f0885274830faceba09221b3165a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Thu, 7 Jul 2022 22:12:37 -0300 Subject: [PATCH 41/45] Add Channels.ts monofile --- structures/channels/Channels.ts | 625 ++++++++++++++++++++++++++++++++ 1 file changed, 625 insertions(+) create mode 100644 structures/channels/Channels.ts diff --git a/structures/channels/Channels.ts b/structures/channels/Channels.ts new file mode 100644 index 0000000..2b2cccc --- /dev/null +++ b/structures/channels/Channels.ts @@ -0,0 +1,625 @@ +/** Types */ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; + +/** External from vendor */ +import { + DiscordChannel, + VideoQualityModes, + ChannelTypes, + GatewayOpcodes, + DiscordInvite, + DiscordMessage, + DiscordWebhook, + TargetTypes, + DiscordInviteMetadata, +DiscordThreadMember +} from "../../vendor/external.ts"; + +/** Functions and others */ +import { calculateShardId } from "../../vendor/gateway/calculateShardId.ts"; +import { urlToBase64 } from "../../util/urlToBase64.ts"; + +/** Classes and routes */ +import * as Routes from "../../util/Routes.ts"; +import Message, { CreateMessage, EditMessage, ReactionResolvable } from "../Message.ts"; +import Invite from "../Invite.ts"; +import Webhook from "../Webhook.ts"; +import User from "../User.ts"; +import ThreadMember from "../ThreadMember.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; + } + + isStage(): this is StageChannel { + return this.type === ChannelTypes.GuildStageVoice; + } + + toString(): string { + return `<#${this.id}>`; + } +} + +/** TextChannel */ +/** + * 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.GuildVoice, + ChannelTypes.GuildText, +]; + +export type TextBasedChannels = + | ChannelTypes.DM + | ChannelTypes.GroupDm + | ChannelTypes.GuildPrivateThread + | ChannelTypes.GuildPublicThread + | ChannelTypes.GuildNews + | ChannelTypes.GuildVoice + | 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 + */ + // deno-lint-ignore ban-types + static applyTo(klass: Function, ignore: Array = []) { + const methods: Array = [ + "fetchPins", + "createInvite", + "fetchMessages", + "sendTyping", + "pinMessage", + "unpinMessage", + "addReaction", + "removeReaction", + "nukeReactions", + "fetchPins", + "sendMessage", + "editMessage", + "createWebhook", + ]; + + for (const method of methods) { + if (ignore.includes(method)) continue; + + klass.prototype[method] = TextChannel.prototype[method]; + } + } + + 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?: Routes.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?: Routes.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); + } +} + +/** GuildChannel */ +/** + * 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; + rateLimitPerUser?: number; + reason?: string; +} + +/** + * Represents the option object to create a thread channel from a message + * @link https://discord.com/developers/docs/resources/channel#start-thread-from-message + */ +export interface ThreadCreateOptions { + name: string; + autoArchiveDuration?: 60 | 1440 | 4320 | 10080; + rateLimitPerUser?: number; + messageId: Snowflake; +} + +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 getArchivedThreads(options: ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }) { + let func: (channelId: Snowflake, options: ListArchivedThreads) => string; + + switch (options.type) { + case "public": + func = Routes.THREAD_ARCHIVED_PUBLIC; + break; + case "private": + func = Routes.THREAD_START_PRIVATE; + break; + case "privateJoinedThreads": + func = Routes.THREAD_ARCHIVED_PRIVATE_JOINED; + break; + } + + const { threads, members, has_more } = await this.session.rest.runMethod( + this.session.rest, + "GET", + func(this.id, options), + ); + + return { + threads: Object.fromEntries( + threads.map((thread) => [thread.id, new ThreadChannel(this.session, thread, this.id)]), + ) as Record, + members: Object.fromEntries( + members.map((threadMember) => [threadMember.id, new ThreadMember(this.session, threadMember)]), + ) as Record, + hasMore: has_more, + }; + } + + async createThread(options: ThreadCreateOptions): Promise { + const thread = await this.session.rest.runMethod( + this.session.rest, + "POST", + "messageId" in options + ? Routes.THREAD_START_PUBLIC(this.id, options.messageId) + : Routes.THREAD_START_PRIVATE(this.id), + { + name: options.name, + auto_archive_duration: options.autoArchiveDuration, + }, + ); + + return new ThreadChannel(this.session, thread, thread.guild_id ?? this.guildId); + }*/ +} + +/** BaseVoiceChannel */ +/** + * @link https://discord.com/developers/docs/topics/gateway#update-voice-state + */ + export interface UpdateVoiceState { + guildId: string; + channelId?: string; + selfMute: boolean; + selfDeaf: boolean; +} + +export abstract class BaseVoiceChannel 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; + this.type = data.type as number; + + if (data.rtc_region) { + this.rtcRegion = data.rtc_region; + } + } + override type: ChannelTypes.GuildVoice | ChannelTypes.GuildStageVoice; + 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, + }, + }); + } +} + +/** DMChannel */ +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 {} + +/** VoiceChannel */ +export class VoiceChannel extends BaseVoiceChannel { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + this.type = data.type as number; + } + override type: ChannelTypes.GuildVoice; +} + +export interface VoiceChannel extends TextChannel, BaseVoiceChannel {} + +TextChannel.applyTo(VoiceChannel); + +/** NewsChannel */ +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 {} + +/** StageChannel */ +export class StageChannel extends BaseVoiceChannel { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + this.type = data.type as number; + this.topic = data.topic ? data.topic : undefined; + } + override type: ChannelTypes.GuildStageVoice; + topic?: string; +} + +/** ThreadChannel */ +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; + + if (data.member) { + this.member = new ThreadMember(session, data.member); + } + } + + override type: ChannelTypes.GuildNewsThread | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread; + archived?: boolean; + archiveTimestamp?: string; + autoArchiveDuration?: number; + locked?: boolean; + messageCount?: number; + memberCount?: number; + member?: ThreadMember; + ownerId?: Snowflake; + + async joinThread() { + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.THREAD_ME(this.id), + ); + } + + async addToThread(guildMemberId: Snowflake) { + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.THREAD_USER(this.id, guildMemberId), + ); + } + + async leaveToThread(guildMemberId: Snowflake) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.THREAD_USER(this.id, guildMemberId), + ); + } + + removeMember(memberId: Snowflake = this.session.botId) { + return ThreadMember.prototype.quitThread.call({ id: this.id, session: this.session }, memberId); + } + + fetchMember(memberId: Snowflake = this.session.botId) { + return ThreadMember.prototype.fetchMember.call({ id: this.id, session: this.session }, memberId); + } + + async fetchMembers(): Promise { + const members = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.THREAD_MEMBERS(this.id), + ); + + return members.map((threadMember) => new ThreadMember(this.session, threadMember)); + } +} + +TextChannel.applyTo(ThreadChannel); + +export interface ThreadChannel extends Omit, Omit {} + +/** ChannelFactory */ +export type Channel = + | TextChannel + | VoiceChannel + | DMChannel + | NewsChannel + | ThreadChannel + | StageChannel; + +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!); + case ChannelTypes.GuildStageVoice: + return new StageChannel(session, channel, channel.guild_id!); + default: + if (textBasedChannels.includes(channel.type)) { + return new TextChannel(session, channel); + } + throw new Error("Channel was not implemented"); + } + } +} \ No newline at end of file From c1ad9b160e95a982e9d7fd738c16743c9e730536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Thu, 7 Jul 2022 23:00:12 -0300 Subject: [PATCH 42/45] Rename and move of Channels.ts -> channels.ts --- .../{channels/Channels.ts => channels.ts} | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) rename structures/{channels/Channels.ts => channels.ts} (94%) diff --git a/structures/channels/Channels.ts b/structures/channels.ts similarity index 94% rename from structures/channels/Channels.ts rename to structures/channels.ts index 2b2cccc..76aa920 100644 --- a/structures/channels/Channels.ts +++ b/structures/channels.ts @@ -1,7 +1,7 @@ /** Types */ -import type { Model } from "../Base.ts"; -import type { Snowflake } from "../../util/Snowflake.ts"; -import type { Session } from "../../session/Session.ts"; +import type { Model } from "./Base.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { Session } from "../session/Session.ts"; /** External from vendor */ import { @@ -15,19 +15,19 @@ import { TargetTypes, DiscordInviteMetadata, DiscordThreadMember -} from "../../vendor/external.ts"; +} from "../vendor/external.ts"; /** Functions and others */ -import { calculateShardId } from "../../vendor/gateway/calculateShardId.ts"; -import { urlToBase64 } from "../../util/urlToBase64.ts"; +import { calculateShardId } from "../vendor/gateway/calculateShardId.ts"; +import { urlToBase64 } from "../util/urlToBase64.ts"; /** Classes and routes */ -import * as Routes from "../../util/Routes.ts"; -import Message, { CreateMessage, EditMessage, ReactionResolvable } from "../Message.ts"; -import Invite from "../Invite.ts"; -import Webhook from "../Webhook.ts"; -import User from "../User.ts"; -import ThreadMember from "../ThreadMember.ts"; +import * as Routes from "../util/Routes.ts"; +import Message, { CreateMessage, EditMessage, ReactionResolvable } from "./Message.ts"; +import Invite from "./Invite.ts"; +import Webhook from "./Webhook.ts"; +import User from "./User.ts"; +import ThreadMember from "./ThreadMember.ts"; export abstract class BaseChannel implements Model { constructor(session: Session, data: DiscordChannel) { From 7d92892c9fc37d0fbf197c7b63747c1f45185516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Thu, 7 Jul 2022 23:35:40 -0300 Subject: [PATCH 43/45] Update GuildChannel: method getArchivedThreads --- structures/channels.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/structures/channels.ts b/structures/channels.ts index 76aa920..325f9bc 100644 --- a/structures/channels.ts +++ b/structures/channels.ts @@ -14,7 +14,8 @@ import { DiscordWebhook, TargetTypes, DiscordInviteMetadata, -DiscordThreadMember +DiscordThreadMember, +DiscordListArchivedThreads } from "../vendor/external.ts"; /** Functions and others */ @@ -334,9 +335,8 @@ export class GuildChannel extends BaseChannel implements Model { return invites.map((invite) => new Invite(this.session, invite)); } - /* - async getArchivedThreads(options: ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }) { - let func: (channelId: Snowflake, options: ListArchivedThreads) => string; + async getArchivedThreads(options: Routes.ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }) { + let func: (channelId: Snowflake, options: Routes.ListArchivedThreads) => string; switch (options.type) { case "public": @@ -381,7 +381,7 @@ export class GuildChannel extends BaseChannel implements Model { ); return new ThreadChannel(this.session, thread, thread.guild_id ?? this.guildId); - }*/ + } } /** BaseVoiceChannel */ From 217c100e49d99994421d639f4af3bd883e7fee87 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 7 Jul 2022 23:44:48 -0300 Subject: [PATCH 44/45] Refactor monorepo (#36) * feat: GuildChannel.edit * fix: fmt Co-authored-by: socram03 --- handlers/Actions.ts | 39 +++-- structures/Integration.ts | 157 +++++++++--------- structures/Invite.ts | 30 ++-- structures/Webhook.ts | 2 +- structures/channels/GuildChannel.ts | 72 +++++++- .../interactions/AutoCompleteInteraction.ts | 3 +- structures/interactions/CommandInteraction.ts | 20 ++- .../CommandInteractionOptionResolver.ts | 36 ++-- .../interactions/ModalSubmitInteraction.ts | 8 +- structures/interactions/PingInteraction.ts | 3 +- util/permissions.ts | 9 + 11 files changed, 246 insertions(+), 133 deletions(-) create mode 100644 util/permissions.ts diff --git a/handlers/Actions.ts b/handlers/Actions.ts index e64c58d..ce32b9a 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -11,6 +11,8 @@ import type { DiscordGuildRoleCreate, DiscordGuildRoleDelete, DiscordGuildRoleUpdate, + DiscordIntegration, + DiscordIntegrationDelete, DiscordInteraction, DiscordMemberWithUser, DiscordMessage, @@ -26,9 +28,6 @@ import type { DiscordThreadListSync, DiscordUser, DiscordWebhookUpdate, - DiscordIntegration, - DiscordIntegrationDelete - } from "../vendor/external.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; @@ -41,8 +40,8 @@ import ThreadMember from "../structures/ThreadMember.ts"; import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; -import Integration from "../structures/Integration.ts" -import Guild from "../structures/guilds/Guild.ts"; +import Integration from "../structures/Integration.ts"; +import Guild from "../structures/guilds/Guild.ts"; import InteractionFactory from "../structures/interactions/InteractionFactory.ts"; export type RawHandler = (...args: [Session, number, T]) => void; @@ -186,16 +185,28 @@ export const WEBHOOKS_UPDATE: RawHandler = (session, _shar session.emit("webhooksUpdate", { guildId: webhook.guild_id, channelId: webhook.channel_id }); }; -export const INTEGRATION_CREATE: RawHandler = (session, _shardId, payload) => { +export const INTEGRATION_CREATE: RawHandler = ( + session, + _shardId, + payload, +) => { session.emit("integrationCreate", new Integration(session, payload)); }; -export const INTEGRATION_UPDATE: RawHandler = (session, _shardId, payload) => { +export const INTEGRATION_UPDATE: RawHandler = ( + session, + _shardId, + payload, +) => { session.emit("integrationCreate", new Integration(session, payload)); }; export const INTEGRATION_DELETE: RawHandler = (session, _shardId, payload) => { - session.emit("integrationDelete", { id: payload.id, guildId: payload.guild_id, applicationId: payload.application_id }); + session.emit("integrationDelete", { + id: payload.id, + guildId: payload.guild_id, + applicationId: payload.application_id, + }); }; export const MESSAGE_REACTION_ADD: RawHandler = (session, _shardId, reaction) => { @@ -206,11 +217,19 @@ export const MESSAGE_REACTION_REMOVE: RawHandler = session.emit("messageReactionRemove", null); }; -export const MESSAGE_REACTION_REMOVE_ALL: RawHandler = (session, _shardId, reaction) => { +export const MESSAGE_REACTION_REMOVE_ALL: RawHandler = ( + session, + _shardId, + reaction, +) => { session.emit("messageReactionRemoveAll", null); }; -export const MESSAGE_REACTION_REMOVE_EMOJI: RawHandler = (session, _shardId, reaction) => { +export const MESSAGE_REACTION_REMOVE_EMOJI: RawHandler = ( + session, + _shardId, + reaction, +) => { session.emit("messageReactionRemoveEmoji", null); }; diff --git a/structures/Integration.ts b/structures/Integration.ts index 9a3a90c..673e48a 100644 --- a/structures/Integration.ts +++ b/structures/Integration.ts @@ -1,80 +1,77 @@ -import type { Model } from "./Base.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; -import type { Session } from "../session/Session.ts"; -import type { - DiscordIntegration, - IntegrationExpireBehaviors -} from "../vendor/external.ts"; -import User from "./User.ts" - -export interface IntegrationAccount { - id: Snowflake; - name: string; -} - -export interface IntegrationApplication { - id: Snowflake; - name: string; - icon?: string; - description: string; - bot?: User; -} - -export class Integration implements Model { - constructor(session: Session, data: DiscordIntegration & { guild_id?: Snowflake }) { - this.id = data.id; - this.session = session; - - data.guild_id ? this.guildId = data.guild_id : null; - - this.name = data.name; - this.type = data.type; - this.enabled = !!data.enabled; - this.syncing = !!data.syncing; - this.roleId = data.role_id; - this.enableEmoticons = !!data.enable_emoticons; - this.expireBehavior = data.expire_behavior; - this.expireGracePeriod = data.expire_grace_period; - this.syncedAt = data.synced_at; - this.subscriberCount = data.subscriber_count; - this.revoked = !!data.revoked; - - this.user = data.user ? new User(session, data.user) : undefined; - this.account = { - id: data.account.id, - name: data.account.name - } - - if (data.application) { - this.application = { - id: data.application.id, - name: data.application.name, - icon: data.application.icon ? data.application.icon : undefined, - description: data.application.description, - bot: data.application.bot ? new User(session, data.application.bot) : undefined - }; - } - } - - id: Snowflake; - session: Session; - guildId?: Snowflake; - - name: string - type: "twitch" | "youtube" | "discord"; - enabled?: boolean; - syncing?: boolean; - roleId?: string; - enableEmoticons?: boolean; - expireBehavior?: IntegrationExpireBehaviors; - expireGracePeriod?: number; - syncedAt?: string; - subscriberCount?: number; - revoked?: boolean; - - user?: User; - account: IntegrationAccount; - application?: IntegrationApplication; -} - -export default Integration; +import type { Model } from "./Base.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { Session } from "../session/Session.ts"; +import type { DiscordIntegration, IntegrationExpireBehaviors } from "../vendor/external.ts"; +import User from "./User.ts"; + +export interface IntegrationAccount { + id: Snowflake; + name: string; +} + +export interface IntegrationApplication { + id: Snowflake; + name: string; + icon?: string; + description: string; + bot?: User; +} + +export class Integration implements Model { + constructor(session: Session, data: DiscordIntegration & { guild_id?: Snowflake }) { + this.id = data.id; + this.session = session; + + data.guild_id ? this.guildId = data.guild_id : null; + + this.name = data.name; + this.type = data.type; + this.enabled = !!data.enabled; + this.syncing = !!data.syncing; + this.roleId = data.role_id; + this.enableEmoticons = !!data.enable_emoticons; + this.expireBehavior = data.expire_behavior; + this.expireGracePeriod = data.expire_grace_period; + this.syncedAt = data.synced_at; + this.subscriberCount = data.subscriber_count; + this.revoked = !!data.revoked; + + this.user = data.user ? new User(session, data.user) : undefined; + this.account = { + id: data.account.id, + name: data.account.name, + }; + + if (data.application) { + this.application = { + id: data.application.id, + name: data.application.name, + icon: data.application.icon ? data.application.icon : undefined, + description: data.application.description, + bot: data.application.bot ? new User(session, data.application.bot) : undefined, + }; + } + } + + id: Snowflake; + session: Session; + guildId?: Snowflake; + + name: string; + type: "twitch" | "youtube" | "discord"; + enabled?: boolean; + syncing?: boolean; + roleId?: string; + enableEmoticons?: boolean; + expireBehavior?: IntegrationExpireBehaviors; + expireGracePeriod?: number; + syncedAt?: string; + subscriberCount?: number; + revoked?: boolean; + + user?: User; + account: IntegrationAccount; + application?: IntegrationApplication; +} + +export default Integration; diff --git a/structures/Invite.ts b/structures/Invite.ts index 5b4eaa6..5c511c2 100644 --- a/structures/Invite.ts +++ b/structures/Invite.ts @@ -2,12 +2,12 @@ import type { Session } from "../session/Session.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { DiscordChannel, - DiscordMemberWithUser, DiscordInvite, + DiscordMemberWithUser, DiscordScheduledEventEntityMetadata, + ScheduledEventEntityType, ScheduledEventPrivacyLevel, ScheduledEventStatus, - ScheduledEventEntityType } from "../vendor/external.ts"; import { TargetTypes } from "../vendor/external.ts"; import InviteGuild from "./guilds/InviteGuild.ts"; @@ -67,7 +67,7 @@ export class Invite { if (data.channel) { const guildId = (data.guild && data.guild?.id) ? data.guild.id : ""; - this.channel = new GuildChannel(session, (data.channel as DiscordChannel), guildId); + this.channel = new GuildChannel(session, data.channel as DiscordChannel, guildId); } this.code = data.code; @@ -83,17 +83,25 @@ export class Invite { channelId: data.guild_scheduled_event.channel_id ? data.guild_scheduled_event.channel_id : undefined, creatorId: data.guild_scheduled_event.creator_id ? data.guild_scheduled_event.creator_id : undefined, name: data.guild_scheduled_event.name, - description: data.guild_scheduled_event.description ? data.guild_scheduled_event.description : undefined, + description: data.guild_scheduled_event.description + ? data.guild_scheduled_event.description + : undefined, scheduledStartTime: data.guild_scheduled_event.scheduled_start_time, - scheduledEndTime: data.guild_scheduled_event.scheduled_end_time ? data.guild_scheduled_event.scheduled_end_time : undefined, + scheduledEndTime: data.guild_scheduled_event.scheduled_end_time + ? data.guild_scheduled_event.scheduled_end_time + : undefined, privacyLevel: data.guild_scheduled_event.privacy_level, status: data.guild_scheduled_event.status, entityType: data.guild_scheduled_event.entity_type, entityId: data.guild ? data.guild.id : undefined, - entityMetadata: data.guild_scheduled_event.entity_metadata ? data.guild_scheduled_event.entity_metadata : undefined, - creator: data.guild_scheduled_event.creator ? new User(session, data.guild_scheduled_event.creator) : undefined, + entityMetadata: data.guild_scheduled_event.entity_metadata + ? data.guild_scheduled_event.entity_metadata + : undefined, + creator: data.guild_scheduled_event.creator + ? new User(session, data.guild_scheduled_event.creator) + : undefined, userCount: data.guild_scheduled_event.user_count ? data.guild_scheduled_event.user_count : undefined, - image: data.guild_scheduled_event.image ? data.guild_scheduled_event.image : undefined + image: data.guild_scheduled_event.image ? data.guild_scheduled_event.image : undefined, }; } @@ -108,10 +116,12 @@ export class Invite { if (data.stage_instance) { const guildId = (data.guild && data.guild?.id) ? data.guild.id : ""; this.stageInstance = { - members: data.stage_instance.members.map(m => new Member(session, (m as DiscordMemberWithUser), guildId)), + members: data.stage_instance.members.map((m) => + new Member(session, m as DiscordMemberWithUser, guildId) + ), participantCount: data.stage_instance.participant_count, speakerCount: data.stage_instance.speaker_count, - topic: data.stage_instance.topic + topic: data.stage_instance.topic, }; } diff --git a/structures/Webhook.ts b/structures/Webhook.ts index c17a7ff..629ac36 100644 --- a/structures/Webhook.ts +++ b/structures/Webhook.ts @@ -47,7 +47,7 @@ export class Webhook implements Model { guildId?: Snowflake; user?: User; - async execute(options?: WebhookOptions & CreateMessage & { avatarUrl?: string, username?: string }) { + async execute(options?: WebhookOptions & CreateMessage & { avatarUrl?: string; username?: string }) { if (!this.token) { return; } diff --git a/structures/channels/GuildChannel.ts b/structures/channels/GuildChannel.ts index 4a3600a..d2b93a5 100644 --- a/structures/channels/GuildChannel.ts +++ b/structures/channels/GuildChannel.ts @@ -1,17 +1,23 @@ import type { Model } from "../Base.ts"; import type { Snowflake } from "../../util/Snowflake.ts"; +import type { PermissionsOverwrites } from "../../util/permissions.ts"; import type { Session } from "../../session/Session.ts"; import type { ChannelTypes, DiscordChannel, DiscordInviteMetadata, DiscordListArchivedThreads, + VideoQualityModes, } from "../../vendor/external.ts"; import type { ListArchivedThreads } from "../../util/Routes.ts"; import BaseChannel from "./BaseChannel.ts"; +import VoiceChannel from "./VoiceChannel.ts"; +import NewsChannel from "./NewsChannel.ts"; +import StageChannel from "./StageChannel.ts"; import ThreadMember from "../ThreadMember.ts"; import Invite from "../Invite.ts"; import * as Routes from "../../util/Routes.ts"; +import { Channel, ChannelFactory } from "./ChannelFactory.ts"; /** * Represent the options object to create a thread channel @@ -26,6 +32,40 @@ export interface ThreadCreateOptions { reason?: string; } +/** + * Representations of the objects to edit a guild channel + * @link https://discord.com/developers/docs/resources/channel#modify-channel-json-params-guild-channel + */ +export interface EditGuildChannelOptions { + name?: string; + position?: number; + permissionOverwrites?: PermissionsOverwrites[]; +} + +export interface EditNewsChannelOptions extends EditGuildChannelOptions { + type?: ChannelTypes.GuildNews | ChannelTypes.GuildText; + topic?: string | null; + nfsw?: boolean | null; + parentId?: Snowflake | null; + defaultAutoArchiveDuration?: number | null; +} + +export interface EditGuildTextChannelOptions extends EditNewsChannelOptions { + rateLimitPerUser?: number | null; +} + +export interface EditStageChannelOptions extends EditGuildChannelOptions { + bitrate?: number | null; + rtcRegion?: Snowflake | null; +} + +export interface EditVoiceChannelOptions extends EditStageChannelOptions { + nsfw?: boolean | null; + userLimit?: number | null; + parentId?: Snowflake | null; + videoQualityMode?: VideoQualityModes | null; +} + /** * Represents the option object to create a thread channel from a message * @link https://discord.com/developers/docs/resources/channel#start-thread-from-message @@ -63,6 +103,37 @@ export class GuildChannel extends BaseChannel implements Model { return invites.map((invite) => new Invite(this.session, invite)); } + async edit(options: EditNewsChannelOptions): Promise; + async edit(options: EditStageChannelOptions): Promise; + async edit(options: EditVoiceChannelOptions): Promise; + async edit( + options: EditGuildTextChannelOptions | EditNewsChannelOptions | EditVoiceChannelOptions, + ): Promise { + const channel = await this.session.rest.runMethod( + this.session.rest, + "PATCH", + Routes.CHANNEL(this.id), + { + name: options.name, + type: "type" in options ? options.type : undefined, + position: options.position, + topic: "topic" in options ? options.topic : undefined, + nsfw: "nfsw" in options ? options.nfsw : undefined, + rate_limit_per_user: "rateLimitPerUser" in options ? options.rateLimitPerUser : undefined, + bitrate: "bitrate" in options ? options.bitrate : undefined, + user_limit: "userLimit" in options ? options.userLimit : undefined, + permissions_overwrites: options.permissionOverwrites, + parent_id: "parentId" in options ? options.parentId : undefined, + rtc_region: "rtcRegion" in options ? options.rtcRegion : undefined, + video_quality_mode: "videoQualityMode" in options ? options.videoQualityMode : undefined, + default_auto_archive_duration: "defaultAutoArchiveDuration" in options + ? options.defaultAutoArchiveDuration + : undefined, + }, + ); + return ChannelFactory.from(this.session, channel); + } + /* async getArchivedThreads(options: ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }) { let func: (channelId: Snowflake, options: ListArchivedThreads) => string; @@ -113,5 +184,4 @@ export class GuildChannel extends BaseChannel implements Model { }*/ } - export default GuildChannel; diff --git a/structures/interactions/AutoCompleteInteraction.ts b/structures/interactions/AutoCompleteInteraction.ts index 063b407..7fe886e 100644 --- a/structures/interactions/AutoCompleteInteraction.ts +++ b/structures/interactions/AutoCompleteInteraction.ts @@ -1,4 +1,3 @@ - import type { Model } from "../Base.ts"; import type { Snowflake } from "../../util/Snowflake.ts"; import type { Session } from "../../session/Session.ts"; @@ -32,7 +31,7 @@ export class AutoCompleteInteraction extends BaseInteraction implements Model { { data: { choices }, type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, - } + }, ); } } diff --git a/structures/interactions/CommandInteraction.ts b/structures/interactions/CommandInteraction.ts index 0d25e91..40bce4e 100644 --- a/structures/interactions/CommandInteraction.ts +++ b/structures/interactions/CommandInteraction.ts @@ -1,7 +1,12 @@ import type { Model } from "../Base.ts"; import type { Snowflake } from "../../util/Snowflake.ts"; import type { Session } from "../../session/Session.ts"; -import type { ApplicationCommandTypes, DiscordMemberWithUser, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import type { + ApplicationCommandTypes, + DiscordInteraction, + DiscordMemberWithUser, + InteractionTypes, +} from "../../vendor/external.ts"; import type { CreateMessage } from "../Message.ts"; import type { MessageFlags } from "../../util/shared/flags.ts"; import { InteractionResponseTypes } from "../../vendor/external.ts"; @@ -17,7 +22,7 @@ import * as Routes from "../../util/Routes.ts"; /** * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response - * */ + */ export interface InteractionResponse { type: InteractionResponseTypes; data?: InteractionApplicationCommandCallbackData; @@ -25,8 +30,9 @@ export interface InteractionResponse { /** * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionapplicationcommandcallbackdata - * */ -export interface InteractionApplicationCommandCallbackData extends Pick { + */ +export interface InteractionApplicationCommandCallbackData + extends Pick { customId?: string; title?: string; // components?: MessageComponents; @@ -36,10 +42,10 @@ export interface InteractionApplicationCommandCallbackData extends Pick { +export interface CommandInteractionOption extends Omit { Attachment?: string; Boolean?: boolean; User?: bigint; @@ -97,7 +97,7 @@ export class CommandInteractionOptionResolver { } if (required === true && properties.every((prop) => typeof option[prop] === "undefined")) { - throw new TypeError(`Properties ${properties.join(', ')} are missing in option ${name}`); + throw new TypeError(`Properties ${properties.join(", ")} are missing in option ${name}`); } return option; @@ -107,12 +107,12 @@ export class CommandInteractionOptionResolver { get(name: string | number, required: boolean): CommandInteractionOption | undefined; get(name: string | number, required?: boolean) { const option = this.hoistedOptions.find((o) => - typeof name === 'number' ? o.name === name.toString() : o.name === name + typeof name === "number" ? o.name === name.toString() : o.name === name ); if (!option) { if (required && name in this.hoistedOptions.map((o) => o.name)) { - throw new TypeError('Option marked as required was undefined'); + throw new TypeError("Option marked as required was undefined"); } return; @@ -125,7 +125,7 @@ export class CommandInteractionOptionResolver { getString(name: string | number, required: true): string; getString(name: string | number, required?: boolean): string | undefined; getString(name: string | number, required = false) { - const option = this.getTypedOption(name, ApplicationCommandOptionTypes.String, ['Otherwise'], required); + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.String, ["Otherwise"], required); return option?.Otherwise ?? undefined; } @@ -134,7 +134,7 @@ export class CommandInteractionOptionResolver { getNumber(name: string | number, required: true): number; getNumber(name: string | number, required?: boolean): number | undefined; getNumber(name: string | number, required = false) { - const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Number, ['Otherwise'], required); + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Number, ["Otherwise"], required); return option?.Otherwise ?? undefined; } @@ -143,7 +143,7 @@ export class CommandInteractionOptionResolver { getInteger(name: string | number, required: true): number; getInteger(name: string | number, required?: boolean): number | undefined; getInteger(name: string | number, required = false) { - const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Integer, ['Otherwise'], required); + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Integer, ["Otherwise"], required); return option?.Otherwise ?? undefined; } @@ -152,7 +152,7 @@ export class CommandInteractionOptionResolver { getBoolean(name: string | number, required: true): boolean; getBoolean(name: string | number, required?: boolean): boolean | undefined; getBoolean(name: string | number, required = false) { - const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Boolean, ['Otherwise'], required); + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Boolean, ["Otherwise"], required); return option?.Otherwise ?? undefined; } @@ -161,7 +161,7 @@ export class CommandInteractionOptionResolver { getUser(name: string | number, required: true): bigint; getUser(name: string | number, required?: boolean): bigint | undefined; getUser(name: string | number, required = false) { - const option = this.getTypedOption(name, ApplicationCommandOptionTypes.User, ['Otherwise'], required); + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.User, ["Otherwise"], required); return option?.Otherwise ?? undefined; } @@ -170,7 +170,7 @@ export class CommandInteractionOptionResolver { getChannel(name: string | number, required: true): bigint; getChannel(name: string | number, required?: boolean): bigint | undefined; getChannel(name: string | number, required = false) { - const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Channel, ['Otherwise'], required); + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Channel, ["Otherwise"], required); return option?.Otherwise ?? undefined; } @@ -179,7 +179,7 @@ export class CommandInteractionOptionResolver { getMentionable(name: string | number, required: true): string; getMentionable(name: string | number, required?: boolean): string | undefined; getMentionable(name: string | number, required = false) { - const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Mentionable, ['Otherwise'], required); + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Mentionable, ["Otherwise"], required); return option?.Otherwise ?? undefined; } @@ -188,7 +188,7 @@ export class CommandInteractionOptionResolver { getRole(name: string | number, required: true): bigint; getRole(name: string | number, required?: boolean): bigint | undefined; getRole(name: string | number, required = false) { - const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Role, ['Otherwise'], required); + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Role, ["Otherwise"], required); return option?.Otherwise ?? undefined; } @@ -197,7 +197,7 @@ export class CommandInteractionOptionResolver { getAttachment(name: string | number, required: true): string; getAttachment(name: string | number, required?: boolean): string | undefined; getAttachment(name: string | number, required = false) { - const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Attachment, ['Otherwise'], required); + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Attachment, ["Otherwise"], required); return option?.Otherwise ?? undefined; } @@ -207,7 +207,7 @@ export class CommandInteractionOptionResolver { const focusedOption = this.hoistedOptions.find((option) => option.focused); if (!focusedOption) { - throw new TypeError('No option found'); + throw new TypeError("No option found"); } return full ? focusedOption : focusedOption.Otherwise; @@ -215,7 +215,7 @@ export class CommandInteractionOptionResolver { getSubCommand(required = true) { if (required && !this.#subcommand) { - throw new TypeError('Option marked as required was undefined'); + throw new TypeError("Option marked as required was undefined"); } return [this.#subcommand, this.hoistedOptions]; @@ -223,7 +223,7 @@ export class CommandInteractionOptionResolver { getSubCommandGroup(required = false) { if (required && !this.#group) { - throw new TypeError('Option marked as required was undefined'); + throw new TypeError("Option marked as required was undefined"); } return [this.#group, this.hoistedOptions]; diff --git a/structures/interactions/ModalSubmitInteraction.ts b/structures/interactions/ModalSubmitInteraction.ts index ff14a18..4d5f6df 100644 --- a/structures/interactions/ModalSubmitInteraction.ts +++ b/structures/interactions/ModalSubmitInteraction.ts @@ -1,8 +1,12 @@ - import type { Model } from "../Base.ts"; import type { Snowflake } from "../../util/Snowflake.ts"; import type { Session } from "../../session/Session.ts"; -import type { DiscordInteraction, InteractionTypes, MessageComponentTypes, DiscordMessageComponents } from "../../vendor/external.ts"; +import type { + DiscordInteraction, + DiscordMessageComponents, + InteractionTypes, + MessageComponentTypes, +} from "../../vendor/external.ts"; import BaseInteraction from "./BaseInteraction.ts"; import Message from "../Message.ts"; diff --git a/structures/interactions/PingInteraction.ts b/structures/interactions/PingInteraction.ts index 438ac01..a335f7c 100644 --- a/structures/interactions/PingInteraction.ts +++ b/structures/interactions/PingInteraction.ts @@ -1,4 +1,3 @@ - import type { Model } from "../Base.ts"; import type { Snowflake } from "../../util/Snowflake.ts"; import type { Session } from "../../session/Session.ts"; @@ -30,7 +29,7 @@ export class PingInteraction extends BaseInteraction implements Model { Routes.INTERACTION_ID_TOKEN(this.id, this.token), { type: InteractionResponseTypes.Pong, - } + }, ); } } diff --git a/util/permissions.ts b/util/permissions.ts new file mode 100644 index 0000000..e19f5ea --- /dev/null +++ b/util/permissions.ts @@ -0,0 +1,9 @@ +import { Snowflake } from "./Snowflake.ts"; +import { Permissions } from "../structures/Permissions.ts"; + +export interface PermissionsOverwrites { + id: Snowflake; + type: 0 | 1; + allow: Permissions; + deny: Permissions; +} From 45c5632382acedc7f28c966baf7020dddd79f440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Serna?= Date: Thu, 7 Jul 2022 23:50:40 -0300 Subject: [PATCH 45/45] Update GuildChannel --- structures/channels.ts | 66 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/structures/channels.ts b/structures/channels.ts index 325f9bc..35f7a63 100644 --- a/structures/channels.ts +++ b/structures/channels.ts @@ -29,6 +29,7 @@ import Invite from "./Invite.ts"; import Webhook from "./Webhook.ts"; import User from "./User.ts"; import ThreadMember from "./ThreadMember.ts"; +import { PermissionsOverwrites } from "../util/permissions.ts"; export abstract class BaseChannel implements Model { constructor(session: Session, data: DiscordChannel) { @@ -298,6 +299,40 @@ export class TextChannel { reason?: string; } +/** + * Representations of the objects to edit a guild channel + * @link https://discord.com/developers/docs/resources/channel#modify-channel-json-params-guild-channel + */ +export interface EditGuildChannelOptions { + name?: string; + position?: number; + permissionOverwrites?: PermissionsOverwrites[]; +} + +export interface EditNewsChannelOptions extends EditGuildChannelOptions { + type?: ChannelTypes.GuildNews | ChannelTypes.GuildText; + topic?: string | null; + nfsw?: boolean | null; + parentId?: Snowflake | null; + defaultAutoArchiveDuration?: number | null; +} + +export interface EditGuildTextChannelOptions extends EditNewsChannelOptions { + rateLimitPerUser?: number | null; +} + +export interface EditStageChannelOptions extends EditGuildChannelOptions { + bitrate?: number | null; + rtcRegion?: Snowflake | null; +} + +export interface EditVoiceChannelOptions extends EditStageChannelOptions { + nsfw?: boolean | null; + userLimit?: number | null; + parentId?: Snowflake | null; + videoQualityMode?: VideoQualityModes | null; +} + /** * Represents the option object to create a thread channel from a message * @link https://discord.com/developers/docs/resources/channel#start-thread-from-message @@ -335,6 +370,37 @@ export class GuildChannel extends BaseChannel implements Model { return invites.map((invite) => new Invite(this.session, invite)); } + async edit(options: EditNewsChannelOptions): Promise; + async edit(options: EditStageChannelOptions): Promise; + async edit(options: EditVoiceChannelOptions): Promise; + async edit( + options: EditGuildTextChannelOptions | EditNewsChannelOptions | EditVoiceChannelOptions, + ): Promise { + const channel = await this.session.rest.runMethod( + this.session.rest, + "PATCH", + Routes.CHANNEL(this.id), + { + name: options.name, + type: "type" in options ? options.type : undefined, + position: options.position, + topic: "topic" in options ? options.topic : undefined, + nsfw: "nfsw" in options ? options.nfsw : undefined, + rate_limit_per_user: "rateLimitPerUser" in options ? options.rateLimitPerUser : undefined, + bitrate: "bitrate" in options ? options.bitrate : undefined, + user_limit: "userLimit" in options ? options.userLimit : undefined, + permissions_overwrites: options.permissionOverwrites, + parent_id: "parentId" in options ? options.parentId : undefined, + rtc_region: "rtcRegion" in options ? options.rtcRegion : undefined, + video_quality_mode: "videoQualityMode" in options ? options.videoQualityMode : undefined, + default_auto_archive_duration: "defaultAutoArchiveDuration" in options + ? options.defaultAutoArchiveDuration + : undefined, + }, + ); + return ChannelFactory.from(this.session, channel); + } + async getArchivedThreads(options: Routes.ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }) { let func: (channelId: Snowflake, options: Routes.ListArchivedThreads) => string;