Repository management

This commit is contained in:
Evert Prants 2020-11-28 21:08:23 +02:00
parent 8809380c86
commit b4607446b4
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
13 changed files with 680 additions and 17 deletions

94
package-lock.json generated
View File

@ -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=="
}
}
}

View File

@ -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"
}
}

View File

@ -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;
}
}

140
src/common/http.ts Normal file
View File

@ -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<any> {
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<any> {
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();
});
}

3
src/common/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './http';
export * from './time';
export * from './sanitize';

10
src/common/sanitize.ts Normal file
View File

@ -0,0 +1,10 @@
export function sanitizeEscapedText(text: string): string {
return text.replace(/\n/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, '\'')
.trim();
}

116
src/common/time.ts Normal file
View File

@ -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;
}

View File

@ -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<IPlugin> {
// 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);

View File

@ -0,0 +1,2 @@
export * from './manager';
export * from './repository';

View File

@ -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<string, IRepository> = 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<IPluginManifest> {
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<void> {
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<IRepository> {
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<IRepository> {
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<void> {
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<IPluginManifest[]> {
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<IRepository> {
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<void> {
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(', '));
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;