discord-cross-hosting
Version:
The first package which allows broadcastEval() over cross-hosted machines and efficient machine & shard management.
306 lines (305 loc) • 16.7 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Bridge = void 0;
const net_ipc_1 = require("net-ipc");
const IPCMessage_1 = require("../Structures/IPCMessage");
const discord_hybrid_sharding_1 = require("discord-hybrid-sharding");
const shared_1 = require("../types/shared");
class Bridge extends net_ipc_1.Server {
constructor(options) {
var _a, _b, _c, _d;
super(options);
this.authToken = options.authToken;
if (!this.authToken)
throw new Error('MACHINE_MISSING_OPTION - authToken must be provided - String');
this.standAlone = (_a = options.standAlone) !== null && _a !== void 0 ? _a : false;
this.shardsPerCluster = (_b = options.shardsPerCluster) !== null && _b !== void 0 ? _b : 1;
this.totalShards = options.totalShards === 'auto' ? -1 : (_c = options.totalShards) !== null && _c !== void 0 ? _c : -1;
if (this.totalShards !== undefined && !this.standAlone) {
if (this.totalShards !== -1) {
if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) {
throw new TypeError('CLIENT_INVALID_OPTION - Amount of internal shards a number.');
}
if (this.totalShards < 1)
throw new RangeError('CLIENT_INVALID_OPTION - Amount of internal shards at least 1.');
if (!Number.isInteger(this.totalShards)) {
throw new RangeError('CLIENT_INVALID_OPTION - Amount of internal shards an integer.');
}
}
}
this.totalMachines = options.totalMachines;
if (!this.totalMachines)
throw new Error('MISSING_OPTION - Total Machines - Provide the Amount of your Machines');
if (typeof this.totalMachines !== 'number' || isNaN(this.totalMachines)) {
throw new TypeError('MACHINE_INVALID_OPTION - Machine ID must be a number.');
}
if (!Number.isInteger(this.totalMachines)) {
throw new TypeError('MACHINE_INVALID_OPTION - Machine ID must be a number.');
}
this.token = options.token ? options.token.replace(/^Bot\s*/i, '') : undefined;
this.shardList = (_d = options.shardList) !== null && _d !== void 0 ? _d : [];
this.shardClusterList = [];
this.shardClusterListQueue = [];
this.on('ready', this._handleReady.bind(this));
this.on('error', this._handleError.bind(this));
this.on('connect', this._handleConnect.bind(this));
this.on('disconnect', this._handleDisconnect.bind(this));
this.on('message', this._handleMessage.bind(this));
this.on('request', this._handleRequest.bind(this));
this.clients = new Map();
}
_handleReady(url) {
this._debug(`[READY] Bridge operational on ${url}`);
setTimeout(() => {
if (!this.standAlone)
this.initializeShardData();
}, 5000);
}
_handleError(_error) { }
_handleConnect(client, initialData) {
if ((initialData === null || initialData === void 0 ? void 0 : initialData.authToken) !== this.authToken)
return client.close('ACCESS DENIED').catch(e => console.log(e));
const newClient = Object.assign(client, {
authToken: initialData.authToken,
shardList: [],
agent: initialData.agent || 'none',
});
this.clients.set(client.id, newClient);
this._debug(`[CM => Connected][${client.id}]`, { cm: true });
}
_handleDisconnect(client, _reason) {
const cachedClient = this.clients.get(client.id);
if (!cachedClient)
return;
if (cachedClient.agent !== 'bot')
return this.clients.delete(cachedClient.id);
if (!cachedClient.shardList || !cachedClient.shardList.length)
return this.clients.delete(cachedClient.id);
if (!this.standAlone)
this.shardClusterListQueue.push(cachedClient.shardList);
this._debug(`[CM => Disconnected][${cachedClient.id}] New ShardListQueue: ${JSON.stringify(this.shardClusterListQueue)}`);
this.clients.delete(cachedClient.id);
}
_handleMessage(message, _client) {
if (typeof message === 'string')
message = JSON.parse(message);
if ((message === null || message === void 0 ? void 0 : message._type) === undefined)
return;
const client = this.clients.get(_client.id);
if (!client)
return;
if (message._type === shared_1.messageType.CLIENT_SHARDLIST_DATA_CURRENT) {
if (!this.shardClusterListQueue[0])
return;
client.shardList = message.shardList;
this.clients.set(client.id, client);
const checkShardListPositionInQueue = this.shardClusterListQueue.findIndex(x => JSON.stringify(x) === JSON.stringify(message.shardList));
if (checkShardListPositionInQueue === undefined || checkShardListPositionInQueue === -1)
return;
this.shardClusterListQueue.splice(checkShardListPositionInQueue, 1);
this._debug(`[SHARDLIST_DATA_CURRENT][${client.id}] Current ShardListQueue: ${JSON.stringify(this.shardClusterListQueue)}`);
return;
}
let emitMessage;
if (typeof message === 'object')
emitMessage = new IPCMessage_1.IPCMessage(client, message);
else
emitMessage = message;
this.emit('clientMessage', emitMessage, client);
}
_handleRequest(message, res, _client) {
var _a, _b, _c, _d;
if (typeof message === 'string')
message = JSON.parse(message);
if ((message === null || message === void 0 ? void 0 : message._type) === undefined)
return;
const client = this.clients.get(_client.id);
if (!client)
return res({ error: 'Client not registered on Bridges' });
if (message._type === shared_1.messageType.CLIENT_BROADCAST_REQUEST) {
const clients = Array.from(this.clients.values()).filter(((_a = message.options) === null || _a === void 0 ? void 0 : _a.agent) ? c => message.options.agent.includes(c.agent) : c => c.agent === 'bot');
message._type = shared_1.messageType.SERVER_BROADCAST_REQUEST;
const promises = [];
for (const client of clients)
promises.push(client.request(message, (_b = message.options) === null || _b === void 0 ? void 0 : _b.timeout));
Promise.all(promises)
.then(e => res(e))
.catch(_e => null);
return;
}
if (message._type === shared_1.messageType.SHARDLIST_DATA_REQUEST) {
if (!this.shardClusterListQueue[0])
return res([]);
if (!message.maxClusters) {
client.shardList = this.shardClusterListQueue[0];
this.shardClusterListQueue.shift();
}
else {
this.shardClusterListQueue.sort((a, b) => b.length - a.length);
const position = this.shardClusterListQueue.findIndex(x => x.length < message.maxClusters + 1);
if (position === -1) {
return res({ error: 'No Cluster List with less than ' + (message.maxClusters + 1) + ' found!' });
}
else {
client.shardList = this.shardClusterListQueue[position];
this.shardClusterListQueue.splice(position, 1);
}
}
this._debug(`[SHARDLIST_DATA_RESPONSE][${client.id}] ShardList: ${JSON.stringify(client.shardList)}`, {
cm: true,
});
const clusterIds = this.shardClusterList.map(x => x.length);
const shardListPosition = this.shardClusterList.findIndex(x => JSON.stringify(x) === JSON.stringify(client.shardList));
const clusterId = clusterIds.splice(0, shardListPosition);
let r = 0;
r = clusterId.reduce((a, b) => a + b, 0);
const clusterList = [];
for (let i = 0; i < client.shardList.length; i++) {
clusterList.push(r);
r++;
}
res({ shardList: client.shardList, totalShards: this.totalShards, clusterList: clusterList });
this.clients.set(client.id, client);
return;
}
if (message._type === shared_1.messageType.GUILD_DATA_REQUEST) {
if (!message.guildId)
return res({ error: 'Missing guildId for request to Guild' });
this.requestToGuild(message)
.then(e => res(e))
.catch(e => res(Object.assign(Object.assign({}, message), { error: e })));
return;
}
if (message._type === shared_1.messageType.CLIENT_DATA_REQUEST) {
if (!message.agent && !message.clientId)
return res(Object.assign(Object.assign({}, message), { error: 'AGENT MISSING OR CLIENTID MISSING FOR FINDING TARGET CLIENT' }));
if (message.clientId) {
const targetClient = this.clients.get(message.clientId);
if (!targetClient)
return res(Object.assign(Object.assign({}, message), { error: 'CLIENT NOT FOUND WITH PROVIDED CLIENT ID' }));
return targetClient
.request(message, (_c = message.options) === null || _c === void 0 ? void 0 : _c.timeout)
.then(e => res(e))
.catch(e => res(Object.assign(Object.assign({}, message), { error: e })));
}
const clients = Array.from(this.clients.values()).filter(c => c.agent === String(message.agent));
message._type = shared_1.messageType.CLIENT_DATA_REQUEST;
const promises = [];
for (const client of clients)
promises.push(client.request(message, (_d = message.options) === null || _d === void 0 ? void 0 : _d.timeout));
return Promise.all(promises)
.then(e => res(e))
.catch(e => res(Object.assign(Object.assign({}, message), { error: e })));
}
let emitMessage;
if (typeof message === 'object')
emitMessage = new IPCMessage_1.IPCMessage(client, message, res);
else
emitMessage = message;
this.emit('clientRequest', emitMessage, client);
}
initializeShardData() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (this.totalShards === -1 && ((_a = this.shardList) === null || _a === void 0 ? void 0 : _a.length) === 0) {
if (!this.token)
throw new Error('CLIENT_MISSING_OPTION - ' +
'A token must be provided when getting shard count on auto -' +
'Add the Option token: DiscordBOTTOKEN');
this.totalShards = yield (0, discord_hybrid_sharding_1.fetchRecommendedShards)(this.token, 1000);
this.shardList = Array.from(Array(this.totalShards).keys());
}
else {
if (isNaN(this.totalShards) && this.shardList) {
this.totalShards = this.shardList.length;
}
else {
if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) {
throw new TypeError('CLIENT_INVALID_OPTION - Amount of internal shards - a number.');
}
if (this.totalShards < 1)
throw new RangeError('CLIENT_INVALID_OPTION - Amount of internal shards - at least 1.');
if (!Number.isInteger(this.totalShards)) {
throw new RangeError('CLIENT_INVALID_OPTION - Amount of internal shards - an integer.');
}
this.shardList = Array.from(Array(this.totalShards).keys());
}
}
if (this.shardList.some(shardID => shardID >= this.totalShards)) {
throw new RangeError('CLIENT_INVALID_OPTION - ' +
'Amount of Internal Shards - ' +
'bigger than the highest shardID in the shardList option.');
}
const clusterAmount = Math.ceil(this.shardList.length / this.shardsPerCluster);
const ClusterList = (0, discord_hybrid_sharding_1.chunkArray)(this.shardList, Math.ceil(this.shardList.length / clusterAmount));
this.shardClusterList = this.parseClusterList(ClusterList);
this.shardClusterListQueue = this.shardClusterList.slice(0);
this._debug(`Created shardClusterList: ${JSON.stringify(this.shardClusterList)}`);
const clients = Array.from(this.clients.values()).filter(c => c.agent === 'bot');
const message = {
totalShards: this.totalShards,
shardClusterList: this.shardClusterList,
_type: shared_1.messageType.SHARDLIST_DATA_UPDATE,
};
for (const client of clients)
client.send(message);
this._debug(`[SHARDLIST_DATA_UPDATE][${clients.length}] To all connected Clients`, { cm: true });
return this.shardClusterList;
});
}
parseClusterList(ClusterList) {
return (0, discord_hybrid_sharding_1.chunkArray)(ClusterList, Math.ceil(ClusterList.length / this.totalMachines));
}
broadcastEval(script_1) {
return __awaiter(this, arguments, void 0, function* (script, options = { filter: undefined }) {
if (!script || (typeof script !== 'string' && typeof script !== 'function'))
throw new Error('Script for BroadcastEvaling has not been provided or must be a valid String!');
script = typeof script === 'function' ? `(${script})(this, ${JSON.stringify(options.context)})` : script;
const message = { script, options, _type: shared_1.messageType.SERVER_BROADCAST_REQUEST };
const clients = Array.from(this.clients.values()).filter(options.filter || (c => c.agent === 'bot'));
const promises = [];
for (const client of clients)
promises.push(client.request(message, options.timeout));
return Promise.all(promises);
});
}
requestToGuild(message, options) {
return __awaiter(this, void 0, void 0, function* () {
if (!(message === null || message === void 0 ? void 0 : message.guildId))
throw new Error('GuildID has not been provided!');
const internalShard = (0, discord_hybrid_sharding_1.shardIdForGuildId)(message.guildId, this.totalShards);
const targetClient = Array.from(this.clients.values()).find(x => { var _a, _b; return (_b = (_a = x === null || x === void 0 ? void 0 : x.shardList) === null || _a === void 0 ? void 0 : _a.flat()) === null || _b === void 0 ? void 0 : _b.includes(internalShard); });
if (!targetClient)
throw new Error('Internal Shard not found!');
if (!message.options)
message.options = options !== null && options !== void 0 ? options : {};
if (message.eval)
message._type = shared_1.messageType.GUILD_EVAL_REQUEST;
else
message._type = shared_1.messageType.GUILD_DATA_REQUEST;
message.options.shard = internalShard;
return targetClient.request(message, message.options.timeout);
});
}
_debug(message, options) {
let log;
if (options === null || options === void 0 ? void 0 : options.cm) {
log = `[Bridge => CM] ` + message;
}
else {
log = `[Bridge] ` + message;
}
this.emit('debug', log);
return log;
}
}
exports.Bridge = Bridge;