import type { CommandContext, Message } from '..'; import { type Awaitable, type DeepPartial, type If, type MakePartial, type WatcherPayload, type WatcherSendToShard, assertString, lazyLoadPackage, } from '../common'; import { EventHandler } from '../events'; import type { GatewayDispatchPayload, GatewayPresenceUpdateData } from '../types'; import { ShardManager, type ShardManagerOptions, properties } from '../websocket'; import { MemberUpdateHandler } from '../websocket/discord/events/memberUpdate'; import { PresenceUpdateHandler } from '../websocket/discord/events/presenceUpdate'; import type { BaseClientOptions, InternalRuntimeConfig, ServicesOptions, StartOptions } from './base'; import { BaseClient } from './base'; import { Collectors } from './collectors'; import { type ClientUserStructure, type MessageStructure, Transformers } from './transformers'; let parentPort: import('node:worker_threads').MessagePort; export class Client extends BaseClient { gateway!: ShardManager; me!: If; declare options: Omit & { commands: NonNullable; }; memberUpdateHandler = new MemberUpdateHandler(); presenceUpdateHandler = new PresenceUpdateHandler(); collectors = new Collectors(); events = new EventHandler(this); constructor(options?: ClientOptions) { super(options); } setServices({ gateway, ...rest }: ServicesOptions & { gateway?: ShardManager; }) { super.setServices(rest); if (gateway) { const onPacket = this.onPacket.bind(this); const oldFn = gateway.options.handlePayload; gateway.options.handlePayload = async (shardId, packet) => { await onPacket(shardId, packet); return oldFn(shardId, packet); }; this.gateway = gateway; } } get latency() { let acc = 0; this.gateway.forEach(s => (acc += s.latency)); return acc / this.gateway.size; } async loadEvents(dir?: string) { dir ??= await this.getRC().then(x => x.locations.events); if (dir) { await this.events.load(dir); this.logger.info('EventHandler loaded'); } } protected async execute(options: { token?: string; intents?: number } = {}) { await super.execute(options); const worker_threads = lazyLoadPackage('node:worker_threads'); if (worker_threads?.parentPort) { parentPort = worker_threads.parentPort; } if (worker_threads?.workerData?.__USING_WATCHER__) { parentPort?.on('message', (data: WatcherPayload | WatcherSendToShard) => { switch (data.type) { case 'PAYLOAD': this.gateway.options.handlePayload(data.shardId, data.payload); break; case 'SEND_TO_SHARD': this.gateway.send(data.shardId, data.payload); break; } }); } else { await this.gateway.spawnShards(); } } async start(options: Omit, 'httpConnection'> = {}, execute = true) { await super.start(options); await this.loadEvents(options.eventsDir); const { token: tokenRC, intents: intentsRC, debug: debugRC } = await this.getRC(); const token = options?.token ?? tokenRC; const intents = options?.connection?.intents ?? intentsRC; this.cache.intents = intents; if (!this.gateway) { assertString(token, 'token is not a string'); this.gateway = new ShardManager({ token, info: await this.proxy.gateway.bot.get(), intents, handlePayload: async (shardId, packet) => { await this.options?.handlePayload?.(shardId, packet); return this.onPacket(shardId, packet); }, presence: this.options?.presence, debug: debugRC, shardStart: this.options?.shards?.start, shardEnd: this.options?.shards?.end ?? this.options?.shards?.total, totalShards: this.options?.shards?.total ?? this.options?.shards?.end, properties: { ...properties, ...this.options?.gateway?.properties, }, compress: this.options?.gateway?.compress, resharding: { getInfo: this.options.resharding?.getInfo ?? (() => this.proxy.gateway.bot.get()), interval: this.options?.resharding?.interval, percentage: this.options?.resharding?.percentage, }, }); } if (execute) { await this.execute(options.connection); } else { await super.execute(options); } } protected async onPacket(shardId: number, packet: GatewayDispatchPayload) { 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 as Client, shardId); } break; case 'PRESENCE_UPDATE': { if (!this.presenceUpdateHandler.check(packet.d)) { return; } await this.events.execute(packet, this as Client, shardId); } break; //rest of the events default: { switch (packet.t) { case 'INTERACTION_CREATE': { await this.events.execute(packet, this as Client, shardId); await this.handleCommand.interaction(packet.d, shardId); } break; case 'MESSAGE_CREATE': { await this.events.execute(packet, this as Client, 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.ClientUser(this, packet.d.user, packet.d.application) as never; this.debugger?.debug(`#${shardId}[${packet.d.user.username}](${this.botId}) is online...`); await this.events.execute(packet, this as Client, shardId); break; } case 'GUILDS_READY': { await this.events.execute(packet, this as Client, shardId); if ([...this.gateway.values()].every(shard => shard.isReady)) { await this.events.runEvent('BOT_READY', this, this.me, -1); } } break; default: await this.events.execute(packet, this as Client, shardId); break; } break; } } } } export interface ClientOptions extends BaseClientOptions { presence?: (shardId: number) => GatewayPresenceUpdateData; shards?: { start: number; end: number; total?: number; }; gateway?: { properties?: Partial; compress?: ShardManagerOptions['compress']; }; commands?: BaseClientOptions['commands'] & { prefix?: (message: MessageStructure) => Awaitable; deferReplyResponse?: (ctx: CommandContext) => Parameters[0]; reply?: (ctx: CommandContext) => boolean; }; handlePayload?: ShardManagerOptions['handlePayload']; resharding?: MakePartial, 'getInfo'>; }