New API draft

This commit is contained in:
Will Hoskings
2020-02-09 23:00:14 +00:00
parent b2800af61d
commit 2f16b77cc9
11 changed files with 303 additions and 64 deletions
+14 -12
View File
@@ -1,31 +1,33 @@
import Client from "../module/Client.ts";
class RequestManager {
client: Client
token: string
client: Client;
token: string;
currentRatelimit
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, payload?: unknown, shouldRatelimit = true) {
if (shouldRatelimit) {
// let attempts = 0
const headers = {
Authorization: this.token,
"User-Agent": `DiscordBot (https://github.com/skillz4killz/discordeno, 0.0.1)`,
}
const headers = this.getDiscordHeaders();
console.log('payload', payload)
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<string, string> {
return {
Authorization: this.token,
"User-Agent": `DiscordBot (https://github.com/skillz4killz/discordeno, 0.0.1)`,
};
}
}
export default RequestManager
+18 -3
View File
@@ -1,7 +1,22 @@
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 () {
console.log({ configs });
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
})
}
}
})();
+109 -46
View File
@@ -1,78 +1,141 @@
import { endpoints } from "../constants/discord.ts";
import RequestManager from "../managers/RequestManager.ts";
import { DiscordBotGateway, DiscordPayload, DiscordHeartbeatPayload } from "../types/discord.ts";
import ShardingManager from "../managers/ShardingManager.ts";
import { endpoints } from '../constants/discord.ts'
import RequestManager from '../managers/RequestManager.ts'
import { DiscordBotGatewayData, DiscordPayload, DiscordHeartbeatPayload, GatewayOpcode } from '../types/discord.ts'
import ShardingManager from '../managers/ShardingManager.ts'
import {
connectWebSocket,
isWebSocketCloseEvent,
isWebSocketPingEvent,
isWebSocketPongEvent,
WebSocket
} from "https://deno.land/std/ws/mod.ts";
} 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 { blue, green, red, yellow } from "https://deno.land/std/fmt/colors.ts"
import { keepDiscordWebsocketAlive } from "./websocket.ts";
import { blue, green, red, yellow } from 'https://deno.land/std/fmt/colors.ts'
import { keepDiscordWebsocketAlive } from './websocket.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. */
token: string;
token: string
/** The Rate limit manager to handle all outgoing requests to discord. Not meant to be used by users. */
RequestManager: RequestManager;
RequestManager: RequestManager
/** Creates and handles all the shards necessary for the bot. */
ShardingManager: ShardingManager;
ShardingManager: ShardingManager
constructor(token: string) {
this.token = `Bot ${token}`;
this.RequestManager = new RequestManager(this, this.token);
this.ShardingManager = new ShardingManager();
/** 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()
}
getGatewayData() {
return this.RequestManager.get(endpoints.GATEWAY_BOT) as Promise<DiscordBotGatewayData>
}
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)
}
} 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() {
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)
console.log(green("ws connected! (type 'close' to quit)"));
for await (const msg of sock.receive()) {
if (typeof msg === "string") {
try {
const json = JSON.parse(msg)
this.handleDiscordPayload(json, sock)
} catch {
console.log(red(`Invalid JSON String send by discord: ${msg}`))
}
console.log(yellow("< " + msg));
} else if (isWebSocketPingEvent(msg)) {
console.log(blue("< ping"));
} else if (isWebSocketPongEvent(msg)) {
console.log(blue("< pong"));
} else if (isWebSocketCloseEvent(msg)) {
console.log(red(`closed: code=${msg.code}, reason=${msg.reason}`));
async *connect(gateway: Gateway, data: DiscordBotGatewayData): AsyncGenerator<{ type: CollectedMessageType, data?: DiscordPayload, action?: Promise<void> }> {
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
}
}
// Begin spawning all necessary shards
this.spawnShards(data.shards);
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)
}
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) {
// this.ShardingManager.spawnShard(id);
if (id < total) this.spawnShards(total, id + 1);
if (id < total) this.spawnShards(total, id + 1)
}
}
export default Client;
export default Client
+3
View File
@@ -0,0 +1,3 @@
export class Ratelimiter {
}
+56
View File
@@ -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<void> {
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));
}
}
+2
View File
@@ -0,0 +1,2 @@
import { delay } from 'https://deno.land/std/util/async.ts';
+3 -1
View File
@@ -1,13 +1,15 @@
import { WebSocket } from "https://deno.land/std/ws/mod.ts";
import { GatewayOpcode } from "../types/discord.ts";
export const keepDiscordWebsocketAlive = (socket: WebSocket, millesecondsInterval: number, payload: number | null = null) => {
let previousSequenceNumber = payload
let doneInitial = false;
setInterval(async () => {
const response = await socket.send(JSON.stringify({
op: 1,
d: previousSequenceNumber
}))
}));
console.log(response)
+41 -2
View File
@@ -9,7 +9,7 @@ export interface DiscordPayload {
t?: string
}
export interface DiscordBotGateway {
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. */
@@ -35,7 +35,7 @@ export enum GatewayOpcode {
Identify,
StatusUpdate,
VoiceStateUpdate,
Resume,
Resume = 6,
Reconnect,
RequestGuildMembers,
InvalidSession,
@@ -154,3 +154,42 @@ 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 interface Activity {
name: string;
type: number;
url?: string;
created_at: number;
timestamps: Timestamps;
details?: string;
}
export enum StatusType {
Online = 'online',
DoNotDisturb = 'dnd',
Idle = 'idle',
Invisible = 'invisible',
Offline = 'offline'
}
export interface Status {
afk: boolean;
status: StatusType;
}
+6
View File
@@ -0,0 +1,6 @@
export enum CollectedMessageType {
Ping,
Pong,
Close,
Message
}
+13
View File
@@ -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;
}
+38
View File
@@ -0,0 +1,38 @@
import { DiscordPayload } from "./discord";
import Gateway from "../module/gateway.ts";
export abstract class ActionQueue<Action> {
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<DiscordPayload> {
constructor (protected gateway: Gateway) {
super();
}
dispatch (action: DiscordPayload) {
this.gateway.sendObject(action);
}
shouldDispatchImmediately ()
}