seyfert
Version:
The most advanced framework for discord bots
499 lines (498 loc) • 19.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkerClient = void 0;
exports.generateShardInfo = generateShardInfo;
const node_crypto_1 = require("node:crypto");
const __1 = require("..");
const cache_1 = require("../cache");
const common_1 = require("../common");
const events_1 = require("../events");
const websocket_1 = require("../websocket");
const base_1 = require("./base");
const memberUpdate_1 = require("../websocket/discord/events/memberUpdate");
const presenceUpdate_1 = require("../websocket/discord/events/presenceUpdate");
const collectors_1 = require("./collectors");
const transformers_1 = require("./transformers");
let workerData;
let manager;
try {
workerData = {
debug: String(process.env.SEYFERT_WORKER_DEBUG) === 'true',
intents: Number(process.env.SEYFERT_WORKER_INTENTS),
path: process.env.SEYFERT_WORKER_PATH,
shards: JSON.parse(process.env.SEYFERT_WORKER_SHARDS),
token: process.env.SEYFERT_WORKER_TOKEN,
workerId: Number(process.env.SEYFERT_WORKER_WORKERID),
workerProxy: String(process.env.SEYFERT_WORKER_WORKERPROXY) === 'true',
totalShards: Number(process.env.SEYFERT_WORKER_TOTALSHARDS),
mode: process.env.SEYFERT_WORKER_MODE,
resharding: String(process.env.SEYFERT_WORKER_RESHARDING) === 'true',
totalWorkers: Number(process.env.SEYFERT_WORKER_TOTALWORKERS),
info: JSON.parse(process.env.SEYFERT_WORKER_INFO),
compress: String(process.env.SEYFERT_WORKER_COMPRESS) === 'true',
};
}
catch {
//
}
class WorkerClient extends base_1.BaseClient {
memberUpdateHandler = new memberUpdate_1.MemberUpdateHandler();
presenceUpdateHandler = new presenceUpdate_1.PresenceUpdateHandler();
collectors = new collectors_1.Collectors();
events = new events_1.EventHandler(this);
me;
promises = new Map();
shards = new Map();
resharding = new Map();
constructor(options) {
super(options);
if (options?.postMessage) {
this.postMessage = options.postMessage;
}
if (this.options.handleManagerMessages) {
const oldFn = this.handleManagerMessages.bind(this);
this.handleManagerMessages = async (message) => {
await this.options.handleManagerMessages(message);
return oldFn(message);
};
}
}
get workerId() {
return workerData.workerId;
}
get latency() {
let acc = 0;
this.shards.forEach(s => (acc += s.latency));
return acc / this.shards.size;
}
setServices(rest) {
super.setServices(rest);
if (this.options.postMessage && rest.cache?.adapter instanceof cache_1.WorkerAdapter) {
rest.cache.adapter.postMessage = this.options.postMessage;
}
}
setWorkerData(data) {
workerData = data;
}
get workerData() {
return workerData;
}
async start(options = {}) {
const worker_threads = (0, common_1.lazyLoadPackage)('node:worker_threads');
if (worker_threads?.parentPort) {
manager = worker_threads?.parentPort;
}
if (workerData.mode !== 'custom')
(manager ?? process).on('message', (data) => this.handleManagerMessages(data));
this.logger = new __1.Logger({
name: `[Worker #${workerData.workerId}]`,
});
if (workerData.debug) {
this.debugger = new __1.Logger({
name: `[Worker #${workerData.workerId}]`,
logLevel: common_1.LogLevels.Debug,
});
}
if (workerData.workerProxy) {
this.setServices({
rest: new __1.ApiHandler({
token: workerData.token,
workerProxy: true,
debug: workerData.debug,
}),
});
}
this.cache.intents = workerData.intents;
this.rest.workerData = workerData;
this.postMessage({
type: workerData.resharding ? 'WORKER_START_RESHARDING' : 'WORKER_START',
workerId: workerData.workerId,
});
await super.start(options);
await this.loadEvents(options.eventsDir);
}
async loadEvents(dir) {
dir ??= await this.getRC().then(x => x.locations.events);
if (dir) {
await this.events.load(dir);
this.logger.info('EventHandler loaded');
}
}
postMessage(body) {
if (manager)
return manager.postMessage(body);
return process.send(body);
}
async handleManagerMessages(data) {
switch (data.type) {
case 'CACHE_RESULT':
if (this.cache.adapter instanceof cache_1.WorkerAdapter && this.cache.adapter.promises.has(data.nonce)) {
const cacheData = this.cache.adapter.promises.get(data.nonce);
clearTimeout(cacheData.timeout);
cacheData.resolve(data.result);
this.cache.adapter.promises.delete(data.nonce);
}
break;
case 'SEND_PAYLOAD':
{
const shard = this.shards.get(data.shardId);
if (!shard) {
this.logger.fatal('Worker trying send payload by non-existent shard');
return;
}
await shard.send(true, {
...data,
});
this.postMessage({
type: 'RESULT_PAYLOAD',
nonce: data.nonce,
workerId: this.workerId,
});
}
break;
case 'ALLOW_CONNECT_RESHARDING':
{
const shard = this.resharding.get(data.shardId);
if (!shard) {
this.logger.fatal('Worker trying reshard non-existent shard');
return;
}
shard.options.presence = data.presence;
await shard.connect();
}
break;
case 'ALLOW_CONNECT':
{
const shard = this.shards.get(data.shardId);
if (!shard) {
this.logger.fatal('Worker trying connect non-existent shard');
return;
}
shard.options.presence = data.presence;
await shard.connect();
}
break;
case 'SPAWN_SHARDS_RESHARDING':
{
let shardsConnected = 0;
const self = this;
for (const id of workerData.shards) {
const existsShard = this.resharding.has(id);
if (existsShard) {
this.logger.warn(`Trying to re-spawn existing shard #${id}`);
continue;
}
const shard = new websocket_1.Shard(id, {
token: workerData.token,
intents: workerData.intents,
info: data.info,
compress: data.compress,
debugger: this.debugger,
properties: {
...websocket_1.properties,
...this.options.gateway?.properties,
},
handlePayload(_, payload) {
if (payload.t !== 'GUILDS_READY')
return;
if (++shardsConnected === workerData.shards.length) {
self.postMessage({
type: 'WORKER_READY_RESHARDING',
workerId: workerData.workerId,
});
}
},
});
this.resharding.set(id, shard);
this.postMessage({
type: 'CONNECT_QUEUE_RESHARDING',
shardId: id,
workerId: workerData.workerId,
});
}
}
break;
case 'SPAWN_SHARDS':
{
for (const id of workerData.shards) {
const existsShard = this.shards.has(id);
if (existsShard) {
this.logger.warn(`Trying to spawn existing shard #${id}`);
continue;
}
const shard = this.createShard(id, data);
this.shards.set(id, shard);
this.postMessage({
type: 'CONNECT_QUEUE',
shardId: id,
workerId: workerData.workerId,
});
}
}
break;
case 'SHARD_INFO':
{
const shard = this.shards.get(data.shardId);
if (!shard) {
this.logger.fatal('Worker trying get non-existent shard');
return;
}
this.postMessage({
...generateShardInfo(shard),
nonce: data.nonce,
type: 'SHARD_INFO',
workerId: this.workerId,
});
}
break;
case 'WORKER_INFO':
{
this.postMessage({
shards: [...this.shards.values()].map(generateShardInfo),
workerId: workerData.workerId,
type: 'WORKER_INFO',
nonce: data.nonce,
});
}
break;
case 'BOT_READY':
await this.events.runEvent('BOT_READY', this, this.me, -1);
break;
case 'API_RESPONSE':
{
const promise = this.rest.workerPromises.get(data.nonce);
if (!promise)
return;
this.rest.workerPromises.delete(data.nonce);
if (data.error)
return promise.reject(data.error);
promise.resolve(data.response);
}
break;
case 'EXECUTE_EVAL':
case 'EXECUTE_EVAL_TO_WORKER':
{
let result;
try {
result = await eval(`
(${data.func})(this, ${data.vars})
`);
}
catch (e) {
result = e;
}
this.postMessage({
type: 'EVAL_RESPONSE',
response: result,
workerId: workerData.workerId,
nonce: data.nonce,
});
}
break;
case 'EVAL_RESPONSE':
{
const evalResponse = this.promises.get(data.nonce);
if (!evalResponse)
return;
this.promises.delete(data.nonce);
clearTimeout(evalResponse.timeout);
evalResponse.resolve(data.response);
}
break;
case 'WORKER_ALREADY_EXISTS_RESHARDING':
{
this.postMessage({
type: 'WORKER_START_RESHARDING',
workerId: workerData.workerId,
});
}
break;
case 'DISCONNECT_ALL_SHARDS_RESHARDING':
{
for (const i of this.shards.values()) {
await i.disconnect();
}
this.postMessage({
type: 'DISCONNECTED_ALL_SHARDS_RESHARDING',
workerId: workerData.workerId,
});
}
break;
case 'CONNECT_ALL_SHARDS_RESHARDING':
{
this.shards.clear();
const handlePayload = this.options?.handlePayload?.bind(this);
for (const [id, shard] of this.resharding) {
this.shards.set(id, shard);
shard.options.handlePayload = async (shardId, packet) => {
await handlePayload?.(shardId, packet);
return this.onPacket(packet, shardId);
};
}
this.resharding.clear();
}
break;
}
}
generateNonce() {
const uuid = (0, node_crypto_1.randomUUID)();
if (this.promises.has(uuid))
return this.generateNonce();
return uuid;
}
generateSendPromise(nonce, message = 'Timeout') {
return new Promise((res, rej) => {
const timeout = setTimeout(() => {
this.promises.delete(nonce);
rej(new Error(message));
}, 60e3);
this.promises.set(nonce, { resolve: res, timeout });
});
}
tellWorker(workerId, func, vars) {
const nonce = this.generateNonce();
this.postMessage({
type: 'EVAL_TO_WORKER',
func: func.toString(),
toWorkerId: workerId,
workerId: workerData.workerId,
nonce,
vars: JSON.stringify(vars),
});
return this.generateSendPromise(nonce);
}
tellWorkers(func, vars) {
const promises = [];
for (let i = 0; i < workerData.totalWorkers; i++) {
promises.push(this.tellWorker(i, func, vars));
}
return Promise.all(promises);
}
createShard(id, data) {
const onPacket = this.onPacket.bind(this);
const handlePayload = this.options?.handlePayload?.bind(this);
const self = this;
const shard = new websocket_1.Shard(id, {
token: workerData.token,
intents: workerData.intents,
info: data.info,
compress: data.compress,
debugger: this.debugger,
properties: {
...websocket_1.properties,
...this.options.gateway?.properties,
},
async handlePayload(shardId, payload) {
await handlePayload?.(shardId, payload);
await onPacket(payload, shardId);
if (self.options.sendPayloadToParent)
self.postMessage({
workerId: workerData.workerId,
shardId,
type: 'RECEIVE_PAYLOAD',
payload,
});
},
});
return shard;
}
async resumeShard(shardId, shardData) {
const exists = (await this.tellWorkers((r, vars) => r.shards.has(vars.shardId), {
shardId,
})).some(x => x);
if (exists)
throw new Error('Cannot override existing shard');
const shard = this.createShard(shardId, {
info: this.workerData.info,
compress: this.workerData.compress,
});
shard.data = shardData;
this.shards.set(shardId, shard);
return this.postMessage({
workerId: this.workerId,
shardId,
type: 'CONNECT_QUEUE',
});
}
async onPacket(packet, shardId) {
Promise.allSettled([
this.events.runEvent('RAW', this, packet, shardId, false),
this.collectors.run('RAW', packet, this),
]); //ignore promise
switch (packet.t) {
case 'GUILD_MEMBER_UPDATE':
{
if (!this.memberUpdateHandler.check(packet.d)) {
return;
}
await this.events.execute(packet, this, shardId);
}
break;
case 'PRESENCE_UPDATE':
{
if (!this.presenceUpdateHandler.check(packet.d)) {
return;
}
await this.events.execute(packet, this, shardId);
}
break;
default: {
switch (packet.t) {
case 'INTERACTION_CREATE':
{
await this.events.execute(packet, this, shardId);
await this.handleCommand.interaction(packet.d, shardId);
}
break;
case 'MESSAGE_CREATE':
{
await this.events.execute(packet, this, shardId);
await this.handleCommand.message(packet.d, shardId);
}
break;
case 'READY': {
this.botId = packet.d.user.id;
this.applicationId = packet.d.application.id;
this.me = transformers_1.Transformers.ClientUser(this, packet.d.user, packet.d.application);
if ([...this.shards.values()].every(shard => shard.data.session_id)) {
this.postMessage({
type: 'WORKER_SHARDS_CONNECTED',
workerId: this.workerId,
});
await this.events.runEvent('WORKER_SHARDS_CONNECTED', this, this.me, -1);
}
await this.events.execute(packet, this, shardId);
this.debugger?.debug(`#${shardId}[${packet.d.user.username}](${this.botId}) is online...`);
break;
}
case 'GUILDS_READY':
{
if ([...this.shards.values()].every(shard => shard.isReady)) {
this.postMessage({
type: 'WORKER_READY',
workerId: this.workerId,
});
await this.events.runEvent('WORKER_READY', this, this.me, -1);
}
await this.events.execute(packet, this, shardId);
}
break;
default:
await this.events.execute(packet, this, shardId);
break;
}
break;
}
}
}
}
exports.WorkerClient = WorkerClient;
function generateShardInfo(shard) {
return {
open: shard.isOpen,
shardId: shard.id,
latency: shard.latency,
resumable: shard.resumable,
workerId: workerData.workerId,
};
}