diff --git a/README.md b/README.md index bf6af9d96..4db86244c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Discord API library wrapper in Deno ## TODO - [Review compression of payloads with GZIP](https://discordapp.com/developers/docs/topics/gateway#sending-payloads-example-gateway-dispatch) -- +- Handle checking if guild is unavailable before taking guild related actions. ## Motivations/Features diff --git a/events/ready.ts b/events/ready.ts new file mode 100644 index 000000000..d4d61d6e6 --- /dev/null +++ b/events/ready.ts @@ -0,0 +1,3 @@ +export const handleInternalReady = () => { + +} diff --git a/mod.ts b/mod.ts index fbaee0c1e..6326ec71b 100644 --- a/mod.ts +++ b/mod.ts @@ -1,41 +1,13 @@ import Client from "./module/client.ts" import { configs } from "./configs.ts" -import { StatusType, GatewayOpcode } from "./types/discord.ts" +import { Intents } from "./types/options.ts" const startup = async () => { - const client = new Client({ + new Client({ token: configs.token, - bot_id: '675412054529540107' + bot_id: "675412054529540107", + intents: [Intents.GUILDS, Intents.GUILD_MESSAGES] }) - - const { gateway, connection } = await client.bootstrap() - - for await (const message of connection) { - if (message.data?.op === GatewayOpcode.Hello) { - await message.action - gateway.updateStatus({ - afk: false, - status: StatusType.DoNotDisturb - }) - } - } } startup() -// ;(async function() { -// const client = new Client({ -// token: configs.token -// }) - -// const { gateway, connection } = await client.bootstrap() - -// for await (const message of connection) { -// if (message.data?.op === GatewayOpcode.Hello) { -// await message.action -// await gateway.updateStatus({ -// afk: false, -// status: StatusType.DoNotDisturb -// }) -// } -// } -// })() diff --git a/module/client.ts b/module/client.ts index 9218cbc17..8ad65d9f0 100644 --- a/module/client.ts +++ b/module/client.ts @@ -1,16 +1,26 @@ import { endpoints } from "../constants/discord.ts" import DiscordRequestManager from "./discord-request-manager.ts" -import { DiscordBotGatewayData, DiscordPayload, DiscordHeartbeatPayload, GatewayOpcode } from "../types/discord.ts" -import ShardingManager from "./sharding-manager.ts" +import { DiscordBotGatewayData, DiscordPayload, DiscordHeartbeatPayload, GatewayOpcode, StatusType } from "../types/discord.ts" +import { spawnShards } from "./sharding-manager.ts" import { connectWebSocket, isWebSocketCloseEvent, isWebSocketPingEvent, - isWebSocketPongEvent + isWebSocketPongEvent, + WebSocket } from "https://deno.land/std/ws/mod.ts" -import Gateway from "./gateway.ts" import { ClientOptions, FulfilledClientOptions } from "../types/options.ts" import { CollectedMessageType } from "../types/message-type.ts" +import { sendConstantHeartbeats } from "./gateway.ts" + +const defaultOptions = { + properties: { + $os: "linux", + $browser: "Discordeno", + $device: "Discordeno" + }, + compress: false +} class Client { bot_id: string @@ -18,8 +28,6 @@ class Client { token: string /** The Rate limit manager to handle all outgoing requests to discord. Not meant to be used by users. */ discordRequestManager: DiscordRequestManager - /** Creates and handles all the shards necessary for the bot. */ - shardingManager: ShardingManager /** The options (with defaults) passed to the `Client` constructor. */ options: FulfilledClientOptions @@ -28,51 +36,53 @@ class Client { constructor(options: ClientOptions) { // Assign some defaults to the options to make them fulfilled / not annoying to use. - this.options = Object.assign( - { - properties: { - $os: "...", - $browser: "...", - $device: "..." - }, - compress: false - }, - options - ) + this.options = { + ...defaultOptions, + ...options, + intents: options.intents.reduce((bits, next) => (bits |= next), 0) + } this.bot_id = options.bot_id this.token = options.token this.authorization = `Bot ${this.options.token}` this.discordRequestManager = new DiscordRequestManager(this, this.authorization) - this.shardingManager = new ShardingManager() - } - getGatewayData() { - return this.discordRequestManager.get(endpoints.GATEWAY_BOT) as Promise - } - - createWebsocketConnection(data: DiscordBotGatewayData) { - console.log({ data }) - return connectWebSocket(data.url) + this.bootstrap() } async bootstrap() { - const data = await this.getGatewayData() - const socket = await this.createWebsocketConnection(data) - const gateway = new Gateway(socket) - const messages = this.collectMessages(gateway) - await gateway.identify(this.options) - return { - data, - socket, - gateway, - messages, - connection: this.connect(gateway, data) + const data = await this.discordRequestManager.get(endpoints.GATEWAY_BOT) as DiscordBotGatewayData + const socket = await connectWebSocket(data.url) + this.collectMessages(socket) + // Intial identify with the gateway + await socket.send( + JSON.stringify({ + op: GatewayOpcode.Identify, + d: { + token: this.options.token, + // TODO: Let's get compression working, eh? + compress: false, + properties: this.options.properties, + intents: this.options.intents + } + }) + ) + + for await (const message of this.connect(socket, data)) { + console.log("mymessage", message) + if (message.data?.op === GatewayOpcode.Hello) { + await message.action + } + + if (message.data?.t === 'READY') { + console.log('ready event was received') + // this.options.eventHandlers.ready() + } } } - async *collectMessages(gateway: Gateway) { - const { socket } = gateway + async *collectMessages(socket: WebSocket) { for await (const message of socket.receive()) { + console.log("collecting", message) if (typeof message === "string") { yield { type: CollectedMessageType.Message, @@ -91,10 +101,10 @@ class Client { /** Begins initial handshake, creates the websocket with Discord and spawns all necessary shards. */ async *connect( - gateway: Gateway, + socket: WebSocket, data: DiscordBotGatewayData ): AsyncGenerator<{ type: CollectedMessageType; data?: DiscordPayload; action?: Promise }> { - for await (const message of this.collectMessages(gateway)) { + for await (const message of this.collectMessages(socket)) { switch (message.type) { case CollectedMessageType.Ping: console.log("Ping!") @@ -109,32 +119,25 @@ class Client { yield message break case CollectedMessageType.Message: - await this.handleDiscordPayload(message.data, gateway) + this.handleDiscordPayload(message.data, socket) yield message - console.log({ yay: true, ...message }) break } } // Begin spawning all necessary shards - this.spawnShards(data.shards) + spawnShards(data.shards) } - handleDiscordPayload(data: DiscordPayload, gateway: Gateway) { + handleDiscordPayload(data: DiscordPayload, socket: WebSocket) { switch (data.op) { case GatewayOpcode.Hello: - console.log("heartbeating...") - return gateway.sendConstantHeartbeats((data.d as DiscordHeartbeatPayload).heartbeat_interval, data.s) + sendConstantHeartbeats(socket, (data.d as DiscordHeartbeatPayload).heartbeat_interval, data.s) + return } - - // Make all code paths return a promise for consistency. - return Promise.resolve(undefined) } - spawnShards(total: number, id = 1) { - // this.ShardingManager.spawnShard(id); - if (id < total) this.spawnShards(total, id + 1) - } + } export default Client diff --git a/module/gateway.ts b/module/gateway.ts index 94f94c0b6..75047c48f 100644 --- a/module/gateway.ts +++ b/module/gateway.ts @@ -1,54 +1,13 @@ import { WebSocket } from "https://deno.land/std/ws/mod.ts" -import { GatewayOpcode, Status } from "../types/discord.ts" -import { FulfilledClientOptions } from "../types/options.ts" +import { GatewayOpcode } from "../types/discord.ts" import { delay } from "https://deno.land/std/util/async.ts" -export default class Gateway { - constructor(public socket: WebSocket) {} +export const sendConstantHeartbeats = async (socket: WebSocket, interval: number, previousSequenceNumber: number | null = null) => { + await delay(interval) - identify(options: FulfilledClientOptions) { - return this.sendObject({ - op: GatewayOpcode.Identify, - d: { - token: options.token, - // TOOD: Let's get compression working, eh? - compress: false, - properties: options.properties - } - }) - } + if (previousSequenceNumber) previousSequenceNumber += 1 - sendHeartbeat(previousSequenceNumber: number | null = null) { - return this.sendObject({ - op: GatewayOpcode.Heartbeat, - d: previousSequenceNumber - }) - } - - updateStatus(status: Status) { - this.sendObject({ - op: GatewayOpcode.StatusUpdate, - d: status - }) - } - - async sendConstantHeartbeats( - interval: number, - previousSequenceNumber: number | null = null, - shouldContinue: () => boolean = () => true - ): Promise { - await delay(interval) - - if (!shouldContinue()) { - return - } - - // TODO: If the initial seq num is null, this will make it forever null until a restart. Is this good? - this.sendHeartbeat(previousSequenceNumber === null ? previousSequenceNumber : previousSequenceNumber++) - return this.sendConstantHeartbeats(interval, previousSequenceNumber) - } - - sendObject(object: object) { - return this.socket.send(JSON.stringify(object)) - } + // TODO: If the initial seq num is null, this will make it forever null until a restart. Is this good? + socket.send(JSON.stringify({ op: GatewayOpcode.Heartbeat, d: previousSequenceNumber })) + sendConstantHeartbeats(socket, interval, previousSequenceNumber) } diff --git a/module/sharding-manager.ts b/module/sharding-manager.ts index 80c13145f..09237294c 100644 --- a/module/sharding-manager.ts +++ b/module/sharding-manager.ts @@ -1,3 +1,4 @@ -class ShardingManager extends Map {} - -export default ShardingManager +export const spawnShards = (total: number, id = 1) => { + // this.ShardingManager.spawnShard(id); + if (id < total) spawnShards(total, id + 1) +} diff --git a/types/options.ts b/types/options.ts index 4aa185de5..8902cbd11 100644 --- a/types/options.ts +++ b/types/options.ts @@ -3,7 +3,8 @@ import { Properties } from "./discord.ts" export interface FulfilledClientOptions { token: string properties: Properties - compress: boolean + compress: boolean, + intents: number } export interface ClientOptions { @@ -11,4 +12,92 @@ export interface ClientOptions { properties?: Properties compress?: boolean bot_id: string + intents: Intents[] +} + +export enum Intents { + /** Enables the following events: + * - GUILD_CREATE + * - GUILD_DELETE + * - GUILD_ROLE_CREATE + * - GUILD_ROLE_UPDATE + * - GUILD_ROLE_DELETE + * - CHANNEL_CREATE + * - CHANNEL_UPDATE + * - CHANNEL_DELETE + * - CHANNEL_PINS_UPDATE + */ + GUILDS = 1 << 0, + /** Enables the following events: + * - GUILD_MEMBER_ADD + * - GUILD_MEMBER_UPDATE + * - GUILD_MEMBER_REMOVE + */ + GUILD_MEMBERS = 1 << 1, + /** Enables the following events: + * - GUILD_BAN_ADD + * - GUILD_BAN_REMOVE + */ + GUILD_BANS = 1 << 2, + /** Enables the following events: + * - GUILD_EMOJIS_UPDATE + */ + GUILD_EMOJIS = 1 << 3, + /** Enables the following events: + * - GUILD_INTEGRATIONS_UPDATE + */ + GUILD_INTEGRATIONS = 1 << 4, + /** Enables the following events: + * - WEBHOOKS_UPDATE + */ + GUILD_WEBHOOKS = 1 << 5, + /** Enables the following events: + * - INVITE_CREATE + * - INVITE_DELETE + */ + GUILD_INVITES = 1 << 6, + /** Enables the following events: + * - VOICE_STATE_UPDATE + */ + GUILD_VOICE_STATES = 1 << 7, + /** Enables the following events: + * - PRESENCE_UPDATE + */ + GUILD_PRESENCES = 1 << 8, + /** Enables the following events: + * - MESSAGE_CREATE + * - MESSAGE_UPDATE + * - MESSAGE_DELETE + */ + GUILD_MESSAGES = 1 << 9, + /** Enables the following events: + * - MESSAGE_REACTION_ADD + * - MESSAGE_REACTION_REMOVE + * - MESSAGE_REACTION_REMOVE_ALL + * - MESSAGE_REACTION_REMOVE_EMOJI + */ + GUILD_MESSAGE_REACTIONS = 1 << 10, + /** Enables the following events: + * - TYPING_START + */ + GUILD_MESSAGE_TYPING = 1 << 11, + /** Enables the following events: + * - CHANNEL_CREATE + * - MESSAGE_CREATE + * - MESSAGE_UPDATE + * - MESSAGE_DELETE + * - CHANNEL_PINS_UPDATE + */ + DIRECT_MESSAGES = 1 << 12, + /** Enables the following events: + * - MESSAGE_REACTION_ADD + * - MESSAGE_REACTION_REMOVE + * - MESSAGE_REACTION_REMOVE_ALL + * - MESSAGE_REACTION_REMOVE_EMOJI + */ + DIRECT_MESSAGE_REACTIONS = 1 << 13, + /** Enables the following events: + * - TYPING_START + */ + DIRECT_MESSAGE_TYPING = 1 << 14 } diff --git a/types/queue.ts b/types/queue.ts index d5dabbd47..61058a790 100644 --- a/types/queue.ts +++ b/types/queue.ts @@ -1,4 +1,4 @@ -import { DiscordPayload } from "./discord"; +import { DiscordPayload } from "./discord.ts"; import Gateway from "../module/gateway.ts"; export abstract class ActionQueue { @@ -35,4 +35,4 @@ export class GatewayActionQueue extends ActionQueue { } shouldDispatchImmediately () -} \ No newline at end of file +}