Repository management
This commit is contained in:
parent
8809380c86
commit
b4607446b4
94
package-lock.json
generated
94
package-lock.json
generated
@ -45,12 +45,36 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"@types/node": {
|
||||||
"version": "14.14.9",
|
"version": "14.14.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz",
|
||||||
"integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw==",
|
"integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw==",
|
||||||
"dev": true
|
"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": {
|
"ansi-styles": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||||
@ -107,6 +131,11 @@
|
|||||||
"supports-color": "^5.3.0"
|
"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": {
|
"color-convert": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
@ -168,6 +197,14 @@
|
|||||||
"universalify": "^1.0.0"
|
"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": {
|
"fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@ -286,6 +323,23 @@
|
|||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||||
"dev": true
|
"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": {
|
"mkdirp": {
|
||||||
"version": "0.5.5",
|
"version": "0.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||||
@ -327,10 +381,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.7.1",
|
"version": "7.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"sprintf-js": {
|
"sprintf-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
@ -347,6 +400,26 @@
|
|||||||
"has-flag": "^3.0.0"
|
"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": {
|
"tslib": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
@ -372,6 +445,14 @@
|
|||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
"tslib": "^1.13.0",
|
"tslib": "^1.13.0",
|
||||||
"tsutils": "^2.29.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": {
|
"tsutils": {
|
||||||
@ -399,6 +480,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
},
|
||||||
|
"yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,15 @@
|
|||||||
"@types/dateformat": "^3.0.1",
|
"@types/dateformat": "^3.0.1",
|
||||||
"@types/fs-extra": "^9.0.4",
|
"@types/fs-extra": "^9.0.4",
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.14.9",
|
||||||
|
"@types/tar": "^4.0.4",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"typescript": "^4.0.5"
|
"typescript": "^4.0.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/semver": "^7.3.4",
|
||||||
"dateformat": "^4.0.0",
|
"dateformat": "^4.0.0",
|
||||||
"fs-extra": "^9.0.1"
|
"fs-extra": "^9.0.1",
|
||||||
|
"semver": "^7.3.2",
|
||||||
|
"tar": "^6.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ export interface IChannel {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Source specification to support plugin services.
|
||||||
|
|
||||||
export class ChannelManager {
|
export class ChannelManager {
|
||||||
private channels: IChannel[] = [];
|
private channels: IChannel[] = [];
|
||||||
|
|
||||||
@ -107,4 +109,8 @@ export class ChannelManager {
|
|||||||
}
|
}
|
||||||
this.channels.splice(this.channels.indexOf(chan), 1);
|
this.channels.splice(this.channels.indexOf(chan), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAll(): any[] {
|
||||||
|
return this.channels;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
140
src/common/http.ts
Normal file
140
src/common/http.ts
Normal 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
3
src/common/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './http';
|
||||||
|
export * from './time';
|
||||||
|
export * from './sanitize';
|
10
src/common/sanitize.ts
Normal file
10
src/common/sanitize.ts
Normal file
@ -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();
|
||||||
|
}
|
116
src/common/time.ts
Normal file
116
src/common/time.ts
Normal 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;
|
||||||
|
}
|
@ -25,7 +25,7 @@ export class PluginManager {
|
|||||||
private configs: PluginConfigurator = new PluginConfigurator(this.environment);
|
private configs: PluginConfigurator = new PluginConfigurator(this.environment);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private availablePlugins: IPluginManifest[],
|
public availablePlugins: IPluginManifest[],
|
||||||
private stream: ScopedEventEmitter,
|
private stream: ScopedEventEmitter,
|
||||||
private environment: IEnvironment,
|
private environment: IEnvironment,
|
||||||
private npm: NPMExecutor) {
|
private npm: NPMExecutor) {
|
||||||
@ -48,6 +48,14 @@ export class PluginManager {
|
|||||||
return null;
|
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 {
|
public addAvailable(manifest: IPluginManifest | IPluginManifest[]): boolean {
|
||||||
// Automatically add arrays of manifests
|
// Automatically add arrays of manifests
|
||||||
if (Array.isArray(manifest)) {
|
if (Array.isArray(manifest)) {
|
||||||
@ -103,16 +111,6 @@ export class PluginManager {
|
|||||||
return returnValue;
|
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> {
|
public async load(plugin: IPluginManifest): Promise<IPlugin> {
|
||||||
// Don't load plugins twice
|
// Don't load plugins twice
|
||||||
const ready = this.getLoadedByName(plugin.name);
|
const ready = this.getLoadedByName(plugin.name);
|
||||||
@ -163,6 +161,11 @@ export class PluginManager {
|
|||||||
// Load the configuration
|
// Load the configuration
|
||||||
const config: PluginConfiguration = await this.configs.loadConfig(plugin);
|
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
|
// Load the module
|
||||||
logger.debug('Loading plugin %s module', plugin.name);
|
logger.debug('Loading plugin %s module', plugin.name);
|
||||||
const PluginModule = requireNoCache(path.resolve(plugin.fullPath, plugin.main)) as any;
|
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
|
// Delete plugin from the list of loaded plugins
|
||||||
this.plugins.delete(mf);
|
this.plugins.delete(mf);
|
||||||
|
|
||||||
|
2
src/plugin/repository/index.ts
Normal file
2
src/plugin/repository/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './manager';
|
||||||
|
export * from './repository';
|
239
src/plugin/repository/manager.ts
Normal file
239
src/plugin/repository/manager.ts
Normal 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(', '));
|
||||||
|
}
|
||||||
|
}
|
14
src/plugin/repository/repository.ts
Normal file
14
src/plugin/repository/repository.ts
Normal 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;
|
||||||
|
}
|
@ -3,7 +3,7 @@ import * as fs from 'fs-extra';
|
|||||||
import { IEnvironment } from './environment';
|
import { IEnvironment } from './environment';
|
||||||
|
|
||||||
export class Configuration {
|
export class Configuration {
|
||||||
private config: any = {};
|
public config: any = {};
|
||||||
private loaded = false;
|
private loaded = false;
|
||||||
|
|
||||||
constructor(private env: IEnvironment, private file: string, private defaults: any = {}) {}
|
constructor(private env: IEnvironment, private file: string, private defaults: any = {}) {}
|
||||||
@ -62,6 +62,42 @@ export class Configuration {
|
|||||||
return from[key];
|
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 {
|
public setDefaults(defconf: any): void {
|
||||||
this.defaults = defconf;
|
this.defaults = defconf;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { IPlugin } from '../plugin/plugin';
|
import { IPlugin } from '../plugin/plugin';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
|
||||||
|
// TODO: Source specification to support plugin services.
|
||||||
|
|
||||||
export interface IMessage {
|
export interface IMessage {
|
||||||
data: any;
|
data: any;
|
||||||
source: IPlugin | Protocol;
|
source: IPlugin | Protocol;
|
||||||
|
Loading…
Reference in New Issue
Block a user