import * as path from 'path'; import { ChannelManager } from '@squeebot/core/lib/channel'; import { PluginManager, PluginMetaLoader } from '@squeebot/core/lib/plugin'; import { RepositoryManager } from '@squeebot/core/lib/plugin/repository'; import { NPMExecutor } from '@squeebot/core/lib/npm'; import { Configuration, IEnvironment } from '@squeebot/core/lib/types'; import { ScopedEventEmitter } from '@squeebot/core/lib/util'; import { ISqueebotCore, logger } from '@squeebot/core/lib/core'; const defaultConfiguration: {[key: string]: any} = { channels: [], enabled: [], name: 'squeebot', }; /** * Reference implementation of a Squeebot system using core classes. */ export class Squeebot implements ISqueebotCore { public npm: NPMExecutor = new NPMExecutor(this.environment, '@squeebot/core'); public stream: ScopedEventEmitter = new ScopedEventEmitter(); public pluginManager: PluginManager = new PluginManager( [], this.stream, this.environment, this.npm); public repositoryManager: RepositoryManager = new RepositoryManager( this.environment, this.pluginManager, ); public channelManager: ChannelManager = new ChannelManager(this.stream); public pluginLoader: PluginMetaLoader = new PluginMetaLoader( this.environment, ); public config: Configuration = new Configuration( this.environment, path.join(this.environment.configurationPath, 'squeebot.json'), defaultConfiguration, ); private shuttingDown = false; constructor(public environment: IEnvironment) {} public async initialize(autostart = true): Promise { // Load configuration await this.config.load(); // Initialize npm executor await this.npm.loadPackageFile(); // Load all plugins const plugins = await this.pluginLoader.loadAll(); // Give manager all loaded manifests this.pluginManager.addAvailable(plugins); // Load repositories await this.repositoryManager.loadFromFiles(); // Start channels this.channelManager.initialize(this.config.get('channels')); // Send core on request this.stream.on('core', 'request-core', (pl: string) => this.stream.emitTo(pl, 'core', this)); // Handle uncaught exceptions to prevent the whole bot from getting bricked process.on('uncaughtException', (err) => { logger.error('Process caught an unhandled exception:', err.stack); this.stream.emit('error', err); }); // Handle uncaught rejections to prevent the whole bot from getting bricked process.on('unhandledRejection', (reason, promise) => { logger.error('Process caught an unhandled promise rejection:', reason); logger.error('In promise:', promise); const error = new Error(reason?.toString()); error.name = 'unhandledRejection'; this.stream.emit('error', error); }); // Start enabled plugins if (autostart) { await this.startPlugins(); } } public async startPlugins(): Promise { for (const pluginName of this.config.get('enabled')) { const getManifest = this.pluginManager.getAvailableByName(pluginName); if (!getManifest) { logger.error(new Error(`Failed to start ${pluginName}: no manifest available. You might not have installed it.`)); continue; } try { await this.pluginManager.load(getManifest); } catch (e: any) { logger.error('Failed to start %s:', pluginName, e.stack); } } } public attachReadline(rl: any): void { // Pass readline to logger logger.setReadline(rl); // Send readline on request this.stream.on('core', 'request-cli', () => this.stream.emit('cli', rl)); // Handle readline events rl.on('line', (l: string) => this.stream.emit('cli-line', l)); rl.on('SIGINT', () => this.shutdown()); } public shutdown(): void { if (this.shuttingDown) { return; } logger.warn('Shutting down..'); this.shuttingDown = true; // Subsystems confirmed shutdown, go ahead and exit. this.stream.on('core', 'shutdown', (state: number) => { if (state > 0) { this.config.save().then((x) => process.exit(0)); } }); // Prompt subsystems to prepare for shutting down this.stream.emitTo('core', 'shutdown', 0); } }