diff --git a/handlers/HandlerManager.ts b/handlers/HandlerManager.ts index 3a95ec7..0721737 100644 --- a/handlers/HandlerManager.ts +++ b/handlers/HandlerManager.ts @@ -1 +1 @@ -export * from "./MessageRelated.ts"; \ No newline at end of file +export * from "./MessageRelated.ts"; diff --git a/handlers/MessageRelated.ts b/handlers/MessageRelated.ts index b03f7b5..afd1a3d 100644 --- a/handlers/MessageRelated.ts +++ b/handlers/MessageRelated.ts @@ -2,17 +2,17 @@ import type { DiscordMessage, DiscordReady } from "../vendor/external.ts"; import type { Session } from "../session/Session.ts"; import { Message } from "../structures/Message.ts"; -type Handler = (...args: [ Session, number, T ]) => void; +type Handler = (...args: [Session, number, T]) => void; // TODO: move this lol export const READY: Handler = (session, shardId, payload) => { - session.emit("ready", shardId, payload); + session.emit("ready", shardId, payload); }; export const MESSAGE_CREATE: Handler = (session, _shardId, message) => { - session.emit("messageCreate", new Message(session, message)); + session.emit("messageCreate", new Message(session, message)); }; export const raw: Handler = (session, shardId, data) => { - session.emit("raw", data, shardId); -} \ No newline at end of file + session.emit("raw", data, shardId); +}; diff --git a/session/Session.ts b/session/Session.ts index d8e3592..7c45152 100644 --- a/session/Session.ts +++ b/session/Session.ts @@ -1,8 +1,4 @@ -import type { - DiscordGetGatewayBot, - GatewayBot, - GatewayIntents, -} from "../vendor/external.ts"; +import type { DiscordGetGatewayBot, GatewayBot, GatewayIntents } from "../vendor/external.ts"; import { EventEmitter, Routes, Snowflake } from "../util/mod.ts"; diff --git a/structures/Base.ts b/structures/Base.ts index 031020e..1d79680 100644 --- a/structures/Base.ts +++ b/structures/Base.ts @@ -3,10 +3,10 @@ import type { Session } from "../session/mod.ts"; /** * Represents a Discord data model - * */ -export interface Base { + */ +export interface Model { /** id of the model */ - id: Snowflake; + id: Snowflake; /** reference to the client that instantiated the model */ session: Session; -} \ No newline at end of file +} diff --git a/structures/Message.ts b/structures/Message.ts index 3d4dd67..ba14a99 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -1,94 +1,136 @@ -import type { Base } from "./Base.ts"; +import type { Model } from "./Base.ts"; import type { Snowflake } from "../util/Snowflake.ts"; import type { Session } from "../session/mod.ts"; -import type { DiscordMessage, AllowedMentionsTypes } from "../vendor/external.ts"; -import { Routes } from "../util/mod.ts"; +import type { AllowedMentionsTypes, DiscordMessage } from "../vendor/external.ts"; +import { User } from "./User.ts"; +import { MessageFlags, Routes } from "../util/mod.ts"; -/** +/** * @link https://discord.com/developers/docs/resources/channel#allowed-mentions-object - * */ + */ export interface AllowedMentions { - parse?: AllowedMentionsTypes[]; - repliedUser?: boolean; - roles?: Snowflake[]; - users?: Snowflake[]; + parse?: AllowedMentionsTypes[]; + repliedUser?: boolean; + roles?: Snowflake[]; + users?: Snowflake[]; } -/** +/** * @link https://discord.com/developers/docs/resources/channel#edit-message-json-params - * */ + */ export interface EditMessage { - content?: string; - allowedMentions?: AllowedMentions; + content?: string; + allowedMentions?: AllowedMentions; + flags?: MessageFlags; } -/** +/** * @link https://discord.com/developers/docs/resources/channel#create-message-json-params - * */ + */ export interface CreateMessage { - content?: string; - allowedMentions?: AllowedMentions; + content?: string; + allowedMentions?: AllowedMentions; } /** * Represents a message * @link https://discord.com/developers/docs/resources/channel#message-object - * */ -export class Message implements Base { - constructor(session: Session, data: DiscordMessage) { - this.session = session; + */ +export class Message implements Model { + constructor(session: Session, data: DiscordMessage) { + this.session = session; + this.id = data.id; - this.id = data.id; + this.channelId = data.channel_id; + this.guildId = data.guild_id; - this.channelId = data.channel_id; - } + this.author = new User(session, data.author); + this.flags = data.flags; + this.pinned = !!data.pinned; + this.tts = !!data.tts; + this.content = data.content!; + } - /** the session that instantiated the message */ - session: Session; + readonly session: Session; + readonly id: Snowflake; - /** the id of the message */ - id: Snowflake; + channelId: Snowflake; + guildId?: Snowflake; + author: User; + flags?: MessageFlags; + pinned: boolean; + tts: boolean; + content: string; - /** the id of the channel where the message was sent */ - channelId: Snowflake; + get url() { + return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`; + } - /** Edits the current message */ - async edit({ content, allowedMentions }: EditMessage): Promise { - const message = await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_MESSAGE(this.id, this.channelId), - { - content, - allowed_mentions: { - parse: allowedMentions?.parse, - roles: allowedMentions?.roles, - users: allowedMentions?.users, - replied_user: allowedMentions?.repliedUser, - }, - } - ); + /** Edits the current message */ + async edit({ content, allowedMentions, flags }: EditMessage): Promise { + const message = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_MESSAGE(this.id, this.channelId), + { + content, + allowed_mentions: { + parse: allowedMentions?.parse, + roles: allowedMentions?.roles, + users: allowedMentions?.users, + replied_user: allowedMentions?.repliedUser, + }, + flags, + }, + ); - return message; - } + return message; + } - /** Responds directly in the channel the message was sent */ - async respond({ content, allowedMentions }: CreateMessage): Promise { - const message = await this.session.rest.runMethod( - this.session.rest, - "POST", - Routes.CHANNEL_MESSAGES(this.channelId), - { - content, - allowed_mentions: { - parse: allowedMentions?.parse, - roles: allowedMentions?.roles, - users: allowedMentions?.users, - replied_user: allowedMentions?.repliedUser, - }, - } - ); + async suppressEmbeds(suppress: true): Promise; + async suppressEmbeds(suppress: false): Promise; + async suppressEmbeds(suppress = true) { + if (this.flags === MessageFlags.SUPPRESS_EMBEDS && suppress === false) { + return; + } - return message; - } -} \ No newline at end of file + const message = await this.edit({ flags: MessageFlags.SUPPRESS_EMBEDS }); + + return message; + } + + async delete({ reason }: { reason: string }): Promise { + await this.session.rest.runMethod( + this.session.rest, + "DELETE", + Routes.CHANNEL_MESSAGE(this.channelId, this.id), + { reason }, + ); + + return this; + } + + /** Responds directly in the channel the message was sent */ + async respond({ content, allowedMentions }: CreateMessage): Promise { + const message = await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.CHANNEL_MESSAGES(this.channelId), + { + content, + allowed_mentions: { + parse: allowedMentions?.parse, + roles: allowedMentions?.roles, + users: allowedMentions?.users, + replied_user: allowedMentions?.repliedUser, + }, + }, + ); + + return message; + } + + inGuild(): this is { guildId: string } & Message { + return Boolean(this.guildId); + } +} diff --git a/structures/User.ts b/structures/User.ts new file mode 100644 index 0000000..fea9493 --- /dev/null +++ b/structures/User.ts @@ -0,0 +1,64 @@ +import type { Model } from "./Base.ts"; +import type { Snowflake } from "../util/Snowflake.ts"; +import type { Session } from "../session/mod.ts"; +import type { DiscordUser } from "../vendor/external.ts"; +import { iconBigintToHash, iconHashToBigInt } from "../util/hash.ts"; +import { Routes } from "../util/mod.ts"; + +/** + * @link https://discord.com/developers/docs/reference#image-formatting + */ +export type ImageFormat = "jpg" | "jpeg" | "png" | "webp" | "gif" | "json"; + +/** + * @link https://discord.com/developers/docs/reference#image-formatting + */ +export type ImageSize = 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096; + +/** + * Represents a user + * @link https://discord.com/developers/docs/resources/user#user-object + */ +export class User implements Model { + constructor(session: Session, data: DiscordUser) { + this.session = session; + this.id = data.id; + + this.username = data.username; + this.discriminator = data.discriminator; + this.avatarHash = data.avatar ? iconHashToBigInt(data.avatar) : undefined; + this.accentColor = data.accent_color; + this.bot = !!data.bot; + this.system = !!data.system; + this.banner = data.banner; + } + + readonly session: Session; + readonly id: Snowflake; + + username: string; + discriminator: string; + avatarHash?: bigint; + accentColor?: number; + bot: boolean; + system: boolean; + banner?: string; + + /** gets the user's username#discriminator */ + get tag() { + return `${this.username}#${this.discriminator}}`; + } + + /** gets the user's avatar */ + avatarUrl(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }) { + let url: string; + + if (!this.avatarHash) { + url = Routes.USER_DEFAULT_AVATAR(Number(this.discriminator) % 5); + } else { + url = Routes.USER_AVATAR(this.id, iconBigintToHash(this.avatarHash)); + } + + return `${url}.${options.format ?? (url.includes("/a_") ? "gif" : "jpg")}?size=${options.size}`; + } +} diff --git a/tests/mod.ts b/tests/mod.ts index 236c75b..fdecaca 100644 --- a/tests/mod.ts +++ b/tests/mod.ts @@ -1,19 +1,20 @@ -import * as Discord from "./deps.ts"; +import { GatewayIntents, Session } from "./deps.ts"; if (!Deno.args[0]) { throw new Error("Please provide a token"); } -const session = new Discord.Session({ - token: Deno.args[0], - intents: Discord.GatewayIntents.MessageContent | Discord.GatewayIntents.Guilds | - Discord.GatewayIntents.GuildMessages, +const intents = GatewayIntents.MessageContent | GatewayIntents.Guilds | GatewayIntents.GuildMessages; +const session = new Session({ token: Deno.args[0], intents }); + +session.on("ready", (_shardId, payload) => { + console.log("Logged in as:", payload.user.username); }); -session.on("ready", (payload) => console.log(payload)); -session.on("messageCreate", (payload) => console.log(payload)); -// session.on("raw", (data, shardId) => console.log(shardId, data)); +session.on("messageCreate", (message) => { + if (message.content === "!ping") { + message.respond({ content: "pong!" }); + } +}); -console.log("hello"); - -session.start(); +await session.start(); diff --git a/util/Cdn.ts b/util/Cdn.ts new file mode 100644 index 0000000..ed9d8e6 --- /dev/null +++ b/util/Cdn.ts @@ -0,0 +1,13 @@ +import type { Snowflake } from "./Snowflake.ts"; +import { baseEndpoints as Endpoints } from "../vendor/external.ts"; + +export function USER_AVATAR(userId: Snowflake, icon: string) { + return `${Endpoints.CDN_URL}/avatars/${userId}/${icon}`; +} + +export function USER_DEFAULT_AVATAR( + /** user discriminator */ + altIcon: number, +) { + return `${Endpoints.CDN_URL}/embed/avatars/${altIcon}.png`; +} diff --git a/util/Routes.ts b/util/Routes.ts index 5819ba4..e0dc644 100644 --- a/util/Routes.ts +++ b/util/Routes.ts @@ -1,26 +1,29 @@ import type { Snowflake } from "./Snowflake.ts"; +// cdn endpoints +export * from "./Cdn.ts"; + export function GATEWAY_BOT() { return "/gateway/bot"; } export interface GetMessagesOptions { - limit?: number; + limit?: number; } export interface GetMessagesOptions { - around?: Snowflake; - limit?: number; + around?: Snowflake; + limit?: number; } export interface GetMessagesOptions { - before?: Snowflake; - limit?: number; + before?: Snowflake; + limit?: number; } export interface GetMessagesOptions { - after?: Snowflake; - limit?: number; + after?: Snowflake; + limit?: number; } /** used to send messages */ @@ -40,4 +43,4 @@ export function CHANNEL_MESSAGES(channelId: Snowflake, options?: GetMessagesOpti /** used to edit messages */ export function CHANNEL_MESSAGE(channelId: Snowflake, messageId: Snowflake) { return `/channels/${channelId}/messages/${messageId}`; -} \ No newline at end of file +} diff --git a/util/hash.ts b/util/hash.ts new file mode 100644 index 0000000..e3c5d48 --- /dev/null +++ b/util/hash.ts @@ -0,0 +1,14 @@ +/** + * Memory optimizations + * All credits to the Discordeno authors + */ + +export function iconHashToBigInt(hash: string) { + return BigInt("0x" + hash.startsWith("a_") ? `a${hash.substring(2)}` : `b${hash}`); +} + +export function iconBigintToHash(icon: bigint) { + const hash = icon.toString(16); + + return hash.startsWith("a") ? `a_${hash.substring(1)}` : hash.substring(1); +} diff --git a/util/mod.ts b/util/mod.ts index 1cfabbb..53eb3dc 100644 --- a/util/mod.ts +++ b/util/mod.ts @@ -1,3 +1,5 @@ export * from "./EventEmmiter.ts"; export * from "./Snowflake.ts"; +export * from "./hash.ts"; +export * from "./shared/flags.ts"; export * as Routes from "./Routes.ts"; diff --git a/util/shared/flags.ts b/util/shared/flags.ts new file mode 100644 index 0000000..ea342b8 --- /dev/null +++ b/util/shared/flags.ts @@ -0,0 +1,23 @@ +/** + * @link https://discord.com/developers/docs/resources/channel#message-object-message-flags + */ +export enum MessageFlags { + /** this message has been published to subscribed channels (via Channel Following) */ + CROSSPOSTED = 1 << 0, + /** this message originated from a message in another channel (via Channel Following) */ + IS_CROSSPOST = 1 << 1, + /** do not include any embeds when serializing this message */ + SUPPRESS_EMBEDS = 1 << 2, + /** the source message for this crosspost has been deleted (via Channel Following) */ + SOURCE_MESSAGE_DELETED = 1 << 3, + /** this message came from the urgent message system */ + URGENT = 1 << 4, + /** this message has an associated thread, with the same id as the message */ + HAS_THREAD = 1 << 5, + /** this message is only visible to the user who invoked the Interaction */ + EPHEMERAL = 1 << 6, + /** this message is an Interaction Response and the bot is "thinking" */ + LOADING = 1 << 7, + /** this message failed to mention some roles and add their members to the thread */ + FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8, +} diff --git a/vendor/external.ts b/vendor/external.ts index fd42f50..efb9d06 100644 --- a/vendor/external.ts +++ b/vendor/external.ts @@ -1,3 +1,4 @@ export * from "./gateway/mod.ts"; export * from "./rest/mod.ts"; export * from "./types/mod.ts"; +export * from "./util/constants.ts";