From b87c0716cc68e7fda728b00e883d3bca5811381f Mon Sep 17 00:00:00 2001 From: Yuzu Date: Thu, 30 Jun 2022 10:31:02 -0500 Subject: [PATCH 01/22] working on interactions --- structures/Component.ts | 41 ++++++++++++++++++++++++++++++++++ structures/Interaction.ts | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 structures/Component.ts create mode 100644 structures/Interaction.ts diff --git a/structures/Component.ts b/structures/Component.ts new file mode 100644 index 0000000..4999b8c --- /dev/null +++ b/structures/Component.ts @@ -0,0 +1,41 @@ +import type { Session } from "../session/Session.ts"; +import type { DiscordComponent, MessageComponentTypes } from "../vendor/external.ts"; +import Emoji from "./Emoji.ts"; + +export class Component { + constructor(session: Session, data: DiscordComponent) { + this.session = session; + this.customId = data.custom_id; + this.type = data.type + this.components = data.components?.map((component) => new Component(session, component)); + this.disabled = !!data.disabled; + + if (data.emoji) { + this.emoji = new Emoji(session, data.emoji); + } + + this.maxValues = data.max_values; + this.minValues = data.min_values; + this.label = data.label; + this.value = data.value; + this.options = data.options ?? []; + this.placeholder = data.placeholder; + } + + readonly session: Session; + + customId?: string; + type: MessageComponentTypes; + components?: Component[]; + disabled: boolean; + emoji?: Emoji; + maxValues?: number; + minValues?: number; + label?: string; + value?: string; + // deno-lint-ignore no-explicit-any + options: any[]; + placeholder?: string; +} + +export default Component; diff --git a/structures/Interaction.ts b/structures/Interaction.ts new file mode 100644 index 0000000..d120f93 --- /dev/null +++ b/structures/Interaction.ts @@ -0,0 +1,47 @@ +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 User from "./User.ts"; +// import Member from "./Member.ts"; + +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 { + // TODO: member transformer + // pass + } + } + + 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; + + // TODO: do methods + async respond() { + } +} + +export default Interaction; From 8653e190eac00f44fdf1b6faec32cd64c1bc0e54 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 09:15:47 -0500 Subject: [PATCH 02/22] events and stuff --- handlers/Actions.ts | 63 +++++++++++++++++++++++++++++++------------- session/Session.ts | 5 ++++ structures/Member.ts | 15 ++++++----- 3 files changed, 57 insertions(+), 26 deletions(-) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index e9ec833..1e1fa36 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -1,39 +1,64 @@ -import type { DiscordMessage, DiscordReady } from "../vendor/external.ts"; +import type { + DiscordGuildMemberAdd, + DiscordGuildMemberRemove, + DiscordGuildMemberUpdate, + DiscordMessage, + DiscordMessageDelete, + DiscordReady, +} from "../vendor/external.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; +import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; +import User from "../structures/User.ts"; -export type RawHandler = (...args: [Session, number, ...T]) => void; +export type RawHandler = (...args: [Session, number, T]) => void; export type Handler = (...args: T) => unknown; -export type Ready = [DiscordReady]; -export const READY: RawHandler = (session, shardId, payload) => { - session.emit("ready", payload, shardId); +export const READY: RawHandler = (session, shardId, payload) => { + session.emit("ready", { ...payload, user: new User(session, payload.user) }, shardId); }; -export type MessageCreate = [DiscordMessage]; -export const MESSAGE_CREATE: RawHandler = (session, _shardId, message) => { +export const MESSAGE_CREATE: RawHandler = (session, _shardId, message) => { session.emit("messageCreate", new Message(session, message)); }; -export type MessageUpdate = [DiscordMessage]; -export const MESSAGE_UPDATE: RawHandler = (session, _shardId, new_message) => { +export const MESSAGE_UPDATE: RawHandler = (session, _shardId, new_message) => { session.emit("messageUpdate", new Message(session, new_message)); }; -export type MessageDelete = [Snowflake]; -export const MESSAGE_DELETE: RawHandler = (session, _shardId, deleted_message_id) => { - session.emit("messageDelete", deleted_message_id); +export const MESSAGE_DELETE: RawHandler = (session, _shardId, { id, channel_id, guild_id }) => { + session.emit("messageDelete", { id, channelId: channel_id, guildId: guild_id }); }; -export const raw: RawHandler<[unknown]> = (session, shardId, data) => { +export const GUILD_MEMBER_ADD: RawHandler = (session, _shardId, member) => { + session.emit("guildMemberAdd", new Member(session, member, member.guild_id)); +}; + +export const GUILD_MEMBER_UPDATE: RawHandler = (session, _shardId, member) => { + session.emit("guildMemberUpdate", new Member(session, member, member.guild_id)); +}; + +export const GUILD_MEMBER_REMOVE: RawHandler = (session, _shardId, member) => { + session.emit("guildMemberRemove", new User(session, member.user), member.guild_id); +}; + +export const raw: RawHandler = (session, shardId, data) => { session.emit("raw", data, shardId); }; -export interface Events { - "ready": Handler<[DiscordReady, number]>; - "messageCreate": Handler<[Message]>; - "messageUpdate": Handler<[Message]>; - "messageDelete": Handler<[Snowflake]>; - "raw": Handler<[unknown]>; +export interface Ready extends Omit { + user: User; +} + +// 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]>; + "raw": Handler<[unknown, number]>; } diff --git a/session/Session.ts b/session/Session.ts index 31263fb..fe7a188 100644 --- a/session/Session.ts +++ b/session/Session.ts @@ -89,6 +89,11 @@ export class Session extends EventEmitter { return super.once(event, func); } + override emit(event: K, ...params: Parameters): boolean; + override emit(event: K, ...params: unknown[]): boolean { + return super.emit(event, ...params); + } + async start() { const getGatewayBot = () => this.rest.runMethod(this.rest, "GET", Routes.GATEWAY_BOT()); diff --git a/structures/Member.ts b/structures/Member.ts index 355d9d4..e3d302e 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -1,7 +1,7 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { DiscordMember, MakeRequired } from "../vendor/external.ts"; +import type { DiscordMemberWithUser } from "../vendor/external.ts"; import type { ImageFormat, ImageSize } from "../util/shared/images.ts"; import { iconBigintToHash, iconHashToBigInt } from "../util/hash.ts"; import User from "./User.ts"; @@ -23,9 +23,10 @@ export interface CreateGuildBan { * @link https://discord.com/developers/docs/resources/guild#guild-member-object */ export class Member implements Model { - constructor(session: Session, data: MakeRequired) { + constructor(session: Session, data: DiscordMemberWithUser, guildId: Snowflake) { this.session = session; this.user = new User(session, data.user); + this.guildId = guildId; this.avatarHash = data.avatar ? iconHashToBigInt(data.avatar) : undefined; this.nickname = data.nick ? data.nick : undefined; this.joinedTimestamp = Number.parseInt(data.joined_at); @@ -39,8 +40,8 @@ export class Member implements Model { } readonly session: Session; - user: User; + guildId: Snowflake; avatarHash?: bigint; nickname?: string; joinedTimestamp: number; @@ -66,11 +67,11 @@ export class Member implements Model { /** * Bans the member */ - async ban(guildId: Snowflake, options: CreateGuildBan): Promise { + async ban(options: CreateGuildBan): Promise { await this.session.rest.runMethod( this.session.rest, "PUT", - Routes.GUILD_BAN(guildId, this.id), + Routes.GUILD_BAN(this.guildId, this.id), options ? { delete_message_days: options.deleteMessageDays, @@ -85,11 +86,11 @@ export class Member implements Model { /** * Kicks the member */ - async kick(guildId: Snowflake, { reason }: { reason?: string }): Promise { + async kick({ reason }: { reason?: string }): Promise { await this.session.rest.runMethod( this.session.rest, "DELETE", - Routes.GUILD_MEMBER(guildId, this.id), + Routes.GUILD_MEMBER(this.guildId, this.id), { reason }, ); From 60484dab40680e79dcca7f3f4a4a3dee7c80a489 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 09:49:04 -0500 Subject: [PATCH 03/22] feat: interaction.Respond --- session/Session.ts | 17 +++++- structures/Guild.ts | 2 +- structures/Interaction.ts | 109 +++++++++++++++++++++++++++++++++++--- structures/Message.ts | 10 ++-- util/Routes.ts | 15 ++++++ vendor/external.ts | 1 + 6 files changed, 139 insertions(+), 15 deletions(-) diff --git a/session/Session.ts b/session/Session.ts index fe7a188..aa7477c 100644 --- a/session/Session.ts +++ b/session/Session.ts @@ -4,7 +4,7 @@ import type { Events } from "../handlers/Actions.ts"; import { Snowflake } from "../util/Snowflake.ts"; import { EventEmitter } from "../util/EventEmmiter.ts"; -import { createGatewayManager, createRestManager } from "../vendor/external.ts"; +import { createGatewayManager, createRestManager, getBotIdFromToken } from "../vendor/external.ts"; import * as Routes from "../util/Routes.ts"; import * as Actions from "../handlers/Actions.ts"; @@ -39,6 +39,18 @@ export class Session extends EventEmitter { rest: ReturnType; gateway: ReturnType; + unrepliedInteractions: Set = new Set(); + + #botId: Snowflake; + + set botId(id: Snowflake) { + this.#botId = id; + } + + get botId() { + return this.#botId; + } + constructor(options: SessionOptions) { super(); this.options = options; @@ -71,7 +83,8 @@ export class Session extends EventEmitter { }, handleDiscordPayload: this.options.rawHandler ?? defHandler, }); - // TODO: set botId in Session.botId or something + + this.#botId = getBotIdFromToken(options.token).toString(); } override on(event: K, func: Events[K]): this; diff --git a/structures/Guild.ts b/structures/Guild.ts index ef265cb..8cf752d 100644 --- a/structures/Guild.ts +++ b/structures/Guild.ts @@ -62,7 +62,7 @@ export class Guild extends BaseGuild implements Model { this.vefificationLevel = data.verification_level; this.defaultMessageNotificationLevel = data.default_message_notifications; this.explicitContentFilterLevel = data.explicit_content_filter; - this.members = data.members?.map((member) => new Member(session, { ...member, user: member.user! })) ?? []; + this.members = data.members?.map((member) => new Member(session, { ...member, user: member.user! }, data.id)) ?? []; this.roles = data.roles.map((role) => new Role(session, role, data.id)); this.emojis = data.emojis.map((guildEmoji) => new GuildEmoji(session, guildEmoji, data.id)); } diff --git a/structures/Interaction.ts b/structures/Interaction.ts index d120f93..29a6744 100644 --- a/structures/Interaction.ts +++ b/structures/Interaction.ts @@ -1,9 +1,42 @@ 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 type { + DiscordMessage, + DiscordInteraction, + InteractionTypes, + InteractionResponseTypes, + FileContent, +} from "../vendor/external.ts"; +import type { MessageFlags } from "../util/shared/flags.ts"; +import type { AllowedMentions } from "./Message.ts"; import User from "./User.ts"; -// import Member from "./Member.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; +} export class Interaction implements Model { constructor(session: Session, data: DiscordInteraction) { @@ -21,8 +54,7 @@ export class Interaction implements Model { this.user = new User(session, data.user!); } else { - // TODO: member transformer - // pass + this.member = new Member(session, data.member!, data.guild_id); } } @@ -38,9 +70,74 @@ export class Interaction implements Model { // deno-lint-ignore no-explicit-any data: any; user?: User; + member?: Member; - // TODO: do methods - async respond() { + 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.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 "guildId" in this; } } diff --git a/structures/Message.ts b/structures/Message.ts index 2f101b0..0505daf 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -63,12 +63,10 @@ export class Message implements Model { this.attachments = data.attachments.map((attachment) => new Attachment(session, attachment)); // user is always null on MessageCreate and its replaced with author - this.member = data.member - ? new Member(session, { - ...data.member, - user: data.author, - }) - : undefined; + + if (data.guild_id && data.member) { + this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id); + } } readonly session: Session; diff --git a/util/Routes.ts b/util/Routes.ts index 03442a7..035042d 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -139,3 +139,18 @@ export function INVITE(inviteCode: string, options?: GetInvite) { export function GUILD_INVITES(guildId: Snowflake) { return `/guilds/${guildId}/invites`; } + +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}?`; + + if (options) { + if (options.wait !== undefined) url += `wait=${options.wait}`; + if (options.threadId) url += `threadId=${options.threadId}`; + } + + return url; +} diff --git a/vendor/external.ts b/vendor/external.ts index efb9d06..b96e207 100644 --- a/vendor/external.ts +++ b/vendor/external.ts @@ -2,3 +2,4 @@ export * from "./gateway/mod.ts"; export * from "./rest/mod.ts"; export * from "./types/mod.ts"; export * from "./util/constants.ts"; +export * from "./util/token.ts"; From fcf8c60cca8a4988d1997555691e8ce9e43a3525 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 10:09:29 -0500 Subject: [PATCH 04/22] patch: move Member.ban() to Guild.banMember() --- mod.ts | 2 ++ structures/Guild.ts | 40 ++++++++++++++++++++++++++++++++++++++++ structures/Member.ts | 41 ++++++----------------------------------- 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/mod.ts b/mod.ts index b462a6d..559e08d 100644 --- a/mod.ts +++ b/mod.ts @@ -3,11 +3,13 @@ export * from "./structures/Attachment.ts"; export * from "./structures/Base.ts"; export * from "./structures/BaseGuild.ts"; export * from "./structures/Channel.ts"; +export * from "./structures/Component.ts"; export * from "./structures/DMChannel.ts"; export * from "./structures/Emoji.ts"; export * from "./structures/Guild.ts"; export * from "./structures/GuildChannel.ts"; export * from "./structures/GuildEmoji.ts"; +export * from "./structures/Interaction.ts"; export * from "./structures/Invite.ts"; export * from "./structures/InviteGuild.ts"; export * from "./structures/Member.ts"; diff --git a/structures/Guild.ts b/structures/Guild.ts index 8cf752d..936e3f3 100644 --- a/structures/Guild.ts +++ b/structures/Guild.ts @@ -46,6 +46,16 @@ export interface ModifyGuildEmoji { roles?: Snowflake[]; } +/** + * @link https://discord.com/developers/docs/resources/guild#create-guild-ban + */ +export interface CreateGuildBan { + /** Number of days to delete messages for (0-7) */ + deleteMessageDays?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; + /** Reason for the ban */ + reason?: string; +} + /** * Represents a guild * @link https://discord.com/developers/docs/resources/guild#guild-object @@ -190,6 +200,36 @@ export class Guild extends BaseGuild implements Model { return invites.map((invite) => new Invite(this.session, invite)); } + + + /** + * Bans the member + */ + async banMember(memberId: Snowflake, options: CreateGuildBan) { + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.GUILD_BAN(this.id, memberId), + options + ? { + delete_message_days: options.deleteMessageDays, + reason: options.reason, + } + : {}, + ); + } + + /** + * Kicks the member + */ + async kickMember(memebrId: Snowflake, { reason }: { reason?: string }) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.GUILD_MEMBER(this.id, memebrId), + { reason }, + ); + } } export default Guild; diff --git a/structures/Member.ts b/structures/Member.ts index e3d302e..9d12de1 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -3,20 +3,12 @@ import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { DiscordMemberWithUser } from "../vendor/external.ts"; import type { ImageFormat, ImageSize } from "../util/shared/images.ts"; +import type { CreateGuildBan } from "./Guild.ts"; import { iconBigintToHash, iconHashToBigInt } from "../util/hash.ts"; import User from "./User.ts"; +import Guild from "./Guild.ts"; import * as Routes from "../util/Routes.ts"; -/** - * @link https://discord.com/developers/docs/resources/guild#create-guild-ban - */ -export interface CreateGuildBan { - /** Number of days to delete messages for (0-7) */ - deleteMessageDays?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; - /** Reason for the ban */ - reason?: string; -} - /** * Represents a guild member * TODO: add a `guild` property somehow @@ -64,35 +56,14 @@ export class Member implements Model { return new Date(this.joinedTimestamp); } - /** - * Bans the member - */ async ban(options: CreateGuildBan): Promise { - await this.session.rest.runMethod( - this.session.rest, - "PUT", - Routes.GUILD_BAN(this.guildId, this.id), - options - ? { - delete_message_days: options.deleteMessageDays, - reason: options.reason, - } - : {}, - ); + await Guild.prototype.banMember.call({ id: this.guildId }, this.user.id, options); return this; } - /** - * Kicks the member - */ - async kick({ reason }: { reason?: string }): Promise { - await this.session.rest.runMethod( - this.session.rest, - "DELETE", - Routes.GUILD_MEMBER(this.guildId, this.id), - { reason }, - ); + async kick(options: { reason?: string }): Promise { + await Guild.prototype.kickMember.call({ id: this.guildId }, this.user.id, options); return this; } @@ -104,7 +75,7 @@ export class Member implements Model { if (!this.avatarHash) { url = Routes.USER_DEFAULT_AVATAR(Number(this.user.discriminator) % 5); } else { - url = Routes.USER_AVATAR(this.id, iconBigintToHash(this.avatarHash)); + url = Routes.USER_AVATAR(this.user.id, iconBigintToHash(this.avatarHash)); } return `${url}.${options.format ?? (url.includes("/a_") ? "gif" : "jpg")}?size=${options.size}`; From 42e5c9f62b787cf798fdd227e9c3722e32c5d5ec Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 10:12:48 -0500 Subject: [PATCH 05/22] fix: idk --- structures/Component.ts | 2 +- structures/Guild.ts | 4 ++-- structures/Interaction.ts | 29 ++++++++++++++--------------- structures/Member.ts | 4 ++-- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/structures/Component.ts b/structures/Component.ts index 4999b8c..02daff5 100644 --- a/structures/Component.ts +++ b/structures/Component.ts @@ -6,7 +6,7 @@ export class Component { constructor(session: Session, data: DiscordComponent) { this.session = session; this.customId = data.custom_id; - this.type = data.type + this.type = data.type; this.components = data.components?.map((component) => new Component(session, component)); this.disabled = !!data.disabled; diff --git a/structures/Guild.ts b/structures/Guild.ts index 936e3f3..0fe06e5 100644 --- a/structures/Guild.ts +++ b/structures/Guild.ts @@ -72,7 +72,8 @@ export class Guild extends BaseGuild implements Model { this.vefificationLevel = data.verification_level; this.defaultMessageNotificationLevel = data.default_message_notifications; this.explicitContentFilterLevel = data.explicit_content_filter; - this.members = data.members?.map((member) => new Member(session, { ...member, user: member.user! }, data.id)) ?? []; + this.members = data.members?.map((member) => new Member(session, { ...member, user: member.user! }, data.id)) ?? + []; this.roles = data.roles.map((role) => new Role(session, role, data.id)); this.emojis = data.emojis.map((guildEmoji) => new GuildEmoji(session, guildEmoji, data.id)); } @@ -201,7 +202,6 @@ export class Guild extends BaseGuild implements Model { return invites.map((invite) => new Invite(this.session, invite)); } - /** * Bans the member */ diff --git a/structures/Interaction.ts b/structures/Interaction.ts index 29a6744..59b589c 100644 --- a/structures/Interaction.ts +++ b/structures/Interaction.ts @@ -2,11 +2,11 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { - DiscordMessage, DiscordInteraction, - InteractionTypes, - InteractionResponseTypes, + DiscordMessage, FileContent, + InteractionResponseTypes, + InteractionTypes, } from "../vendor/external.ts"; import type { MessageFlags } from "../util/shared/flags.ts"; import type { AllowedMentions } from "./Message.ts"; @@ -34,8 +34,8 @@ export interface InteractionApplicationCommandCallbackData { /** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice */ export interface ApplicationCommandOptionChoice { - name: string; - value: string | number; + name: string; + value: string | number; } export class Interaction implements Model { @@ -43,7 +43,7 @@ export class Interaction implements Model { this.session = session; this.id = data.id; this.token = data.token; - this.type = data.type + this.type = data.type; this.guildId = data.guild_id; this.channelId = data.channel_id; this.applicationId = data.application_id; @@ -52,8 +52,7 @@ export class Interaction implements Model { if (!data.guild_id) { this.user = new User(session, data.user!); - } - else { + } else { this.member = new Member(session, data.member!, data.guild_id); } } @@ -104,11 +103,11 @@ export class Interaction implements Model { file: data?.files, }, headers: { - // remove authorization header - Authorization: "", + // remove authorization header + Authorization: "", }, }), - } + }, ); return; @@ -123,21 +122,21 @@ export class Interaction implements Model { method: "POST", body: { ...toSend, - file: data?.files + 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 "guildId" in this; + inGuild(): this is Interaction & { user: undefined; guildId: Snowflake; member: Member } { + return !!this.guildId; } } diff --git a/structures/Member.ts b/structures/Member.ts index 9d12de1..d4ef440 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -57,13 +57,13 @@ export class Member implements Model { } async ban(options: CreateGuildBan): Promise { - await Guild.prototype.banMember.call({ id: this.guildId }, this.user.id, options); + await Guild.prototype.banMember.call({ id: this.guildId, session: this.session }, this.user.id, options); return this; } async kick(options: { reason?: string }): Promise { - await Guild.prototype.kickMember.call({ id: this.guildId }, this.user.id, options); + await Guild.prototype.kickMember.call({ id: this.guildId, session: this.session }, this.user.id, options); return this; } From 71aa75c66030edf794742ccd610df0e29eb7efec Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 10:17:05 -0500 Subject: [PATCH 06/22] patch: add interaction event --- handlers/Actions.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 1e1fa36..6174828 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -2,6 +2,7 @@ import type { DiscordGuildMemberAdd, DiscordGuildMemberRemove, DiscordGuildMemberUpdate, + DiscordInteraction, DiscordMessage, DiscordMessageDelete, DiscordReady, @@ -11,6 +12,7 @@ import type { Session } from "../session/Session.ts"; import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; +import Interaction from "../structures/Interaction.ts"; export type RawHandler = (...args: [Session, number, T]) => void; export type Handler = (...args: T) => unknown; @@ -43,6 +45,11 @@ export const GUILD_MEMBER_REMOVE: RawHandler = (sessio session.emit("guildMemberRemove", new User(session, member.user), member.guild_id); }; +export const INTERACTION_CREATE: RawHandler = (session, _shardId, interaction) => { + session.unrepliedInteractions.add(BigInt(interaction.id)); + session.emit("interactionCreate", new Interaction(session, interaction)); +}; + export const raw: RawHandler = (session, shardId, data) => { session.emit("raw", data, shardId); }; @@ -60,5 +67,6 @@ export interface Events { "guildMemberAdd": Handler<[Member]>; "guildMemberUpdate": Handler<[Member]>; "guildMemberRemove": Handler<[User, Snowflake]>; + "interactionCreate": Handler<[Interaction]>; "raw": Handler<[unknown, number]>; } From 44a9f11fd6399e10c08930253149fc07ed49d826 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 10:25:37 -0500 Subject: [PATCH 07/22] feat: set bot's nickname --- structures/Guild.ts | 14 ++++++++++++++ util/Routes.ts | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/structures/Guild.ts b/structures/Guild.ts index 0fe06e5..9beee88 100644 --- a/structures/Guild.ts +++ b/structures/Guild.ts @@ -90,6 +90,20 @@ export class Guild extends BaseGuild implements Model { roles: Role[]; emojis: GuildEmoji[]; + /** + * 'null' would reset the nickname + * */ + async editBotNickname(options: { nick: string | null; reason?: string }) { + const result = await this.session.rest.runMethod<{ nick?: string } | undefined>( + this.session.rest, + "PATCH", + Routes.USER_NICK(this.id), + options, + ); + + return result?.nick; + } + async createEmoji(options: CreateGuildEmoji): Promise { if (options.image && !options.image.startsWith("data:image/")) { options.image = await urlToBase64(options.image); diff --git a/util/Routes.ts b/util/Routes.ts index 035042d..9ebfcb6 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -154,3 +154,7 @@ export function WEBHOOK(webhookId: Snowflake, token: string, options?: { wait?: return url; } + +export function USER_NICK(guildId: Snowflake) { + return `/guilds/${guildId}/members/@me`; +} From 5abbe95750fa26cf9fb9a04aacc81fe27843d1ea Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 10:34:45 -0500 Subject: [PATCH 08/22] feat: Guild.editMember --- structures/Guild.ts | 36 +++++++++++++++++++++++++++++++++--- structures/Member.ts | 8 +++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/structures/Guild.ts b/structures/Guild.ts index 9beee88..85f5ede 100644 --- a/structures/Guild.ts +++ b/structures/Guild.ts @@ -1,7 +1,7 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { DiscordEmoji, DiscordGuild, DiscordInviteMetadata, DiscordRole } from "../vendor/external.ts"; +import type { DiscordEmoji, DiscordGuild, DiscordMemberWithUser, DiscordInviteMetadata, DiscordRole } from "../vendor/external.ts"; import type { GetInvite } from "../util/Routes.ts"; import { DefaultMessageNotificationLevels, @@ -56,6 +56,18 @@ export interface CreateGuildBan { reason?: string; } +/** + * @link https://discord.com/developers/docs/resources/guild#modify-guild-member + * */ +export interface ModifyGuildMember { + nick?: string; + roles?: Snowflake[]; + mute?: boolean; + deaf?: boolean; + channelId?: Snowflake; + communicationDisabledUntil?: number; +} + /** * Represents a guild * @link https://discord.com/developers/docs/resources/guild#guild-object @@ -236,14 +248,32 @@ export class Guild extends BaseGuild implements Model { /** * Kicks the member */ - async kickMember(memebrId: Snowflake, { reason }: { reason?: string }) { + async kickMember(memberId: Snowflake, { reason }: { reason?: string }) { await this.session.rest.runMethod( this.session.rest, "DELETE", - Routes.GUILD_MEMBER(this.id, memebrId), + Routes.GUILD_MEMBER(this.id, memberId), { reason }, ); } + + async editMember(memberId: Snowflake, options: ModifyGuildMember) { + const member = await this.session.rest.runMethod( + this.session.rest, + "PATCH", + Routes.GUILD_MEMBER(this.id, memberId) + { + nick: options.nick, + roles: options.roles, + mute: options.mute, + deaf: options.deaf, + channel_id: options.channelId, + communication_disabled_until: options.communicationDisabledUntil ? new Date(options.communicationDisabledUntil).toISOString() : undefined, + }, + ); + + return new Member(this.session, member, this.id); + } } export default Guild; diff --git a/structures/Member.ts b/structures/Member.ts index d4ef440..f19a192 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -3,7 +3,7 @@ import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { DiscordMemberWithUser } from "../vendor/external.ts"; import type { ImageFormat, ImageSize } from "../util/shared/images.ts"; -import type { CreateGuildBan } from "./Guild.ts"; +import type { CreateGuildBan, ModifyGuildMember } from "./Guild.ts"; import { iconBigintToHash, iconHashToBigInt } from "../util/hash.ts"; import User from "./User.ts"; import Guild from "./Guild.ts"; @@ -68,6 +68,12 @@ export class Member implements Model { return this; } + async edit(options: ModifyGuildMember): Promise { + const member = await Guild.prototype.editMember.call({ id: this.guildId, session: this.session }, this.user.id, options); + + return member; + } + /** gets the user's avatar */ avatarUrl(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }) { let url: string; From ea5f3d53a8f5ab2a8c5c65e3f8f91b2c1af22305 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 10:46:41 -0500 Subject: [PATCH 09/22] feat: Guild.pruneMembers etc --- structures/Guild.ts | 40 ++++++++++++++++++++++++++++++++++++---- structures/Role.ts | 1 - util/Routes.ts | 23 +++++++++++++++++++---- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/structures/Guild.ts b/structures/Guild.ts index 85f5ede..80742ca 100644 --- a/structures/Guild.ts +++ b/structures/Guild.ts @@ -50,9 +50,7 @@ export interface ModifyGuildEmoji { * @link https://discord.com/developers/docs/resources/guild#create-guild-ban */ export interface CreateGuildBan { - /** Number of days to delete messages for (0-7) */ deleteMessageDays?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; - /** Reason for the ban */ reason?: string; } @@ -68,6 +66,15 @@ export interface ModifyGuildMember { communicationDisabledUntil?: number; } +/** + * @link https://discord.com/developers/docs/resources/guild#begin-guild-prune + * */ +export interface BeginGuildPrune { + days?: number; + computePruneCount?: boolean; + includeRoles?: Snowflake[]; +} + /** * Represents a guild * @link https://discord.com/developers/docs/resources/guild#guild-object @@ -261,7 +268,7 @@ export class Guild extends BaseGuild implements Model { const member = await this.session.rest.runMethod( this.session.rest, "PATCH", - Routes.GUILD_MEMBER(this.id, memberId) + Routes.GUILD_MEMBER(this.id, memberId), { nick: options.nick, roles: options.roles, @@ -269,11 +276,36 @@ export class Guild extends BaseGuild implements Model { deaf: options.deaf, channel_id: options.channelId, communication_disabled_until: options.communicationDisabledUntil ? new Date(options.communicationDisabledUntil).toISOString() : undefined, - }, + } ); return new Member(this.session, member, this.id); } + + async pruneMembers(options: BeginGuildPrune): Promise { + const result = await this.session.rest.runMethod<{ pruned: number }>( + this.session.rest, + "POST", + Routes.GUILD_PRUNE(this.id), + { + days: options.days, + compute_prune_count: options.computePruneCount, + include_roles: options.includeRoles, + }, + ); + + return result.pruned; + } + + async getPruneCount(): Promise { + const result = await this.session.rest.runMethod<{ pruned: number }>( + this.session.rest, + "GET", + Routes.GUILD_PRUNE(this.id), + ); + + return result.pruned; + } } export default Guild; diff --git a/structures/Role.ts b/structures/Role.ts index b8a53cf..be1f878 100644 --- a/structures/Role.ts +++ b/structures/Role.ts @@ -48,7 +48,6 @@ export class Role implements Model { } async delete(): Promise { - // cool jS trick await Guild.prototype.deleteRole.call({ id: this.guildId, session: this.session }, this.id); } diff --git a/util/Routes.ts b/util/Routes.ts index 9ebfcb6..34638c7 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -147,10 +147,8 @@ export function INTERACTION_ID_TOKEN(interactionId: Snowflake, token: string) { export function WEBHOOK(webhookId: Snowflake, token: string, options?: { wait?: boolean; threadId?: Snowflake }) { let url = `/webhooks/${webhookId}/${token}?`; - if (options) { - if (options.wait !== undefined) url += `wait=${options.wait}`; - if (options.threadId) url += `threadId=${options.threadId}`; - } + if (options?.wait !== undefined) url += `wait=${options.wait}`; + if (options?.threadId) url += `threadId=${options.threadId}`; return url; } @@ -158,3 +156,20 @@ export function WEBHOOK(webhookId: Snowflake, token: string, options?: { wait?: export function USER_NICK(guildId: Snowflake) { return `/guilds/${guildId}/members/@me`; } + +/** + * @link https://discord.com/developers/docs/resources/guild#get-guild-prune-count + * */ +export interface GetGuildPruneCountQuery { + days?: number; + includeRoles?: Snowflake | Snowflake[]; +} + +export function GUILD_PRUNE(guildId: Snowflake, options?: GetGuildPruneCountQuery) { + let url = `/guilds/${guildId}/prune?`; + + if (options?.days) url += `days=${options.days}`; + if (options?.includeRoles) url += `&include_roles=${options.includeRoles}`; + + return url; +} From bc324f5d29670dd1e211dcc1904597eb012498d4 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 11:16:22 -0500 Subject: [PATCH 10/22] feat: Guild.unbanMember --- structures/Guild.ts | 11 +++++++++++ structures/Member.ts | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/structures/Guild.ts b/structures/Guild.ts index 80742ca..2f12982 100644 --- a/structures/Guild.ts +++ b/structures/Guild.ts @@ -264,6 +264,17 @@ export class Guild extends BaseGuild implements Model { ); } + /* + * Unbans the member + * */ + async unbanMember(memberId: Snowflake) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.GUILD_BAN(this.id, memberId), + ); + } + async editMember(memberId: Snowflake, options: ModifyGuildMember) { const member = await this.session.rest.runMethod( this.session.rest, diff --git a/structures/Member.ts b/structures/Member.ts index f19a192..4603599 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -68,6 +68,10 @@ export class Member implements Model { return this; } + async unban() { + await Guild.prototype.unbanMember.call({ id: this.guildId, session: this.session }, this.user.id); + } + async edit(options: ModifyGuildMember): Promise { const member = await Guild.prototype.editMember.call({ id: this.guildId, session: this.session }, this.user.id, options); From 6e57f95a4de0c5ffc4ac946e8e63fce7d89b8b37 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 13:16:46 -0500 Subject: [PATCH 11/22] feat: VoiceChannel.connect --- structures/VoiceChannel.ts | 41 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/structures/VoiceChannel.ts b/structures/VoiceChannel.ts index 30a4d50..2c65c9f 100644 --- a/structures/VoiceChannel.ts +++ b/structures/VoiceChannel.ts @@ -1,16 +1,31 @@ import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { DiscordChannel, VideoQualityModes } from "../vendor/external.ts"; +import { GatewayOpcodes } from "../vendor/external.ts"; +import { calculateShardId } from "../vendor/gateway/calculateShardId.ts"; import GuildChannel from "./GuildChannel.ts"; +/** + * @link https://discord.com/developers/docs/topics/gateway#update-voice-state + * */ +export interface UpdateVoiceState { + guildId: string; + channelId?: string; + selfMute: boolean; + selfDeaf: boolean; +} + export class VoiceChannel extends GuildChannel { - constructor(session: Session, guildId: Snowflake, data: DiscordChannel) { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { super(session, data, guildId); this.bitRate = data.bitrate; this.userLimit = data.user_limit ?? 0; - data.rtc_region ? this.rtcRegion = data.rtc_region : undefined; this.videoQuality = data.video_quality_mode; this.nsfw = !!data.nsfw; + + if (data.rtc_region) { + this.rtcRegion = data.rtc_region; + } } bitRate?: number; userLimit: number; @@ -18,6 +33,28 @@ export class VoiceChannel extends GuildChannel { videoQuality?: VideoQualityModes; nsfw: boolean; + + /** + * This function was gathered from Discordeno it may not work + * */ + async connect(options?: UpdateVoiceState) { + const shardId = calculateShardId(this.session.gateway, BigInt(super.guildId)); + const shard = this.session.gateway.manager.shards.get(shardId); + + if (!shard) { + throw new Error(`Shard (id: ${shardId} not found`); + } + + await shard.send({ + op: GatewayOpcodes.VoiceStateUpdate, + d: { + guild_id: super.guildId, + channel_id: super.id, + self_mute: Boolean(options?.selfMute), + self_deaf: options?.selfDeaf ?? true, + }, + }); + } } export default VoiceChannel; From 486fcbe2896a4dc530a554833126f005d1e89788 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 13:28:59 -0500 Subject: [PATCH 12/22] feat: Message.pin/unpin TextChannel.pinMessage/unpinMessage --- structures/Guild.ts | 28 ++++++++++++++++++---------- structures/Member.ts | 6 +++++- structures/Message.ts | 16 ++++++++++++++++ structures/TextChannel.ts | 8 ++++++++ structures/VoiceChannel.ts | 12 ++++++------ util/Routes.ts | 14 +++++++++----- 6 files changed, 62 insertions(+), 22 deletions(-) diff --git a/structures/Guild.ts b/structures/Guild.ts index 2f12982..1fde9c6 100644 --- a/structures/Guild.ts +++ b/structures/Guild.ts @@ -1,7 +1,13 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { DiscordEmoji, DiscordGuild, DiscordMemberWithUser, DiscordInviteMetadata, DiscordRole } from "../vendor/external.ts"; +import type { + DiscordEmoji, + DiscordGuild, + DiscordInviteMetadata, + DiscordMemberWithUser, + DiscordRole, +} from "../vendor/external.ts"; import type { GetInvite } from "../util/Routes.ts"; import { DefaultMessageNotificationLevels, @@ -56,7 +62,7 @@ export interface CreateGuildBan { /** * @link https://discord.com/developers/docs/resources/guild#modify-guild-member - * */ + */ export interface ModifyGuildMember { nick?: string; roles?: Snowflake[]; @@ -68,11 +74,11 @@ export interface ModifyGuildMember { /** * @link https://discord.com/developers/docs/resources/guild#begin-guild-prune - * */ + */ export interface BeginGuildPrune { - days?: number; - computePruneCount?: boolean; - includeRoles?: Snowflake[]; + days?: number; + computePruneCount?: boolean; + includeRoles?: Snowflake[]; } /** @@ -110,8 +116,8 @@ export class Guild extends BaseGuild implements Model { emojis: GuildEmoji[]; /** - * 'null' would reset the nickname - * */ + * 'null' would reset the nickname + */ async editBotNickname(options: { nick: string | null; reason?: string }) { const result = await this.session.rest.runMethod<{ nick?: string } | undefined>( this.session.rest, @@ -286,8 +292,10 @@ export class Guild extends BaseGuild implements Model { mute: options.mute, deaf: options.deaf, channel_id: options.channelId, - communication_disabled_until: options.communicationDisabledUntil ? new Date(options.communicationDisabledUntil).toISOString() : undefined, - } + communication_disabled_until: options.communicationDisabledUntil + ? new Date(options.communicationDisabledUntil).toISOString() + : undefined, + }, ); return new Member(this.session, member, this.id); diff --git a/structures/Member.ts b/structures/Member.ts index 4603599..352331a 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -73,7 +73,11 @@ export class Member implements Model { } async edit(options: ModifyGuildMember): Promise { - const member = await Guild.prototype.editMember.call({ id: this.guildId, session: this.session }, this.user.id, options); + const member = await Guild.prototype.editMember.call( + { id: this.guildId, session: this.session }, + this.user.id, + options, + ); return member; } diff --git a/structures/Message.ts b/structures/Message.ts index 0505daf..98c9819 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -87,6 +87,22 @@ export class Message implements Model { return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`; } + async pin() { + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.CHANNEL_PIN(this.channelId, this.id), + ); + } + + async unpin() { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.CHANNEL_PIN(this.channelId, this.id), + ); + } + /** Edits the current message */ async edit({ content, allowedMentions, flags }: EditMessage): Promise { const message = await this.session.rest.runMethod( diff --git a/structures/TextChannel.ts b/structures/TextChannel.ts index a24dc29..aa090a2 100644 --- a/structures/TextChannel.ts +++ b/structures/TextChannel.ts @@ -107,6 +107,14 @@ export class TextChannel extends GuildChannel { 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 }); + } } export default TextChannel; diff --git a/structures/VoiceChannel.ts b/structures/VoiceChannel.ts index 2c65c9f..fa26b7e 100644 --- a/structures/VoiceChannel.ts +++ b/structures/VoiceChannel.ts @@ -7,12 +7,12 @@ 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; + guildId: string; + channelId?: string; + selfMute: boolean; + selfDeaf: boolean; } export class VoiceChannel extends GuildChannel { @@ -36,7 +36,7 @@ export class VoiceChannel extends GuildChannel { /** * 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); diff --git a/util/Routes.ts b/util/Routes.ts index 34638c7..7004e3f 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -46,10 +46,6 @@ export function MESSAGE_CREATE_THREAD(channelId: Snowflake, messageId: Snowflake return `/channels/${channelId}/messages/${messageId}/threads`; } -export function CHANNEL_PINS(channelId: Snowflake) { - return `/channels/${channelId}/pins`; -} - /** used to send messages */ export function CHANNEL_MESSAGES(channelId: Snowflake, options?: GetMessagesOptions) { let url = `/channels/${channelId}/messages?`; @@ -159,7 +155,7 @@ export function USER_NICK(guildId: Snowflake) { /** * @link https://discord.com/developers/docs/resources/guild#get-guild-prune-count - * */ + */ export interface GetGuildPruneCountQuery { days?: number; includeRoles?: Snowflake | Snowflake[]; @@ -173,3 +169,11 @@ export function GUILD_PRUNE(guildId: Snowflake, options?: GetGuildPruneCountQuer return url; } + +export function CHANNEL_PIN(channelId: Snowflake, messageId: Snowflake) { + return `/channels/${channelId}/pins/${messageId}`; +} + +export function CHANNEL_PINS(channelId: Snowflake) { + return `/channels/${channelId}/pins`; +} From 050d5a39735f9b49cd51a1517e6bed798933a2dd Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 14:10:25 -0500 Subject: [PATCH 13/22] feat: all reaction-related endpoints --- structures/Message.ts | 76 ++++++++++++++++++++++++++++++++++++++- structures/TextChannel.ts | 25 ++++++++++++- util/Routes.ts | 30 ++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) diff --git a/structures/Message.ts b/structures/Message.ts index 98c9819..a84722f 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -1,7 +1,8 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { AllowedMentionsTypes, DiscordMessage, FileContent } from "../vendor/external.ts"; +import type { AllowedMentionsTypes, DiscordMessage, DiscordUser, FileContent } from "../vendor/external.ts"; +import type { GetReactions } from "../util/Routes.ts"; import { MessageFlags } from "../util/shared/flags.ts"; import User from "./User.ts"; import Member from "./Member.ts"; @@ -42,6 +43,11 @@ export interface EditMessage extends Partial { flags?: MessageFlags; } +export type ReactionResolvable = string | { + name: string; + id: Snowflake; +}; + /** * Represents a message * @link https://discord.com/developers/docs/resources/channel#message-object @@ -176,6 +182,74 @@ export class Message implements Model { return new Message(this.session, message); } + /** + * alias for Message.addReaction + * */ + get react() { + return this.addReaction; + } + + async addReaction(reaction: ReactionResolvable) { + const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`; + + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.CHANNEL_MESSAGE_REACTION_ME(this.channelId, this.id, r), + {}, + ); + } + + async removeReaction(reaction: ReactionResolvable, options?: { userId: Snowflake }) { + const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`; + + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + options?.userId + ? Routes.CHANNEL_MESSAGE_REACTION_USER( + this.channelId, + this.id, + r, + options.userId, + ) + : Routes.CHANNEL_MESSAGE_REACTION_ME(this.channelId, this.id, r) + ); + } + + /** + * Get users who reacted with this emoji + * */ + async fetchReactions(reaction: ReactionResolvable, options?: GetReactions): Promise { + const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`; + + const users = await this.session.rest.runMethod( + this.session.rest, + "GET", + Routes.CHANNEL_MESSAGE_REACTION(this.channelId, this.id, encodeURIComponent(r), options), + ); + + return users.map((user) => new User(this.session, user)); + } + + async removeReactionEmoji(reaction: ReactionResolvable) { + const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`; + + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.CHANNEL_MESSAGE_REACTION(this.channelId, this.id, r) + ); + } + + async nukeReactions() { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.CHANNEL_MESSAGE_REACTIONS(this.channelId, this.id) + ); + } + inGuild(): this is { guildId: Snowflake } & Message { return !!this.guildId; } diff --git a/structures/TextChannel.ts b/structures/TextChannel.ts index aa090a2..a332079 100644 --- a/structures/TextChannel.ts +++ b/structures/TextChannel.ts @@ -1,7 +1,8 @@ import type { Session } from "../session/Session.ts"; import type { Snowflake } from "../util/Snowflake.ts"; -import type { GetMessagesOptions } from "../util/Routes.ts"; +import type { GetMessagesOptions, GetReactions } from "../util/Routes.ts"; import type { DiscordChannel, DiscordInvite, DiscordMessage, TargetTypes } from "../vendor/external.ts"; +import type { ReactionResolvable } from "./Message.ts"; import GuildChannel from "./GuildChannel.ts"; import Guild from "./Guild.ts"; import ThreadChannel from "./ThreadChannel.ts"; @@ -115,6 +116,28 @@ export class TextChannel extends GuildChannel { 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 }, reaction); + } + + async removeReaction(messageId: Snowflake, reaction: ReactionResolvable, options?: { userId: Snowflake }) { + await Message.prototype.removeReaction.call({ channelId: this.id, id: messageId }, reaction, options); + } + + async removeReactionEmoji(messageId: Snowflake, reaction: ReactionResolvable) { + await Message.prototype.removeReactionEmoji.call({ channelId: this.id, id: messageId }, reaction); + } + + async nukeReactions(messageId: Snowflake) { + await Message.prototype.nukeReactions.call({ channelId: this.id, id: messageId }); + } + + async fetchReactions(messageId: Snowflake, reaction: ReactionResolvable, options?: GetReactions) { + const users = await Message.prototype.fetchReactions.call({ channelId: this.id, id: messageId }, reaction, options); + + return users; + } } export default TextChannel; diff --git a/util/Routes.ts b/util/Routes.ts index 7004e3f..9204472 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -177,3 +177,33 @@ export function CHANNEL_PIN(channelId: Snowflake, messageId: Snowflake) { export function CHANNEL_PINS(channelId: Snowflake) { return `/channels/${channelId}/pins`; } + + +export function CHANNEL_MESSAGE_REACTION_ME(channelId: Snowflake, messageId: Snowflake, emoji: string) { + return `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`; +} + +export function CHANNEL_MESSAGE_REACTION_USER(channelId: Snowflake, messageId: Snowflake, emoji: string, userId: Snowflake) { + return `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/${userId}`; +} + +export function CHANNEL_MESSAGE_REACTIONS(channelId: Snowflake, messageId: Snowflake) { + return `/channels/${channelId}/messages/${messageId}/reactions`; +} + +/** + * @link https://discord.com/developers/docs/resources/channel#get-reactions-query-string-params + * */ +export interface GetReactions { + after?: string; + limit?: number; +} + +export function CHANNEL_MESSAGE_REACTION(channelId: Snowflake, messageId: Snowflake, emoji: string, options?: GetReactions) { + let url = `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}?`; + + if (options?.after) url += `after=${options.after}`; + if (options?.limit) url += `&limit=${options.limit}`; + + return url; +} From c6e6a6392eb181760834f77d6284055325fc944f Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 14:31:43 -0500 Subject: [PATCH 14/22] feat: Message.crosspost and fix latest commit --- structures/Message.ts | 17 +++++++++++++++++ structures/NewsChannel.ts | 9 +++++++++ structures/TextChannel.ts | 8 ++++---- util/Routes.ts | 4 ++++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/structures/Message.ts b/structures/Message.ts index a84722f..500185f 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -250,6 +250,23 @@ export class Message implements Model { ); } + async crosspost() { + const message = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_MESSAGE_CROSSPOST(this.channelId, this.id), + ); + + return new Message(this.session, message); + } + + /* + * alias of Message.crosspost + * */ + get publish() { + return this.crosspost; + } + inGuild(): this is { guildId: Snowflake } & Message { return !!this.guildId; } diff --git a/structures/NewsChannel.ts b/structures/NewsChannel.ts index fcd15aa..ef76049 100644 --- a/structures/NewsChannel.ts +++ b/structures/NewsChannel.ts @@ -2,6 +2,7 @@ import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { DiscordChannel } from "../vendor/external.ts"; import TextChannel from "./TextChannel.ts"; +import Message from "./Message.ts"; export class NewsChannel extends TextChannel { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { @@ -9,6 +10,14 @@ export class NewsChannel extends TextChannel { this.defaultAutoArchiveDuration = data.default_auto_archive_duration; } defaultAutoArchiveDuration?: number; + + crosspostMessage(messageId: Snowflake): Promise { + return Message.prototype.crosspost.call({ id: messageId, channelId: this.id, session: this.session }); + } + + get publishMessage() { + return this.crosspostMessage; + } } export default NewsChannel; diff --git a/structures/TextChannel.ts b/structures/TextChannel.ts index a332079..e5e9d1e 100644 --- a/structures/TextChannel.ts +++ b/structures/TextChannel.ts @@ -118,15 +118,15 @@ export class TextChannel extends GuildChannel { } async addReaction(messageId: Snowflake, reaction: ReactionResolvable) { - await Message.prototype.addReaction.call({ channelId: this.id, id: messageId }, reaction); + 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 }, reaction, options); + 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 }, reaction); + await Message.prototype.removeReactionEmoji.call({ channelId: this.id, id: messageId, session: this.session }, reaction); } async nukeReactions(messageId: Snowflake) { @@ -134,7 +134,7 @@ export class TextChannel extends GuildChannel { } async fetchReactions(messageId: Snowflake, reaction: ReactionResolvable, options?: GetReactions) { - const users = await Message.prototype.fetchReactions.call({ channelId: this.id, id: messageId }, reaction, options); + const users = await Message.prototype.fetchReactions.call({ channelId: this.id, id: messageId, session: this.session }, reaction, options); return users; } diff --git a/util/Routes.ts b/util/Routes.ts index 9204472..a53b7de 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -207,3 +207,7 @@ export function CHANNEL_MESSAGE_REACTION(channelId: Snowflake, messageId: Snowfl return url; } + +export function CHANNEL_MESSAGE_CROSSPOST(channelId: Snowflake, messageId: Snowflake) { + return `/channels/${channelId}/messages/${messageId}/crosspost`; +} From 852e4d58c6f15c8c7709badc6030786e1467ed06 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 14:38:48 -0500 Subject: [PATCH 15/22] dry code a bit --- structures/DMChannel.ts | 15 +++++++++++---- structures/TextChannel.ts | 3 +-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/structures/DMChannel.ts b/structures/DMChannel.ts index 077514f..d3407f1 100644 --- a/structures/DMChannel.ts +++ b/structures/DMChannel.ts @@ -1,16 +1,23 @@ +import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { DiscordChannel } from "../vendor/external.ts"; import Channel from "./Channel.ts"; +import User from "./User.ts"; import * as Routes from "../util/Routes.ts"; -export class DMChannel extends Channel { +export class DMChannel extends Channel implements Model { constructor(session: Session, data: DiscordChannel) { super(session, data); - data.last_message_id ? this.lastMessageId = data.last_message_id : undefined; - // Waiting for implementation of botId in session - //this.user = new User(this.session, data.recipents!.find((r) => r.id !== this.session.botId)); + + this.user = new User(this.session, data.recipents!.find((r) => r.id !== this.session.botId)!); + + if (data.last_message_id) { + this.lastMessageId = data.last_message_id; + } } + + user: User; lastMessageId?: Snowflake; async close() { diff --git a/structures/TextChannel.ts b/structures/TextChannel.ts index e5e9d1e..bbc9bd2 100644 --- a/structures/TextChannel.ts +++ b/structures/TextChannel.ts @@ -4,7 +4,6 @@ import type { GetMessagesOptions, GetReactions } from "../util/Routes.ts"; import type { DiscordChannel, DiscordInvite, DiscordMessage, TargetTypes } from "../vendor/external.ts"; import type { ReactionResolvable } from "./Message.ts"; import GuildChannel from "./GuildChannel.ts"; -import Guild from "./Guild.ts"; import ThreadChannel from "./ThreadChannel.ts"; import Message from "./Message.ts"; import Invite from "./Invite.ts"; @@ -38,7 +37,7 @@ export interface ThreadCreateOptions { } export class TextChannel extends GuildChannel { - constructor(session: Session, data: DiscordChannel, guildId: Guild["id"]) { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { super(session, data, guildId); data.last_message_id ? this.lastMessageId = data.last_message_id : undefined; data.last_pin_timestamp ? this.lastPinTimestamp = data.last_pin_timestamp : undefined; From 5b98c593afc595ba916868092b963f973f25613c Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 14:44:38 -0500 Subject: [PATCH 16/22] add interaction timeouts --- handlers/Actions.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index 6174828..e556d0b 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -47,6 +47,10 @@ export const GUILD_MEMBER_REMOVE: RawHandler = (sessio 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)); }; From dba2de730501adc66f309491590511fe45db567b Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 14:47:51 -0500 Subject: [PATCH 17/22] fix: fmt --- structures/Message.ts | 10 +++++----- structures/TextChannel.ts | 22 ++++++++++++++++++---- util/Routes.ts | 21 +++++++++++++++------ 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/structures/Message.ts b/structures/Message.ts index 500185f..da8b573 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -184,7 +184,7 @@ export class Message implements Model { /** * alias for Message.addReaction - * */ + */ get react() { return this.addReaction; } @@ -213,13 +213,13 @@ export class Message implements Model { r, options.userId, ) - : Routes.CHANNEL_MESSAGE_REACTION_ME(this.channelId, this.id, r) + : Routes.CHANNEL_MESSAGE_REACTION_ME(this.channelId, this.id, r), ); } /** * Get users who reacted with this emoji - * */ + */ async fetchReactions(reaction: ReactionResolvable, options?: GetReactions): Promise { const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`; @@ -238,7 +238,7 @@ export class Message implements Model { await this.session.rest.runMethod( this.session.rest, "DELETE", - Routes.CHANNEL_MESSAGE_REACTION(this.channelId, this.id, r) + Routes.CHANNEL_MESSAGE_REACTION(this.channelId, this.id, r), ); } @@ -246,7 +246,7 @@ export class Message implements Model { await this.session.rest.runMethod( this.session.rest, "DELETE", - Routes.CHANNEL_MESSAGE_REACTIONS(this.channelId, this.id) + Routes.CHANNEL_MESSAGE_REACTIONS(this.channelId, this.id), ); } diff --git a/structures/TextChannel.ts b/structures/TextChannel.ts index bbc9bd2..7bade2f 100644 --- a/structures/TextChannel.ts +++ b/structures/TextChannel.ts @@ -117,15 +117,25 @@ export class TextChannel extends GuildChannel { } async addReaction(messageId: Snowflake, reaction: ReactionResolvable) { - await Message.prototype.addReaction.call({ channelId: this.id, id: messageId, session: this.session }, reaction); + 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); + 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); + await Message.prototype.removeReactionEmoji.call( + { channelId: this.id, id: messageId, session: this.session }, + reaction, + ); } async nukeReactions(messageId: Snowflake) { @@ -133,7 +143,11 @@ export class TextChannel extends GuildChannel { } async fetchReactions(messageId: Snowflake, reaction: ReactionResolvable, options?: GetReactions) { - const users = await Message.prototype.fetchReactions.call({ channelId: this.id, id: messageId, session: this.session }, reaction, options); + const users = await Message.prototype.fetchReactions.call( + { channelId: this.id, id: messageId, session: this.session }, + reaction, + options, + ); return users; } diff --git a/util/Routes.ts b/util/Routes.ts index a53b7de..492d95b 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -178,12 +178,16 @@ export function CHANNEL_PINS(channelId: Snowflake) { return `/channels/${channelId}/pins`; } - export function CHANNEL_MESSAGE_REACTION_ME(channelId: Snowflake, messageId: Snowflake, emoji: string) { return `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`; } -export function CHANNEL_MESSAGE_REACTION_USER(channelId: Snowflake, messageId: Snowflake, emoji: string, userId: Snowflake) { +export function CHANNEL_MESSAGE_REACTION_USER( + channelId: Snowflake, + messageId: Snowflake, + emoji: string, + userId: Snowflake, +) { return `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/${userId}`; } @@ -193,13 +197,18 @@ export function CHANNEL_MESSAGE_REACTIONS(channelId: Snowflake, messageId: Snowf /** * @link https://discord.com/developers/docs/resources/channel#get-reactions-query-string-params - * */ + */ export interface GetReactions { - after?: string; - limit?: number; + after?: string; + limit?: number; } -export function CHANNEL_MESSAGE_REACTION(channelId: Snowflake, messageId: Snowflake, emoji: string, options?: GetReactions) { +export function CHANNEL_MESSAGE_REACTION( + channelId: Snowflake, + messageId: Snowflake, + emoji: string, + options?: GetReactions, +) { let url = `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}?`; if (options?.after) url += `after=${options.after}`; From 37f4071da66fed2f4db313bf3a1204b8d70f372b Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 16:04:58 -0500 Subject: [PATCH 18/22] feat: Embed struct --- mod.ts | 1 + structures/Embed.ts | 102 ++++++++++++++++++++++++++++++++++++++++++ structures/Message.ts | 19 ++++---- 3 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 structures/Embed.ts diff --git a/mod.ts b/mod.ts index 559e08d..dc393a4 100644 --- a/mod.ts +++ b/mod.ts @@ -5,6 +5,7 @@ export * from "./structures/BaseGuild.ts"; export * from "./structures/Channel.ts"; export * from "./structures/Component.ts"; export * from "./structures/DMChannel.ts"; +export * from "./structures/Embed.ts"; export * from "./structures/Emoji.ts"; export * from "./structures/Guild.ts"; export * from "./structures/GuildChannel.ts"; diff --git a/structures/Embed.ts b/structures/Embed.ts new file mode 100644 index 0000000..2d5e624 --- /dev/null +++ b/structures/Embed.ts @@ -0,0 +1,102 @@ +import type { DiscordEmbed, EmbedTypes } from "../vendor/external.ts"; + +export interface Embed { + title?: string; + timestamp?: string; + type?: EmbedTypes; + url?: string; + color?: number; + description?: string; + author?: { + name: string; + iconURL?: string; + proxyIconURL?: string; + url?: string; + }; + footer?: { + text: string; + iconURL?: string; + proxyIconURL?: string; + }; + fields?: Array<{ + name: string; + value: string; + inline?: boolean; + }>; + thumbnail?: { + url: string; + proxyURL?: string; + width?: number; + height?: number; + }; + video?: { + url?: string; + proxyURL?: string; + width?: number; + height?: number; + }; + image?: { + url: string; + proxyURL?: string; + width?: number; + height?: number; + }; + provider?: { + url?: string; + name?: string; + } + +} + +export function embed(data: Embed): DiscordEmbed { + return { + title: data.title, + timestamp: data.timestamp, + type: data.type, + url: data.url, + color: data.color, + description: data.description, + author: { + name: data.author?.name!, + url: data.author?.url, + icon_url: data.author?.iconURL, + proxy_icon_url: data.author?.proxyIconURL, + }, + footer: data.footer || { + text: data.footer!.text, + icon_url: data.footer!.iconURL, + proxy_icon_url: data.footer!.proxyIconURL, + }, + fields: data.fields?.map((f) => { + return { + name: f.name, + value: f.value, + inline: f.inline, + }; + }), + thumbnail: data.thumbnail || { + url: data.thumbnail!.url, + proxy_url: data.thumbnail!.proxyURL, + width: data.thumbnail!.width, + height: data.thumbnail!.height, + }, + video: { + url: data.video?.url, + proxy_url: data.video?.proxyURL, + width: data.video?.width, + height: data.video?.height, + }, + image: data.image || { + url: data.image!.url, + proxy_url: data.image!.proxyURL, + width: data.image!.width, + height: data.image!.height, + }, + provider: { + url: data.provider?.url, + name: data.provider?.name, + }, + }; +} + +export default Embed; diff --git a/structures/Message.ts b/structures/Message.ts index da8b573..483f661 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -1,7 +1,7 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { AllowedMentionsTypes, DiscordMessage, DiscordUser, FileContent } from "../vendor/external.ts"; +import type { AllowedMentionsTypes, DiscordEmbed, DiscordMessage, DiscordUser, FileContent } from "../vendor/external.ts"; import type { GetReactions } from "../util/Routes.ts"; import { MessageFlags } from "../util/shared/flags.ts"; import User from "./User.ts"; @@ -34,6 +34,7 @@ export interface CreateMessage { allowedMentions?: AllowedMentions; files?: FileContent[]; messageReference?: CreateMessageReference; + embeds?: DiscordEmbed[]; } /** @@ -110,20 +111,21 @@ export class Message implements Model { } /** Edits the current message */ - async edit({ content, allowedMentions, flags }: EditMessage): Promise { + async edit(options: EditMessage): Promise { const message = await this.session.rest.runMethod( this.session.rest, "POST", Routes.CHANNEL_MESSAGE(this.id, this.channelId), { - content, + content: options.content, allowed_mentions: { - parse: allowedMentions?.parse, - roles: allowedMentions?.roles, - users: allowedMentions?.users, - replied_user: allowedMentions?.repliedUser, + parse: options.allowedMentions?.parse, + roles: options.allowedMentions?.roles, + users: options.allowedMentions?.users, + replied_user: options.allowedMentions?.repliedUser, }, - flags, + flags: options.flags, + embeds: options.embeds, }, ); @@ -176,6 +178,7 @@ export class Message implements Model { fail_if_not_exists: options.messageReference.failIfNotExists ?? true, } : undefined, + embeds: options.embeds, }, ); From 3f714eb295c16a59b6fe67c478aed5aaa453b900 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 16:10:56 -0500 Subject: [PATCH 19/22] feat: TextChannel.sendMessage/editMessage --- structures/Embed.ts | 3 +-- structures/Message.ts | 8 +++++++- structures/TextChannel.ts | 10 +++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/structures/Embed.ts b/structures/Embed.ts index 2d5e624..19b5de3 100644 --- a/structures/Embed.ts +++ b/structures/Embed.ts @@ -44,8 +44,7 @@ export interface Embed { provider?: { url?: string; name?: string; - } - + }; } export function embed(data: Embed): DiscordEmbed { diff --git a/structures/Message.ts b/structures/Message.ts index 483f661..65f3c35 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -1,7 +1,13 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; -import type { AllowedMentionsTypes, DiscordEmbed, DiscordMessage, DiscordUser, FileContent } from "../vendor/external.ts"; +import type { + AllowedMentionsTypes, + DiscordEmbed, + DiscordMessage, + DiscordUser, + FileContent, +} from "../vendor/external.ts"; import type { GetReactions } from "../util/Routes.ts"; import { MessageFlags } from "../util/shared/flags.ts"; import User from "./User.ts"; diff --git a/structures/TextChannel.ts b/structures/TextChannel.ts index 7bade2f..7ecd39b 100644 --- a/structures/TextChannel.ts +++ b/structures/TextChannel.ts @@ -2,7 +2,7 @@ import type { Session } from "../session/Session.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { GetMessagesOptions, GetReactions } from "../util/Routes.ts"; import type { DiscordChannel, DiscordInvite, DiscordMessage, TargetTypes } from "../vendor/external.ts"; -import type { ReactionResolvable } from "./Message.ts"; +import type { CreateMessage, EditMessage, ReactionResolvable } from "./Message.ts"; import GuildChannel from "./GuildChannel.ts"; import ThreadChannel from "./ThreadChannel.ts"; import Message from "./Message.ts"; @@ -151,6 +151,14 @@ export class TextChannel extends GuildChannel { 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); + } } export default TextChannel; From b99218bdc3916842a5b3a047431c45af06cfe568 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 16:18:59 -0500 Subject: [PATCH 20/22] refactor: Channel -> BaseChannel --- mod.ts | 2 +- structures/BaseChannel.ts | 49 ++++++++++++++++++++++++++++++++++++++ structures/Channel.ts | 24 ------------------- structures/DMChannel.ts | 4 ++-- structures/GuildChannel.ts | 4 ++-- 5 files changed, 54 insertions(+), 29 deletions(-) create mode 100644 structures/BaseChannel.ts delete mode 100644 structures/Channel.ts diff --git a/mod.ts b/mod.ts index dc393a4..8e72101 100644 --- a/mod.ts +++ b/mod.ts @@ -2,7 +2,7 @@ export * from "./structures/AnonymousGuild.ts"; export * from "./structures/Attachment.ts"; export * from "./structures/Base.ts"; export * from "./structures/BaseGuild.ts"; -export * from "./structures/Channel.ts"; +export * from "./structures/BaseChannel.ts"; export * from "./structures/Component.ts"; export * from "./structures/DMChannel.ts"; export * from "./structures/Embed.ts"; diff --git a/structures/BaseChannel.ts b/structures/BaseChannel.ts new file mode 100644 index 0000000..a0ed80b --- /dev/null +++ b/structures/BaseChannel.ts @@ -0,0 +1,49 @@ +import type { Model } from "./Base.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { Session } from "../session/Session.ts"; +import type { ChannelTypes, DiscordChannel } from "../vendor/external.ts"; +import TextChannel from "./TextChannel.ts"; +import VoiceChannel from "./VoiceChannel.ts"; +import DMChannel from "./DMChannel.ts"; +import NewsChannel from "./NewsChannel.ts"; +import ThreadChannel from "./ThreadChannel.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 this instanceof TextChannel; + } + + isVoice(): this is VoiceChannel { + return this instanceof VoiceChannel; + } + + isDM(): this is DMChannel { + return this instanceof DMChannel; + } + + isNews(): this is NewsChannel { + return this instanceof NewsChannel; + } + + isThread(): this is ThreadChannel { + return this instanceof ThreadChannel; + } + + toString(): string { + return `<#${this.id}>`; + } +} + +export default BaseChannel; diff --git a/structures/Channel.ts b/structures/Channel.ts deleted file mode 100644 index daf18ac..0000000 --- a/structures/Channel.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Model } from "./Base.ts"; -import type { Snowflake } from "../util/Snowflake.ts"; -import type { Session } from "../session/Session.ts"; -import type { ChannelTypes, DiscordChannel } from "../vendor/external.ts"; - -export abstract class Channel 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; - - toString(): string { - return `<#${this.id}>`; - } -} - -export default Channel; diff --git a/structures/DMChannel.ts b/structures/DMChannel.ts index d3407f1..d13bd5d 100644 --- a/structures/DMChannel.ts +++ b/structures/DMChannel.ts @@ -2,11 +2,11 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { DiscordChannel } from "../vendor/external.ts"; -import Channel from "./Channel.ts"; +import BaseChannel from "./BaseChannel.ts"; import User from "./User.ts"; import * as Routes from "../util/Routes.ts"; -export class DMChannel extends Channel implements Model { +export class DMChannel extends BaseChannel implements Model { constructor(session: Session, data: DiscordChannel) { super(session, data); diff --git a/structures/GuildChannel.ts b/structures/GuildChannel.ts index 99ea4ff..73dfd3b 100644 --- a/structures/GuildChannel.ts +++ b/structures/GuildChannel.ts @@ -2,11 +2,11 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/Session.ts"; import type { DiscordChannel, DiscordInviteMetadata } from "../vendor/external.ts"; -import Channel from "./Channel.ts"; +import BaseChannel from "./BaseChannel.ts"; import Invite from "./Invite.ts"; import * as Routes from "../util/Routes.ts"; -export abstract class GuildChannel extends Channel implements Model { +export abstract class GuildChannel extends BaseChannel implements Model { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { super(session, data); this.guildId = guildId; From 5b0d12438029169d3fca994d7e357cf7800c6b70 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 16:50:16 -0500 Subject: [PATCH 21/22] feat: all role-related endpoints --- structures/Guild.ts | 39 +++++++++++++++++++++++++++++++++++++++ structures/Member.ts | 8 ++++++++ structures/Role.ts | 8 ++++++++ util/Routes.ts | 4 ++++ 4 files changed, 59 insertions(+) diff --git a/structures/Guild.ts b/structures/Guild.ts index 1fde9c6..1013814 100644 --- a/structures/Guild.ts +++ b/structures/Guild.ts @@ -81,6 +81,11 @@ export interface BeginGuildPrune { includeRoles?: Snowflake[]; } +export interface ModifyRolePositions { + id: Snowflake; + position?: number | null; +} + /** * Represents a guild * @link https://discord.com/developers/docs/resources/guild#guild-object @@ -212,6 +217,40 @@ export class Guild extends BaseGuild implements Model { return new Role(this.session, role, this.id); } + + + async addRole(memberId: Snowflake, roleId: Snowflake, { reason }: { reason?: string } = {}) { + await this.session.rest.runMethod( + this.session.rest, + "PUT", + Routes.GUILD_MEMBER_ROLE(this.id, memberId, roleId), + { reason }, + ); + } + + async removeRole(memberId: Snowflake, roleId: Snowflake, { reason }: { reason?: string } = {}) { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.GUILD_MEMBER_ROLE(this.id, memberId, roleId), + { reason }, + ); + } + + /** + * Returns the roles moved + * */ + async moveRoles(options: ModifyRolePositions[]) { + const roles = await this.session.rest.runMethod( + this.session.rest, + "PATCH", + Routes.GUILD_ROLES(this.id), + options, + ); + + return roles.map((role) => new Role(this.session, role, this.id)); + } + async deleteInvite(inviteCode: string): Promise { await this.session.rest.runMethod( this.session.rest, diff --git a/structures/Member.ts b/structures/Member.ts index 352331a..6468ce3 100644 --- a/structures/Member.ts +++ b/structures/Member.ts @@ -82,6 +82,14 @@ export class Member implements Model { return member; } + async addRole(roleId: Snowflake, options: { reason?: string } = {}) { + await Guild.prototype.addRole.call({ id: this.guildId, session: this.session }, this.user.id, roleId, options); + } + + async removeRole(roleId: Snowflake, options: { reason?: string } = {}) { + await Guild.prototype.removeRole.call({ id: this.guildId, session: this.session }, this.user.id, roleId, options); + } + /** gets the user's avatar */ avatarUrl(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }) { let url: string; diff --git a/structures/Role.ts b/structures/Role.ts index be1f878..41a4c97 100644 --- a/structures/Role.ts +++ b/structures/Role.ts @@ -56,6 +56,14 @@ export class Role implements Model { return role; } + async add(memberId: Snowflake, options: { reason?: string } = {}) { + await Guild.prototype.addRole.call({ id: this.guildId, session: this.session }, memberId, this.id, options); + } + + async remove(memberId: Snowflake, options: { reason?: string } = {}) { + await Guild.prototype.removeRole.call({ id: this.guildId, session: this.session }, memberId, this.id, options); + } + toString() { switch (this.id) { case this.guildId: diff --git a/util/Routes.ts b/util/Routes.ts index 492d95b..db2654e 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -220,3 +220,7 @@ export function CHANNEL_MESSAGE_REACTION( export function CHANNEL_MESSAGE_CROSSPOST(channelId: Snowflake, messageId: Snowflake) { return `/channels/${channelId}/messages/${messageId}/crosspost`; } + +export function GUILD_MEMBER_ROLE(guildId: Snowflake, memberId: Snowflake, roleId: Snowflake) { + return `/guilds/${guildId}/members/${memberId}/roles/${roleId}`; +} From d10adfdec392a657cd62c47b489579a9e602a9e4 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 1 Jul 2022 17:10:34 -0500 Subject: [PATCH 22/22] patch: Session.applicationId --- handlers/Actions.ts | 2 ++ session/Session.ts | 9 +++++++++ structures/Interaction.ts | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/handlers/Actions.ts b/handlers/Actions.ts index e556d0b..032853c 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -18,6 +18,8 @@ export type RawHandler = (...args: [Session, number, T]) => void; export type Handler = (...args: T) => unknown; export const READY: RawHandler = (session, shardId, payload) => { + session.applicationId = payload.application.id; + session.botId = payload.user.id; session.emit("ready", { ...payload, user: new User(session, payload.user) }, shardId); }; diff --git a/session/Session.ts b/session/Session.ts index aa7477c..10e74e4 100644 --- a/session/Session.ts +++ b/session/Session.ts @@ -42,6 +42,15 @@ export class Session extends EventEmitter { unrepliedInteractions: Set = new Set(); #botId: Snowflake; + #applicationId?: Snowflake; + + set applicationId(id: Snowflake) { + this.#applicationId = id; + } + + get applicationId() { + return this.#applicationId!; + } set botId(id: Snowflake) { this.#botId = id; diff --git a/structures/Interaction.ts b/structures/Interaction.ts index 59b589c..f11dc99 100644 --- a/structures/Interaction.ts +++ b/structures/Interaction.ts @@ -116,7 +116,7 @@ export class Interaction implements Model { const result = await this.session.rest.sendRequest( this.session.rest, { - url: Routes.WEBHOOK(this.session.botId, this.token), + url: Routes.WEBHOOK(this.session.applicationId ?? this.session.botId, this.token), method: "POST", payload: this.session.rest.createRequestBody(this.session.rest, { method: "POST",