Documentation of most methods

This commit is contained in:
Evert Prants 2021-10-02 11:07:01 +03:00
parent 68489c733d
commit 9a2142688b
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
20 changed files with 427 additions and 35 deletions

View File

@ -8,11 +8,24 @@ export interface IChannel {
enabled: boolean; enabled: boolean;
} }
/**
* This class is used to direct messages and events from one plugin to many others
* using a pre-set list of plugins that are allowed to talk to one another.
*
* Generally when creating a channel, the first plugin should be the source of messages
* or events, such as a protocol or other service, and the rest of the plugins in the
* list are the handlers.
*/
export class ChannelManager { export class ChannelManager {
private channels: IChannel[] = []; private channels: IChannel[] = [];
constructor(private stream: ScopedEventEmitter) {} constructor(private stream: ScopedEventEmitter) {}
/**
* Ensure that the message or event source is a plugin
* @param source Event source
* @returns Plugin or null
*/
public static determinePlugin(source: any): IPlugin | null { public static determinePlugin(source: any): IPlugin | null {
if (source != null) { if (source != null) {
if (source.manifest) { if (source.manifest) {
@ -27,6 +40,10 @@ export class ChannelManager {
return null; return null;
} }
/**
* Initialize the event handlers for channels
* @param configured Initial configuration of channels
*/
public initialize(configured: IChannel[]): void { public initialize(configured: IChannel[]): void {
this.addPreconfiguredChannels(configured); this.addPreconfiguredChannels(configured);
@ -61,6 +78,12 @@ export class ChannelManager {
} }
} }
/**
* Get all the channels a plugin is in
* @param plugin Plugin name
* @param source Source protocol of the event
* @returns List of channels to send to
*/
private getChannelsByPluginName(plugin: string, source: Protocol): IChannel[] { private getChannelsByPluginName(plugin: string, source: Protocol): IChannel[] {
const list = []; const list = [];
for (const chan of this.channels) { for (const chan of this.channels) {
@ -81,6 +104,10 @@ export class ChannelManager {
return list; return list;
} }
/**
* Validate a preconfigured channel list and add them to the list
* @param channels Preconfigured channel list
*/
private addPreconfiguredChannels(channels: IChannel[]): void { private addPreconfiguredChannels(channels: IChannel[]): void {
for (const chan of channels) { for (const chan of channels) {
if (!chan.name) { if (!chan.name) {
@ -95,10 +122,20 @@ export class ChannelManager {
} }
} }
/**
* Get a channel by name
* @param name Channel name
* @returns Channel or undefined
*/
public getChannelByName(name: string): IChannel | undefined { public getChannelByName(name: string): IChannel | undefined {
return this.channels.find(c => c.name === name); return this.channels.find(c => c.name === name);
} }
/**
* Add a new channel to the channels list
* @param chan Channel configuration
* @returns Channel
*/
public addChannel(chan: IChannel): IChannel { public addChannel(chan: IChannel): IChannel {
const exists = this.getChannelByName(chan.name); const exists = this.getChannelByName(chan.name);
if (exists) { if (exists) {
@ -108,6 +145,10 @@ export class ChannelManager {
return chan; return chan;
} }
/**
* Remove a channel by name or the channel itself
* @param chan Name of channel or channel
*/
public removeChannel(chan: string | IChannel): void { public removeChannel(chan: string | IChannel): void {
if (typeof chan === 'string') { if (typeof chan === 'string') {
const getchan = this.getChannelByName(chan); const getchan = this.getChannelByName(chan);
@ -119,6 +160,10 @@ export class ChannelManager {
this.channels.splice(this.channels.indexOf(chan), 1); this.channels.splice(this.channels.indexOf(chan), 1);
} }
/**
* Get all channels
* @returns All channels
*/
public getAll(): IChannel[] { public getAll(): IChannel[] {
return this.channels; return this.channels;
} }

View File

@ -1,17 +1,24 @@
import http from 'http'; import http, { RequestOptions } from 'http';
import https from 'https'; import https from 'https';
import qs from 'querystring';
import fs from 'fs-extra'; import fs from 'fs-extra';
import url from 'url'; import { URL } from 'url';
/**
* Create an HTTP GET request.
* @param link Request URL
* @param headers Request headers
* @param restrictToText Only allow textual responses
* @param saveTo Save to a file
* @returns Response data
*/
export function httpGET( export function httpGET(
link: string, link: string,
headers: any = {}, headers: any = {},
restrictToText = true, restrictToText = true,
saveTo?: string, saveTo?: string,
lback?: number): Promise<any> { lback?: number
): Promise<any> {
const parsed = url.parse(link); const parsed = new URL(link);
const opts = { const opts = {
headers: { headers: {
Accept: '*/*', Accept: '*/*',
@ -19,8 +26,8 @@ export function httpGET(
'User-Agent': 'Squeebot/Commons-3.0.0', 'User-Agent': 'Squeebot/Commons-3.0.0',
}, },
host: parsed.hostname, host: parsed.hostname,
path: parsed.path, path: `${parsed.pathname}${parsed.search}`,
port: parsed.port, port: parsed.port || null,
}; };
if (headers) { if (headers) {
@ -63,13 +70,13 @@ export function httpGET(
if (restrictToText) { if (restrictToText) {
const cType = res.headers['content-type']; const cType = res.headers['content-type'];
if (cType && cType.indexOf('text') === -1 && cType.indexOf('json') === -1) { if (cType && cType.indexOf('text') === -1 && cType.indexOf('json') === -1) {
req.abort(); req.destroy();
return reject(new Error('Response type is not supported by httpGET!')); return reject(new Error('Response type is not supported by httpGET!'));
} }
} }
reqTimeOut = setTimeout(() => { reqTimeOut = setTimeout(() => {
req.abort(); req.destroy();
data = null; data = null;
reject(new Error('Request took too long!')); reject(new Error('Request took too long!'));
}, 5000); }, 5000);
@ -91,33 +98,53 @@ export function httpGET(
}); });
} }
/**
* Create an HTTP POST request.
*
* Note: Content-Type defaults to `application/x-www-form-urlencoded`.
* Set the `Content-Type` to `application/json` to post and parse JSON.
* @param link Request URL
* @param headers Request headers
* @param data Submit data
* @returns Response data
*/
export function httpPOST( export function httpPOST(
link: string, link: string,
headers: any = {}, headers: any = {},
data: any): Promise<any> { data: any
const parsed = url.parse(link); ): Promise<any> {
let postData = qs.stringify(data); const parsed = new URL(link);
let postData: string | URLSearchParams = new URLSearchParams(data);
const opts = { const opts: RequestOptions = {
headers: { headers: {
'Content-Length': Buffer.byteLength(postData),
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Squeebot/Commons-3.0.0', 'User-Agent': 'Squeebot/Commons-3.0.0',
}, },
host: parsed.host, host: parsed.host,
method: 'POST', method: 'POST',
path: parsed.path, path: `${parsed.pathname}${parsed.search}`,
port: parsed.port, port: parsed.port || null,
}; };
// Assign provided headers
if (headers) { if (headers) {
opts.headers = Object.assign(opts.headers, headers); opts.headers = Object.assign({}, opts.headers, headers);
} }
// Ensure headers list exists
if (!opts.headers) {
opts.headers = {};
}
// If content type is JSON, add it to body
if (opts.headers['Content-Type'] === 'application/json') { if (opts.headers['Content-Type'] === 'application/json') {
postData = JSON.stringify(data); postData = JSON.stringify(data);
} }
// Set content length accordingly
opts.headers['Content-Length'] = Buffer.byteLength(postData.toString());
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const httpModule = parsed.protocol === 'https:' ? https : http; const httpModule = parsed.protocol === 'https:' ? https : http;
const req = httpModule.request(opts, (res) => { const req = httpModule.request(opts, (res) => {

View File

@ -1,4 +1,9 @@
/**
* Remove escaped HTML entities from string.
* @param text HTML escaped string
* @returns Un-escaped string
*/
export function sanitizeEscapedText(text: string): string { export function sanitizeEscapedText(text: string): string {
return text.replace(/\n/g, ' ') return text.replace(/\n/g, ' ')
.replace(/&amp;/g, '&') .replace(/&amp;/g, '&')

View File

@ -1,4 +1,9 @@
/**
* Convert seconds to HH:MM:SS format
* @param input seconds
* @returns string in HH:MM:SS format
*/
export function toHHMMSS(input: string | number): string { export function toHHMMSS(input: string | number): string {
const secNum = parseInt(input.toString(), 10); const secNum = parseInt(input.toString(), 10);
let hours: string | number = Math.floor(secNum / 3600); let hours: string | number = Math.floor(secNum / 3600);
@ -27,12 +32,18 @@ export function toHHMMSS(input: string | number): string {
return time; return time;
} }
// Add a zero in front of single-digit numbers /**
* Add a zero in front of single-digit numbers
*/
function zf(v: number): string { function zf(v: number): string {
return v < 9 ? '0' + v : '' + v; return v < 9 ? '0' + v : '' + v;
} }
// Convert seconds into years days hours minutes seconds(.milliseconds) /**
* Convert seconds into years days hours minutes seconds
* @param timems time in seconds
* @returns y d h m s string
*/
export function readableTime(timems: number): string { export function readableTime(timems: number): string {
const time = Math.floor(timems); const time = Math.floor(timems);
@ -59,6 +70,11 @@ export function readableTime(timems: number): string {
} }
} }
/**
* Convert y d h m s string to seconds
* @param input y d h m s string
* @returns seconds
*/
export function parseTimeToSeconds(input: string): number { export function parseTimeToSeconds(input: string): number {
let seconds = 0; let seconds = 0;
let match; let match;
@ -101,6 +117,11 @@ export function parseTimeToSeconds(input: string): number {
return seconds; return seconds;
} }
/**
* Add a comma to separate thousands in number
* @param input number (example: 1000000)
* @returns string (example: 1,000,000)
*/
export function thousandsSeparator(input: number | string): string { export function thousandsSeparator(input: number | string): string {
const nStr = input.toString(); const nStr = input.toString();
const x = nStr.split('.'); const x = nStr.split('.');
@ -115,6 +136,11 @@ export function thousandsSeparator(input: number | string): string {
return x1 + x2; return x1 + x2;
} }
/**
* Time since a unix timestamp
* @param date unix timestamp
* @returns x years, months, days, hours, minutes, seconds
*/
export function timeSince(date: number): string { export function timeSince(date: number): string {
const seconds = Math.floor((Date.now() - date) / 1000); const seconds = Math.floor((Date.now() - date) / 1000);
let interval = Math.floor(seconds / 31536000); let interval = Math.floor(seconds / 31536000);

View File

@ -5,6 +5,10 @@ import { RepositoryManager } from '../plugin/repository';
import { Configuration, IEnvironment } from '../types'; import { Configuration, IEnvironment } from '../types';
import { ScopedEventEmitter } from '../util'; import { ScopedEventEmitter } from '../util';
/**
* Reference of a fully featured Squeebot core.
* Recommended implementation of a squeebot runner implements this interface.
*/
export interface ISqueebotCore { export interface ISqueebotCore {
environment: IEnvironment; environment: IEnvironment;
npm: NPMExecutor; npm: NPMExecutor;

View File

@ -9,6 +9,12 @@ const dirs: {[key: string]: string} = {
repositoryPath: 'repos', repositoryPath: 'repos',
}; };
/**
* Load a Squeebot environment from a file
* @param enviroFile Environment JSON file
* @param chroot Change bot root to this instead of the path in the environment
* @returns Squeebot environment
*/
export async function loadEnvironment(enviroFile: string = 'squeebot.env.json', chroot?: string): Promise<IEnvironment> { export async function loadEnvironment(enviroFile: string = 'squeebot.env.json', chroot?: string): Promise<IEnvironment> {
if (!await fs.pathExists(enviroFile)) { if (!await fs.pathExists(enviroFile)) {
throw new Error('Environment file does not exist.'); throw new Error('Environment file does not exist.');

View File

@ -1,23 +1,39 @@
import dateFmt from 'dateformat'; import dateFmt from 'dateformat';
import util from 'util'; import util from 'util';
type LogType = 'info' | 'debug' | 'warn' | 'error';
/**
* Logger for all of Squeebot. Use this instead of console.log/warn/error!
*/
export class Logger { export class Logger {
public timestamp = 'dd/mm/yy HH:MM:ss';
private console = [console.log, console.warn, console.error]; private console = [console.log, console.warn, console.error];
constructor() {} constructor(
public timestamp = 'dd/mm/yy HH:MM:ss'
) {}
/**
* Set node.js readline consideration
* @param rl Readline instance
*/
public setReadline(rl: any): void { public setReadline(rl: any): void {
for (const index in this.console) { for (const index in this.console) {
const old = this.console[index]; const old = this.console[index];
this.console[index] = (...data: any[]): void => { this.console[index] = (...data: any[]): void => {
rl.output.write('\x1b[2K\r'); rl.output.write('\x1b[2K\r');
old.apply(null, data); old.apply(null, data);
rl.prompt(true);
}; };
} }
} }
private write(ltype: string, ...data: any[]): void { /**
* Write out to log
* @param ltype Logger level
* @param data Data to log
*/
private write(ltype: LogType, ...data: any[]): void {
const message = []; const message = [];
let cfunc = this.console[0]; let cfunc = this.console[0];
@ -52,27 +68,52 @@ export class Logger {
cfunc.apply(null, message); cfunc.apply(null, message);
} }
/**
* Logger level: `INFO`
*
* See `console.log` for more information.
*/
public log(...data: any[]): void { public log(...data: any[]): void {
this.write('info', ...data); this.write('info', ...data);
} }
/**
* Logger level: `WARN`
*
* See `console.warn` for more information.
*/
public warn(...data: any[]): void { public warn(...data: any[]): void {
this.write('warn', ...data); this.write('warn', ...data);
} }
/**
* Logger level: `INFO`
*
* See `console.log` for more information.
*/
public info(...data: any[]): void { public info(...data: any[]): void {
this.write('info', ...data); this.write('info', ...data);
} }
/**
* Logger level: `ERROR`
*
* See `console.error` for more information.
*/
public error(...data: any[]): void { public error(...data: any[]): void {
this.write('error', ...data); this.write('error', ...data);
} }
/**
* Logger level: `DEBUG`
*
* See `console.log` for more information.
*/
public debug(...data: any[]): void { public debug(...data: any[]): void {
this.write('debug', ...data); this.write('debug', ...data);
} }
} }
// Create singleton for Logger to be used anywhere
const logger = new Logger(); const logger = new Logger();
export { logger }; export { logger };

View File

@ -4,12 +4,21 @@ import semver from 'semver';
import { IEnvironment } from '../types/environment'; import { IEnvironment } from '../types/environment';
import { spawnProcess } from '../util/run'; import { spawnProcess } from '../util/run';
/**
* Execute NPM commands
*/
export class NPMExecutor { export class NPMExecutor {
private installed: Record<string, string> = {}; private installed: Record<string, string> = {};
private packageFile: string = path.join(this.environment.path, 'package.json'); private packageFile: string = path.join(this.environment.path, 'package.json');
constructor(private environment: IEnvironment, private coreModule: string) {} constructor(
private environment: IEnvironment,
private coreModule: string
) {}
/**
* Create a package.json file and install the core.
*/
public async init(): Promise<void> { public async init(): Promise<void> {
// Initialize npm environment // Initialize npm environment
const c1 = await spawnProcess('npm', ['init', '-y'], this.environment); const c1 = await spawnProcess('npm', ['init', '-y'], this.environment);
@ -24,6 +33,9 @@ export class NPMExecutor {
} }
} }
/**
* Load a package.json file into memory
*/
public async loadPackageFile(): Promise<void> { public async loadPackageFile(): Promise<void> {
if (!await fs.pathExists(this.packageFile)) { if (!await fs.pathExists(this.packageFile)) {
await this.init(); await this.init();
@ -37,6 +49,10 @@ export class NPMExecutor {
this.installed = jsonData.dependencies; this.installed = jsonData.dependencies;
} }
/**
* Install a npm package (examples: `@squeebot/core`, `@squeebot/core@3.3.3`, `node-ical`)
* @param pkg Package name
*/
public async installPackage(pkg: string): Promise<void> { public async installPackage(pkg: string): Promise<void> {
if (!await fs.pathExists(this.packageFile)) { if (!await fs.pathExists(this.packageFile)) {
await this.init(); await this.init();
@ -66,6 +82,12 @@ export class NPMExecutor {
await this.loadPackageFile(); await this.loadPackageFile();
} }
/**
* Uninstall a npm package.
*
* See `installPackage` for more info.
* @param pkg Package name
*/
public async uninstallPackage(pkg: string): Promise<void> { public async uninstallPackage(pkg: string): Promise<void> {
if (!await fs.pathExists(this.packageFile)) { if (!await fs.pathExists(this.packageFile)) {
await this.init(); await this.init();

View File

@ -2,11 +2,19 @@ import { IEnvironment } from '../types/environment';
import { PluginConfiguration } from '../types/plugin-config'; import { PluginConfiguration } from '../types/plugin-config';
import { IPluginManifest } from './plugin'; import { IPluginManifest } from './plugin';
/**
* Plugin configuration memory storage
*/
export class PluginConfigurator { export class PluginConfigurator {
private configs: Map<string, PluginConfiguration> = new Map(); private configs: Map<string, PluginConfiguration> = new Map();
constructor(private env: IEnvironment) {} constructor(private env: IEnvironment) {}
/**
* Load a plugin configuration file by plugin manifest.
* @param mf Plugin manifest
* @returns Plugin's configuration object
*/
public async loadConfig(mf: IPluginManifest): Promise<PluginConfiguration> { public async loadConfig(mf: IPluginManifest): Promise<PluginConfiguration> {
if (!this.configs.has(mf.name)) { if (!this.configs.has(mf.name)) {
const conf = new PluginConfiguration(this.env, mf.name); const conf = new PluginConfiguration(this.env, mf.name);

View File

@ -6,9 +6,17 @@ import { IPluginManifest } from './plugin';
import { logger } from '../core'; import { logger } from '../core';
/**
* Plugin manifest loader
*/
export class PluginMetaLoader { export class PluginMetaLoader {
constructor(private env: IEnvironment) {} constructor(private env: IEnvironment) {}
/**
* Load a plugin manifest by plugin name.
* @param name Plugin name
* @returns Plugin manifest
*/
public async load(name: string): Promise<IPluginManifest> { public async load(name: string): Promise<IPluginManifest> {
if (name === 'squeebot') { if (name === 'squeebot') {
throw new Error('Illegal name.'); throw new Error('Illegal name.');
@ -60,6 +68,11 @@ export class PluginMetaLoader {
return json; return json;
} }
/**
* Load all plugin manifests from files.
* @param ignoreErrors Ignore loading errors instead of throwing them
* @returns List of plugin manifests ready to be loaded
*/
public async loadAll(ignoreErrors = false): Promise<IPluginManifest[]> { public async loadAll(ignoreErrors = false): Promise<IPluginManifest[]> {
const dirlist = await fs.readdir(this.env.pluginsPath); const dirlist = await fs.readdir(this.env.pluginsPath);
const plugins: IPluginManifest[] = []; const plugins: IPluginManifest[] = [];

View File

@ -11,6 +11,9 @@ import { NPMExecutor } from '../npm/executor';
import { logger } from '../core/logger'; import { logger } from '../core/logger';
/**
* Plugin management and execution system
*/
export class PluginManager { export class PluginManager {
private plugins: Map<string, IPlugin> = new Map(); private plugins: Map<string, IPlugin> = new Map();
private configs: PluginConfigurator = new PluginConfigurator(this.environment); private configs: PluginConfigurator = new PluginConfigurator(this.environment);
@ -21,14 +24,25 @@ export class PluginManager {
public availablePlugins: IPluginManifest[], public availablePlugins: IPluginManifest[],
private stream: ScopedEventEmitter, private stream: ScopedEventEmitter,
private environment: IEnvironment, private environment: IEnvironment,
private npm: NPMExecutor) { private npm: NPMExecutor
) {
this.addEvents(); this.addEvents();
} }
/**
* Get a plugin manifest by plugin name, if it is available.
* @param name Plugin name
* @returns Plugin manifest
*/
public getAvailableByName(name: string): IPluginManifest | undefined { public getAvailableByName(name: string): IPluginManifest | undefined {
return this.availablePlugins.find(p => p.name === name); return this.availablePlugins.find(p => p.name === name);
} }
/**
* Get a loaded plugin by name.
* @param name Plugin name
* @returns Plugin instance
*/
public getLoadedByName(name: string): IPlugin | undefined { public getLoadedByName(name: string): IPlugin | undefined {
if (this.plugins.has(name)) { if (this.plugins.has(name)) {
return this.plugins.get(name) as IPlugin; return this.plugins.get(name) as IPlugin;
@ -36,10 +50,21 @@ export class PluginManager {
return; return;
} }
/**
* Get a list of all loaded plugins.
* @returns List of loaded plugins
*/
public getLoaded(): IPlugin[] { public getLoaded(): IPlugin[] {
return Array.from(this.plugins.values()); return Array.from(this.plugins.values());
} }
/**
* Add plugin manifests as available (ready to load).
*
* Also used to replace existing manifest after installing a plugin update.
* @param manifest Plugin manifest or list of
* @returns true on success
*/
public addAvailable(manifest: IPluginManifest | IPluginManifest[]): boolean { public addAvailable(manifest: IPluginManifest | IPluginManifest[]): boolean {
// Automatically add arrays of manifests // Automatically add arrays of manifests
if (Array.isArray(manifest)) { if (Array.isArray(manifest)) {
@ -67,6 +92,12 @@ export class PluginManager {
return true; return true;
} }
/**
* Remove a plugin from available list, usually called after uninstalling a plugin.
* @param plugin Plugin name, plugin manifest or list of
* @param unload If they're loaded, unload them
* @returns true on success
*/
public removeAvailable( public removeAvailable(
plugin: IPluginManifest | IPluginManifest[] | string | string[], plugin: IPluginManifest | IPluginManifest[] | string | string[],
unload = true unload = true
@ -107,6 +138,11 @@ export class PluginManager {
return returnValue; return returnValue;
} }
/**
* Load a plugin into memory and start executing it.
* @param plugin Plugin manifest
* @returns Plugin instance
*/
public async load(plugin: IPluginManifest): Promise<IPlugin> { public async load(plugin: IPluginManifest): Promise<IPlugin> {
// Ignore loading when we're shutting down // Ignore loading when we're shutting down
if (this.stopping) { if (this.stopping) {
@ -245,6 +281,10 @@ export class PluginManager {
return loaded; return loaded;
} }
/**
* Restart a loaded plugin.
* @param mf Plugin instance, plugin manifest or plugin name
*/
public async restart(mf: IPluginManifest | IPlugin | string): Promise<void> { public async restart(mf: IPluginManifest | IPlugin | string): Promise<void> {
let manifest; let manifest;
if (typeof mf === 'string') { if (typeof mf === 'string') {
@ -268,6 +308,9 @@ export class PluginManager {
this.stream.emitTo(manifest.name, 'pluginUnload', manifest.name); this.stream.emitTo(manifest.name, 'pluginUnload', manifest.name);
} }
/**
* Listen for plugin status events.
*/
private addEvents(): void { private addEvents(): void {
this.stream.on('core', 'pluginLoad', (mf: IPluginManifest | string) => { this.stream.on('core', 'pluginLoad', (mf: IPluginManifest | string) => {
if (typeof mf === 'string') { if (typeof mf === 'string') {

View File

@ -8,6 +8,9 @@ export interface IPlugin {
service: Service | null; service: Service | null;
} }
/**
* Base class for all plugins
*/
export class Plugin implements IPlugin { export class Plugin implements IPlugin {
public service: Service | null = null; public service: Service | null = null;
protected on = this.addEventListener; protected on = this.addEventListener;
@ -17,6 +20,10 @@ export class Plugin implements IPlugin {
public stream: ScopedEventEmitter, public stream: ScopedEventEmitter,
public config: PluginConfiguration) {} public config: PluginConfiguration) {}
/**
* Called when plugin first starts.
* Please use this instead of the constructor.
*/
public initialize(): void {} public initialize(): void {}
public get name(): string { public get name(): string {

View File

@ -12,29 +12,60 @@ import { PluginManager } from '../manager';
import { IPlugin, IPluginManifest } from '../plugin'; import { IPlugin, IPluginManifest } from '../plugin';
import { IRepoPluginDef, IRepository } from './repository'; import { IRepoPluginDef, IRepository } from './repository';
/**
* Plugin repository manager
*/
export class RepositoryManager { export class RepositoryManager {
private repositories: Map<string, IRepository> = new Map(); private repositories: Map<string, IRepository> = new Map();
constructor(private env: IEnvironment, private plugins: PluginManager) {} constructor(private env: IEnvironment, private plugins: PluginManager) {}
public repoProvidesPlugin(repo: IRepository, mf: IPluginManifest): IRepoPluginDef | undefined { /**
* Determine if this repository provides a specific plugin
* @param repo Repository manifest
* @param mf Plugin manifest
* @returns Repository plugin definition
*/
public repoProvidesPlugin(
repo: IRepository,
mf: IPluginManifest
): IRepoPluginDef | undefined {
return repo.plugins.find(plugin => plugin.name === mf.name && repo.name === mf.repository); return repo.plugins.find(plugin => plugin.name === mf.name && repo.name === mf.repository);
} }
/**
* Find repository providing plugin
* @param pname Plugin name
* @returns Repository manifest
*/
public findRepoForPlugin(pname: string): IRepository | undefined { public findRepoForPlugin(pname: string): IRepository | undefined {
return Array.from(this.repositories.values()).find(repo => return Array.from(this.repositories.values()).find(repo =>
repo.plugins.find(plugin => plugin.name === pname) !== undefined repo.plugins.find(plugin => plugin.name === pname) !== undefined
); );
} }
/**
* Find a repository by name
* @param name Repository name
* @returns Repository manifest
*/
public getRepoByName(name: string): IRepository | undefined { public getRepoByName(name: string): IRepository | undefined {
return this.repositories.get(name); return this.repositories.get(name);
} }
/**
* Get the list of all installed repositories
* @returns All repositories
*/
public getAll(): IRepository[] { public getAll(): IRepository[] {
return Array.from(this.repositories.values()); return Array.from(this.repositories.values());
} }
/**
* Install a plugin by name
* @param name Plugin name
* @returns Plugin manifest
*/
public async installPlugin(name: string): Promise<IPluginManifest> { public async installPlugin(name: string): Promise<IPluginManifest> {
let repo; let repo;
if (name.indexOf('/') !== -1) { if (name.indexOf('/') !== -1) {
@ -94,6 +125,10 @@ export class RepositoryManager {
return manifest; return manifest;
} }
/**
* Uninstall a plugin
* @param plugin Plugin name, manifest or object
*/
public async uninstallPlugin(plugin: string | IPluginManifest | IPlugin): Promise<void> { public async uninstallPlugin(plugin: string | IPluginManifest | IPlugin): Promise<void> {
let realName: string; let realName: string;
if (typeof plugin === 'string') { if (typeof plugin === 'string') {
@ -115,6 +150,11 @@ export class RepositoryManager {
this.plugins.removeAvailable(pluginMeta); this.plugins.removeAvailable(pluginMeta);
} }
/**
* Get an up-to-date version of a plugin manifest from remote server.
* @param url Repository manifest URL
* @returns Repository manifest
*/
public async getRemote(url: string): Promise<IRepository> { public async getRemote(url: string): Promise<IRepository> {
const indexFileGet = await httpGET(url); const indexFileGet = await httpGET(url);
const meta = JSON.parse(indexFileGet); const meta = JSON.parse(indexFileGet);
@ -134,6 +174,11 @@ export class RepositoryManager {
return meta; return meta;
} }
/**
* Install a repository by remote URL
* @param url Remote repository manifest URL
* @returns Repository manifest
*/
public async installRepository(url: string): Promise<IRepository> { public async installRepository(url: string): Promise<IRepository> {
// Add standard repository file path, if missing // Add standard repository file path, if missing
if (!url.endsWith('/repository.json')) { if (!url.endsWith('/repository.json')) {
@ -158,6 +203,10 @@ export class RepositoryManager {
return remote; return remote;
} }
/**
* Uninstall a repository
* @param repo Repository name or manifest
*/
public async uninstallRepository(repo: string | IRepository): Promise<void> { public async uninstallRepository(repo: string | IRepository): Promise<void> {
if (typeof repo === 'string') { if (typeof repo === 'string') {
repo = this.getRepoByName(repo) as IRepository; repo = this.getRepoByName(repo) as IRepository;
@ -180,6 +229,11 @@ export class RepositoryManager {
} }
} }
/**
* Check for plugin updates from this repository.
* @param repo Repository manifest
* @returns Plugin manifests of plugins that can be updated
*/
public async checkForUpdates(repo: IRepository): Promise<IPluginManifest[]> { public async checkForUpdates(repo: IRepository): Promise<IPluginManifest[]> {
const oprep = await this.updateRepository(repo); const oprep = await this.updateRepository(repo);
@ -194,6 +248,11 @@ export class RepositoryManager {
}); });
} }
/**
* Update a repository manifest by going to the remote.
* @param repo Repository manifest
* @returns Up-to-date repository manifest
*/
public async updateRepository(repo: IRepository): Promise<IRepository> { public async updateRepository(repo: IRepository): Promise<IRepository> {
const remote = await this.getRemote(repo.url + '/repository.json'); const remote = await this.getRemote(repo.url + '/repository.json');
if (remote.created <= repo.created) { if (remote.created <= repo.created) {
@ -213,6 +272,9 @@ export class RepositoryManager {
return remote; return remote;
} }
/**
* Load installed repositories from manifest files.
*/
public async loadFromFiles(): Promise<void> { public async loadFromFiles(): Promise<void> {
const repos = await fs.readdir(this.env.repositoryPath); const repos = await fs.readdir(this.env.repositoryPath);
const loaded = []; const loaded = [];

View File

@ -2,12 +2,22 @@ import * as fs from 'fs-extra';
import { IEnvironment } from './environment'; import { IEnvironment } from './environment';
/**
* Configuration object
*/
export class Configuration { export class Configuration {
public config: any = {}; public config: any = {};
private loaded = false; public loaded = false;
constructor(private env: IEnvironment, private file: string, private defaults: any = {}) {} constructor(
private env: IEnvironment,
private file: string,
private defaults: any = {}
) {}
/**
* Load the configuration from its file.
*/
public async load(): Promise<void> { public async load(): Promise<void> {
this.loaded = true; this.loaded = true;
if (!await fs.pathExists(this.file)) { if (!await fs.pathExists(this.file)) {
@ -23,10 +33,20 @@ export class Configuration {
} }
} }
/**
* Save configuration to its file.
*/
public async save(): Promise<void> { public async save(): Promise<void> {
return fs.writeJson(this.file, this.config); return fs.writeJson(this.file, this.config);
} }
/**
* Get a configuration value by key.
* @param key JSON traverse key (foo.bar.one)
* @param defval Default value
* @param from Internal use only
* @returns Configuration value or default or null
*/
public get(key: string, defval?: any, from?: any): any { public get(key: string, defval?: any, from?: any): any {
if (!from) { if (!from) {
from = this.config; from = this.config;
@ -62,6 +82,10 @@ export class Configuration {
return from[key]; return from[key];
} }
/**
* Set a configuration value by key.
* @returns true on success
*/
public set(key: string, value?: any, from?: any): boolean { public set(key: string, value?: any, from?: any): boolean {
if (!from) { if (!from) {
from = this.config; from = this.config;
@ -94,12 +118,19 @@ export class Configuration {
return true; return true;
} }
/**
* Set the default configuration before loading.
* @param defconf Default configuration
*/
public setDefaults(defconf: any): void { public setDefaults(defconf: any): void {
this.defaults = defconf; this.defaults = defconf;
} }
public saveDefaults(): void { /**
* Save default values to configuration file
*/
public async saveDefaults(): Promise<void> {
this.config = this.defaults || {}; this.config = this.defaults || {};
this.save(); await this.save();
} }
} }

View File

@ -3,6 +3,9 @@ import * as path from 'path';
import { Configuration } from './config'; import { Configuration } from './config';
import { IEnvironment } from './environment'; import { IEnvironment } from './environment';
/**
* Class that acts as a Configuration for plugins.
*/
export class PluginConfiguration extends Configuration { export class PluginConfiguration extends Configuration {
constructor(env: IEnvironment, name: string) { constructor(env: IEnvironment, name: string) {
super(env, path.join(env.configurationPath, name + '.json')); super(env, path.join(env.configurationPath, name + '.json'));

View File

@ -5,6 +5,9 @@ import { IPlugin } from '../plugin';
import { IMessage, IMessageTarget } from './message'; import { IMessage, IMessageTarget } from './message';
import { Formatter } from './message-format'; import { Formatter } from './message-format';
/**
* The base class for a protocol handler.
*/
export class Protocol extends EventEmitter { export class Protocol extends EventEmitter {
public format: Formatter = new Formatter(false, false); public format: Formatter = new Formatter(false, false);

View File

@ -1,6 +1,9 @@
import { EMessageType, IMessage } from './message'; import { EMessageType, IMessage } from './message';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
/**
* Service is used to run, keep track of and kill Protocols.
*/
export class Service { export class Service {
private protocols: Map<string, Protocol> = new Map(); private protocols: Map<string, Protocol> = new Map();
private stopped = false; private stopped = false;
@ -35,7 +38,12 @@ export class Service {
}); });
} }
// Add a new protocol to this service /**
* Add a new protocol to this service.
* @param pto Protocol instance
* @param autostart Automatically start the protocol
* @returns Protocol instance
*/
public use(pto: Protocol, autostart = true): Protocol { public use(pto: Protocol, autostart = true): Protocol {
// This service is no longer accepting new protocols // This service is no longer accepting new protocols
if (this.stopped) { if (this.stopped) {
@ -61,7 +69,11 @@ export class Service {
return pto; return pto;
} }
// Stop a protocol running in this service /**
* Stop a protocol running in this service
* @param pto Protocol instance or name
* @param force Force stop
*/
public stop(pto: string | Protocol, force = false): void { public stop(pto: string | Protocol, force = false): void {
let proto: Protocol; let proto: Protocol;
if (typeof pto === 'string') { if (typeof pto === 'string') {
@ -80,15 +92,26 @@ export class Service {
this.protocols.delete(proto.name); this.protocols.delete(proto.name);
} }
/**
* Find a protocol by name.
* @param name Protocol name
* @returns Protocol instance or undefined
*/
public getProtocolByName(name: string): Protocol | undefined { public getProtocolByName(name: string): Protocol | undefined {
return this.protocols.get(name); return this.protocols.get(name);
} }
/**
* Get all protocol instances running in this service.
* @returns List of all protocol instances running in this service
*/
public getAll(): Protocol[] { public getAll(): Protocol[] {
return Array.from(this.protocols.values()); return Array.from(this.protocols.values());
} }
// Gracefully stops everything running in this service /**
* Gracefully stops everything running in this service
*/
public stopAll(): Promise<void> { public stopAll(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.stopped) { if (this.stopped) {
@ -133,7 +156,9 @@ export class Service {
}); });
} }
// Kills everything running in this service /**
* Forcefully kills everything running in this service,
*/
public die(): void { public die(): void {
this.stopped = true; this.stopped = true;
for (const [name, proto] of this.protocols) { for (const [name, proto] of this.protocols) {

View File

@ -1,5 +1,8 @@
import { logger } from '../core'; import { logger } from '../core';
/**
* Event emitter that can be scoped by plugin name or core.
*/
export class ScopedEventEmitter { export class ScopedEventEmitter {
private listeners: {[key: string]: any[]}; private listeners: {[key: string]: any[]};
public addEventListener = this.on; public addEventListener = this.on;

View File

@ -3,6 +3,11 @@ import path from 'path';
export { ScopedEventEmitter } from './events'; export { ScopedEventEmitter } from './events';
export { IProcessData, spawnProcess, execProcess } from './run'; export { IProcessData, spawnProcess, execProcess } from './run';
/**
* Load a Node.js module without caching it.
* @param file JavaScript file
* @returns Loaded module
*/
export function requireNoCache(file: string): object | undefined { export function requireNoCache(file: string): object | undefined {
const fullPath = path.resolve(file); const fullPath = path.resolve(file);
const mod = require(fullPath); const mod = require(fullPath);

View File

@ -7,6 +7,13 @@ export interface IProcessData {
stdout: string[]; stdout: string[];
} }
/**
* Spawn a process.
* @param execp Executable
* @param args Executable arguments
* @param env Squeebot environment
* @returns Process output
*/
export async function spawnProcess(execp: string, args: any[], env: IEnvironment): Promise<IProcessData> { export async function spawnProcess(execp: string, args: any[], env: IEnvironment): Promise<IProcessData> {
return new Promise((resolve): void => { return new Promise((resolve): void => {
const process = spawn(execp, args, { cwd: env.path }); const process = spawn(execp, args, { cwd: env.path });
@ -28,6 +35,12 @@ export async function spawnProcess(execp: string, args: any[], env: IEnvironment
}); });
} }
/**
* Execute an executable file while buffering its output.
* @param execp Executable
* @param env Squeebot environment
* @returns Process output
*/
export async function execProcess(execp: string, env: IEnvironment): Promise<IProcessData> { export async function execProcess(execp: string, env: IEnvironment): Promise<IProcessData> {
return new Promise((resolve): void => { return new Promise((resolve): void => {
exec(execp, (error: any, stdout: any, stderr: any): void => resolve({ exec(execp, (error: any, stdout: any, stderr: any): void => resolve({