Selaa lähdekoodia

:sparkles: commands

Signed-off-by: LI JIAHAO <lijiahao99131@gmail.com>
LI JIAHAO 6 vuotta sitten
vanhempi
commit
58eab286af
15 muutettua tiedostoa jossa 521 lisäystä ja 111 poistoa
  1. 7 2
      config.examle.json
  2. 73 19
      dist/command.js
  3. 22 0
      dist/helper.js
  4. 44 18
      dist/main.js
  5. 5 10
      dist/qq.js
  6. 29 0
      dist/twitter.js
  7. 5 2
      package.json
  8. 65 20
      src/command.ts
  9. 24 0
      src/helper.ts
  10. 45 19
      src/main.ts
  11. 12 0
      src/model.d.ts
  12. 12 15
      src/qq.ts
  13. 31 0
      src/twitter.ts
  14. 3 1
      tslint.json
  15. 144 5
      yarn.lock

+ 7 - 2
config.examle.json

@@ -1,5 +1,10 @@
 {
   "cq_ws_host": "127.0.0.1",
-  "cq_ws_port": "6700",
-  "cq_access_token": ""
+  "cq_ws_port": 6700,
+  "cq_access_token": "",
+  "twitter_consumer_key": "",
+  "twitter_consumer_secret": "",
+  "twitter_access_token_key": "",
+  "twitter_access_token_secret": "",
+  "lockfile": "subscriber.lock"
 }

+ 73 - 19
dist/command.js

