more work!

This commit is contained in:
Skillz
2020-11-17 23:43:04 -05:00
parent 933688f9a3
commit af5814e8a7
7 changed files with 131 additions and 181 deletions
-4
View File
@@ -1,5 +1,3 @@
import createClient from "./src/module/client.ts";
export * from "./src/controllers/bans.ts";
export * from "./src/controllers/cache.ts";
export * from "./src/controllers/channels.ts";
@@ -42,5 +40,3 @@ export * from "./src/utils/cdn.ts";
export * from "./src/utils/collection.ts";
export * from "./src/utils/permissions.ts";
export * from "./src/utils/utils.ts";
export default createClient;
-58
View File
@@ -1,58 +0,0 @@
import { DiscordBotGatewayData, RequestManager, spawnBigBrainBotShards } from "../../mod.ts";
import { endpoints } from "../constants/discord.ts";
import { ClientOptions, EventHandlers } from "../types/options.ts";
const botOptions = {
createNextShard: false,
workers: new Map<number, Worker>(),
eventHandlers: {} as EventHandlers,
botGatewayData: {} as DiscordBotGatewayData,
identifyPayload: {
token: "",
compress: true,
properties: {
$os: "linux",
$browser: "Discordeno",
$device: "Discordeno",
},
intents: 0,
shard: [0, 0] as [number, number],
},
};
/**
* This function should be used only by bot developers whose bots are in over 25,000 servers.
* Please be aware if you are a beginner developer using this, things will not work as per the guides. This is for advanced developers only!
*
* Advanced Devs: This function will allow you to have an insane amount of customization potential as when you get to large bots you need to be able to optimize every tiny detail to make you bot work the way you need.
*/
export async function startBigBrainBot(data: BigBrainBotOptions) {
botOptions.identifyPayload.token = `Bot ${data.token}`;
if (data.eventHandlers) botOptions.eventHandlers = data.eventHandlers;
if (data.compress) botOptions.identifyPayload.compress = data.compress;
// Initial API connection to get info about bots connection
botOptions.botGatewayData = await RequestManager.get(
endpoints.GATEWAY_BOT,
) as DiscordBotGatewayData;
botOptions.identifyPayload.intents = data.intents.reduce(
(bits, next) => (bits |= next),
0,
);
spawnBigBrainBotShards(botOptions.botGatewayData, botOptions.identifyPayload, data.firstShardID, data.lastShardID || (data.firstShardID + 25));
}
export interface BigBrainBotOptions extends ClientOptions {
/** The first shard to start at for this worker. Use this to control which shards to run in each worker. */
firstShardID: number;
/** The last shard to start for this worker. By default it will be 25 + the firstShardID. */
lastShardID?: number;
/** This can be used to forward the ws handling to a proxy. */
wsURL?: string;
/** This can be used to forward the REST handling to a proxy. */
restURL?: string;
}
+57 -3
View File
@@ -1,6 +1,6 @@
import { DiscordBotGatewayData } from "../types/discord.ts";
import { ClientOptions, EventHandlers } from "../types/options.ts";
import { endpoints } from "../utils/constants.ts";
import { baseEndpoints, endpoints } from "../utils/constants.ts";
import { RequestManager } from "./requestManager.ts";
import { spawnShards } from "./shardingManager.ts";
@@ -10,6 +10,7 @@ export let botID = "";
export let eventHandlers: EventHandlers = {};
export let botGatewayData: DiscordBotGatewayData;
export let proxyWSURL = "";
export const identifyPayload: IdentifyPayload = {
token: "",
@@ -51,8 +52,8 @@ export async function createClient(data: ClientOptions) {
);
identifyPayload.shard = [0, botGatewayData.shards];
spawnShards(botGatewayData, identifyPayload);
};
spawnShards(botGatewayData, identifyPayload, 0, botGatewayData.shards);
}
export default createClient;
@@ -63,3 +64,56 @@ export function updateEventHandlers(newEventHandlers: EventHandlers) {
export function setBotID(id: string) {
if (botID !== id) botID = id;
}
// BIG BRAIN BOT STUFF ONLY BELOW THIS
/**
* This function should be used only by bot developers whose bots are in over 25,000 servers.
* Please be aware if you are a beginner developer using this, things will not work as per the guides. This is for advanced developers only!
*
* Advanced Devs: This function will allow you to have an insane amount of customization potential as when you get to large bots you need to be able to optimize every tiny detail to make you bot work the way you need.
*/
export async function startBigBrainBot(data: BigBrainBotOptions) {
authorization = `Bot ${data.token}`;
identifyPayload.token = `Bot ${data.token}`;
if (data.restURL) baseEndpoints.BASE_URL = data.restURL;
if (data.cdnURL) baseEndpoints.CDN_URL = data.cdnURL;
if (data.wsURL) proxyWSURL = data.wsURL;
if (data.eventHandlers) eventHandlers = data.eventHandlers;
if (data.compress) {
identifyPayload.compress = data.compress;
}
identifyPayload.intents = data.intents.reduce(
(bits, next) => (bits |= next),
0,
);
// Initial API connection to get info about bots connection
botGatewayData = await RequestManager.get(
endpoints.GATEWAY_BOT,
) as DiscordBotGatewayData;
spawnShards(
botGatewayData,
identifyPayload,
data.firstShardID,
data.lastShardID || botGatewayData.shards >= 25
? (data.firstShardID + 25)
: botGatewayData.shards,
);
}
export interface BigBrainBotOptions extends ClientOptions {
/** The first shard to start at for this worker. Use this to control which shards to run in each worker. */
firstShardID: number;
/** The last shard to start for this worker. By default it will be 25 + the firstShardID. */
lastShardID?: number;
/** This can be used to forward the ws handling to a proxy. */
wsURL?: string;
/** This can be used to forward the REST handling to a proxy. */
restURL?: string;
/** This can be used to forward the CDN handling to a proxy. */
cdnURL?: string;
}
+16 -2
View File
@@ -2,7 +2,7 @@ import { delay } from "../../deps.ts";
import { HttpResponseCode } from "../types/discord.ts";
import { Errors } from "../types/errors.ts";
import { RequestMethods } from "../types/fetch.ts";
import { baseEndpoints } from "../utils/constants.ts";
import { baseEndpoints, discordAPIURLS } from "../utils/constants.ts";
import { authorization, eventHandlers } from "./client.ts";
const pathQueues: { [key: string]: QueuedRequest[] } = {};
@@ -144,7 +144,7 @@ function createRequestBody(body: any, method: RequestMethods) {
const headers: { [key: string]: string } = {
Authorization: authorization,
"User-Agent":
`DiscordBot (https://github.com/skillz4killz/discordeno, 6.0.0)`,
`DiscordBot (https://github.com/skillz4killz/discordeno, v10)`,
};
if (method === "get") body = undefined;
@@ -203,6 +203,20 @@ async function runMethod(
const errorStack = new Error("Location:");
Error.captureStackTrace(errorStack);
// For proxies we don't need to do any of the legwork so we just forward the request
if (
!url.startsWith(discordAPIURLS.BASE_URL) &&
!url.startsWith(discordAPIURLS.CDN_URL)
) {
return fetch(url, { method, body: body ? JSON.stringify(body) : undefined })
.then((res) => res.json())
.catch((error) => {
console.error(error);
throw errorStack;
});
}
// No proxy so we need to handl all rate limiting and such
return new Promise((resolve, reject) => {
const callback = async () => {
try {
@@ -7,6 +7,7 @@ import {
isWebSocketPongEvent,
WebSocket,
} from "../../deps.ts";
import { eventHandlers } from "../../mod.ts";
import {
DiscordBotGatewayData,
DiscordHeartbeatPayload,
@@ -15,7 +16,7 @@ import {
} from "../types/discord.ts";
import { FetchMembersOptions } from "../types/guild.ts";
import { BotStatusRequest } from "../utils/utils.ts";
import { botGatewayData, eventHandlers, IdentifyPayload } from "./client.ts";
import { IdentifyPayload, proxyWSURL } from "./client.ts";
import { handleDiscordPayload } from "./shardingManager.ts";
const basicShards = new Map<number, BasicShard>();
@@ -40,7 +41,7 @@ interface RequestMemberQueuedRequest {
options?: FetchMembersOptions;
}
export async function createBasicShard(
export async function createShard(
data: DiscordBotGatewayData,
identifyPayload: IdentifyPayload,
resuming = false,
@@ -50,7 +51,9 @@ export async function createBasicShard(
const basicShard: BasicShard = {
id: shardID,
socket: await connectWebSocket(`${data.url}?v=8&encoding=json`),
socket: await connectWebSocket(
proxyWSURL || `${data.url}?v=8&encoding=json`,
),
resumeInterval: 0,
sessionID: oldShard?.sessionID || "",
previousSequenceNumber: oldShard?.previousSequenceNumber || 0,
@@ -94,10 +97,10 @@ export async function createBasicShard(
data: { shardID: basicShard.id, message },
},
);
createBasicShard(botGatewayData, identifyPayload, false, shardID);
createShard(data, identifyPayload, false, shardID);
} else {
basicShard.needToResume = true;
resumeConnection(botGatewayData, identifyPayload, basicShard.id);
resumeConnection(data, identifyPayload, basicShard.id);
}
continue;
} else if (isWebSocketPingEvent(message) || isWebSocketPongEvent(message)) {
@@ -122,6 +125,7 @@ export async function createBasicShard(
basicShard,
(data.d as DiscordHeartbeatPayload).heartbeat_interval,
identifyPayload,
data,
);
}
break;
@@ -133,7 +137,7 @@ export async function createBasicShard(
{ type: "reconnect", data: { shardID: basicShard.id } },
);
basicShard.needToResume = true;
resumeConnection(botGatewayData, identifyPayload, basicShard.id);
resumeConnection(data, identifyPayload, basicShard.id);
break;
case GatewayOpcode.InvalidSession:
eventHandlers.debug?.(
@@ -141,11 +145,11 @@ export async function createBasicShard(
);
// When d is false we need to reidentify
if (!data.d) {
createBasicShard(botGatewayData, identifyPayload, false, shardID);
createShard(data, identifyPayload, false, shardID);
break;
}
basicShard.needToResume = true;
resumeConnection(botGatewayData, identifyPayload, basicShard.id);
resumeConnection(data, identifyPayload, basicShard.id);
break;
default:
if (data.t === "RESUMED") {
@@ -206,11 +210,12 @@ async function heartbeat(
shard: BasicShard,
interval: number,
payload: IdentifyPayload,
data: DiscordBotGatewayData,
) {
// We lost socket connection between heartbeats, resume connection
if (shard.socket.isClosed) {
shard.needToResume = true;
resumeConnection(botGatewayData, payload, shard.id);
resumeConnection(data, payload, shard.id);
heartbeating.delete(shard.id);
return;
}
@@ -252,11 +257,11 @@ async function heartbeat(
},
);
await delay(interval);
heartbeat(shard, interval, payload);
heartbeat(shard, interval, payload, data);
}
async function resumeConnection(
botGatewayData: DiscordBotGatewayData,
data: DiscordBotGatewayData,
payload: IdentifyPayload,
shardID: number,
) {
@@ -272,10 +277,10 @@ async function resumeConnection(
eventHandlers.debug?.({ type: "resuming", data: { shardID: shard.id } });
// Run it once
createBasicShard(botGatewayData, payload, true, shard.id);
createShard(data, payload, true, shard.id);
// Then retry every 15 seconds
await delay(1000 * 15);
if (shard.needToResume) resumeConnection(botGatewayData, payload, shardID);
if (shard.needToResume) resumeConnection(data, payload, shardID);
}
export function requestGuildMembers(
+32 -93
View File
@@ -11,20 +11,11 @@ import { cache } from "../utils/cache.ts";
import { BotStatusRequest } from "../utils/utils.ts";
import {
botGatewayStatusRequest,
createBasicShard,
createShard,
requestGuildMembers,
} from "./basicShard.ts";
import {
botGatewayData,
eventHandlers,
IdentifyPayload,
identifyPayload,
} from "./client.ts";
} from "./shard.ts";
import { eventHandlers, IdentifyPayload } from "./client.ts";
let shardCounter = 0;
let basicSharding = false;
const shards: Worker[] = [];
let createNextShard = true;
/** This function is meant to be used on the ready event to alert the library to start the next shard. */
@@ -32,48 +23,28 @@ export function allowNextShard(enabled = true) {
createNextShard = enabled;
}
export function createShardWorker(shardID?: number) {
const path = new URL("./shard.ts", import.meta.url).toString();
const shard = new Worker(path, { type: "module", deno: true });
shard.onmessage = (message) => {
if (message.data.type === "REQUEST_CLIENT_OPTIONS") {
identifyPayload.shard = [
shardID || shardCounter,
botGatewayData.shards,
];
shard.postMessage(
{
type: "CREATE_SHARD",
botGatewayData,
identifyPayload,
shardID: shardCounter,
},
);
// Update the shard counter
shardCounter++;
} else if (message.data.type === "HANDLE_DISCORD_PAYLOAD") {
handleDiscordPayload(
JSON.parse(message.data.payload),
message.data.shardID,
);
} else if (message.data.type === "DEBUG_LOG") {
eventHandlers.debug?.(message.data.details);
}
};
shards.push(shard);
}
export async function spawnBigBrainBotShards(data: DiscordBotGatewayData, payload: IdentifyPayload, shardID: number, lastShardID: number, skipChecks?: number) {
export async function spawnShards(
data: DiscordBotGatewayData,
payload: IdentifyPayload,
shardID: number,
lastShardID: number,
skipChecks?: number,
) {
// All shards on this worker have started! Cancel out.
if (shardID > lastShardID) return;
if (shardID >= lastShardID) return;
if (skipChecks) {
payload.shard = [shardID, data.shards];
// Start The shard
createBasicShard(data, payload, false, shardID);
createShard(data, payload, false, shardID);
// Spawn next shard
spawnBigBrainBotShards(data, payload, shardID, lastShardID, skipChecks - 1);
spawnShards(
data,
payload,
shardID + 1,
lastShardID,
skipChecks - 1,
);
return;
}
@@ -81,35 +52,20 @@ export async function spawnBigBrainBotShards(data: DiscordBotGatewayData, payloa
if (createNextShard) {
createNextShard = false;
// Start the next few shards based on max concurrency
spawnBigBrainBotShards(data, payload, shardID + 1, lastShardID, data.session_start_limit.max_concurrency);
spawnShards(
data,
payload,
shardID,
lastShardID,
data.session_start_limit.max_concurrency,
);
return;
}
await delay(1000);
spawnBigBrainBotShards(data, payload, shardID, lastShardID, skipChecks);
spawnShards(data, payload, shardID, lastShardID, skipChecks);
}
export const spawnShards = async (
data: DiscordBotGatewayData,
payload: IdentifyPayload,
id = 1,
) => {
if ((data.shards === 1 && id === 1) || id <= data.shards) {
if (createNextShard) {
createNextShard = false;
if (data.shards >= 25) createShardWorker();
else {
basicSharding = true;
createBasicShard(data, payload, false, id - 1);
}
spawnShards(data, payload, id + 1);
} else {
await delay(1000);
spawnShards(data, payload, id);
}
}
};
export async function handleDiscordPayload(
data: DiscordPayload,
shardID: number,
@@ -138,30 +94,13 @@ export async function requestAllMembers(
const nonce = `${guild.id}-${Math.random().toString()}`;
cache.fetchAllMembersProcessingRequests.set(nonce, resolve);
if (basicSharding) {
return requestGuildMembers(guild.id, guild.shardID, nonce, options);
}
shards[guild.shardID].postMessage({
type: "FETCH_MEMBERS",
guildID: guild.id,
nonce,
options,
});
return requestGuildMembers(guild.id, guild.shardID, nonce, options);
}
export function sendGatewayCommand(type: "EDIT_BOTS_STATUS", payload: object) {
if (basicSharding) {
if (type === "EDIT_BOTS_STATUS") {
botGatewayStatusRequest(payload as BotStatusRequest);
}
return;
if (type === "EDIT_BOTS_STATUS") {
botGatewayStatusRequest(payload as BotStatusRequest);
}
shards.forEach((shard) => {
shard.postMessage({
type,
...payload,
});
});
return;
}
+8 -8
View File
@@ -1,14 +1,14 @@
let API_VERSION = "v8";
export const baseEndpoints = {
/** Although, the version can be defaulted, keep the v6 as it can be changed to test newer versions when necessary. */
BASE_URL: `https://discord.com/api/${API_VERSION}`,
// These will never be modified and remain constants
export const discordAPIURLS = {
BASE_URL: `https://discord.com/api/v8`,
CDN_URL: "https://cdn.discordapp.com",
};
export function changeAPIVersion(number = 7) {
API_VERSION = `v${number}`;
}
// This can be modified by big brain bots and use a proxy
export const baseEndpoints = {
BASE_URL: discordAPIURLS.BASE_URL,
CDN_URL: discordAPIURLS.CDN_URL,
};
const GUILDS_BASE = (id: string) => `${baseEndpoints.BASE_URL}/guilds/${id}`;