mirror of
https://github.com/discordeno/discordeno.git
synced 2026-09-17 08:47:22 +00:00
New API draft
This commit is contained in:
+14
-12
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export class Ratelimiter {
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { delay } from 'https://deno.land/std/util/async.ts';
|
||||
|
||||
+3
-1
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum CollectedMessageType {
|
||||
Ping,
|
||||
Pong,
|
||||
Close,
|
||||
Message
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 ()
|
||||
}
|
||||
Reference in New Issue
Block a user