Documentation of most methods
This commit is contained in:
parent
68489c733d
commit
9a2142688b
@ -8,11 +8,24 @@ export interface IChannel {
|
||||
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 {
|
||||
private channels: IChannel[] = [];
|
||||
|
||||
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 {
|
||||
if (source != null) {
|
||||
if (source.manifest) {
|
||||
@ -27,6 +40,10 @@ export class ChannelManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the event handlers for channels
|
||||
* @param configured Initial configuration of channels
|
||||
*/
|
||||
public initialize(configured: IChannel[]): void {
|
||||
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[] {
|
||||
const list = [];
|
||||
for (const chan of this.channels) {
|
||||
@ -81,6 +104,10 @@ export class ChannelManager {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a preconfigured channel list and add them to the list
|
||||
* @param channels Preconfigured channel list
|
||||
*/
|
||||
private addPreconfiguredChannels(channels: IChannel[]): void {
|
||||
for (const chan of channels) {
|
||||
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 {
|
||||
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 {
|
||||
const exists = this.getChannelByName(chan.name);
|
||||
if (exists) {
|
||||
@ -108,6 +145,10 @@ export class ChannelManager {
|
||||
return chan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a channel by name or the channel itself
|
||||
* @param chan Name of channel or channel
|
||||
*/
|
||||
public removeChannel(chan: string | IChannel): void {
|
||||
if (typeof chan === 'string') {
|
||||
const getchan = this.getChannelByName(chan);
|
||||
@ -119,6 +160,10 @@ export class ChannelManager {
|
||||
this.channels.splice(this.channels.indexOf(chan), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all channels
|
||||
* @returns All channels
|
||||
*/
|
||||
public getAll(): IChannel[] {
|
||||
return this.channels;
|
||||
}
|
||||
|
@ -1,17 +1,24 @@
|
||||
import http from 'http';
|
||||
import http, { RequestOptions } from 'http';
|
||||
import https from 'https';
|
||||
import qs from 'querystring';
|
||||
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(
|
||||
link: string,
|
||||
headers: any = {},
|
||||
restrictToText = true,
|
||||
saveTo?: string,
|
||||
lback?: number): Promise<any> {
|
||||
|
||||
const parsed = url.parse(link);
|
||||
lback?: number
|
||||
): Promise<any> {
|
||||
const parsed = new URL(link);
|
||||
const opts = {
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
@ -19,8 +26,8 @@ export function httpGET(
|
||||
'User-Agent': 'Squeebot/Commons-3.0.0',
|
||||
},
|
||||
host: parsed.hostname,
|
||||
path: parsed.path,
|
||||
port: parsed.port,
|
||||
path: `${parsed.pathname}${parsed.search}`,
|
||||
port: parsed.port || null,
|
||||
};
|
||||
|
||||
if (headers) {
|
||||
@ -63,13 +70,13 @@ export function httpGET(
|
||||
if (restrictToText) {
|
||||
const cType = res.headers['content-type'];
|
||||
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!'));
|
||||
}
|
||||
}
|
||||
|
||||
reqTimeOut = setTimeout(() => {
|
||||
req.abort();
|
||||
req.destroy();
|
||||
data = null;
|
||||
reject(new Error('Request took too long!'));
|
||||
}, 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(
|
||||
link: string,
|
||||
headers: any = {},
|
||||
data: any): Promise<any> {
|
||||
const parsed = url.parse(link);
|
||||
let postData = qs.stringify(data);
|
||||
data: any
|
||||
): Promise<any> {
|
||||
const parsed = new URL(link);
|
||||
let postData: string | URLSearchParams = new URLSearchParams(data);
|
||||
|
||||
const opts = {
|
||||
const opts: RequestOptions = {
|
||||
headers: {
|
||||
'Content-Length': Buffer.byteLength(postData),
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'Squeebot/Commons-3.0.0',
|
||||
},
|
||||
host: parsed.host,
|
||||
method: 'POST',
|
||||
path: parsed.path,
|
||||
port: parsed.port,
|
||||
path: `${parsed.pathname}${parsed.search}`,
|
||||
port: parsed.port || null,
|
||||
};
|
||||
|
||||
// Assign provided 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') {
|
||||
postData = JSON.stringify(data);
|
||||
}
|
||||
|
||||
// Set content length accordingly
|
||||
opts.headers['Content-Length'] = Buffer.byteLength(postData.toString());
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const httpModule = parsed.protocol === 'https:' ? https : http;
|
||||
const req = httpModule.request(opts, (res) => {
|
||||
|
@ -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 {
|
||||
return text.replace(/\n/g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
|
@ -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 {
|
||||
const secNum = parseInt(input.toString(), 10);
|
||||
let hours: string | number = Math.floor(secNum / 3600);
|
||||
@ -27,12 +32,18 @@ export function toHHMMSS(input: string | number): string {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
let seconds = 0;
|
||||
let match;
|
||||
@ -101,6 +117,11 @@ export function parseTimeToSeconds(input: string): number {
|
||||
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 {
|
||||
const nStr = input.toString();
|
||||
const x = nStr.split('.');
|
||||
@ -115,6 +136,11 @@ export function thousandsSeparator(input: number | string): string {
|
||||
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 {
|
||||
const seconds = Math.floor((Date.now() - date) / 1000);
|
||||
let interval = Math.floor(seconds / 31536000);
|
||||
|
@ -5,6 +5,10 @@ import { RepositoryManager } from '../plugin/repository';
|
||||
import { Configuration, IEnvironment } from '../types';
|
||||
import { ScopedEventEmitter } from '../util';
|
||||
|
||||
/**
|
||||
* Reference of a fully featured Squeebot core.
|
||||
* Recommended implementation of a squeebot runner implements this interface.
|
||||
*/
|
||||
export interface ISqueebotCore {
|
||||
environment: IEnvironment;
|
||||
npm: NPMExecutor;
|
||||
|
@ -9,6 +9,12 @@ const dirs: {[key: string]: string} = {
|
||||
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> {
|
||||
if (!await fs.pathExists(enviroFile)) {
|
||||
throw new Error('Environment file does not exist.');
|
||||
|
@ -1,23 +1,39 @@
|
||||
import dateFmt from 'dateformat';
|
||||
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 {
|
||||
public timestamp = 'dd/mm/yy HH:MM:ss';
|
||||
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 {
|
||||
for (const index in this.console) {
|
||||
const old = this.console[index];
|
||||
this.console[index] = (...data: any[]): void => {
|
||||
rl.output.write('\x1b[2K\r');
|
||||
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 = [];
|
||||
let cfunc = this.console[0];
|
||||
|
||||
@ -52,27 +68,52 @@ export class Logger {
|
||||
cfunc.apply(null, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger level: `INFO`
|
||||
*
|
||||
* See `console.log` for more information.
|
||||
*/
|
||||
public log(...data: any[]): void {
|
||||
this.write('info', ...data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger level: `WARN`
|
||||
*
|
||||
* See `console.warn` for more information.
|
||||
*/
|
||||
public warn(...data: any[]): void {
|
||||
this.write('warn', ...data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger level: `INFO`
|
||||
*
|
||||
* See `console.log` for more information.
|
||||
*/
|
||||
public info(...data: any[]): void {
|
||||
this.write('info', ...data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger level: `ERROR`
|
||||
*
|
||||
* See `console.error` for more information.
|
||||
*/
|
||||
public error(...data: any[]): void {
|
||||
this.write('error', ...data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger level: `DEBUG`
|
||||
*
|
||||
* See `console.log` for more information.
|
||||
*/
|
||||
public debug(...data: any[]): void {
|
||||
this.write('debug', ...data);
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton for Logger to be used anywhere
|
||||
const logger = new Logger();
|
||||
|
||||
export { logger };
|
||||
|
@ -4,12 +4,21 @@ import semver from 'semver';
|
||||
import { IEnvironment } from '../types/environment';
|
||||
import { spawnProcess } from '../util/run';
|
||||
|
||||
/**
|
||||
* Execute NPM commands
|
||||
*/
|
||||
export class NPMExecutor {
|
||||
private installed: Record<string, string> = {};
|
||||
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> {
|
||||
// Initialize npm 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> {
|
||||
if (!await fs.pathExists(this.packageFile)) {
|
||||
await this.init();
|
||||
@ -37,6 +49,10 @@ export class NPMExecutor {
|
||||
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> {
|
||||
if (!await fs.pathExists(this.packageFile)) {
|
||||
await this.init();
|
||||
@ -66,6 +82,12 @@ export class NPMExecutor {
|
||||
await this.loadPackageFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall a npm package.
|
||||
*
|
||||
* See `installPackage` for more info.
|
||||
* @param pkg Package name
|
||||
*/
|
||||
public async uninstallPackage(pkg: string): Promise<void> {
|
||||
if (!await fs.pathExists(this.packageFile)) {
|
||||
await this.init();
|
||||
|
@ -2,11 +2,19 @@ import { IEnvironment } from '../types/environment';
|
||||
import { PluginConfiguration } from '../types/plugin-config';
|
||||
import { IPluginManifest } from './plugin';
|
||||
|
||||
/**
|
||||
* Plugin configuration memory storage
|
||||
*/
|
||||
export class PluginConfigurator {
|
||||
private configs: Map<string, PluginConfiguration> = new Map();
|
||||
|
||||
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> {
|
||||
if (!this.configs.has(mf.name)) {
|
||||
const conf = new PluginConfiguration(this.env, mf.name);
|
||||
|
@ -6,9 +6,17 @@ import { IPluginManifest } from './plugin';
|
||||
|
||||
import { logger } from '../core';
|
||||
|
||||
/**
|
||||
* Plugin manifest loader
|
||||
*/
|
||||
export class PluginMetaLoader {
|
||||
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> {
|
||||
if (name === 'squeebot') {
|
||||
throw new Error('Illegal name.');
|
||||
@ -60,6 +68,11 @@ export class PluginMetaLoader {
|
||||
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[]> {
|
||||
const dirlist = await fs.readdir(this.env.pluginsPath);
|
||||
const plugins: IPluginManifest[] = [];
|
||||
|
@ -11,6 +11,9 @@ import { NPMExecutor } from '../npm/executor';
|
||||
|
||||
import { logger } from '../core/logger';
|
||||
|
||||
/**
|
||||
* Plugin management and execution system
|
||||
*/
|
||||
export class PluginManager {
|
||||
private plugins: Map<string, IPlugin> = new Map();
|
||||
private configs: PluginConfigurator = new PluginConfigurator(this.environment);
|
||||
@ -21,14 +24,25 @@ export class PluginManager {
|
||||
public availablePlugins: IPluginManifest[],
|
||||
private stream: ScopedEventEmitter,
|
||||
private environment: IEnvironment,
|
||||
private npm: NPMExecutor) {
|
||||
private npm: NPMExecutor
|
||||
) {
|
||||
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 {
|
||||
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 {
|
||||
if (this.plugins.has(name)) {
|
||||
return this.plugins.get(name) as IPlugin;
|
||||
@ -36,10 +50,21 @@ export class PluginManager {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all loaded plugins.
|
||||
* @returns List of loaded plugins
|
||||
*/
|
||||
public getLoaded(): IPlugin[] {
|
||||
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 {
|
||||
// Automatically add arrays of manifests
|
||||
if (Array.isArray(manifest)) {
|
||||
@ -67,6 +92,12 @@ export class PluginManager {
|
||||
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(
|
||||
plugin: IPluginManifest | IPluginManifest[] | string | string[],
|
||||
unload = true
|
||||
@ -107,6 +138,11 @@ export class PluginManager {
|
||||
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> {
|
||||
// Ignore loading when we're shutting down
|
||||
if (this.stopping) {
|
||||
@ -245,6 +281,10 @@ export class PluginManager {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart a loaded plugin.
|
||||
* @param mf Plugin instance, plugin manifest or plugin name
|
||||
*/
|
||||
public async restart(mf: IPluginManifest | IPlugin | string): Promise<void> {
|
||||
let manifest;
|
||||
if (typeof mf === 'string') {
|
||||
@ -268,6 +308,9 @@ export class PluginManager {
|
||||
this.stream.emitTo(manifest.name, 'pluginUnload', manifest.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for plugin status events.
|
||||
*/
|
||||
private addEvents(): void {
|
||||
this.stream.on('core', 'pluginLoad', (mf: IPluginManifest | string) => {
|
||||
if (typeof mf === 'string') {
|
||||
|
@ -8,6 +8,9 @@ export interface IPlugin {
|
||||
service: Service | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for all plugins
|
||||
*/
|
||||
export class Plugin implements IPlugin {
|
||||
public service: Service | null = null;
|
||||
protected on = this.addEventListener;
|
||||
@ -17,6 +20,10 @@ export class Plugin implements IPlugin {
|
||||
public stream: ScopedEventEmitter,
|
||||
public config: PluginConfiguration) {}
|
||||
|
||||
/**
|
||||
* Called when plugin first starts.
|
||||
* Please use this instead of the constructor.
|
||||
*/
|
||||
public initialize(): void {}
|
||||
|
||||
public get name(): string {
|
||||
|
@ -12,29 +12,60 @@ import { PluginManager } from '../manager';
|
||||
import { IPlugin, IPluginManifest } from '../plugin';
|
||||
import { IRepoPluginDef, IRepository } from './repository';
|
||||
|
||||
/**
|
||||
* Plugin repository manager
|
||||
*/
|
||||
export class RepositoryManager {
|
||||
private repositories: Map<string, IRepository> = new Map();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find repository providing plugin
|
||||
* @param pname Plugin name
|
||||
* @returns Repository manifest
|
||||
*/
|
||||
public findRepoForPlugin(pname: string): IRepository | undefined {
|
||||
return Array.from(this.repositories.values()).find(repo =>
|
||||
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 {
|
||||
return this.repositories.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of all installed repositories
|
||||
* @returns All repositories
|
||||
*/
|
||||
public getAll(): IRepository[] {
|
||||
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> {
|
||||
let repo;
|
||||
if (name.indexOf('/') !== -1) {
|
||||
@ -94,6 +125,10 @@ export class RepositoryManager {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall a plugin
|
||||
* @param plugin Plugin name, manifest or object
|
||||
*/
|
||||
public async uninstallPlugin(plugin: string | IPluginManifest | IPlugin): Promise<void> {
|
||||
let realName: string;
|
||||
if (typeof plugin === 'string') {
|
||||
@ -115,6 +150,11 @@ export class RepositoryManager {
|
||||
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> {
|
||||
const indexFileGet = await httpGET(url);
|
||||
const meta = JSON.parse(indexFileGet);
|
||||
@ -134,6 +174,11 @@ export class RepositoryManager {
|
||||
return meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a repository by remote URL
|
||||
* @param url Remote repository manifest URL
|
||||
* @returns Repository manifest
|
||||
*/
|
||||
public async installRepository(url: string): Promise<IRepository> {
|
||||
// Add standard repository file path, if missing
|
||||
if (!url.endsWith('/repository.json')) {
|
||||
@ -158,6 +203,10 @@ export class RepositoryManager {
|
||||
return remote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall a repository
|
||||
* @param repo Repository name or manifest
|
||||
*/
|
||||
public async uninstallRepository(repo: string | IRepository): Promise<void> {
|
||||
if (typeof repo === 'string') {
|
||||
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[]> {
|
||||
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> {
|
||||
const remote = await this.getRemote(repo.url + '/repository.json');
|
||||
if (remote.created <= repo.created) {
|
||||
@ -213,6 +272,9 @@ export class RepositoryManager {
|
||||
return remote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load installed repositories from manifest files.
|
||||
*/
|
||||
public async loadFromFiles(): Promise<void> {
|
||||
const repos = await fs.readdir(this.env.repositoryPath);
|
||||
const loaded = [];
|
||||
|
@ -2,12 +2,22 @@ import * as fs from 'fs-extra';
|
||||
|
||||
import { IEnvironment } from './environment';
|
||||
|
||||
/**
|
||||
* Configuration object
|
||||
*/
|
||||
export class Configuration {
|
||||
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> {
|
||||
this.loaded = true;
|
||||
if (!await fs.pathExists(this.file)) {
|
||||
@ -23,10 +33,20 @@ export class Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration to its file.
|
||||
*/
|
||||
public async save(): Promise<void> {
|
||||
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 {
|
||||
if (!from) {
|
||||
from = this.config;
|
||||
@ -62,6 +82,10 @@ export class Configuration {
|
||||
return from[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a configuration value by key.
|
||||
* @returns true on success
|
||||
*/
|
||||
public set(key: string, value?: any, from?: any): boolean {
|
||||
if (!from) {
|
||||
from = this.config;
|
||||
@ -94,12 +118,19 @@ export class Configuration {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default configuration before loading.
|
||||
* @param defconf Default configuration
|
||||
*/
|
||||
public setDefaults(defconf: any): void {
|
||||
this.defaults = defconf;
|
||||
}
|
||||
|
||||
public saveDefaults(): void {
|
||||
/**
|
||||
* Save default values to configuration file
|
||||
*/
|
||||
public async saveDefaults(): Promise<void> {
|
||||
this.config = this.defaults || {};
|
||||
this.save();
|
||||
await this.save();
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ import * as path from 'path';
|
||||
import { Configuration } from './config';
|
||||
import { IEnvironment } from './environment';
|
||||
|
||||
/**
|
||||
* Class that acts as a Configuration for plugins.
|
||||
*/
|
||||
export class PluginConfiguration extends Configuration {
|
||||
constructor(env: IEnvironment, name: string) {
|
||||
super(env, path.join(env.configurationPath, name + '.json'));
|
||||
|
@ -5,6 +5,9 @@ import { IPlugin } from '../plugin';
|
||||
import { IMessage, IMessageTarget } from './message';
|
||||
import { Formatter } from './message-format';
|
||||
|
||||
/**
|
||||
* The base class for a protocol handler.
|
||||
*/
|
||||
export class Protocol extends EventEmitter {
|
||||
public format: Formatter = new Formatter(false, false);
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { EMessageType, IMessage } from './message';
|
||||
import { Protocol } from './protocol';
|
||||
|
||||
/**
|
||||
* Service is used to run, keep track of and kill Protocols.
|
||||
*/
|
||||
export class Service {
|
||||
private protocols: Map<string, Protocol> = new Map();
|
||||
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 {
|
||||
// This service is no longer accepting new protocols
|
||||
if (this.stopped) {
|
||||
@ -61,7 +69,11 @@ export class Service {
|
||||
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 {
|
||||
let proto: Protocol;
|
||||
if (typeof pto === 'string') {
|
||||
@ -80,15 +92,26 @@ export class Service {
|
||||
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 {
|
||||
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[] {
|
||||
return Array.from(this.protocols.values());
|
||||
}
|
||||
|
||||
// Gracefully stops everything running in this service
|
||||
/**
|
||||
* Gracefully stops everything running in this service
|
||||
*/
|
||||
public stopAll(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
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 {
|
||||
this.stopped = true;
|
||||
for (const [name, proto] of this.protocols) {
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { logger } from '../core';
|
||||
|
||||
/**
|
||||
* Event emitter that can be scoped by plugin name or core.
|
||||
*/
|
||||
export class ScopedEventEmitter {
|
||||
private listeners: {[key: string]: any[]};
|
||||
public addEventListener = this.on;
|
||||
|
@ -3,6 +3,11 @@ import path from 'path';
|
||||
export { ScopedEventEmitter } from './events';
|
||||
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 {
|
||||
const fullPath = path.resolve(file);
|
||||
const mod = require(fullPath);
|
||||
|
@ -7,6 +7,13 @@ export interface IProcessData {
|
||||
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> {
|
||||
return new Promise((resolve): void => {
|
||||
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> {
|
||||
return new Promise((resolve): void => {
|
||||
exec(execp, (error: any, stdout: any, stderr: any): void => resolve({
|
||||
|
Loading…
Reference in New Issue
Block a user