diff --git a/package-lock.json b/package-lock.json index 177b617..90cf41e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,12 +45,36 @@ "@types/node": "*" } }, + "@types/minipass": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-2.2.0.tgz", + "integrity": "sha512-wuzZksN4w4kyfoOv/dlpov4NOunwutLA/q7uc00xU02ZyUY+aoM5PWIXEKBMnm0NHd4a+N71BMjq+x7+2Af1fg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "14.14.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz", "integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw==", "dev": true }, + "@types/semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ==" + }, + "@types/tar": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-4.0.4.tgz", + "integrity": "sha512-0Xv+xcmkTsOZdIF4yCnd7RkOOyfyqPaqJ7RZFKnwdxfDbkN3eAAE9sHl8zJFqBz4VhxolW9EErbjR1oyH7jK2A==", + "dev": true, + "requires": { + "@types/minipass": "*", + "@types/node": "*" + } + }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -107,6 +131,11 @@ "supports-color": "^5.3.0" } }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -168,6 +197,14 @@ "universalify": "^1.0.0" } }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -286,6 +323,23 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -327,10 +381,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" }, "sprintf-js": { "version": "1.0.3", @@ -347,6 +400,26 @@ "has-flag": "^3.0.0" } }, + "tar": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", + "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -372,6 +445,14 @@ "semver": "^5.3.0", "tslib": "^1.13.0", "tsutils": "^2.29.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "tsutils": { @@ -399,6 +480,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/package.json b/package.json index 93dc361..a7e0c56 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,15 @@ "@types/dateformat": "^3.0.1", "@types/fs-extra": "^9.0.4", "@types/node": "^14.14.9", + "@types/tar": "^4.0.4", "tslint": "^6.1.3", "typescript": "^4.0.5" }, "dependencies": { + "@types/semver": "^7.3.4", "dateformat": "^4.0.0", - "fs-extra": "^9.0.1" + "fs-extra": "^9.0.1", + "semver": "^7.3.2", + "tar": "^6.0.5" } } diff --git a/src/channel/index.ts b/src/channel/index.ts index 7e68d4b..f3650d0 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -8,6 +8,8 @@ export interface IChannel { enabled: boolean; } +// TODO: Source specification to support plugin services. + export class ChannelManager { private channels: IChannel[] = []; @@ -107,4 +109,8 @@ export class ChannelManager { } this.channels.splice(this.channels.indexOf(chan), 1); } + + public getAll(): any[] { + return this.channels; + } } diff --git a/src/common/http.ts b/src/common/http.ts new file mode 100644 index 0000000..893abdd --- /dev/null +++ b/src/common/http.ts @@ -0,0 +1,140 @@ +import http from 'http'; +import https from 'https'; +import qs from 'querystring'; +import fs from 'fs-extra'; +import url from 'url'; + +export function httpGET( + link: string, + headers: any = {}, + restrictToText = true, + saveTo?: string, + lback?: number): Promise { + if (lback && lback >= 4) { + throw new Error('infinite loop!'); + } + + const parsed = url.parse(link); + const opts = { + headers: { + Accept: '*/*', + 'Accept-Language': 'en-US', + 'User-Agent': 'Squeebot/Commons-3.0.0', + }, + host: parsed.hostname, + path: parsed.path, + port: parsed.port, + }; + + if (headers) { + opts.headers = Object.assign(opts.headers, headers); + } + + let reqTimeOut: any; + let data: string | null = ''; + + const httpModule = parsed.protocol === 'https:' ? https : http; + return new Promise((resolve, reject) => { + const req = httpModule.get(opts, (res) => { + if (res.statusCode === 302 || res.statusCode === 301) { + if (!lback) { + lback = 1; + } else { + lback += 1; + } + + return httpGET(res.headers.location as string, + headers, + restrictToText, + saveTo, + lback, + ).then(resolve, reject); + } + + if (saveTo) { + const file = fs.createWriteStream(saveTo); + res.pipe(file); + file.on('close', () => resolve(saveTo)); + file.on('error', (e) => reject(e)); + return; + } + + if (restrictToText) { + const cType = res.headers['content-type']; + if (cType && cType.indexOf('text') === -1 && cType.indexOf('json') === -1) { + req.abort(); + return reject(new Error('Response type is not supported by httpGET!')); + } + } + + reqTimeOut = setTimeout(() => { + req.abort(); + data = null; + reject(new Error('Request took too long!')); + }, 5000); + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + clearTimeout(reqTimeOut); + + resolve(data || saveTo); + }); + }).on('error', (e) => { + reject(e); + }); + + req.setTimeout(10000); + }); +} + +export function httpPOST( + link: string, + headers: any = {}, + data: any): Promise { + const parsed = url.parse(link); + let postData = qs.stringify(data); + + const opts = { + 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, + }; + + if (headers) { + opts.headers = Object.assign(opts.headers, headers); + } + + if (opts.headers['Content-Type'] === 'application/json') { + postData = JSON.stringify(data); + } + + return new Promise((resolve, reject) => { + const httpModule = parsed.protocol === 'https:' ? https : http; + const req = httpModule.request(opts, (res) => { + res.setEncoding('utf8'); + let resp = ''; + + res.on('data', (chunk) => { + resp += chunk; + }); + + res.on('end', () => { + resolve(resp); + }); + }).on('error', (e) => { + reject(e); + }); + + req.write(postData); + req.end(); + }); +} diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 0000000..a1c6b2a --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1,3 @@ +export * from './http'; +export * from './time'; +export * from './sanitize'; diff --git a/src/common/sanitize.ts b/src/common/sanitize.ts new file mode 100644 index 0000000..19f23a9 --- /dev/null +++ b/src/common/sanitize.ts @@ -0,0 +1,10 @@ + +export function sanitizeEscapedText(text: string): string { + return text.replace(/\n/g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '\'') + .trim(); +} diff --git a/src/common/time.ts b/src/common/time.ts new file mode 100644 index 0000000..5b41ef8 --- /dev/null +++ b/src/common/time.ts @@ -0,0 +1,116 @@ + +export function toHHMMSS(input: string | number): string { + const secNum = parseInt(input.toString(), 10); + let hours: string | number = Math.floor(secNum / 3600); + let minutes: string | number = Math.floor((secNum - (hours * 3600)) / 60); + let seconds: string | number = secNum - (hours * 3600) - (minutes * 60); + + if (hours < 10) { + hours = '0' + hours; + } + + if (minutes < 10) { + minutes = '0' + minutes; + } + + if (seconds < 10) { + seconds = '0' + seconds; + } + + let time = ''; + if (parseInt(hours.toString(), 10) > 0) { + time = hours + ':' + minutes + ':' + seconds; + } else { + time = minutes + ':' + seconds; + } + + return time; +} + +// 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) +export function readableTime(timems: number): string { + const time = Math.floor(timems); + + if (time < 60) { + return zf(time) + 's'; + } else if (time < 3600) { + return zf(time / 60) + + 'm ' + zf(time % 60) + 's'; + } else if (time < 86400) { + return zf(time / 3600) + + 'h ' + zf((time % 3600) / 60) + + 'm ' + zf((time % 3600) % 60) + 's'; + } else if (time < 31536000) { + return (time / 86400) + + 'd ' + zf((time % 86400) / 3600) + + 'h ' + zf((time % 3600) / 60) + + 'm ' + zf((time % 3600) % 60) + 's'; + } else { + return (time / 31536000) + + 'y ' + zf((time % 31536000) / 86400) + + 'd ' + zf((time % 86400) / 3600) + + 'h ' + zf((time % 3600) / 60) + + 'm ' + zf((time % 3600) % 60) + 's'; + } +} + +export function parseTimeToSeconds(input: string): number { + let seconds = 0; + let match; + const secMinute = 1 * 60; + const secHour = secMinute * 60; + const secDay = secHour * 24; + const secWeek = secDay * 7; + const secYear = secDay * 365; + + match = input.match('([0-9]+)y'); + if (match != null) { + seconds += +match[1] * secYear; + } + + match = input.match('([0-9]+)w'); + if (match != null) { + seconds += +match[1] * secWeek; + } + + match = input.match('([0-9]+)d'); + if (match != null) { + seconds += +match[1] * secDay; + } + + match = input.match('([0-9]+)h'); + if (match != null) { + seconds += +match[1] * secHour; + } + + match = input.match('([0-9]+)m'); + if (match != null) { + seconds += +match[1] * secMinute; + } + + match = input.match('([0-9]+)s'); + if (match != null) { + seconds += +match[1]; + } + + return seconds; +} + +export function thousandsSeparator(input: number | string): string { + const nStr = input.toString(); + const x = nStr.split('.'); + let x1 = x[0]; + const x2 = x.length > 1 ? '.' + x[1] : ''; + const rgx = /(\d+)(\d{3})/; + + while (rgx.test(x1)) { + x1 = x1.replace(rgx, '$1' + ',' + '$2'); + } + + return x1 + x2; +} diff --git a/src/plugin/manager.ts b/src/plugin/manager.ts index 4d90b3b..81c1a70 100644 --- a/src/plugin/manager.ts +++ b/src/plugin/manager.ts @@ -25,7 +25,7 @@ export class PluginManager { private configs: PluginConfigurator = new PluginConfigurator(this.environment); constructor( - private availablePlugins: IPluginManifest[], + public availablePlugins: IPluginManifest[], private stream: ScopedEventEmitter, private environment: IEnvironment, private npm: NPMExecutor) { @@ -48,6 +48,14 @@ export class PluginManager { return null; } + public getLoaded(): IPlugin[] { + const list = [] + for (const pl of this.plugins.values()) { + list.push(pl); + } + return list; + } + public addAvailable(manifest: IPluginManifest | IPluginManifest[]): boolean { // Automatically add arrays of manifests if (Array.isArray(manifest)) { @@ -103,16 +111,6 @@ export class PluginManager { return returnValue; } - public removeRepository(repo: string): boolean { - const list: IPluginManifest[] = []; - for (const mf of this.availablePlugins) { - if (mf.repository && mf.repository === repo) { - list.push(mf); - } - } - return this.removeAvailable(list); - } - public async load(plugin: IPluginManifest): Promise { // Don't load plugins twice const ready = this.getLoadedByName(plugin.name); @@ -163,6 +161,11 @@ export class PluginManager { // Load the configuration const config: PluginConfiguration = await this.configs.loadConfig(plugin); + // Ensure plugin has a full path + if (!plugin.fullPath) { + plugin.fullPath = path.join(this.environment.pluginsPath, plugin.name); + } + // Load the module logger.debug('Loading plugin %s module', plugin.name); const PluginModule = requireNoCache(path.resolve(plugin.fullPath, plugin.main)) as any; @@ -243,6 +246,8 @@ export class PluginManager { } } + logger.debug('%s has unloaded.', mf); + // Delete plugin from the list of loaded plugins this.plugins.delete(mf); diff --git a/src/plugin/repository/index.ts b/src/plugin/repository/index.ts new file mode 100644 index 0000000..0614be6 --- /dev/null +++ b/src/plugin/repository/index.ts @@ -0,0 +1,2 @@ +export * from './manager'; +export * from './repository'; \ No newline at end of file diff --git a/src/plugin/repository/manager.ts b/src/plugin/repository/manager.ts new file mode 100644 index 0000000..53098e9 --- /dev/null +++ b/src/plugin/repository/manager.ts @@ -0,0 +1,239 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { URL } from 'url'; +import semver from 'semver'; +import tar from 'tar'; + +import { httpGET } from '../../common'; +import { logger } from '../../core'; + +import { IEnvironment } from "../../types"; +import { PluginManager } from '../manager'; +import { IPlugin, IPluginManifest } from "../plugin"; +import { IRepoPluginDef, IRepository } from "./repository"; + +export class RepositoryManager { + private repositories: Map = new Map(); + + constructor(private env: IEnvironment, private plugins: PluginManager) {} + + public repoProvidesPlugin(repo: IRepository, name: string | IPlugin | IPluginManifest): IRepoPluginDef | null { + let realName: string; + if (typeof name === 'string') { + realName = name; + } else if ('manifest' in name) { + realName = name.manifest.name; + } else { + realName = name.name; + } + + for (const plugin of repo.plugins) { + if (plugin.name === realName) { + return plugin; + } + } + + return null; + } + + public findRepoForPlugin(pname: string): IRepository | null { + for (const [name, repo] of this.repositories) { + for (const plugin of repo.plugins) { + if (plugin.name === pname) { + return repo; + } + } + } + return null; + } + + public getRepoByName(name: string): IRepository | undefined { + return this.repositories.get(name); + } + + public getAll(): IRepository[] { + const list = [] + for (const irep of this.repositories.values()) { + list.push(irep); + } + return list; + } + + public async installPlugin(name: string): Promise { + const repo = this.findRepoForPlugin(name); + if (!repo) { + throw new Error('Could not find a repository for a plugin named ' + name); + } + + const srcFile = name + '.plugin.tgz'; + const pluginPath = repo.url + '/' + srcFile; + const tempdir = path.join(this.env.path, await fs.mkdtemp('.sbdl')); + const tempfile = path.join(tempdir, srcFile); + let manifest + + try { + const save = await httpGET(pluginPath, {}, false, tempfile); + const extract = await tar.x({ + file: tempfile, + C: tempdir, + }); + + const findByName = path.join(tempdir, name); + if (!await fs.pathExists(findByName)) { + throw new Error('Invalid file provided.'); + } + + const manifestFile = path.join(findByName, 'plugin.json'); + if (!await fs.pathExists(manifestFile)) { + throw new Error('manifest file does not exist.'); + } + + const loadManifest = await fs.readJSON(manifestFile); + if (!loadManifest.name || !loadManifest.version) { + throw new Error('Not a valid plugin manifest file.'); + } + manifest = loadManifest; + + await fs.copy(findByName, path.join(this.env.pluginsPath, manifest.name)); + } catch (e) { + await fs.remove(tempdir); + throw e; + } + + await fs.remove(tempdir); + + this.plugins.addAvailable(manifest); + return manifest; + } + + public async uninstallPlugin(plugin: string | IPluginManifest | IPlugin): Promise { + let realName: string; + if (typeof plugin === 'string') { + realName = plugin; + } else if ('manifest' in plugin) { + realName = plugin.manifest.name; + } else { + realName = plugin.name; + } + + const pluginMeta = this.plugins.getAvailableByName(realName); + if (!pluginMeta) { + throw new Error('No such plugin exists or is available.'); + } + + const remoPath = path.join(this.env.pluginsPath, realName); + await fs.remove(remoPath); + + this.plugins.removeAvailable(pluginMeta); + } + + public async getRemote(url: string): Promise { + const indexFileGet = await httpGET(url); + const meta = JSON.parse(indexFileGet); + + if (!meta.name || !meta.plugins || !meta.schema) { + throw new Error('Invalid metadata file for repository.'); + } + + if (meta.schema > 1) { + throw new Error('Unsupported metadata version!'); + } + + return meta; + } + + public async installRepository(url: string): Promise { + const remote = await this.getRemote(url); + const local = path.join(this.env.repositoryPath, remote.name + '.json'); + const urlParsed = new URL(url); + + // Change the path to not include the metadata file + const basePath = path.dirname(urlParsed.pathname); + urlParsed.pathname = basePath; + remote.url = urlParsed.href; + + await fs.writeJSON(local, remote); + + this.repositories.set(remote.name, remote); + return remote; + } + + public async uninstallRepository(repo: string | IRepository): Promise { + if (typeof repo === 'string') { + repo = this.getRepoByName(repo) as IRepository; + if (!repo) { + throw new Error('No such repository found!'); + } + } + + // Remove repository metadata file + const repoFile = path.join(this.env.repositoryPath, repo.name + '.json'); + await fs.remove(repoFile); + this.repositories.delete(repo.name); + + // Uninstall all plugins from this repository + for (const plugin of repo.plugins) { + // Ignore non-installed plugins + try { + await this.uninstallPlugin(plugin.name); + } catch (e) {} + } + } + + public async checkForUpdates(repo: IRepository): Promise { + const oprep = await this.updateRepository(repo); + + // Checking for version differences in the plugins + // Get locally installed plugins + let needsUpdates: IPluginManifest[] = []; + for (const avail of this.plugins.availablePlugins) { + const repoPlugin = this.repoProvidesPlugin(oprep, avail); + if (repoPlugin) { + if (semver.gt(repoPlugin.version, avail.version)) { + // Plugin needs update + needsUpdates.push(avail); + } + } + } + + return needsUpdates; + } + + public async updateRepository(repo: IRepository): Promise { + const remote = await this.getRemote(repo.url + '/repository.json'); + if (remote.created <= repo.created) { + return repo; + } + + // Repository needs an update + logger.debug('Updating repository %s', repo.name); + const local = path.join(this.env.repositoryPath, remote.name + '.json'); + + // Set the URL to the previous value as it shouldn't change + remote.url = repo.url; + + await fs.writeJSON(local, remote); + this.repositories.set(remote.name, remote); + + return remote; + } + + public async loadFromFiles(): Promise { + const repos = await fs.readdir(this.env.repositoryPath); + let loaded = []; + for (const rf of repos) { + const file = path.join(this.env.repositoryPath, rf); + try { + const contents = await fs.readJSON(file); + if (!contents.name || !contents.url || !contents.plugins) { + throw new Error('Invalid repository file ' + rf); + } + loaded.push(contents.name); + this.repositories.set(contents.name, contents); + } catch (e) { + logger.warn('Could not load repository ' + rf); + } + } + logger.debug('Loaded repositories:', loaded.join(', ')); + } +} diff --git a/src/plugin/repository/repository.ts b/src/plugin/repository/repository.ts new file mode 100644 index 0000000..28de19f --- /dev/null +++ b/src/plugin/repository/repository.ts @@ -0,0 +1,14 @@ +import { IPluginManifest } from "../plugin"; + +export interface IRepoPluginDef { + name: string; + version: string; +} + +export interface IRepository { + name: string; + created: number; + url: string; + plugins: IRepoPluginDef[]; + schema: number; +} \ No newline at end of file diff --git a/src/types/config.ts b/src/types/config.ts index 981af63..0273a9b 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -3,7 +3,7 @@ import * as fs from 'fs-extra'; import { IEnvironment } from './environment'; export class Configuration { - private config: any = {}; + public config: any = {}; private loaded = false; constructor(private env: IEnvironment, private file: string, private defaults: any = {}) {} @@ -62,6 +62,42 @@ export class Configuration { return from[key]; } + public set(key: string, value?: any, from?: any): boolean { + if (!from) { + from = this.config; + } + + // Recursive object traversal + if (key.indexOf('.') !== -1) { + const split = key.split('.'); + const first = this.get(split[0], null, from); + if (first) { + return this.set(split.slice(1).join('.'), value, first); + } + return false; + } + + // Array indexing + if (key.indexOf('[') !== -1 && key.indexOf(']') !== -1) { + const match = key.match(/\[(\d+)\]/i); + const realKey = key.substr(0, key.indexOf('[')); + if (match != null) { + const index = parseInt(match[1], 10); + if (from[realKey]) { + from[realKey][index] = value; + } + } + return false; + } + + if (!from[key]) { + return false; + } + + from[key] = value; + return true; + } + public setDefaults(defconf: any): void { this.defaults = defconf; } diff --git a/src/types/message.ts b/src/types/message.ts index 5403eeb..98f499f 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -1,6 +1,8 @@ import { IPlugin } from '../plugin/plugin'; import { Protocol } from './protocol'; +// TODO: Source specification to support plugin services. + export interface IMessage { data: any; source: IPlugin | Protocol;