From 49c36b471f50595a0156beb4bdbb025347cf3f82 Mon Sep 17 00:00:00 2001 From: Sagi Dayan Date: Wed, 25 Mar 2020 18:40:18 -0400 Subject: [PATCH] Fixes - init/help/version does not throw an error if there is no config file - node_module usage - now working --- README.md | 52 ++++++++++++++++++++- lib/config/config.ts | 33 +++++++------ lib/errors/error_codes.ts | 4 +- lib/errors/invalid_bot_chat_config.error.ts | 7 +++ lib/flows/init.ts | 15 +++--- lib/flows/send_message.ts | 19 ++++++-- lib/flows/task_message.ts | 11 ++++- lib/telme.ts | 11 +++-- lib/telme_cli.ts | 21 +++++---- package.json | 16 ++++++- 10 files changed, 144 insertions(+), 45 deletions(-) create mode 100644 lib/errors/invalid_bot_chat_config.error.ts diff --git a/README.md b/README.md index b735357..2c091df 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,33 @@ 4. Need help? `$ telme --help` ## How do i get a bot token? - - Get a telegram bot token from the botFather - - Just talk to [BotFather](https://telegram.me/botfather) and follow a few simple steps. Once you've created a bot and received your authorization token, copy it for later + - Get a telegram bot token from the BotFather, It takes less than a minute + - Just talk to [BotFather](https://telegram.me/botfather) and follow a few simple steps. Once you've created a bot and received your authorization token, copy it for later ## Configure `telme` 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. +#### Config file structure +The config file `.telmeconfig` should be located in your home folder and contain a valid JSON. + +Example config (`~/.telmeconfig`): +```json +{ + "version": "0.1.0", + "profiles": { + "profile_name": { + "chat_id": 000000, + "bot_token": "", + "task_message_template": "*Task:*\n\n```sh\n%cmd%\n```\nHas finished.\n*Errors*:\n %errors%" + }, + ... + } +} +``` +> `task_message_template` allows the following optional placeholders `%cmd%`, `%errors%`. These will be replaced with the actual command and errors. + ## Profiles You can set multiple profiles, that will target different bots and/or different chats. @@ -53,3 +72,32 @@ $ telme -p movie-club curl https://example.com/large/file/download.mp4 ``` this will send the message to the `movie-club` profile chat. (By the `movie-club` bot) +## Using as a `node_module` +> Typescript users will have definitions +```javascript +import Telme from 'node-telme' // OR const Telme = require('node-telme').default + +const config = { + chat_id: 'somechatid', + bot_token: 'bot-token' +} + +Telme.sendMessage(config, 'Hi there!').then(_=>{ + ... +}).catch(console.error); + +// %cmd% and %errors% will be replaced buy actual values. +config.task_message_template = 'Task: %cmd% is done!. Errors: %errors%'; +const options = { + command: 'ls', + args: ['-lah'] // If no args pass in an empty array +}; + +Telme.runTask(config, options).then(_=>{ + ... +}).catch(console.error); + +``` + + + diff --git a/lib/config/config.ts b/lib/config/config.ts index d075ec8..11396d5 100644 --- a/lib/config/config.ts +++ b/lib/config/config.ts @@ -9,6 +9,8 @@ const HOME = require('os').homedir(); const CONFIG_FILE_NAME = '.telmeconfig'; const FILEPATH = `${HOME}/${CONFIG_FILE_NAME}`; +let singleton = null; + export enum EConfigVersions { V004 = '0.0.4', V010 = '0.1.0' @@ -21,6 +23,9 @@ export default class Config { 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; + static cliInit() { + singleton = new Config(); + } constructor() { const file = this.readConfigFile(); const parsed = this.parseConfig(file); @@ -29,11 +34,11 @@ export default class Config { } this.config = parsed.config; } - static getConfig(profile: string = 'default'): IProfileConfig { + static getConfig(profile: string = 'default'): ITaskConfig { return singleton.getConfig(profile); } - static generateProfileTemplate(): IProfileConfig { + static generateProfileTemplate(): ITaskConfig { return { chat_id: null, task_message_template: Config.DEFAULT_TASK_MESSAGE_TEMPLATE, @@ -49,7 +54,7 @@ export default class Config { return singleton.writeConfigToFile(config); } - private getConfig(profile: string): IProfileConfig { + private getConfig(profile: string): ITaskConfig { if (!this.config.profiles[profile]) { throw new ConfigProfileError(`No profile named ${profile} found.`); } @@ -61,7 +66,7 @@ export default class Config { const file = fs.readFileSync(FILEPATH); return file; } catch (e) { - throw new ConfigFileMissingError(''); + return Buffer.from(JSON.stringify(EMPTY_CONFIG)); } } @@ -77,14 +82,14 @@ export default class Config { return { config, originalConfigVersion: config.version }; } } catch (e) { - throw new ConfigFileFormatError(''); + throw new ConfigFileFormatError('Invalid JSON format in config file. If you modified the file yourself, please double check your modifications'); } } private writeConfigToFile(config: IConfig): boolean { try { fs.writeFileSync(FILEPATH, JSON.stringify(config, null, 2)); - console.log(`created config file at ${FILEPATH}`); + console.log(`✅ created config file at ${FILEPATH}`); return true; } catch (e) { return false; @@ -92,21 +97,21 @@ export default class Config { } } - - -const singleton = new Config(); - - - -export interface IProfileConfig { +const EMPTY_CONFIG: IConfig = { + version: CURRENT_CONFIG_VERSION, + profiles: {} +} +export interface IMessageConfig { chat_id: string; bot_token: string; +} +export interface ITaskConfig extends IMessageConfig { task_message_template: string; } export interface IConfig { version: EConfigVersions, profiles: { - [key: string]: IProfileConfig; + [key: string]: ITaskConfig; } } \ No newline at end of file diff --git a/lib/errors/error_codes.ts b/lib/errors/error_codes.ts index 2e094fb..f2547c8 100644 --- a/lib/errors/error_codes.ts +++ b/lib/errors/error_codes.ts @@ -3,6 +3,8 @@ enum ErrorCodes { CONFIG_FILE_FORMAT_ERROR = 1001, CONFIG_PROFILE_ERROR = 1002, INVALID_ARGS_ERROR = 1003, - INVALID_COMMAND_ERROR = 1004 + INVALID_COMMAND_ERROR = 1004, + INVALID_BOT_CHAT_CONFIG = 1005, + } export { ErrorCodes as default }; \ No newline at end of file diff --git a/lib/errors/invalid_bot_chat_config.error.ts b/lib/errors/invalid_bot_chat_config.error.ts new file mode 100644 index 0000000..4d90ac9 --- /dev/null +++ b/lib/errors/invalid_bot_chat_config.error.ts @@ -0,0 +1,7 @@ +import BaseError from './base_error'; + +export default class InvalidBotOrChatConfig extends BaseError { + constructor() { + super(`bot_token OR chat_id are invalid`, BaseError.ErrorCodes.INVALID_BOT_CHAT_CONFIG); + } +} \ No newline at end of file diff --git a/lib/flows/init.ts b/lib/flows/init.ts index 36c6fd1..ae530dc 100644 --- a/lib/flows/init.ts +++ b/lib/flows/init.ts @@ -41,12 +41,12 @@ export default class Init { 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 + await Telme.sendMessage(currentConfig.profiles[profileName], + `*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; @@ -57,9 +57,12 @@ $ ${Config.APP_NAME} ${profileFlag} } 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.`); + 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 => { diff --git a/lib/flows/send_message.ts b/lib/flows/send_message.ts index b317c63..d294fa0 100644 --- a/lib/flows/send_message.ts +++ b/lib/flows/send_message.ts @@ -1,9 +1,22 @@ 'use strict'; const TelegramBot = require('node-telegram-bot-api'); -import { IProfileConfig } from '../config/config'; +import { IMessageConfig } from '../config/config'; +import InvalidBotOrChatConfig from '../errors/invalid_bot_chat_config.error'; +import InvalidArgumentsError from '../errors/invalid_arguments.error'; export default class SendMessage { - static async send(config: IProfileConfig, msg: string) { + static async send(config: IMessageConfig, msg: string) { + validate(config, msg); const bot = new TelegramBot(config.bot_token); - return await bot.sendMessage(config.chat_id, `${msg}`, { parse_mode: 'Markdown' }); + try { + await bot.sendMessage(config.chat_id, `${msg}`, { parse_mode: 'Markdown' }); + return true; + } catch (e) { + throw new InvalidBotOrChatConfig(); + } } +} + +function validate(config: IMessageConfig, msg: string) { + if (!config.bot_token || !config.chat_id) throw new InvalidArgumentsError(`Config object must have bot_token, chat_id`); + if (typeof msg !== 'string') throw new InvalidArgumentsError(`message must be of type string`) } \ No newline at end of file diff --git a/lib/flows/task_message.ts b/lib/flows/task_message.ts index f9d88f6..393b2b3 100644 --- a/lib/flows/task_message.ts +++ b/lib/flows/task_message.ts @@ -3,12 +3,14 @@ const { spawn } = require('child_process'); // import TelegramBot from 'node-telegram-bot-api'; import SendMessage from './send_message'; -import { IProfileConfig } from '../config/config'; +import { ITaskConfig } from '../config/config'; import { ITaskOptions } from '../utils'; import Config from '../config/config' import InvalidCommandError from '../errors/invalid_command'; +import InvalidArgumentsError from '../errors/invalid_arguments.error'; export default class TaskMessage { - static async run(config: IProfileConfig, options: ITaskOptions) { + static async run(config: ITaskConfig, options: ITaskOptions) { + validate(config, options); 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(' ')}`) @@ -23,6 +25,11 @@ export default class TaskMessage { } } +function validate(config: ITaskConfig, options: ITaskOptions) { + if (!config.bot_token || !config.chat_id || !config.task_message_template) throw new InvalidArgumentsError(`Config object must have bot_token, chat_id, task_message_template`); + if (!options.command || !options.args || !Array.isArray(options.args) || typeof options.command !== 'string') throw new InvalidArgumentsError(`Option object must have command, and args`) +} + function promisifyExec(exec, command): Promise { let errors = null; return new Promise((resolve, reject) => { diff --git a/lib/telme.ts b/lib/telme.ts index 46ac8e8..713d310 100644 --- a/lib/telme.ts +++ b/lib/telme.ts @@ -1,20 +1,21 @@ process.env.NTBA_FIX_319 = 'junk'; import SendMessage from './flows/send_message'; import TaskMessage from './flows/task_message'; -import { IProfileConfig } from './config/config'; +import { ITaskConfig as TaskConfig, IMessageConfig as MessageConfig } 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); + static async sendMessage(config: MessageConfig, msg: string) { + await SendMessage.send(config, msg); return true; } - static async RunTask(config: IProfileConfig, options: ITaskOptions) { + static async runTask(config: TaskConfig, options: ITaskOptions) { await TaskMessage.run(config, options); } } export namespace Interfaces { - export interface IConfig extends IProfileConfig { }; + export interface IMessageConfig extends MessageConfig { }; + export interface ITaskConfig extends TaskConfig { }; 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 index 5eff74b..c11c2ff 100644 --- a/lib/telme_cli.ts +++ b/lib/telme_cli.ts @@ -1,13 +1,14 @@ #!/usr/bin/env node import Telme from './telme'; import { ArgParser, ERunMode, ISimpleMessageOptions, ITaskOptions, IInitProfileOptions } from './utils'; -import Config, { IProfileConfig } from './config/config'; +import Config, { ITaskConfig } 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; + Config.cliInit(); + let config: ITaskConfig; let options: any; switch (parsed.mode) { case ERunMode.VERSION: @@ -22,13 +23,13 @@ async function main() { case ERunMode.SIMPLE_MESSAGE: config = await Config.getConfig(parsed.mode_data.profileName); options = parsed.mode_data as ISimpleMessageOptions; - await Telme.SendMessage(config, options); + await Telme.sendMessage(config, options.message); 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); + await Telme.runTask(config, options); console.log(`[${Config.APP_NAME}]: Told Ya!`); break; } @@ -52,15 +53,15 @@ Options: Examples: init: -\t\t '${Config.APP_NAME} --init' - init a default profile -\t\t '${Config.APP_NAME} -i -p ' - init a 'named' profile +\t '${Config.APP_NAME} --init' - init a default profile +\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 +\t '${Config.APP_NAME} docker-compose pull' - Send a message to default profile once the command 'docker-compose pull' is done +\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 +\t '${Config.APP_NAME} -m "text to send"' - Send a message to default profile +\t '${Config.APP_NAME} -p -m "text to send"' - Send message to profile `; diff --git a/package.json b/package.json index 4e00534..daee861 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-telme", - "version": "0.1.1", + "version": "0.1.2", "bin": { "telme": "dist/telme_cli.js" }, @@ -11,6 +11,9 @@ "type": "git", "url": "https://gitlab.com/sagidayan/telme" }, + "bugs": { + "url": "https://gitlab.com/sagidayan/telme/-/issues" + }, "license": "MIT", "dependencies": { "node-telegram-bot-api": "^0.40.0" @@ -20,5 +23,14 @@ }, "scripts": { "build": "tsc" - } + }, + "keywords": [ + "telegram", + "bot", + "cli", + "telme", + "notifications", + "notification", + "automation" + ] } \ No newline at end of file