From c59eec9c5007f453389ee6dfaf255fa3f34bb24a Mon Sep 17 00:00:00 2001 From: Sagi Dayan Date: Sun, 22 Mar 2020 18:09:33 -0400 Subject: [PATCH] Changed from js to ts - Added implementation for profiles - Can also be used as a node_module --- .npmignore | 1 + README.md | 36 ++++-- bin/telme | 2 - flows/init.js | 86 ------------- flows/simple_message.js | 11 -- flows/task_message.js | 56 --------- index.js | 73 ----------- lib/config/config.0.0.4.ts | 22 ++++ lib/config/config.ts | 112 +++++++++++++++++ lib/errors/base_error.ts | 10 ++ lib/errors/config_file_format.error.ts | 7 ++ lib/errors/config_file_missing.error.ts | 7 ++ lib/errors/config_profile_missing.error.ts | 7 ++ lib/errors/error_codes.ts | 8 ++ lib/errors/invalid_arguments.error.ts | 7 ++ lib/errors/invalid_command.ts | 7 ++ lib/flows/init.ts | 105 ++++++++++++++++ lib/flows/send_message.ts | 9 ++ lib/flows/task_message.ts | 51 ++++++++ lib/telme.ts | 20 +++ lib/telme_cli.ts | 76 +++++++++++ lib/utils/index.ts | 139 +++++++++++++++++++++ package-lock.json | 10 +- package.json | 12 +- tsconfig.json | 22 ++++ tslint.json | 22 ++++ yarn.lock | 5 + 27 files changed, 681 insertions(+), 242 deletions(-) delete mode 100755 bin/telme delete mode 100644 flows/init.js delete mode 100644 flows/simple_message.js delete mode 100644 flows/task_message.js delete mode 100644 index.js create mode 100644 lib/config/config.0.0.4.ts create mode 100644 lib/config/config.ts create mode 100644 lib/errors/base_error.ts create mode 100644 lib/errors/config_file_format.error.ts create mode 100644 lib/errors/config_file_missing.error.ts create mode 100644 lib/errors/config_profile_missing.error.ts create mode 100644 lib/errors/error_codes.ts create mode 100644 lib/errors/invalid_arguments.error.ts create mode 100644 lib/errors/invalid_command.ts create mode 100644 lib/flows/init.ts create mode 100644 lib/flows/send_message.ts create mode 100644 lib/flows/task_message.ts create mode 100644 lib/telme.ts create mode 100644 lib/telme_cli.ts create mode 100644 lib/utils/index.ts create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.npmignore b/.npmignore index a82e93e..ffe1669 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,3 @@ .npmrc +lib/ node_modules/ \ No newline at end of file diff --git a/README.md b/README.md index 4757aef..b735357 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 1. Get a bot token ( [BotFather](https://telegram.me/botfather) ) 2. `$ npm install -g node-telme` 3. `$ telme --init` -4. Do something :) +4. Need help? `$ telme --help` ## How do i get a bot token? - Get a telegram bot token from the botFather @@ -13,25 +13,43 @@ ## Configure `telme` -Simply run `$ telme --init` and follow the easy steps. You will need the bot token at this stage. +Simply run `$ telme --init` and follow 2 easy steps. You will need the bot token at this stage. This will help you generate a `.telmeconfig` file in your home directory. You can always run `--init` again to override values or just edit the file yourself. +## Profiles + +You can set multiple profiles, that will target different bots and/or different chats. +> You can use the same bot in all profiles if you like, But the target chat can be different + +To initialize a new profile: +```shell +$ telme --init --profile +``` + ## Examples: ###### Simple message -``` +```shell $ telme --m "Message to send" ``` + +In the next example a message will be sent every time the user `user` logs in to a tty. +> Added the next lines at the bottom of `~/.profile` file +```shell +# Telme on login +telme --m "A new Login:\n\`\`\` user: $(whoami) | hostname: $(hostname) | remote ip $(who | cut -d'(' -f2 | cut -d')' -f1)\`\`\` Hope this is you!" +``` + ###### Task message Task messages are a simple way to receive a message from your bot once a command has been finished. It will also let you know if you had any errors. -``` +```shell $ telme docker build . -t my-image/build/that/takes/for/ever ``` In this example, once the docker build has finished you will receive a message. -``` -$ telme npm run test -``` +As mentioned before - you can also specify a profile. +```shell +$ telme -p movie-club curl https://example.com/large/file/download.mp4 ``` -$ telme do-a-long-task -``` \ No newline at end of file +this will send the message to the `movie-club` profile chat. (By the `movie-club` bot) + diff --git a/bin/telme b/bin/telme deleted file mode 100755 index 0b76c4b..0000000 --- a/bin/telme +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -require('../index'); \ No newline at end of file diff --git a/flows/init.js b/flows/init.js deleted file mode 100644 index b6a0e01..0000000 --- a/flows/init.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; -const TelegramBot = require('node-telegram-bot-api'); -const HOME = require('os').homedir(); -const CONFIG_FILE_NAME = '.telmeconfig'; -const FILEPATH = `${HOME}/${CONFIG_FILE_NAME}`; -const APP_NAME = 'telme'; -const fs = require('fs'); - -class Init { - static async init() { - console.log( - 'Hi, In order for telme to work you will need to create a bot. If you don\'t know how, Please take a look at README.md'); - const token = await prompt('Please provide your bot token: '); - const code = generateCode(); - try { - const bot = new TelegramBot(token, {polling: true}); - const userId = await listenToMessage(bot, code); - console.log('Cool, Got your userId. Saving config...'); - const _default = { - TOKEN: token, - USER_ID: userId, - DONE_MESSAGE: - '*Task:*\n\n```sh\n%cmd%\n```\nHas finished.\n*Errors*:\n %errors%' - }; - await bot.sendMessage( - userId, `*Thanks!*\nYou are all set.\ntelme usage:\n\`\`\`\n -$ ${APP_NAME} --m "message to send" -$ ${APP_NAME} - \n\`\`\`\nFor more info, visit: [telme repo](https://git.sagidayan.com/sagi/telme)\n\n_Enjoy!_`, - {parse_mode: 'Markdown'}); - fs.writeFileSync(FILEPATH, JSON.stringify(_default, null, 2)); - - console.log(`created config file at ${FILEPATH}`); - return true; - } catch (e) { - throw e; - } - } -} - -function listenToMessage(bot, code) { - console.log(`Thanks! Please send '/code ${code}' to your bot from telegram`); - return new Promise((resolve, reject) => { - const now = Date.now(); - bot.on('message', msg => { - const msgDate = new Date(msg.date * 1000); - if (msgDate < now) return; - const userId = msg.chat.id; - if (msg.text.indexOf('/code') === 0) { - const receivedCode = msg.text.split('/code')[1].trim(); - if (code === receivedCode) - resolve(userId); - else - reject(new Error('Code does not match!')); - } - }); - bot.on('polling_error', () => { - console.log('polling error'); - reject(new Error('Invalid token')); - }) - }); -} - -function generateCode(codeLength = 6) { - const allowedChars = 'aAbBcCdDEeFf0123456789'; - let code = ''; - for (let i = 0; i < codeLength; i++) { - code += - allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)); - } - return code; -} - -function prompt(question) { - return new Promise(resolve => { - var stdin = process.stdin, stdout = process.stdout; - - stdin.resume(); - stdout.write(question); - - stdin.once('data', function(data) { - resolve(data.toString().trim()); - }); - }); -} -module.exports = Init \ No newline at end of file diff --git a/flows/simple_message.js b/flows/simple_message.js deleted file mode 100644 index cd691cc..0000000 --- a/flows/simple_message.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -const TelegramBot = require('node-telegram-bot-api'); -class SendMessage { - static async send(token, userId, msg) { - const bot = new TelegramBot(token); - return await bot.sendMessage(userId, `${msg}`, {parse_mode: 'Markdown'}); - } -} - - -module.exports = SendMessage; \ No newline at end of file diff --git a/flows/task_message.js b/flows/task_message.js deleted file mode 100644 index 224992d..0000000 --- a/flows/task_message.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -const {spawn} = require('child_process'); -const TelegramBot = require('node-telegram-bot-api'); -const APP_NAME = 'telme'; -class TaskMessage { - static async run(token, userId, doneMessage) { - const bot = new TelegramBot(token); - - const command = process.argv[2]; - const args = process.argv.slice(3); - - const exec = spawn(command, args); - const errors = await promisifyExec(exec); - let msg = doneMessage.replace('%cmd%', ` $ ${command} ${args.join(' ')}`) - .replace('%errors%', errors); - try { - await bot.sendMessage(userId, msg, {parse_mode: 'Markdown'}); - console.log(`[${APP_NAME}]: Told ya!`); - } catch (e) { - errors = e.message; - console.error(`[${APP_NAME}]: An error occurred. Error: ${e.message}`); - } - return true; - } -} - -function promisifyExec(exec) { - let errors = null; - return new Promise((resolve) => { - exec.stdout.on('data', (data) => { - console.log(String(data)); - }); - - exec.on('error', (error) => { - errors = error.message; - resolve(errors); - }); - - exec.stderr.on('data', (data) => { - console.error(String(data)); - if (!errors) - errors = data; - else - errors += `\n${data}`; - }); - - exec.on('close', async (code) => { - resolve(errors); - }); - }); -} - - - -module.exports = TaskMessage; \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index f1b9f02..0000000 --- a/index.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; -process.env.NTBA_FIX_319 = 'junk'; -const TelegramBot = require('node-telegram-bot-api'); -const HOME = require('os').homedir(); -const CONFIG_FILE_NAME = '.telmeconfig'; -const FILEPATH = `${HOME}/${CONFIG_FILE_NAME}`; -const APP_NAME = 'telme'; -const fs = require('fs'); -const Init = require('./flows/init'); -const SendMessage = require('./flows/simple_message'); -const TaskMessage = require('./flows/task_message'); - - -function printUsage(withInit = false) { - if (withInit) console.log(`[Usage] $ ${APP_NAME} --init`); - console.log(`[Usage] $ ${APP_NAME} --m "message to send"`); - console.log(`[Usage] $ ${APP_NAME} `); -} - -async function main() { - if (process.argv.indexOf('--init') != -1) { - try { - await Init.init(); - printUsage(); - process.exit(0); - } catch (e) { - console.error(`An error has occurred. Error: ${e.message}`); - process.exit(1); - } - } - - try { - var file = fs.readFileSync(FILEPATH); - } catch (e) { - console.log( - `Please run '${APP_NAME} --init' first. then follow instructions`); - process.exit(1); - } - - const {TOKEN, USER_ID, DONE_MESSAGE} = JSON.parse(file); - - - if (process.argv.indexOf('--m') === 2) { - if (!process.argv[3]) { - console.log('[ERROR] Missing message to send'); - console.log(`[Usage] $ ${APP_NAME} --m "message to send"`); - process.exit(1); - } - const msg = process.argv[3]; - try { - await SendMessage.send(TOKEN, USER_ID, msg); - console.log(`[${APP_NAME}]: Told ya!`); - process.exit(0); - } catch (e) { - console.error(`[${APP_NAME}]: An error occurred. Error: ${e.message}`); - process.exit(1); - } - - } else { - if (process.argv.length < 3) { - printUsage(true); - process.exit(1); - } - try { - await TaskMessage.run(TOKEN, USER_ID, DONE_MESSAGE); - } catch (e) { - console.error(e); - } - } -} - - -main().then(() => {}); diff --git a/lib/config/config.0.0.4.ts b/lib/config/config.0.0.4.ts new file mode 100644 index 0000000..5e583d8 --- /dev/null +++ b/lib/config/config.0.0.4.ts @@ -0,0 +1,22 @@ +import { IConfig, EConfigVersions } from './config'; + +export default class Config004 { + static parse(conf: IConfig004): IConfig { + return { + version: EConfigVersions.V005, + profiles: { + default: { + chat_id: conf.USER_ID, + bot_token: conf.TOKEN, + task_message_template: conf.DONE_MESSAGE + } + } + } + } +} + +export interface IConfig004 { + USER_ID: string; + TOKEN: string; + DONE_MESSAGE: string; +} \ No newline at end of file diff --git a/lib/config/config.ts b/lib/config/config.ts new file mode 100644 index 0000000..2599f34 --- /dev/null +++ b/lib/config/config.ts @@ -0,0 +1,112 @@ + +import * as fs from 'fs'; +import ConfigFileMissingError from '../errors/config_file_missing.error'; +import ConfigProfileError from '../errors/config_profile_missing.error'; +import ConfigFileFormatError from '../errors/config_file_format.error'; +import Config004 from './config.0.0.4'; + +const HOME = require('os').homedir(); +const CONFIG_FILE_NAME = '.telmeconfig'; +const FILEPATH = `${HOME}/${CONFIG_FILE_NAME}`; + +export enum EConfigVersions { + V004 = '0.0.4', + V005 = '0.0.5' +} + +const CURRENT_CONFIG_VERSION = EConfigVersions.V005; + +export default class Config { + static APP_NAME = 'telme'; + static CURRENT_CONFIG_VERSION = CURRENT_CONFIG_VERSION; + static DEFAULT_TASK_MESSAGE_TEMPLATE = '*Task:*\n\n```sh\n%cmd%\n```\nHas finished.\n*Errors*:\n %errors%'; + private config: IConfig; + constructor() { + const file = this.readConfigFile(); + const parsed = this.parseConfig(file); + if (parsed.originalConfigVersion != CURRENT_CONFIG_VERSION) { + this.writeConfigToFile(parsed.config); + } + this.config = parsed.config; + } + static getConfig(profile: string = 'default'): IProfileConfig { + return singleton.getConfig(profile); + } + + static generateProfileTemplate(): IProfileConfig { + return { + chat_id: null, + task_message_template: Config.DEFAULT_TASK_MESSAGE_TEMPLATE, + bot_token: null + } + } + + static getFullConfig(): IConfig { + return singleton.config; + } + + static async writeConfigToFile(config: IConfig) { + return singleton.writeConfigToFile(config); + } + + private getConfig(profile: string): IProfileConfig { + if (!this.config.profiles[profile]) { + throw new ConfigProfileError(`No profile named ${profile} found.`); + } + return this.config.profiles[profile]; + } + + private readConfigFile(): Buffer { + try { + const file = fs.readFileSync(FILEPATH); + return file; + } catch (e) { + throw new ConfigFileMissingError(''); + } + } + + private parseConfig(file: Buffer): { config: IConfig, originalConfigVersion: EConfigVersions } { + try { + const config = JSON.parse(file.toString()); + const confVersion: EConfigVersions = config.version || EConfigVersions.V004; + switch (confVersion) { + case EConfigVersions.V004: + return { config: Config004.parse(config), originalConfigVersion: EConfigVersions.V004 }; + // Using switch to easily add more config version. If needed... + default: + return { config, originalConfigVersion: config.version }; + } + } catch (e) { + throw new ConfigFileFormatError(''); + } + } + + private writeConfigToFile(config: IConfig): boolean { + try { + fs.writeFileSync(FILEPATH, JSON.stringify(config, null, 2)); + console.log(`created config file at ${FILEPATH}`); + return true; + } catch (e) { + return false; + } + } +} + + + +const singleton = new Config(); + + + +export interface IProfileConfig { + chat_id: string; + bot_token: string; + task_message_template: string; +} + +export interface IConfig { + version: EConfigVersions, + profiles: { + [key: string]: IProfileConfig; + } +} \ No newline at end of file diff --git a/lib/errors/base_error.ts b/lib/errors/base_error.ts new file mode 100644 index 0000000..f153c45 --- /dev/null +++ b/lib/errors/base_error.ts @@ -0,0 +1,10 @@ +import ErrorCodes from './error_codes'; + +export default class BaseError extends Error { + readonly exitCode: ErrorCodes; + static readonly ErrorCodes = ErrorCodes; + constructor(message: string, code: ErrorCodes) { + super(message); + this.exitCode = code; + } +} \ No newline at end of file diff --git a/lib/errors/config_file_format.error.ts b/lib/errors/config_file_format.error.ts new file mode 100644 index 0000000..02252a0 --- /dev/null +++ b/lib/errors/config_file_format.error.ts @@ -0,0 +1,7 @@ +import BaseError from './base_error'; + +export default class ConfigFileFormatError extends BaseError { + constructor(msg: string) { + super(msg, BaseError.ErrorCodes.CONFIG_FILE_FORMAT_ERROR); + } +} \ No newline at end of file diff --git a/lib/errors/config_file_missing.error.ts b/lib/errors/config_file_missing.error.ts new file mode 100644 index 0000000..8cf7403 --- /dev/null +++ b/lib/errors/config_file_missing.error.ts @@ -0,0 +1,7 @@ +import BaseError from './base_error'; + +export default class ConfigFileMissingError extends BaseError { + constructor(msg: string) { + super(msg, BaseError.ErrorCodes.CONFIG_FILE_MISSING_ERROR); + } +} \ No newline at end of file diff --git a/lib/errors/config_profile_missing.error.ts b/lib/errors/config_profile_missing.error.ts new file mode 100644 index 0000000..a69576f --- /dev/null +++ b/lib/errors/config_profile_missing.error.ts @@ -0,0 +1,7 @@ +import BaseError from './base_error'; + +export default class ConfigProfileError extends BaseError { + constructor(msg: string) { + super(msg, BaseError.ErrorCodes.CONFIG_PROFILE_ERROR); + } +} \ No newline at end of file diff --git a/lib/errors/error_codes.ts b/lib/errors/error_codes.ts new file mode 100644 index 0000000..2e094fb --- /dev/null +++ b/lib/errors/error_codes.ts @@ -0,0 +1,8 @@ +enum ErrorCodes { + CONFIG_FILE_MISSING_ERROR = 1000, + CONFIG_FILE_FORMAT_ERROR = 1001, + CONFIG_PROFILE_ERROR = 1002, + INVALID_ARGS_ERROR = 1003, + INVALID_COMMAND_ERROR = 1004 +} +export { ErrorCodes as default }; \ No newline at end of file diff --git a/lib/errors/invalid_arguments.error.ts b/lib/errors/invalid_arguments.error.ts new file mode 100644 index 0000000..e613ece --- /dev/null +++ b/lib/errors/invalid_arguments.error.ts @@ -0,0 +1,7 @@ +import BaseError from './base_error'; + +export default class InvalidArgumentsError extends BaseError { + constructor(msg: string) { + super(`Invalid Arguments: ${msg}`, BaseError.ErrorCodes.INVALID_ARGS_ERROR); + } +} \ No newline at end of file diff --git a/lib/errors/invalid_command.ts b/lib/errors/invalid_command.ts new file mode 100644 index 0000000..ef33ca9 --- /dev/null +++ b/lib/errors/invalid_command.ts @@ -0,0 +1,7 @@ +import BaseError from './base_error'; + +export default class InvalidCommandError extends BaseError { + constructor(msg: string) { + super(msg, BaseError.ErrorCodes.INVALID_COMMAND_ERROR); + } +} \ No newline at end of file diff --git a/lib/flows/init.ts b/lib/flows/init.ts new file mode 100644 index 0000000..36c6fd1 --- /dev/null +++ b/lib/flows/init.ts @@ -0,0 +1,105 @@ +'use strict'; +import * as TelegramBot from 'node-telegram-bot-api'; +import { IInitProfileOptions } from '../utils'; +import Telme from '../telme'; +import Config, { IConfig } from '../config/config'; + +export default class Init { + static async init(options: IInitProfileOptions = null) { + const profileName = options.profileName || 'default'; + let currentConfig: IConfig; + try { + currentConfig = await Config.getFullConfig(); + if (currentConfig.profiles[profileName]) { + // This will override existing profile + const response = await prompt(`Do you wish to override you current '${profileName}' profile [y/n]? `); + if (response[0].toLowerCase() !== 'y') { + console.log('Aborting.'); + process.exit(1); + } + } else { + currentConfig.profiles[profileName] = Config.generateProfileTemplate(); + } + } catch (e) { + currentConfig = { + version: Config.CURRENT_CONFIG_VERSION, + profiles: { + [profileName]: Config.generateProfileTemplate() + } + } + } + console.log(`Initializing '${profileName}' profile...`); + console.log( + 'Hi, In order for telme to work you will need to create a bot. If you don\'t know how, Please take a look at README.md'); + const token = await prompt('Please provide your bot token: '); + const code = generateCode(); + try { + const bot = new TelegramBot(token, { polling: true }); + const chatId = await listenToMessage(bot, code); + currentConfig.profiles[profileName].bot_token = token; + currentConfig.profiles[profileName].chat_id = chatId; + console.log('Cool, Got the chat. Saving config...'); + await Config.writeConfigToFile(currentConfig); + const profileFlag = profileName === 'default' ? '' : `-p ${profileName}`; + await Telme.SendMessage(currentConfig.profiles[profileName], { + message: `*Thanks!*\nYou are all set.\ntelme usage:\n\`\`\`\n +$ ${Config.APP_NAME} ${profileFlag} --m "message to send" +$ ${Config.APP_NAME} ${profileFlag} +\n\`\`\`\nFor more info, visit: [telme repo](https://gitlab.com/sagidayan/telme)\n\n_Enjoy!_` + }) + + + return true; + } catch (e) { + throw e; + } + } +} + +function listenToMessage(bot, code): Promise { + console.log(`Thanks! Please send '/code ${code}' to your bot from telegram. +You can send a direct message to the bot OR send this message to a group that this bot is a member of. +Keep in mind that '${Config.APP_NAME}' will send messages to the chat of your choosing.`); + return new Promise((resolve, reject) => { + const now = Date.now(); + bot.on('message', msg => { + const msgDate = msg.date * 1000; + if (msgDate < now) return; + const userId = msg.chat.id; + if (msg.text.indexOf('/code') === 0) { + const receivedCode = msg.text.split('/code')[1].trim(); + if (code === receivedCode) + resolve(userId); + else + reject(new Error('Code does not match!')); + } + }); + bot.on('polling_error', () => { + console.log('polling error'); + reject(new Error('Invalid token')); + }) + }); +} + +function generateCode(codeLength = 6) { + const allowedChars = 'aAbBcCdDEeFf0123456789'; + let code = ''; + for (let i = 0; i < codeLength; i++) { + code += + allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)); + } + return code; +} + +function prompt(question): Promise { + return new Promise(resolve => { + var stdin = process.stdin, stdout = process.stdout; + + stdin.resume(); + stdout.write(question); + + stdin.once('data', function (data) { + resolve(data.toString().trim()); + }); + }); +} \ No newline at end of file diff --git a/lib/flows/send_message.ts b/lib/flows/send_message.ts new file mode 100644 index 0000000..b317c63 --- /dev/null +++ b/lib/flows/send_message.ts @@ -0,0 +1,9 @@ +'use strict'; +const TelegramBot = require('node-telegram-bot-api'); +import { IProfileConfig } from '../config/config'; +export default class SendMessage { + static async send(config: IProfileConfig, msg: string) { + const bot = new TelegramBot(config.bot_token); + return await bot.sendMessage(config.chat_id, `${msg}`, { parse_mode: 'Markdown' }); + } +} \ No newline at end of file diff --git a/lib/flows/task_message.ts b/lib/flows/task_message.ts new file mode 100644 index 0000000..f9d88f6 --- /dev/null +++ b/lib/flows/task_message.ts @@ -0,0 +1,51 @@ +'use strict'; + +const { spawn } = require('child_process'); +// import TelegramBot from 'node-telegram-bot-api'; +import SendMessage from './send_message'; +import { IProfileConfig } from '../config/config'; +import { ITaskOptions } from '../utils'; +import Config from '../config/config' +import InvalidCommandError from '../errors/invalid_command'; +export default class TaskMessage { + static async run(config: IProfileConfig, options: ITaskOptions) { + const exec = spawn(options.command, options.args); + let errors = await promisifyExec(exec, options.command); + let msg = config.task_message_template.replace('%cmd%', ` $ ${options.command} ${options.args.join(' ')}`) + .replace('%errors%', errors); + try { + await SendMessage.send(config, msg); + } catch (e) { + errors = e.message; + console.error(`[${Config.APP_NAME}]: An error occurred. Error: ${e.message}`); + } + return true; + } +} + +function promisifyExec(exec, command): Promise { + let errors = null; + return new Promise((resolve, reject) => { + exec.stdout.on('data', (data) => { + console.log(String(data)); + }); + + exec.on('error', (error) => { + if (error.message.indexOf('ENOENT') >= 0) { + reject(new InvalidCommandError(`Command '${command}' not found`)); + } + }); + + exec.stderr.on('data', (data) => { + console.error(String(data)); + if (!errors) + errors = data; + else + errors += `\n${data}`; + }); + + exec.on('close', async (code) => { + resolve(errors); + }); + }); +} \ No newline at end of file diff --git a/lib/telme.ts b/lib/telme.ts new file mode 100644 index 0000000..46ac8e8 --- /dev/null +++ b/lib/telme.ts @@ -0,0 +1,20 @@ +process.env.NTBA_FIX_319 = 'junk'; +import SendMessage from './flows/send_message'; +import TaskMessage from './flows/task_message'; +import { IProfileConfig } from './config/config'; +import { ISimpleMessageOptions, ITaskOptions } from './utils'; + +export default class Telme { + static async SendMessage(config: IProfileConfig, options: ISimpleMessageOptions) { + await SendMessage.send(config, options.message); + return true; + } + static async RunTask(config: IProfileConfig, options: ITaskOptions) { + await TaskMessage.run(config, options); + } +} +export namespace Interfaces { + export interface IConfig extends IProfileConfig { }; + export interface IMessageOptions extends ISimpleMessageOptions { }; + export interface ITaskMessageOptions extends ITaskOptions { }; +} \ No newline at end of file diff --git a/lib/telme_cli.ts b/lib/telme_cli.ts new file mode 100644 index 0000000..2085b6f --- /dev/null +++ b/lib/telme_cli.ts @@ -0,0 +1,76 @@ +import Telme from './telme'; +import { ArgParser, ERunMode, ISimpleMessageOptions, ITaskOptions, IInitProfileOptions } from './utils'; +import Config, { IProfileConfig } from './config/config'; +import Init from './flows/init' +const { version } = require('../package.json'); + +async function main() { + const parsed = ArgParser.parse(process.argv); + let config: IProfileConfig; + let options: any; + switch (parsed.mode) { + case ERunMode.VERSION: + console.log(`[${Config.APP_NAME}] version ${version}`); + break; + case ERunMode.HELP: + printHelp(); + break; + case ERunMode.INIT: + await Init.init(parsed.mode_data); + break; + case ERunMode.SIMPLE_MESSAGE: + config = await Config.getConfig(parsed.mode_data.profileName); + options = parsed.mode_data as ISimpleMessageOptions; + await Telme.SendMessage(config, options); + console.log(`[${Config.APP_NAME}]: Told Ya!`); + break; + case ERunMode.TASK_MESSAGE: + config = await Config.getConfig(parsed.mode_data.profileName); + options = parsed.mode_data as ITaskOptions; + await Telme.RunTask(config, options); + console.log(`[${Config.APP_NAME}]: Told Ya!`); + break; + } + + return true; +} + +function printHelp() { + const cliFlags = ArgParser.CLI_OPTIONS; + const message = + `${Config.APP_NAME} v${version} - A CLI Telegram message tool + +[Usage]: $ ${Config.APP_NAME} + +Options: +\t ${cliFlags.versionFlags.join(', ')} \t\t Print ${Config.APP_NAME} version. +\t ${cliFlags.helpFlags.join(', ')} \t\t This help page. +\t ${cliFlags.profileFlags.join(', ')} \t\t Specify a profile to use. This is optional. defaults to 'default' profile. +\t ${cliFlags.initFlags.join(', ')} \t\t Will generate a config file for a given profile (defaults to 'default' profile). +\t ${cliFlags.messageFlags.join(', ')} \t\t Send a simple message. + +Examples: +init: +\t\t '${Config.APP_NAME} --init' - init a default profile +\t\t '${Config.APP_NAME} -i -p ' - init a 'named' profile +tasks: +\t\t '${Config.APP_NAME} docker-compose pull' - Send a message to default profile once the command 'docker-compose pull' is done +\t\t '${Config.APP_NAME} -p docker-compose pull' - Send a message to profile once the command 'docker-compose pull' is done + +messages: +\t\t '${Config.APP_NAME} -m "text to send"' - Send a message to default profile +\t\t '${Config.APP_NAME} -p -m "text to send"' - Send message to profile + +`; + + console.log(message); +} + +main().then(_ => { + process.exit(0); +}).catch(error => { + const exitCode = error.exitCode || 1; + console.error(`[${Config.APP_NAME}] ERROR: ${error.message}`); + console.log(`[${Config.APP_NAME}] For help run '$ ${Config.APP_NAME} -h'`); + process.exit(exitCode); +}); \ No newline at end of file diff --git a/lib/utils/index.ts b/lib/utils/index.ts new file mode 100644 index 0000000..a10e609 --- /dev/null +++ b/lib/utils/index.ts @@ -0,0 +1,139 @@ +import InvalidArgumentsError from "../errors/invalid_arguments.error"; + +const CLI_OPTIONS = { + versionFlags: ['--version', '-v'], + helpFlags: ['--help', '-h'], + initFlags: ['--init', '-i'], + profileFlags: ['--profile', '-p'], + messageFlags: ['--message', '-m'], +}; + + +export class ArgParser { + static CLI_OPTIONS = CLI_OPTIONS; + static parse(testArgs: string[] = null): IRunOptions { + let cliArgs = testArgs ? testArgs.slice(2) : process.argv.slice(2); + let tokens = tokenize(cliArgs); + if (tokens.version) { + return { + mode: ERunMode.VERSION + } + } else if (tokens.help) { + return { + mode: ERunMode.HELP + } + } else if (tokens.init) { + return { + mode: ERunMode.INIT, + mode_data: { + profileName: tokens.profile + } + } + } else if (tokens.simpleMessage) { + return { + mode: ERunMode.SIMPLE_MESSAGE, + mode_data: { + profileName: tokens.profile, + message: tokens.simpleMessage + } + } + } else { + // Task + return { + mode: ERunMode.TASK_MESSAGE, + mode_data: { + profileName: tokens.profile, + command: tokens.task.cmd, + args: tokens.task.args || [] + } + } + } + } +} + +function tokenize(args: string[]) { + let cliArgs = args; + if (!cliArgs.length) throw new InvalidArgumentsError('Missing a command to run'); + const tokens: ITokenizeArgs = { init: false, help: false, version: false }; + // ['node', 'execCommand', ...]; + let mode: ERunMode = ERunMode.TASK_MESSAGE; + while (cliArgs.length > 0) { + let tokenCursor = 1; + const token = cliArgs[0]; + if (CLI_OPTIONS.versionFlags.indexOf(token) >= 0) { + tokens.version = true; + return tokens; + } else if (CLI_OPTIONS.helpFlags.indexOf(token) >= 0) { + tokens.help = true; + return tokens; + } else if (CLI_OPTIONS.initFlags.indexOf(token) >= 0) { + tokens.init = true; + } else if (CLI_OPTIONS.profileFlags.indexOf(token) >= 0) { + if (!cliArgs[1]) { + throw new InvalidArgumentsError('Must provide a profile name'); + } + tokens.profile = cliArgs[1]; + tokenCursor = 2; + } else if (CLI_OPTIONS.messageFlags.indexOf(token) >= 0) { + if (!cliArgs[1]) { + throw new InvalidArgumentsError('Must provide a message to send'); + } + tokens.simpleMessage = cliArgs[1]; + tokenCursor = 2 + } else { + // Task + if (!cliArgs[0]) { + throw new InvalidArgumentsError('Missing a command to run'); + } + tokens.task = { + cmd: cliArgs[0], + args: cliArgs.slice(1) + }; + cliArgs = []; + } + if (cliArgs.length) cliArgs = cliArgs.slice(tokenCursor); + } + return tokens; +} + +interface ITokenizeArgs { + version: boolean; + help: boolean; + profile?: string; + init: boolean; + simpleMessage?: string; + task?: { + cmd: string; + args?: string[] + } +} + + +export interface IRunOptions { + mode: ERunMode; + mode_data?: IInitProfileOptions | ISimpleMessageOptions | ITaskOptions; +} + +export interface IBasicOptions { + profileName?: string; +} +export interface IInitProfileOptions extends IBasicOptions { } + +export interface ISimpleMessageOptions extends IBasicOptions { + message: string; +} + +export interface ITaskOptions extends IBasicOptions { + command: string; + args: string[]; +} + + + +export enum ERunMode { + VERSION, + HELP, + INIT, + SIMPLE_MESSAGE, + TASK_MESSAGE +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 83c98bb..d8c6054 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,15 @@ { - "name": "telme", - "version": "1.0.0", + "name": "node-telme", + "version": "0.0.4", "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/node": { + "version": "10.17.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.17.tgz", + "integrity": "sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q==", + "dev": true + }, "ajv": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", diff --git a/package.json b/package.json index 91253b5..18a3395 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "node-telme", - "version": "0.0.4", + "version": "0.1.0", "bin": { - "telme": "bin/telme" + "telme": "dist/telme_cli.js" }, "description": "A CLI tool that will report to you via telegram when a task is done", - "main": "index.js", + "main": "dist/telme.js", "author": "Sagi Dayan", "repository": { "type": "git", @@ -14,5 +14,11 @@ "license": "MIT", "dependencies": { "node-telegram-bot-api": "^0.40.0" + }, + "devDependencies": { + "@types/node": "^10.16.0" + }, + "scripts": { + "build": "tsc" } } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4beadcc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "outDir": "dist", + "sourceMap": true, + "allowJs": true, + "declaration": true, + "noImplicitAny": false, + "moduleResolution": "node", + "types": [ + "node", + ] + }, + "include": [ + "lib/**/*.ts" + ], + "exclude": [ + "node_modules", + "lib/**/*.test.ts" + ] +} \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..59b87d2 --- /dev/null +++ b/tslint.json @@ -0,0 +1,22 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint-config-airbnb" + ], + "jsRules": {}, + "rules": { + "max-line-length": [true, 250], + "no-console": [false], + + "no-unused-variable": [ + true, + { + "ignore-pattern": "^_" + } + ], + "match-default-export-name": false, + "import-name": false, + "variable-name": [false] + }, + "rulesDirectory": [] +} diff --git a/yarn.lock b/yarn.lock index dfd528a..13915c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@types/node@^10.16.0": + version "10.17.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.17.tgz#7a183163a9e6ff720d86502db23ba4aade5999b8" + integrity sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q== + ajv@^6.5.5: version "6.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7"