diff --git a/managers/RequestManager.ts b/managers/RequestManager.ts index 0b6fc7d0a..85170419f 100644 --- a/managers/RequestManager.ts +++ b/managers/RequestManager.ts @@ -1,31 +1,47 @@ -import Client from '../module/Client.ts' +import Client from "../module/Client.ts"; +import { RequestMethod } from "../types/fetch"; + +type RequestBody = string | Blob | ArrayBufferView | ArrayBuffer | FormData | URLSearchParams | null | undefined; class RequestManager { - client: Client - token: string + client: Client; + token: string; constructor(client: Client, token: string) { this.client = client this.token = token } - async get(url: string, payload?: unknown) { - // THIS IS IMPORTANT. It keeps clean stack errors in the users own files to better help debug errors. - // const stackHolder = {}; - // TODO: Figure out why this doesnt work - // Error.captureStackTrace(stackHolder) + async get(url: string) { + const headers = this.getDiscordHeaders(); + return fetch(url, { headers }).then(res => res.json()) + } - // let attempts = 0 - const headers = { - Authorization: this.token, - 'User-Agent': `DiscordBot (https://github.com/skillz4killz/discordeno, 0.0.1)` - } + async post (url: string, body: RequestBody) { + const headers = this.getDiscordHeaders(); + return fetch(url, { + method: RequestMethod.Post, + headers, + body + }); + } - console.log('payload', payload) + async delete (url: string, body: RequestBody) { + const headers = this.getDiscordHeaders(); + return fetch(url, { + method: RequestMethod.Delete, + headers, + body + }); + } - const data = await fetch(url, { headers }).then(res => res.json()) - return data - } + // The Record type here plays nice with Deno's `fetch.headers` expected type. + getDiscordHeaders (): Record { + return { + Authorization: this.token, + "User-Agent": `DiscordBot (https://github.com/skillz4killz/discordeno, 0.0.1)`, + }; + } } export default RequestManager diff --git a/mod.ts b/mod.ts index 7c83caa3d..a34d17415 100644 --- a/mod.ts +++ b/mod.ts @@ -1,7 +1,21 @@ -import Client from './module/Client.ts' -import { configs } from './configs.ts' +import Client from "./module/Client.ts" +import { configs } from "./configs.ts" +import { StatusType, GatewayOpcode } from "./types/discord.ts"; -const Discordeno = new Client(configs.token) -Discordeno.connect() +(async function () { + const client = new Client({ + token: configs.token + }); -export default Discordeno + 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 b27b96bcf..0f2c5268b 100644 --- a/module/Client.ts +++ b/module/Client.ts @@ -1,6 +1,6 @@ import { endpoints } from '../constants/discord.ts' import RequestManager from '../managers/RequestManager.ts' -import { DiscordBotGateway, DiscordPayload, DiscordHeartbeatPayload } from '../types/discord.ts' +import { DiscordBotGatewayData, DiscordPayload, DiscordHeartbeatPayload, GatewayOpcode } from '../types/discord.ts' import ShardingManager from '../managers/ShardingManager.ts' import { connectWebSocket, @@ -9,11 +9,9 @@ import { isWebSocketPongEvent, WebSocket } from 'https://deno.land/std/ws/mod.ts' -// import { encode } from "https://deno.land/std/strings/mod.ts" -// import { BufReader } from "https://deno.land/std/io/bufio.ts" -// import { TextProtoReader } from "https://deno.land/std/textproto/mod.ts" -import { keepDiscordWebsocketAlive, updatePreviousSequenceNumber } from './websocket.ts' -import { logGreen, logRed, logYellow, logBlue } from '../utils/logger.ts' +import Gateway from './gateway.ts' +import { ClientOptions, FulfilledClientOptions } from '../types/options.ts' +import { CollectedMessageType } from '../types/message-type.ts' class Client { /** The bot's token. This should never be used by end users. It is meant to be used internally to make requests to the Discord API. */ @@ -23,35 +21,94 @@ class Client { /** Creates and handles all the shards necessary for the bot. */ ShardingManager: ShardingManager - constructor(token: string) { - this.token = `Bot ${token}` - this.RequestManager = new RequestManager(this, this.token) + /** The options (with defaults) passed to the `Client` constructor. */ + options: FulfilledClientOptions + + protected authorization: string + + 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.token = options.token + this.authorization = `Bot ${this.options.token}` + this.RequestManager = new RequestManager(this, this.authorization) this.ShardingManager = new ShardingManager() } - /** Begins initial handshake, creates the websocket with Discord and spawns all necessary shards. */ - async connect() { - const data = (await this.RequestManager.get(endpoints.GATEWAY_BOT)) as DiscordBotGateway - // Open a WS with the url from discord. - const sock = await connectWebSocket(data.url) - console.log(sock) - logGreen("ws connected! (type 'close' to quit)") + getGatewayData() { + return this.RequestManager.get(endpoints.GATEWAY_BOT) as Promise + } - for await (const msg of sock.receive()) { - if (typeof msg === 'string') { - try { - const json = JSON.parse(msg) - this.handleDiscordPayload(json, sock) - } catch { - logRed(`Invalid JSON String send by discord: ${msg}`) + createWebsocketConnection(data: DiscordBotGatewayData) { + console.log({ data }) + return connectWebSocket(data.url) + } + + 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) + } + } + + async *collectMessages(gateway: Gateway) { + const { socket } = gateway + for await (const message of socket.receive()) { + if (typeof message === 'string') { + yield { + type: CollectedMessageType.Message, + data: JSON.parse(message) } - logYellow('< ' + msg) - } else if (isWebSocketPingEvent(msg)) { - logBlue('< ping') - } else if (isWebSocketPongEvent(msg)) { - logBlue('< pong') - } else if (isWebSocketCloseEvent(msg)) { - logRed(`closed: code=${msg.code}, reason=${msg.reason}`) + } else if (isWebSocketCloseEvent(message)) { + yield { type: CollectedMessageType.Close, ...message } + return + } else if (isWebSocketPingEvent(message)) { + yield { type: CollectedMessageType.Ping } + } else if (isWebSocketPongEvent(message)) { + yield { type: CollectedMessageType.Pong } + } + } + } + + /** Begins initial handshake, creates the websocket with Discord and spawns all necessary shards. */ + async *connect(gateway: Gateway, data: DiscordBotGatewayData): AsyncGenerator<{ type: CollectedMessageType, data?: DiscordPayload, action?: Promise }> { + for await (const message of this.collectMessages(gateway)) { + switch (message.type) { + case CollectedMessageType.Ping: + console.log('Ping!') + yield message; + break + case CollectedMessageType.Pong: + console.log('Pong!') + yield message; + break + case CollectedMessageType.Close: + console.log('Close :(', message) + yield message; + break + case CollectedMessageType.Message: + await this.handleDiscordPayload(message.data, gateway); + yield message; + console.log({ yay: true, ...message }); + break } } @@ -59,15 +116,15 @@ class Client { this.spawnShards(data.shards) } - handleDiscordPayload(data: DiscordPayload, socket: WebSocket) { + handleDiscordPayload(data: DiscordPayload, gateway: Gateway) { switch (data.op) { - case 10: // Initial Heartbeat - keepDiscordWebsocketAlive(socket, (data.d as DiscordHeartbeatPayload).heartbeat_interval, data.s) - break - case 11: - updatePreviousSequenceNumber(data.s) - break - } + case GatewayOpcode.Hello: + console.log('heartbeating...'); + return gateway.sendConstantHeartbeats((data.d as DiscordHeartbeatPayload).heartbeat_interval, data.s); + } + + // Make all code paths return a promise for consistency. + return Promise.resolve(undefined); } spawnShards(total: number, id = 1) { diff --git a/module/Ratelimiter.ts b/module/Ratelimiter.ts new file mode 100644 index 000000000..c7eb410b3 --- /dev/null +++ b/module/Ratelimiter.ts @@ -0,0 +1,3 @@ +export class Ratelimiter { + +} \ No newline at end of file diff --git a/module/gateway.ts b/module/gateway.ts new file mode 100644 index 000000000..e7dd3ddae --- /dev/null +++ b/module/gateway.ts @@ -0,0 +1,56 @@ +import { + connectWebSocket, + isWebSocketCloseEvent, + isWebSocketPingEvent, + isWebSocketPongEvent, + WebSocket + } from "https://deno.land/std/ws/mod.ts"; +import { GatewayOpcode, Status } from "../types/discord.ts"; +import { FulfilledClientOptions } from "../types/options.ts"; +import { delay } from 'https://deno.land/std/util/async.ts'; + +export default class Gateway { + constructor (public socket: WebSocket) {} + + 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 + } + }); + } + + 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)); + } +} \ No newline at end of file diff --git a/module/websocket.ts b/module/websocket.ts deleted file mode 100644 index 004bbe873..000000000 --- a/module/websocket.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { WebSocket } from 'https://deno.land/std/ws/mod.ts' - -let previousSequenceNumber: number | null = null - -export const keepDiscordWebsocketAlive = ( - socket: WebSocket, - millesecondsInterval: number, - payload: number | null = null -) => { - previousSequenceNumber = payload - - setInterval(() => { - socket.send( - JSON.stringify({ - op: 1, - d: previousSequenceNumber - }) - ) - }, millesecondsInterval) -} - -export const updatePreviousSequenceNumber = (sequence: number | null = null) => { - previousSequenceNumber = sequence -} diff --git a/structures/activity.ts b/structures/activity.ts new file mode 100644 index 000000000..e1a492fc2 --- /dev/null +++ b/structures/activity.ts @@ -0,0 +1,20 @@ +import { Timestamps } from "../types/discord.ts"; + +export interface ActivityPayload { + /** The activity's name */ + name: string; + + /** */ + type: number; + url?: string; + created_at: number; + timestamps: Timestamps; + details?: string; +} + +export enum ActivityType { + Game, + Streaming, + Listening, + Custom = 4 +} \ No newline at end of file diff --git a/structures/emoji.ts b/structures/emoji.ts index ec8ade3be..9232abc85 100644 --- a/structures/emoji.ts +++ b/structures/emoji.ts @@ -1,3 +1,9 @@ +export interface EmojiPayload { + name: string; + id?: string; + animated?: boolean; +} + export const createEmoji = (data: unknown) => { console.log(data) } diff --git a/structures/guild.ts b/structures/guild.ts index 5092b074f..3ee6e010e 100644 --- a/structures/guild.ts +++ b/structures/guild.ts @@ -8,239 +8,132 @@ import { createMember } from './member' import { createChannel } from './channel' import { createPresence } from './presence' -interface CreateGuildPayload { - /** The guild id */ - id: string - /** The guild name 2-100 characters */ - name: string - /** The guild icon image hash */ - icon: string | null - /** The guild splash image hash */ - splash: string | null - /** The id of the owner */ - owner_id: string - /** The voice region id for the guild */ - region: string - /** The afk channel id */ - afk_channel_id: string | null - /** AFK Timeout in seconds. */ - afk_timeout: number - /** The verification level required for the guild */ - verification_level: number - /** The roles in the guild */ - roles: Role[] - /** The custom guild emojis */ - emojis: Emoji[] - /** Enabled guild features */ - features: GuildFeatures[] - /** Required MFA level for the guild */ - mfa_level: number - /** The id of the channel to which system mesages are sent */ - system_channel_id: string | null - /** When this guild was joined at */ - joined_at: string - /** Whether this is considered a large guild */ - large: boolean - /** Whether this guild is unavailable */ - unavailable: boolean - /** Total number of members in this guild */ - member_count: number - voice_states: VoiceState[] - /** Users in the guild */ - members: Member[] - /** Channels in the guild */ - channels: Channel[] - presences: Presence[] - /** The maximum amount of presences for the guild(the default value, currently 5000 is in effect when null is returned.) */ - max_presences?: number | null - /** The maximum amount of members for the guild */ - max_members?: number - /** The vanity url code for the guild */ - vanity_url_code: string | null - /** The description for the guild */ - description: string | null - /** The banner hash */ - banner: string | null - /** The premium tier */ - premium_tier: number - /** The total number of users currently boosting this server. */ - premium_subscription_count: number - /** The preferred local of this guild only set if guild has the DISCOVERABLE feature, defaults to en-US */ - preferred_locale: string +export interface CreateGuildPayload { + /** The guild id */ + id: string + /** The guild name 2-100 characters */ + name: string + /** The guild icon image hash */ + icon: string | null + /** The guild splash image hash */ + splash: string | null + /** The id of the owner */ + owner_id: string + /** The voice region id for the guild */ + region: string + /** The afk channel id */ + afk_channel_id: string | null + /** AFK Timeout in seconds. */ + afk_timeout: number + /** The verification level required for the guild */ + verification_level: number + /** The roles in the guild */ + roles: Role[] + /** The custom guild emojis */ + emojis: Emoji[] + /** Enabled guild features */ + features: GuildFeatures[] + /** Required MFA level for the guild */ + mfa_level: number + /** The id of the channel to which system mesages are sent */ + system_channel_id: string | null + /** When this guild was joined at */ + joined_at: string + /** Whether this is considered a large guild */ + large: boolean + /** Whether this guild is unavailable */ + unavailable: boolean + /** Total number of members in this guild */ + member_count: number + voice_states: VoiceState[] + /** Users in the guild */ + members: Member[] + /** Channels in the guild */ + channels: Channel[] + presences: Presence[] + /** The maximum amount of presences for the guild(the default value, currently 5000 is in effect when null is returned.) */ + max_presences?: number | null + /** The maximum amount of members for the guild */ + max_members?: number + /** The vanity url code for the guild */ + vanity_url_code: string | null + /** The description for the guild */ + description: string | null + /** The banner hash */ + banner: string | null + /** The premium tier */ + premium_tier: number + /** The total number of users currently boosting this server. */ + premium_subscription_count: number + /** The preferred local of this guild only set if guild has the DISCOVERABLE feature, defaults to en-US */ + preferred_locale: string } -interface Guild { - /** The guild id */ - id: string - /** The guild name 2-100 characters */ - name: string - /** The guild icon image hash */ - icon: string | null - /** The guild splash image hash */ - splash: string | null - /** The id of the owner */ - ownerID: string - /** The voice region id for the guild */ - region: string - /** The afk channel id */ - afkChannelID: string | null - /** AFK Timeout in seconds. */ - afkTimeout: number - /** The verification level required for the guild */ - verificationLevel: number - /** The roles in the guild */ - roles: Role[] - /** The custom guild emojis */ - emojis: Emoji[] - /** Enabled guild features */ - features: GuildFeatures[] - /** Required MFA level for the guild */ - mfaLevel: number - /** The id of the channel to which system mesages are sent */ - systemChannelID: string | null - /** When this guild was joined at */ - joinedAt: number - /** Whether this is considered a large guild */ - large: boolean - /** Whether this guild is unavailable */ - unavailable: boolean - /** Total number of members in this guild */ - memberCount: number - voiceStates: VoiceState[] - /** Users in the guild */ - members: Member[] - /** Channels in the guild */ - channels: Channel[] - presences: Presence[] - /** The maximum amount of presences for the guild(the default value, currently 5000 is in effect when null is returned.) */ - maxPresences?: number | null - /** The maximum amount of members for the guild */ - maxMembers?: number - /** The vanity url code for the guild */ - vanityURLCode: string | null - /** The description for the guild */ - description: string | null - /** The banner hash */ - banner: string | null - /** The premium tier */ - premiumTier: number - /** The total number of users currently boosting this server. */ - premiumSubscriptionCount: number - /** The preferred local of this guild only set if guild has the DISCOVERABLE feature, defaults to en-US */ - preferredLocale: string - /** The full URL of the icon from Discords CDN. Undefined when no icon is set. */ - iconURL(size?: ImageSize, format?: ImageFormats): string | undefined - /** The full URL of the splash from Discords CDN. Undefined if no splash is set. */ - splashURL(size?: ImageSize, format?: ImageFormats): string | undefined - /** The full URL of the banner from Discords CDN. Undefined if no banner is set. */ - bannerURL(size?: ImageSize, format?: ImageFormats): string | undefined - /** Create a channel in your server. Bot needs MANAGE_CHANNEL permissions in the server. */ - createChannel(name: string, options: ChannelCreateOptions): Promise - /** Create an emoji in the server. Emojis and animated emojis have a maximum file size of 256kb. Attempting to upload an emoji larger than this limit will fail and return 400 Bad Request and an error message, but not a JSON status code. */ - createEmoji(name: string, image: string, options: CreateEmojisOptions): Promise - /** Modify the given emoji. Requires the MANAGE_EMOJIS permission. */ - editEmoji(id: string, options: EditEmojisOptions): Promise - /** Delete the given emoji. Requires the MANAGE_EMOJIS permission. Returns 204 No Content on success. */ - deleteEmoji(id: string, reason?: string): Promise - /** Create a new role for the guild. Requires the MANAGE_ROLES permission. */ - createRole(options: CreateRoleOptions): Promise - /** Edi a guild role. Requires the MANAGE_ROLES permission. */ - editRole(id: string, options: CreateRoleOptions): Promise - /** Delete a guild role. Requires the MANAGE_ROLES permission. */ - deleteRole(id: string): Promise - /** Check how many members would be removed from the server in a prune operation. Requires the KICK_MEMBERS permission */ - getPruneCount(days: number): Promise - /** Begin pruning all members in the given time period */ - pruneMembers(days: number): Promise - getAuditLogs(options: GetAuditLogsOptions): Promise - - leaveVoiceChannel(): Promise -} - -export interface GetAuditLogsOptions { - /** Filter the logs for actions made by this user. */ - user_id?: string - /** The type of audit log. */ - action_type?: AuditLogType - /** Filter the logs before a certain log entry. */ - before?: string - /** How many entries are returned. Between 1-100. Default 50. */ - limit?: number -} - -export type AuditLogType = - | `GUILD_UPDATE` - | `CHANNEL_CREATE` - | `CHANNEL_UPDATE` - | `CHANNEL_DELETE` - | `CHANNEL_OVERWRITE_CREATE` - | `CHANNEL_OVERWRITE_UPDATE` - | `CHANNEL_OVERWRITE_DELETE` - | `MEMBER_KICK` - | `MEMBER_PRUNE` - | `MEMBER_BAN_ADD` - | `MEMBER_BAN_REMOVE` - | `MEMBER_UPDATE` - | `MEMBER_ROLE_UPDATE` - | `MEMBER_MOVE` - | `MEMBER_DISCONNECT` - | `BOT_ADD` - | `ROLE_CREATE` - | `ROLE_UPDATE` - | `ROLE_DELETE` - | `INVITE_CREATE` - | `INVITE_UPDATE` - | `INVITE_DELETE` - | `WEBHOOK_CREATE` - | `WEBHOOK_UPDATE` - | `WEBHOOK_DELETE` - | `EMOJI_CREATE` - | `EMOJI_UPDATE` - | `EMOJI_DELETE` - | `MESSAGE_DELETE` - | `MESSAGE_BULK_DELETE` - | `MESSAGE_PIN` - | `MESSAGE_UNPIN` - | `INTEGRATION_CREATE` - | `INTEGRATION_UPDATE` - | `INTEGRATION_DELETE` - -export enum AuditLogs { - GUILD_UPDATE = 1, - CHANNEL_CREATE = 10, - CHANNEL_UPDATE, - CHANNEL_DELETE, - CHANNEL_OVERWRITE_CREATE, - CHANNEL_OVERWRITE_UPDATE, - CHANNEL_OVERWRITE_DELETE, - MEMBER_KICK = 20, - MEMBER_PRUNE, - MEMBER_BAN_ADD, - MEMBER_BAN_REMOVE, - MEMBER_UPDATE, - MEMBER_ROLE_UPDATE, - MEMBER_MOVE, - MEMBER_DISCONNECT, - BOT_ADD, - ROLE_CREATE = 30, - ROLE_UPDATE, - ROLE_DELETE, - INVITE_CREATE = 40, - INVITE_UPDATE, - INVITE_DELETE, - WEBHOOK_CREATE = 50, - WEBHOOK_UPDATE, - WEBHOOK_DELETE, - EMOJI_CREATE = 60, - EMOJI_UPDATE, - EMOJI_DELETE, - MESSAGE_DELETE = 72, - MESSAGE_BULK_DELETE, - MESSAGE_PIN, - MESSAGE_UNPIN, - INTEGRATION_CREATE = 80, - INTEGRATION_UPDATE, - INTEGRATION_DELETE +export interface Guild { + /** The guild id */ + id: string + /** The guild name 2-100 characters */ + name: string + /** The guild icon image hash */ + icon: string | null + /** The guild splash image hash */ + splash: string | null + /** The id of the owner */ + ownerID: string + /** The voice region id for the guild */ + region: string + /** The afk channel id */ + afkChannelID: string | null + /** AFK Timeout in seconds. */ + afkTimeout: number + /** The verification level required for the guild */ + verificationLevel: number + /** The roles in the guild */ + roles: Role[] + /** The custom guild emojis */ + emojis: Emoji[] + /** Enabled guild features */ + features: GuildFeatures[] + /** Required MFA level for the guild */ + mfaLevel: number + /** The id of the channel to which system mesages are sent */ + systemChannelID: string | null + /** When this guild was joined at */ + joinedAt: number + /** Whether this is considered a large guild */ + large: boolean + /** Whether this guild is unavailable */ + unavailable: boolean + /** Total number of members in this guild */ + memberCount: number + voiceStates: VoiceState[] + /** Users in the guild */ + members: Member[] + /** Channels in the guild */ + channels: Channel[] + presences: Presence[] + /** The maximum amount of presences for the guild(the default value, currently 5000 is in effect when null is returned.) */ + maxPresences?: number | null + /** The maximum amount of members for the guild */ + maxMembers?: number + /** The vanity url code for the guild */ + vanityURLCode: string | null + /** The description for the guild */ + description: string | null + /** The banner hash */ + banner: string | null + /** The premium tier */ + premiumTier: number + /** The total number of users currently boosting this server. */ + premiumSubscriptionCount: number + /** The preferred local of this guild only set if guild has the DISCOVERABLE feature, defaults to en-US */ + preferredLocale: string + /** The full URL of the icon from Discords CDN. Undefined when no icon is set. */ + iconURL(): string | undefined + /** The full URL of the splash from Discords CDN. Undefined if no splash is set. */ + splashURL(): string | undefined + /** The full URL of the banner from Discords CDN. Undefined if no banner is set. */ + bannerURL(): string | undefined } export type ImageSize = 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 diff --git a/structures/presence.ts b/structures/presence.ts index 6f63f9f1f..3ed904190 100644 --- a/structures/presence.ts +++ b/structures/presence.ts @@ -1,3 +1,48 @@ +import { UserPayload } from "./user.ts"; +import { ActivityPayload } from "./activity"; +import { StatusType } from "../types/discord"; + +export type PresencePayload = Partial<{ + /** The user presence is being updated for */ + user: UserPayload; + + /** Roles this user is in */ + roles: string[]; + + /** Null, or the user's current activity */ + game: ActivityPayload; + + /** Id of the guild */ + guild_id: string; + + // This is a deviation from the docs, as it pretty much says `: StatusType`. + /** The updated status */ + status: StatusType; + + /** User's current activities */ + activities: ActivityPayload[]; + + /** User's platform-dependent status */ + client_status: ClientStatusPayload; + + /** When the user used their Nitro boost on the server */ + premium_since: string; + + /** This users guild nickname (if one is set) */ + nick: string; +}> & { id: string }; + +export interface ClientStatusPayload { + /** The user's status set for an active desktop (Windows, Linux, Mac) application session */ + desktop?: StatusType; + + /** The user's status set for an active mobile (iOS, Android) application session */ + mobile?: StatusType; + + /** The user's status set for an active web (browser, bot account) application session */ + web?: StatusType; +} + export const createPresence = (data: unknown) => { console.log(data) } diff --git a/structures/user.ts b/structures/user.ts new file mode 100644 index 000000000..3231c3333 --- /dev/null +++ b/structures/user.ts @@ -0,0 +1,34 @@ +export interface UserPayload { + /** The user's id */ + id: string; + + /** The user's username, not unique across the platform */ + username: string; + + /** The user's 4-digit discord-tag */ + discriminator: string; + + /** The user's avatar hash */ + avatar: string; + + /** Whether the user belongs to an OAuth2 application */ + bot?: boolean; + + /** Whether the user is an Official Discord System user (part of the urgent message system) */ + system?: boolean; + + /** Whether the user has two factor enabled on their account */ + mfa_enabled?: boolean; + + // Types with "email" scope intentionally left out. + /** The flags on a user's account */ + flags?: number; + + /** The type of Nitro subscription on a user's account */ + premium_type?: PremiumType; +} + +export const enum PremiumType { + NitroClassic = 1, + Nitro +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index f2edea5bf..54cdc8971 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,66 @@ { "compilerOptions": { - "target": "esnext", - "strict": true /* Enable all strict type-checking options. */, - "noUnusedLocals": true /* Report errors on unused locals. */, - "noUnusedParameters": true /* Report errors on unused parameters. */, - "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, - "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, - "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "ESNext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "lib": ["ES7", "DOM"], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // Hack to stop VSCode from suggesting imports without ./ or ../ as a prefix. + "baseUrl": "../../", /* Base directory to resolve non-absolute module names. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ } } diff --git a/types/discord.ts b/types/discord.ts index cc155da61..68750defa 100644 --- a/types/discord.ts +++ b/types/discord.ts @@ -9,20 +9,20 @@ export interface DiscordPayload { t?: string } -export interface DiscordBotGateway { - /** The WSS URL that can be used for connecting to the gateway. */ - url: string - /** The recommended number of shards to use when connecting. */ - shards: number - /** Info on the current start limit. */ - session_start_limit: { - /** The total number of session starts the current user is allowed. */ - total: number - /** The remaining number of session starts the current user is allowed. */ - remaining: number - /** Milliseconds left until limit is reset. */ - reset_after: number - } +export interface DiscordBotGatewayData { + /** The WSS URL that can be used for connecting to the gateway. */ + url: string + /** The recommended number of shards to use when connecting. */ + shards: number + /** Info on the current start limit. */ + session_start_limit: { + /** The total number of session starts the current user is allowed. */ + total: number + /** The remaining number of session starts the current user is allowed. */ + remaining: number + /** Milliseconds left until limit is reset. */ + reset_after: number + } } export interface DiscordHeartbeatPayload { @@ -30,17 +30,17 @@ export interface DiscordHeartbeatPayload { } export enum GatewayOpcode { - Dispatch = 0, - Heartbeat, - Identify, - StatusUpdate, - VoiceStateUpdate, - Resume, - Reconnect, - RequestGuildMembers, - InvalidSession, - Hello, - HeartbeatACK + Dispatch = 0, + Heartbeat, + Identify, + StatusUpdate, + VoiceStateUpdate, + Resume = 6, + Reconnect, + RequestGuildMembers, + InvalidSession, + Hello, + HeartbeatACK } export enum GatewayCloseEventCode { @@ -154,3 +154,33 @@ export enum JSONErrorCode { ReactionBlocked = 90001, ResourceOverloaded = 130000 } + +export interface Properties { + $os: string; + $browser: string; + $device: string; +} + +export interface Timestamps { + start?: number; + end?: number; +} + +export interface Emoji { + name: string; + id?: string; + animated?: boolean; +} + +export enum StatusType { + Online = 'online', + DoNotDisturb = 'dnd', + Idle = 'idle', + Invisible = 'invisible', + Offline = 'offline' +} + +export interface Status { + afk: boolean; + status: StatusType; +} \ No newline at end of file diff --git a/types/fetch.ts b/types/fetch.ts new file mode 100644 index 000000000..10e204a7d --- /dev/null +++ b/types/fetch.ts @@ -0,0 +1,8 @@ +export const enum RequestMethod { + Get = 'get', + Post = 'post', + Put = 'put', + Patch = 'patch', + Head = 'head', + Delete = 'delete' +} \ No newline at end of file diff --git a/types/message-type.ts b/types/message-type.ts new file mode 100644 index 000000000..664478ebb --- /dev/null +++ b/types/message-type.ts @@ -0,0 +1,6 @@ +export enum CollectedMessageType { + Ping, + Pong, + Close, + Message +} diff --git a/types/options.ts b/types/options.ts new file mode 100644 index 000000000..10b108c55 --- /dev/null +++ b/types/options.ts @@ -0,0 +1,13 @@ +import { Properties } from "./discord.ts"; + +export interface FulfilledClientOptions { + token: string; + properties: Properties; + compress: boolean; +} + +export interface ClientOptions { + token: string; + properties?: Properties; + compress?: boolean; +} diff --git a/types/queue.ts b/types/queue.ts new file mode 100644 index 000000000..d5dabbd47 --- /dev/null +++ b/types/queue.ts @@ -0,0 +1,38 @@ +import { DiscordPayload } from "./discord"; +import Gateway from "../module/gateway.ts"; + +export abstract class ActionQueue { + protected actions: Action[] = []; + + push (action: Action) { + if (this.shouldDispatchImmediately(action)) { + this.dispatch(action); + } else { + this.actions.push(action); + } + } + + dispatchAll () { + let index = 0; + for (const action of this.actions) { + this.actions.splice(index, 1); + this.dispatch(action); + index++; + } + } + + abstract dispatch (action: Action): void; + abstract shouldDispatchImmediately (action: Action): boolean; +} + +export class GatewayActionQueue extends ActionQueue { + constructor (protected gateway: Gateway) { + super(); + } + + dispatch (action: DiscordPayload) { + this.gateway.sendObject(action); + } + + shouldDispatchImmediately () +} \ No newline at end of file