diff --git a/package.json b/package.json index 5027e7b..ddb9480 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@squeebot/core", - "version": "3.6.3", + "version": "3.7.0", "description": "Squeebot v3 core for the execution environment", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/channel/index.ts b/src/channel/index.ts index e9c3391..2c1df0c 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -17,9 +17,9 @@ export interface IChannel { * list are the handlers. */ export class ChannelManager { - private channels: IChannel[] = []; + protected channels: IChannel[] = []; - constructor(private stream: ScopedEventEmitter) {} + constructor(protected stream: ScopedEventEmitter) {} /** * Ensure that the message or event source is a plugin @@ -68,8 +68,10 @@ export class ChannelManager { } for (const pl of chan.plugins) { - if (pl !== source && - !(pl.indexOf('/') !== -1 && pl.split('/')[0] === source)) { + if ( + pl !== source && + !(pl.indexOf('/') !== -1 && pl.split('/')[0] === source) + ) { this.stream.emitTo(pl, event, data, chan, msr); } } @@ -84,7 +86,10 @@ export class ChannelManager { * @param source Source protocol of the event * @returns List of channels to send to */ - private getChannelsByPluginName(plugin: string, source: Protocol): IChannel[] { + protected getChannelsByPluginName( + plugin: string, + source: Protocol + ): IChannel[] { const list = []; for (const chan of this.channels) { if (chan.enabled === false) { @@ -108,7 +113,7 @@ export class ChannelManager { * Validate a preconfigured channel list and add them to the list * @param channels Preconfigured channel list */ - private addPreconfiguredChannels(channels: IChannel[]): void { + protected addPreconfiguredChannels(channels: IChannel[]): void { for (const chan of channels) { if (!chan.name) { throw new Error('Channel name is mandatory.'); @@ -128,7 +133,7 @@ export class ChannelManager { * @returns Channel or undefined */ public getChannelByName(name: string): IChannel | undefined { - return this.channels.find(c => c.name === name); + return this.channels.find((c) => c.name === name); } /** diff --git a/src/common/full-matcher.ts b/src/common/full-matcher.ts index d2bd778..cf337d1 100644 --- a/src/common/full-matcher.ts +++ b/src/common/full-matcher.ts @@ -1,6 +1,9 @@ /** * Compare full IDs of rooms or users. + * @example + * // returns true + * fullIDMatcher('foo/bar/#test', 'foo/bar/*') * @param compare ID to compare * @param id ID to compare against */ diff --git a/src/core/coreref.ts b/src/core/coreref.ts index 67e53fc..a57658d 100644 --- a/src/core/coreref.ts +++ b/src/core/coreref.ts @@ -10,15 +10,45 @@ import { ScopedEventEmitter } from '../util'; * Recommended implementation of a squeebot runner implements this interface. */ export interface ISqueebotCore { + /** + * Squeebot environment information, mainly paths + */ environment: IEnvironment; + /** + * NPM executor. Used to install and upgrade packages programmatically. + */ npm: NPMExecutor; + /** + * Main scoped event stream. + */ stream: ScopedEventEmitter; + /** + * Squeebot plugin manager. Hot load/unload/restart functionality. + */ pluginManager: PluginManager; + /** + * Squeebot plugin repository manager. Hot install/uninstall/update functionality. + */ repositoryManager: RepositoryManager; + /** + * Squeebot message channel manager. Used to route events between plugins. + */ channelManager: ChannelManager; + /** + * Squeebot plugin metadata/manifest loader. + */ pluginLoader: PluginMetaLoader; + /** + * Squeebot main configuration file. + */ config: Configuration; - + /** + * Initialize all Squeebot components. + * @param autostart Automatically start everything (load plugins, etc) + */ initialize(autostart: boolean): Promise; + /** + * Trigger a graceful shutdown. + */ shutdown(): void; } diff --git a/src/core/environment.ts b/src/core/environment.ts index 019bdd5..8f875ec 100644 --- a/src/core/environment.ts +++ b/src/core/environment.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { IEnvironment } from '../types/environment'; -const dirs: {[key: string]: string} = { +const dirs: { [key: string]: string } = { configurationPath: 'configs', pluginsPath: 'plugins', repositoryPath: 'repos', @@ -15,8 +15,11 @@ const dirs: {[key: string]: string} = { * @param chroot Change bot root to this instead of the path in the environment * @returns Squeebot environment */ -export async function loadEnvironment(enviroFile = 'squeebot.env.json', chroot?: string): Promise { - if (!await fs.pathExists(enviroFile)) { +export async function loadEnvironment( + enviroFile = 'squeebot.env.json', + chroot?: string +): Promise { + if (!(await fs.pathExists(enviroFile))) { throw new Error('Environment file does not exist.'); } @@ -31,7 +34,7 @@ export async function loadEnvironment(enviroFile = 'squeebot.env.json', chroot?: env.path = chroot; } - if (!await fs.pathExists(root)) { + if (!(await fs.pathExists(root))) { throw new Error('Root path does not exist.'); } diff --git a/src/core/logger.ts b/src/core/logger.ts index 0511179..a0df54d 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -11,7 +11,7 @@ export enum LogLevel { } export type LogListener = ( - ltype: LogLevel, + logLevel: LogLevel, ...data: any[] ) => void | Promise; @@ -211,5 +211,10 @@ export class Logger { } // Create singleton for Logger to be used anywhere +/** + * Logger for all of Squeebot. Use this instead of console.log/warn/error in your plugins! + * + * This is a global singleton. + */ const logger = new Logger(); export { logger }; diff --git a/src/npm/executor.ts b/src/npm/executor.ts index fcb138f..34273b8 100644 --- a/src/npm/executor.ts +++ b/src/npm/executor.ts @@ -5,16 +5,19 @@ import { IEnvironment } from '../types/environment'; import { spawnProcess } from '../util/run'; /** - * Execute NPM commands + * Execute NPM commands. Can be extended to implement different package managers. */ export class NPMExecutor { - private installed: Record = {}; - private packageFile: string = path.join( + protected installed: Record = {}; + protected packageFile: string = path.join( this.environment.path, 'package.json' ); - constructor(private environment: IEnvironment, private coreModule: string) {} + constructor( + protected environment: IEnvironment, + protected coreModule: string + ) {} /** * Create a package.json file and install the core. @@ -126,7 +129,7 @@ export class NPMExecutor { await this.loadPackageFile(); } - private removeVersionWildcard(str?: string): string | undefined { + protected removeVersionWildcard(str?: string): string | undefined { return str?.replace(/\^|>|=/, ''); } } diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index 335dab6..5fd1f6b 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -2,9 +2,23 @@ import { PluginConfiguration, Service } from '../types'; import { ScopedEventEmitter } from '../util/events'; export interface IPlugin { + /** + * Plugin manifest information. + */ manifest: IPluginManifest; + /** + * Core scoped event stream. + * This is how you talk to other plugins and the core using events. + */ stream: ScopedEventEmitter; + /** + * Plugin configuration file. + * Note: `@Configurable` decorator is required for the configuration file. + */ config: PluginConfiguration; + /** + * Service provider, only used for Protocol service plugins, such as `irc`. + */ service: Service | null; } @@ -18,44 +32,102 @@ export class Plugin implements IPlugin { constructor( public manifest: IPluginManifest, public stream: ScopedEventEmitter, - public config: PluginConfiguration) {} + public config: PluginConfiguration + ) {} /** * Called when plugin first starts. - * Please use this instead of the constructor. + * + * Please use this instead of the constructor to set up your plugin, + * as this will be called at the appropriate time for initialization. */ // eslint-disable-next-line @typescript-eslint/no-empty-function public initialize(): void {} + /** + * Shortcut to the plugin's manifest name. + */ public get name(): string { return this.manifest.name; } + /** + * Shortcut to the plugin's manifest version. + */ public get version(): string { return this.manifest.version; } + /** + * Shortcut to the core event stream. + * @param name Event name + * @param fn Listener + */ protected addEventListener(name: string, fn: any): void { this.stream.on(this.name, name, fn); } + /** + * Shortcut to the core stream event emitter. + * @param event Event name + * @param data Data + */ protected emit(event: string, data: any): void { this.stream.emit.call(this.stream, event, data); } + /** + * Shortcut to the core stream event emitter, named variant. + * Emit events to a particular recipient. + * @param name Scope name + * @param event Event name + * @param data Data + */ protected emitTo(name: string, event: string, data: any): void { this.stream.emitTo.call(this.stream, name, event, data); } } +/** + * A plugin's manifest JSON. + */ export interface IPluginManifest { + /** + * Source repository name. + * Populated automatically, not present in file. + */ repository: string; + /** + * Full path for the plugin directory. + * Populated automatically, not present in file. + */ fullPath: string; + /** + * Main file name. + */ main: string; + /** + * Plugin name. + */ name: string; + /** + * Plugin tags for organization. + */ tags?: string[]; + /** + * Plugin version (semver). + */ version: string; + /** + * Plugin description. + */ description: string; + /** + * Other plugins this plugin depends on. + */ dependencies: string[]; + /** + * NPM packages, including version number, the plugin depends on. + */ npmDependencies: string[]; } diff --git a/src/types/environment.ts b/src/types/environment.ts index 7742656..583fcf6 100644 --- a/src/types/environment.ts +++ b/src/types/environment.ts @@ -1,8 +1,26 @@ +/** + * Squeebot environment configuration and paths. + */ export interface IEnvironment { + /** + * Root path of the current environment. + */ path: string; + /** + * Plugins directory path. + */ pluginsPath: string; + /** + * Repositories directory path. + */ repositoryPath: string; + /** + * Configuration files directory path. + */ configurationPath: string; + /** + * Environment name. + */ environment: string; } diff --git a/src/types/staged-handler.ts b/src/types/staged-handler.ts index 39c449f..842a516 100644 --- a/src/types/staged-handler.ts +++ b/src/types/staged-handler.ts @@ -15,6 +15,7 @@ export type IMessageHandler = (data: IMessageData | string, reject: (error: Erro /** * Staged messaging handler + * @deprecated Do not use for new plugins. */ export class MessageResolver { private handlers: IMessageHandler[] = []; diff --git a/src/util/events.ts b/src/util/events.ts index 07f82d7..1e0ed2c 100644 --- a/src/util/events.ts +++ b/src/util/events.ts @@ -4,13 +4,29 @@ import { logger } from '../core'; * Event emitter that can be scoped by plugin name or core. */ export class ScopedEventEmitter { - private listeners: {[key: string]: any[]}; + protected listeners: {[key: string]: any[]}; + + /** + * Add an event listener on `name` scope for event `event`. Alias of `on`. + * @param name Event scope + * @param event Event name + * @param func Listener + * @param once Listen once + * @see on + */ public addEventListener = this.on; constructor() { this.listeners = {}; } + /** + * Add an event listener on `name` scope for event `event`. + * @param name Event scope + * @param event Event name + * @param func Listener + * @param once Listen once + */ public on(name: string, event: string, func: any, once = false): void { if (!func || !event || !name) { throw new Error('missing arguments'); @@ -25,10 +41,21 @@ export class ScopedEventEmitter { }); } + /** + * Add an one-off event listener on `name` scope for event `event`. + * @param name Event scope + * @param event Event name + * @param func Listener + */ public once(name: string, event: string, func: any): void { this.on(name, event, func, true); } + /** + * Emit an event `event` to all listeners in all scopes. + * @param event Event name + * @param args Data + */ public emit(event: string, ...args: any[]): void { for (const name in this.listeners) { for (const i in this.listeners[name]) { @@ -53,6 +80,12 @@ export class ScopedEventEmitter { } } + /** + * Emit an event `event` to listeners listening for scope `name`. + * @param name Scope name + * @param event Event name + * @param args Data + */ public emitTo(name: string, event: string, ...args: any[]): void { if (!this.listeners[name]) { return; @@ -80,6 +113,10 @@ export class ScopedEventEmitter { } } + /** + * Remove all listeners for scope `name`. + * @param name Scope name + */ public removeName(name: string): void { if (this.listeners[name]) { delete this.listeners[name];