@@ -1,23 +1,77 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
-function default_1(message) {
-    message = message.trim();
-    message = message.replace('\\\\', '\\0x5c');
-    message = message.replace('\\\"', '\\0x22');
-    message = message.replace('\\\'', '\\0x27');
-    let strs = message.match(/'[\s\S]*?'|"[\s\S]*?"|\S*\[CQ:[\s\S]*?\]\S*|\S+/mg);
-    const cmd = strs.length ? strs[0].length ? strs[0].substring(0, 1) === '/' ? strs[0].substring(1) : '' : '' : '';
-    const args = strs.slice(1).map(arg => {
-        arg = arg.replace(/^["']+|["']+$/g, '');
-        arg = arg.replace('\\0x27', '\\\'');
-        arg = arg.replace('\\0x22', '\\\"');
-        arg = arg.replace('\\0x5c', '\\\\');
-        return arg;
+function sub(chat, args, lock) {
+    if (args.length === 0) {
+        return '找不到要订阅的链接。';
+    }
+    const link = args[0];
+    let flag = false;
+    let match = link.match(/https:\/\/twitter.com\/([^\/]+)\/lists\/([^\/]+)/);
+    if (match)
+        flag = true;
+    else {
+        match = link.match(/https:\/\/twitter.com\/([^\/]+)/);
+        if (match)
+            flag = true;
+    }
+    if (!flag) {
+        return `订阅链接格式错误:
+示例:
+https://twitter.com/Saito_Shuka
+https://twitter.com/rikakomoe/lists/lovelive`;
+    }
+    flag = false;
+    lock.feed.forEach(fl => {
+        if (fl === link)
+            flag = true;
     });
-    return {
-        cmd,
-        args,
-    };
+    if (!flag)
+        lock.feed.push(link);
+    if (!lock.threads[link]) {
+        lock.threads[link] = {
+            offset: 0,
+            subscribers: [],
+        };
+    }
+    flag = false;
+    lock.threads[link].subscribers.forEach(c => {
+        if (c.chatID === chat.chatID && c.chatType === chat.chatType)
+            flag = true;
+    });
+    if (!flag)
+        lock.threads[link].subscribers.push(chat);
+    return `已为此聊天订阅 ${link}`;
+}
+exports.sub = sub;
+function unsub(chat, args, lock) {
+    if (args.length === 0) {
+        return '找不到要退订的链接。';
+    }
+    const link = args[0];
+    if (!lock.threads[link]) {
+        return '您没有订阅此链接。\n' + list(chat, args, lock);
+    }
+    let flag = false;
+    lock.threads[link].subscribers.forEach((c, index) => {
+        if (c.chatID === chat.chatID && c.chatType === chat.chatType) {
+            flag = true;
+            lock.threads[link].subscribers.splice(index, 1);
+        }
+    });
+    if (flag) {
+        return `已为此聊天退订 ${link}`;
+    }
+    return '您没有订阅此链接。\n' + list(chat, args, lock);
+}
+exports.unsub = unsub;
+function list(chat, args, lock) {
+    const links = [];
+    Object.keys(lock.threads).forEach(key => {
+        lock.threads[key].subscribers.forEach(c => {
+            if (c.chatID === chat.chatID && c.chatType === chat.chatType)
+                links.push(key);
+        });
+    });
+    return '此聊天中订阅的链接:\n' + links.join('\n');
 }
-exports.default = default_1;
-;
+exports.list = list;

+ 22 - 0
dist/helper.js

@@ -0,0 +1,22 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+function default_1(message) {
+    message = message.trim();
+    message = message.replace('\\\\', '\\0x5c');
+    message = message.replace('\\\"', '\\0x22');
+    message = message.replace('\\\'', '\\0x27');
+    const strs = message.match(/'[\s\S]*?'|"[\s\S]*?"|\S*\[CQ:[\s\S]*?\]\S*|\S+/mg);
+    const cmd = strs.length ? strs[0].length ? strs[0].substring(0, 1) === '/' ? strs[0].substring(1) : '' : '' : '';
+    const args = strs.slice(1).map(arg => {
+        arg = arg.replace(/^["']+|["']+$/g, '');
+        arg = arg.replace('\\0x27', '\\\'');
+        arg = arg.replace('\\0x22', '\\\"');
+        arg = arg.replace('\\0x5c', '\\\\');
+        return arg;
+    });
+    return {
+        cmd,
+        args,
+    };
+}
+exports.default = default_1;

+ 44 - 18
dist/main.js

@@ -2,30 +2,33 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
 const commandLineUsage = require("command-line-usage");
+const fs = require("fs");
 const log4js = require("log4js");
 const path = require("path");
+const command_1 = require("./command");
 const qq_1 = require("./qq");
+const twitter_1 = require("./twitter");
 const logger = log4js.getLogger();
 logger.level = 'info';
 const sections = [
     {
         header: 'CQHTTP Twitter Bot',
-        content: 'The QQ Bot that forwards twitters.'
+        content: 'The QQ Bot that forwards twitters.',
     },
     {
         header: 'Synopsis',
         content: [
             '$ cqhttp-twitter-bot {underline config.json}',
-            '$ cqhttp-twitter-bot {bold --help}'
-        ]
+            '$ cqhttp-twitter-bot {bold --help}',
+        ],
     },
     {
         header: 'Documentation',
         content: [
             'Project home: {underline https://github.com/rikakomoe/cqhttp-twitter-bot}',
-            'Example config: {underline https://qwqq.pw/b96yt}'
-        ]
-    }
+            'Example config: {underline https://qwqq.pw/b96yt}',
+        ],
+    },
 ];
 const usage = commandLineUsage(sections);
 const args = process.argv.slice(2);
@@ -39,7 +42,7 @@ try {
     config = require(path.resolve(configPath));
 }
 catch (e) {
-    console.log("Failed to parse config file: ", configPath);
+    console.log('Failed to parse config file: ', configPath);
     console.log(usage);
     process.exit(1);
 }
@@ -55,22 +58,45 @@ if (config.cq_access_token === undefined) {
     config.cq_access_token = '';
     logger.warn('cq_access_token is undefined, use empty string as default');
 }
-function handler(chat, args, bot) {
-    const config = {
-        message_type: chat.chatType,
-        user_id: chat.chatID,
-        group_id: chat.chatID,
-        discuss_id: chat.chatID,
-        message: JSON.stringify(args),
+if (config.lockfile === undefined) {
+    config.lockfile = 'subscriber.lock';
+}
+fs.access(path.resolve(config.lockfile), fs.constants.W_OK, err => {
+    if (err) {
+        logger.fatal(`cannot write lockfile ${path.resolve(config.lockfile)}, permission denied`);
+        process.exit(1);
+    }
+});
+let lock;
+if (fs.existsSync(path.resolve(config.lockfile))) {
+    try {
+        lock = require(path.resolve(config.lockfile));
+    }
+    catch (e) {
+        logger.error('Failed to parse lockfile: ', config.lockfile);
+        lock = {
+            workon: 0,
+            feed: [],
+            threads: {},
+        };
+    }
+}
+else {
+    lock = {
+        workon: 0,
+        feed: [],
+        threads: {},
     };
-    bot('send_msg', config);
 }
 const qq = new qq_1.default({
     access_token: config.cq_access_token,
     host: config.cq_ws_host,
     port: config.cq_ws_port,
-    list: handler,
-    sub: handler,
-    unsub: handler,
+    list: (c, a) => command_1.list(c, a, lock),
+    sub: (c, a) => command_1.sub(c, a, lock),
+    unsub: (c, a) => command_1.unsub(c, a, lock),
 });
+setTimeout(() => {
+    twitter_1.default(lock);
+}, 60000);
 qq.bot.connect();

+ 5 - 10
dist/qq.js

@@ -2,7 +2,7 @@
 Object.defineProperty(exports, "__esModule", { value: true });
 const CQWebsocket = require("cq-websocket");
 const log4js = require("log4js");
-const command_1 = require("./command");
+const helper_1 = require("./helper");
 const logger = log4js.getLogger('cq-websocket');
 logger.level = 'info';
 class default_1 {
@@ -57,22 +57,18 @@ class default_1 {
                     break;
                 case ChatType.Discuss:
                     chat.chatID = context.discuss_id;
-                    break;
             }
-            let cmdObj = command_1.default(context.raw_message);
+            const cmdObj = helper_1.default(context.raw_message);
             switch (cmdObj.cmd) {
                 case 'twitter_sub':
                 case 'twitter_subscribe':
-                    opt.sub(chat, cmdObj.args, this.bot);
-                    return;
+                    return opt.sub(chat, cmdObj.args);
                 case 'twitter_unsub':
                 case 'twitter_unsubscribe':
-                    opt.unsub(chat, cmdObj.args, this.bot);
-                    return;
+                    return opt.unsub(chat, cmdObj.args);
                 case 'ping':
                 case 'twitter':
-                    opt.list(chat, cmdObj.args, this.bot);
-                    return;
+                    return opt.list(chat, cmdObj.args);
                 case 'help':
                     return `推特搬运机器人:
 /twitter - 查询当前聊天中的订阅
@@ -83,4 +79,3 @@ class default_1 {
     }
 }
 exports.default = default_1;
-;

+ 29 - 0
dist/twitter.js

@@ -0,0 +1,29 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const log4js = require("log4js");
+const logger = log4js.getLogger('twitter');
+logger.level = 'info';
+function work(lock) {
+    if (lock.feed.length === 0) {
+        setTimeout(() => {
+            work(lock);
+        }, 60000);
+        return;
+    }
+    if (lock.workon >= lock.feed.length)
+        lock.workon = 0;
+    if (!lock.threads[lock.feed[lock.workon]] ||
+        !lock.threads[lock.feed[lock.workon]].subscribers ||
+        lock.threads[lock.feed[lock.workon]].subscribers.length === 0) {
+        logger.error(`nobody subscribes thread ${lock.feed[lock.workon]}, removing from feed`);
+        lock.feed.splice(lock.workon, 1);
+        work(lock);
+        return;
+    }
+    // TODO: Work on lock.feed[lock.workon]
+    lock.workon++;
+    setTimeout(() => {
+        work(lock);
+    }, 60000);
+}
+exports.default = work;

+ 5 - 2
package.json

@@ -3,7 +3,8 @@
     "cqhttp-twitter-bot": "./dist/main.js"
   },
   "scripts": {
-    "build": "tsc --outDir dist"
+    "build": "tsc --outDir dist",
+    "lint": "tslint --fix -c tslint.json --project ./"
   },
   "dependencies": {
     "command-line-usage": "^5.0.5",
@@ -14,6 +15,8 @@
     "webshot": "^0.18.0"
   },
   "devDependencies": {
-    "@types/node": "^10.5.1"
+    "@types/node": "^10.5.1",
+    "tslint": "^5.10.0",
+    "tslint-config-prettier": "^1.13.0"
   }
 }

+ 65 - 20
src/command.ts

@@ -1,24 +1,69 @@
-interface ICommand {
-  cmd: string;
-  args: string[];
+function sub(chat: IChat, args: string[], lock: ILock): string {
+  if (args.length === 0) {
+    return '找不到要订阅的链接。';
+  }
+  const link = args[0];
+  let flag = false;
+  let match = link.match(/https:\/\/twitter.com\/([^\/]+)\/lists\/([^\/]+)/);
+  if (match) flag = true;
+  else {
+    match = link.match(/https:\/\/twitter.com\/([^\/]+)/);
+    if (match) flag = true;
+  }
+  if (!flag) {
+    return `订阅链接格式错误:
+示例:
+https://twitter.com/Saito_Shuka
+https://twitter.com/rikakomoe/lists/lovelive`;
+  }
+  flag = false;
+  lock.feed.forEach(fl => {
+    if (fl === link) flag = true;
+  });
+  if (!flag) lock.feed.push(link);
+  if (!lock.threads[link]) {
+    lock.threads[link] = {
+      offset: 0,
+      subscribers: [],
+    };
+  }
+  flag = false;
+  lock.threads[link].subscribers.forEach(c => {
+    if (c.chatID === chat.chatID && c.chatType === chat.chatType) flag = true;
+  });
+  if (!flag) lock.threads[link].subscribers.push(chat);
+  return `已为此聊天订阅 ${link}`;
 }
 
-export default function (message: string): ICommand {
-  message = message.trim();
-  message = message.replace('\\\\', '\\0x5c');
-  message = message.replace('\\\"', '\\0x22');
-  message = message.replace('\\\'', '\\0x27');
-  let strs = message.match(/'[\s\S]*?'|"[\s\S]*?"|\S*\[CQ:[\s\S]*?\]\S*|\S+/mg);
-  const cmd = strs.length ? strs[0].length ? strs[0].substring(0, 1) === '/' ? strs[0].substring(1) : '' : '' : '';
-  const args = strs.slice(1).map(arg => {
-    arg = arg.replace(/^["']+|["']+$/g, '');
-    arg = arg.replace('\\0x27', '\\\'');
-    arg = arg.replace('\\0x22', '\\\"');
-    arg = arg.replace('\\0x5c', '\\\\');
-    return arg;
+function unsub(chat: IChat, args: string[], lock: ILock): string {
+  if (args.length === 0) {
+    return '找不到要退订的链接。';
+  }
+  const link = args[0];
+  if (!lock.threads[link]) {
+    return '您没有订阅此链接。\n' + list(chat, args, lock);
+  }
+  let flag = false;
+  lock.threads[link].subscribers.forEach((c, index) => {
+    if (c.chatID === chat.chatID && c.chatType === chat.chatType) {
+      flag = true;
+      lock.threads[link].subscribers.splice(index, 1);
+    }
   });
-  return {
-    cmd,
-    args,
+  if (flag) {
+    return `已为此聊天退订 ${link}`;
   }
-};
+  return '您没有订阅此链接。\n' + list(chat, args, lock);
+}
+
+function list(chat: IChat, args: string[], lock: ILock): string {
+  const links = [];
+  Object.keys(lock.threads).forEach(key => {
+    lock.threads[key].subscribers.forEach(c => {
+      if (c.chatID === chat.chatID && c.chatType === chat.chatType) links.push(key);
+    });
+  });
+  return '此聊天中订阅的链接:\n' + links.join('\n');
+}
+
+export { sub, list, unsub };

+ 24 - 0
src/helper.ts

@@ -0,0 +1,24 @@
+interface ICommand {
+  cmd: string;
+  args: string[];
+}
+
+export default function (message: string): ICommand {
+  message = message.trim();
+  message = message.replace('\\\\', '\\0x5c');
+  message = message.replace('\\\"', '\\0x22');
+  message = message.replace('\\\'', '\\0x27');
+  const strs = message.match(/'[\s\S]*?'|"[\s\S]*?"|\S*\[CQ:[\s\S]*?\]\S*|\S+/mg);
+  const cmd = strs.length ? strs[0].length ? strs[0].substring(0, 1) === '/' ? strs[0].substring(1) : '' : '' : '';
+  const args = strs.slice(1).map(arg => {
+    arg = arg.replace(/^["']+|["']+$/g, '');
+    arg = arg.replace('\\0x27', '\\\'');
+    arg = arg.replace('\\0x22', '\\\"');
+    arg = arg.replace('\\0x5c', '\\\\');
+    return arg;
+  });
+  return {
+    cmd,
+    args,
+  };
+}

+ 45 - 19
src/main.ts

@@ -1,10 +1,13 @@
 #!/usr/bin/env node
 
 import * as commandLineUsage from 'command-line-usage';
+import * as fs from 'fs';
 import * as log4js from 'log4js';
 import * as path from 'path';
+
+import { list, sub, unsub } from './command';
 import QQBot from './qq';
-import * as CQWebsocket from 'cq-websocket';
+import work from './twitter';
 
 const logger = log4js.getLogger();
 logger.level = 'info';
@@ -12,22 +15,22 @@ logger.level = 'info';
 const sections = [
   {
     header: 'CQHTTP Twitter Bot',
-    content: 'The QQ Bot that forwards twitters.'
+    content: 'The QQ Bot that forwards twitters.',
   },
   {
     header: 'Synopsis',
     content: [
       '$ cqhttp-twitter-bot {underline config.json}',
-      '$ cqhttp-twitter-bot {bold --help}'
-    ]
+      '$ cqhttp-twitter-bot {bold --help}',
+    ],
   },
   {
     header: 'Documentation',
     content: [
       'Project home: {underline https://github.com/rikakomoe/cqhttp-twitter-bot}',
-      'Example config: {underline https://qwqq.pw/b96yt}'
-    ]
-  }
+      'Example config: {underline https://qwqq.pw/b96yt}',
+    ],
+  },
 ];
 
 const usage = commandLineUsage(sections);
@@ -45,7 +48,7 @@ let config;
 try {
   config = require(path.resolve(configPath));
 } catch (e) {
-  console.log("Failed to parse config file: ", configPath);
+  console.log('Failed to parse config file: ', configPath);
   console.log(usage);
   process.exit(1);
 }
@@ -62,25 +65,48 @@ if (config.cq_access_token === undefined) {
   config.cq_access_token = '';
   logger.warn('cq_access_token is undefined, use empty string as default');
 }
+if (config.lockfile === undefined) {
+  config.lockfile = 'subscriber.lock';
+}
 
-function handler(chat: IChat, args: string[], bot: CQWebsocket) {
-  const config = {
-    message_type: chat.chatType,
-    user_id: chat.chatID,
-    group_id: chat.chatID,
-    discuss_id: chat.chatID,
-    message: JSON.stringify(args),
+fs.access(path.resolve(config.lockfile), fs.constants.W_OK, err => {
+  if (err) {
+    logger.fatal(`cannot write lockfile ${path.resolve(config.lockfile)}, permission denied`);
+    process.exit(1);
+  }
+});
+
+let lock: ILock;
+if (fs.existsSync(path.resolve(config.lockfile))) {
+  try {
+    lock = require(path.resolve(config.lockfile));
+  } catch (e) {
+    logger.error('Failed to parse lockfile: ', config.lockfile);
+    lock = {
+      workon: 0,
+      feed: [],
+      threads: {},
+    };
+  }
+} else {
+  lock = {
+    workon: 0,
+    feed: [],
+    threads: {},
   };
-  bot('send_msg', config)
 }
 
 const qq = new QQBot({
   access_token: config.cq_access_token,
   host: config.cq_ws_host,
   port: config.cq_ws_port,
-  list: handler,
-  sub: handler,
-  unsub: handler,
+  list: (c, a) => list(c, a, lock),
+  sub: (c, a) => sub(c, a, lock),
+  unsub: (c, a) => unsub(c, a, lock),
 });
 
+setTimeout(() => {
+  work(lock);
+}, 60000);
+
 qq.bot.connect();

+ 12 - 0
src/model.d.ts

@@ -7,4 +7,16 @@ declare enum ChatType {
 interface IChat {
   chatID: number,
   chatType: ChatType,
+}
+
+interface ILock {
+  workon: number,
+  feed: string[],
+  threads: {
+    [key: string]:
+      {
+        offset: number,
+        subscribers: IChat[],
+      }
+  }
 }

+ 12 - 15
src/qq.ts

@@ -1,6 +1,7 @@
 import * as CQWebsocket from 'cq-websocket';
 import * as log4js from 'log4js';
-import command from './command';
+
+import command from './helper';
 
 const logger = log4js.getLogger('cq-websocket');
 logger.level = 'info';
@@ -9,9 +10,9 @@ interface IQQProps {
   access_token: string;
   host: string;
   port: number;
-  list: (chat: IChat, args: string[], bot: CQWebsocket) => void;
-  sub: (chat: IChat, args: string[], bot: CQWebsocket) => void;
-  unsub: (chat: IChat, args: string[], bot: CQWebsocket) => void;
+  list(chat: IChat, args: string[]): string;
+  sub(chat: IChat, args: string[]): string;
+  unsub(chat: IChat, args: string[]): string;
 }
 
 export default class {
@@ -21,7 +22,7 @@ export default class {
   private connect = () => {
     logger.warn('connecting to websocket...');
     this.bot.connect();
-  };
+  }
 
   private reconnect = () => {
     this.retryInterval *= 2;
@@ -31,7 +32,7 @@ export default class {
       logger.warn('reconnecting to websocket...');
       this.connect();
     }, this.retryInterval);
-  };
+  }
 
   constructor(opt: IQQProps) {
     logger.info(`init cqwebsocket for ${opt.host}:${opt.port}, with access_token ${opt.access_token}`);
@@ -74,22 +75,18 @@ export default class {
           break;
         case ChatType.Discuss:
           chat.chatID = context.discuss_id;
-          break;
       }
-      let cmdObj = command(context.raw_message);
+      const cmdObj = command(context.raw_message);
       switch (cmdObj.cmd) {
         case 'twitter_sub':
         case 'twitter_subscribe':
-          opt.sub(chat, cmdObj.args, this.bot);
-          return;
+          return opt.sub(chat, cmdObj.args);
         case 'twitter_unsub':
         case 'twitter_unsubscribe':
-          opt.unsub(chat, cmdObj.args, this.bot);
-          return;
+          return opt.unsub(chat, cmdObj.args);
         case 'ping':
         case 'twitter':
-          opt.list(chat, cmdObj.args, this.bot);
-          return;
+          return opt.list(chat, cmdObj.args);
         case 'help':
           return `推特搬运机器人:
 /twitter - 查询当前聊天中的订阅
@@ -99,4 +96,4 @@ export default class {
     });
 
   }
-};
+}

+ 31 - 0
src/twitter.ts

@@ -0,0 +1,31 @@
+import * as log4js from 'log4js';
+
+const logger = log4js.getLogger('twitter');
+logger.level = 'info';
+
+function work(lock: ILock) {
+  if (lock.feed.length === 0) {
+    setTimeout(() => {
+      work(lock);
+    }, 60000);
+    return;
+  }
+  if (lock.workon >= lock.feed.length) lock.workon = 0;
+  if (!lock.threads[lock.feed[lock.workon]] ||
+    !lock.threads[lock.feed[lock.workon]].subscribers ||
+    lock.threads[lock.feed[lock.workon]].subscribers.length === 0) {
+    logger.error(`nobody subscribes thread ${lock.feed[lock.workon]}, removing from feed`);
+    lock.feed.splice(lock.workon, 1);
+    work(lock);
+    return;
+  }
+
+  // TODO: Work on lock.feed[lock.workon]
+
+  lock.workon++;
+  setTimeout(() => {
+    work(lock);
+  }, 60000);
+}
+
+export default work;

+ 3 - 1
tslint.json

@@ -2,7 +2,8 @@
   "extends": ["tslint:recommended", "tslint-config-prettier"],
   "linterOptions": {
     "exclude": [
-      "node_modules/**/*.ts"
+      "node_modules/**/*.ts",
+      "dist/*.js"
     ]
   },
   "rules": {
@@ -22,6 +23,7 @@
     "linebreak-style": [true, "LF"],
     "comment-format": [true, "check-lowercase", {"ignore-words": ["TODO", "HACK", "BUG"]}],
     "no-require-imports": true,
+    "no-var-requires": false,
     "prefer-const": true,
     "prefer-method-signature": true,
     "trailing-comma": [

+ 144 - 5
yarn.lock

@@ -49,6 +49,12 @@ ansi-styles@^3.2.1:
   dependencies:
     color-convert "^1.9.0"
 
+argparse@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+  dependencies:
+    sprintf-js "~1.0.2"
+
 array-back@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-2.0.0.tgz#6877471d51ecc9c9bfa6136fb6c7d5fe69748022"
@@ -99,6 +105,18 @@ axios@^0.15.3:
   dependencies:
     follow-redirects "1.0.0"
 
+babel-code-frame@^6.22.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+  dependencies:
+    chalk "^1.1.3"
+    esutils "^2.0.2"
+    js-tokens "^3.0.2"
+
+balanced-match@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
 bcrypt-pbkdf@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
@@ -127,6 +145,13 @@ boom@2.x.x:
   dependencies:
     hoek "2.x.x"
 
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
 buffer-from@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04"
@@ -147,6 +172,10 @@ buildmail@4.0.1:
     nodemailer-shared "1.1.0"
     punycode "1.4.1"
 
+builtin-modules@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
 bytes@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@@ -159,7 +188,7 @@ caseless@~0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
 
-chalk@^1.1.1:
+chalk@^1.1.1, chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   dependencies:
@@ -169,7 +198,7 @@ chalk@^1.1.1:
     strip-ansi "^3.0.0"
     supports-color "^2.0.0"
 
-chalk@^2.4.1:
+chalk@^2.3.0, chalk@^2.4.1:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
   dependencies:
@@ -210,10 +239,14 @@ command-line-usage@^5.0.5:
     table-layout "^0.4.3"
     typical "^2.6.1"
 
-commander@^2.9.0:
+commander@^2.12.1, commander@^2.9.0:
   version "2.16.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50"
 
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
 concat-stream@1.6.2:
   version "1.6.2"
   resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
@@ -300,6 +333,10 @@ depd@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
 
+diff@^3.2.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
+
 double-ended-queue@^2.1.0-0:
   version "2.1.0-0"
   resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
@@ -339,6 +376,10 @@ esprima@3.x.x, esprima@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
 
+esprima@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+
 estraverse@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
@@ -424,6 +465,10 @@ fs-extra@^1.0.0:
     jsonfile "^2.1.0"
     klaw "^1.0.0"
 
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
 ftp@~0.3.10:
   version "0.3.10"
   resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d"
@@ -458,6 +503,17 @@ getpass@^0.1.1:
   dependencies:
     assert-plus "^1.0.0"
 
+glob@^7.1.1:
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
 graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
   version "4.1.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
@@ -593,7 +649,14 @@ inflection@~1.3.0:
   version "1.3.8"
   resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.3.8.tgz#cbd160da9f75b14c3cc63578d4f396784bf3014e"
 
-inherits@2.0.3, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@2.0.3, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
 
@@ -643,6 +706,17 @@ isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
 
+js-tokens@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@^3.7.0:
+  version "3.12.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^4.0.0"
+
 jsbn@~0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@@ -792,6 +866,12 @@ mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7:
   dependencies:
     mime-db "~1.33.0"
 
+minimatch@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  dependencies:
+    brace-expansion "^1.1.7"
+
 minimist@0.0.8:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@@ -875,6 +955,12 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
 
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  dependencies:
+    wrappy "1"
+
 optionator@^0.8.1:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
@@ -913,6 +999,14 @@ pac-resolver@^3.0.0:
     netmask "^1.0.6"
     thunkify "^2.1.2"
 
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-parse@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
 path-proxy@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/path-proxy/-/path-proxy-1.0.0.tgz#18e8a36859fc9d2f1a53b48dee138543c020de5e"
@@ -1133,6 +1227,12 @@ requestretry@^1.2.2:
     request "^2.74.0"
     when "^3.7.7"
 
+resolve@^1.3.2:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
+  dependencies:
+    path-parse "^1.0.5"
+
 safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -1141,7 +1241,7 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
 
-semver@^5.5.0:
+semver@^5.3.0, semver@^5.5.0:
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
 
@@ -1197,6 +1297,10 @@ source-map@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
 
+sprintf-js@~1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
 sshpk@^1.7.0:
   version "1.14.2"
   resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98"
@@ -1289,10 +1393,41 @@ tough-cookie@~2.3.0, tough-cookie@~2.3.3:
   dependencies:
     punycode "^1.4.1"
 
+tslib@^1.8.0, tslib@^1.8.1:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
+
+tslint-config-prettier@^1.13.0:
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.13.0.tgz#189e821931ad89e0364e4e292d5c44a14e90ecd6"
+
+tslint@^5.10.0:
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.10.0.tgz#11e26bccb88afa02dd0d9956cae3d4540b5f54c3"
+  dependencies:
+    babel-code-frame "^6.22.0"
+    builtin-modules "^1.1.1"
+    chalk "^2.3.0"
+    commander "^2.12.1"
+    diff "^3.2.0"
+    glob "^7.1.1"
+    js-yaml "^3.7.0"
+    minimatch "^3.0.4"
+    resolve "^1.3.2"
+    semver "^5.3.0"
+    tslib "^1.8.0"
+    tsutils "^2.12.1"
+
 tsscmp@~1.0.0:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.5.tgz#7dc4a33af71581ab4337da91d85ca5427ebd9a97"
 
+tsutils@^2.12.1:
+  version "2.27.1"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.1.tgz#ab0276ac23664f36ce8fd4414daec4aebf4373ee"
+  dependencies:
+    tslib "^1.8.1"
+
 tunnel-agent@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -1406,6 +1541,10 @@ wordwrapjs@^3.0.0:
     reduce-flatten "^1.0.1"
     typical "^2.6.1"
 
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
 xregexp@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"