- init/help/version does not throw an error if there is no config file
 - node_module usage - now working
This commit is contained in:
Sagi Dayan 2020-03-25 18:40:18 -04:00
parent e73b8b9bf9
commit 49c36b471f
10 changed files with 144 additions and 45 deletions

View file

@ -8,14 +8,33 @@
4. Need help? `$ telme --help` 4. Need help? `$ telme --help`
## How do i get a bot token? ## How do i get a bot token?
- Get a telegram bot token from the botFather - 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 - 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` ## Configure `telme`
Simply run `$ telme --init` and follow 2 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. 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": "<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 ## Profiles
You can set multiple profiles, that will target different bots and/or different chats. 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) 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);
```

View file

@ -9,6 +9,8 @@ const HOME = require('os').homedir();
const CONFIG_FILE_NAME = '.telmeconfig'; const CONFIG_FILE_NAME = '.telmeconfig';
const FILEPATH = `${HOME}/${CONFIG_FILE_NAME}`; const FILEPATH = `${HOME}/${CONFIG_FILE_NAME}`;
let singleton = null;
export enum EConfigVersions { export enum EConfigVersions {
V004 = '0.0.4', V004 = '0.0.4',
V010 = '0.1.0' V010 = '0.1.0'
@ -21,6 +23,9 @@ export default class Config {
static CURRENT_CONFIG_VERSION = CURRENT_CONFIG_VERSION; 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%'; static DEFAULT_TASK_MESSAGE_TEMPLATE = '*Task:*\n\n```sh\n%cmd%\n```\nHas finished.\n*Errors*:\n %errors%';
private config: IConfig; private config: IConfig;
static cliInit() {
singleton = new Config();
}
constructor() { constructor() {
const file = this.readConfigFile(); const file = this.readConfigFile();
const parsed = this.parseConfig(file); const parsed = this.parseConfig(file);
@ -29,11 +34,11 @@ export default class Config {
} }
this.config = parsed.config; this.config = parsed.config;
} }
static getConfig(profile: string = 'default'): IProfileConfig { static getConfig(profile: string = 'default'): ITaskConfig {
return singleton.getConfig(profile); return singleton.getConfig(profile);
} }
static generateProfileTemplate(): IProfileConfig { static generateProfileTemplate(): ITaskConfig {
return { return {
chat_id: null, chat_id: null,
task_message_template: Config.DEFAULT_TASK_MESSAGE_TEMPLATE, task_message_template: Config.DEFAULT_TASK_MESSAGE_TEMPLATE,
@ -49,7 +54,7 @@ export default class Config {
return singleton.writeConfigToFile(config); return singleton.writeConfigToFile(config);
} }
private getConfig(profile: string): IProfileConfig { private getConfig(profile: string): ITaskConfig {
if (!this.config.profiles[profile]) { if (!this.config.profiles[profile]) {
throw new ConfigProfileError(`No profile named ${profile} found.`); throw new ConfigProfileError(`No profile named ${profile} found.`);
} }
@ -61,7 +66,7 @@ export default class Config {
const file = fs.readFileSync(FILEPATH); const file = fs.readFileSync(FILEPATH);
return file; return file;
} catch (e) { } 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 }; return { config, originalConfigVersion: config.version };
} }
} catch (e) { } 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 { private writeConfigToFile(config: IConfig): boolean {
try { try {
fs.writeFileSync(FILEPATH, JSON.stringify(config, null, 2)); fs.writeFileSync(FILEPATH, JSON.stringify(config, null, 2));
console.log(`created config file at ${FILEPATH}`); console.log(`created config file at ${FILEPATH}`);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
@ -92,21 +97,21 @@ export default class Config {
} }
} }
const EMPTY_CONFIG: IConfig = {
version: CURRENT_CONFIG_VERSION,
const singleton = new Config(); profiles: {}
}
export interface IMessageConfig {
export interface IProfileConfig {
chat_id: string; chat_id: string;
bot_token: string; bot_token: string;
}
export interface ITaskConfig extends IMessageConfig {
task_message_template: string; task_message_template: string;
} }
export interface IConfig { export interface IConfig {
version: EConfigVersions, version: EConfigVersions,
profiles: { profiles: {
[key: string]: IProfileConfig; [key: string]: ITaskConfig;
} }
} }

View file

@ -3,6 +3,8 @@ enum ErrorCodes {
CONFIG_FILE_FORMAT_ERROR = 1001, CONFIG_FILE_FORMAT_ERROR = 1001,
CONFIG_PROFILE_ERROR = 1002, CONFIG_PROFILE_ERROR = 1002,
INVALID_ARGS_ERROR = 1003, INVALID_ARGS_ERROR = 1003,
INVALID_COMMAND_ERROR = 1004 INVALID_COMMAND_ERROR = 1004,
INVALID_BOT_CHAT_CONFIG = 1005,
} }
export { ErrorCodes as default }; export { ErrorCodes as default };

View file

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

View file

@ -41,12 +41,12 @@ export default class Init {
console.log('Cool, Got the chat. Saving config...'); console.log('Cool, Got the chat. Saving config...');
await Config.writeConfigToFile(currentConfig); await Config.writeConfigToFile(currentConfig);
const profileFlag = profileName === 'default' ? '' : `-p ${profileName}`; const profileFlag = profileName === 'default' ? '' : `-p ${profileName}`;
await Telme.SendMessage(currentConfig.profiles[profileName], { await Telme.sendMessage(currentConfig.profiles[profileName],
message: `*Thanks!*\nYou are all set.\ntelme usage:\n\`\`\`\n `*Thanks!*\nYou are all set.\ntelme usage:\n\`\`\`\n
$ ${Config.APP_NAME} ${profileFlag} --m "message to send" $ ${Config.APP_NAME} ${profileFlag} --m "message to send"
$ ${Config.APP_NAME} ${profileFlag} <command> <args> $ ${Config.APP_NAME} ${profileFlag} <command> <args>
\n\`\`\`\nFor more info, visit: [telme repo](https://gitlab.com/sagidayan/telme)\n\n_Enjoy!_` \n\`\`\`\nFor more info, visit: [telme repo](https://gitlab.com/sagidayan/telme)\n\n_Enjoy!_`
}) );
return true; return true;
@ -57,9 +57,12 @@ $ ${Config.APP_NAME} ${profileFlag} <command> <args>
} }
function listenToMessage(bot, code): Promise<string> { function listenToMessage(bot, code): Promise<string> {
console.log(`Thanks! Please send '/code ${code}' to your bot from telegram. console.log(`
You can send a direct message to the bot OR send this message to a group that this bot is a member of. Thanks! Please send '/code ${code}' to your bot from telegram.
Keep in mind that '${Config.APP_NAME}' will send messages to the chat of your choosing.`);
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) => { return new Promise((resolve, reject) => {
const now = Date.now(); const now = Date.now();
bot.on('message', msg => { bot.on('message', msg => {

View file

@ -1,9 +1,22 @@
'use strict'; 'use strict';
const TelegramBot = require('node-telegram-bot-api'); 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 { 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); 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<string>, chat_id<string>`);
if (typeof msg !== 'string') throw new InvalidArgumentsError(`message must be of type string`)
} }

View file

@ -3,12 +3,14 @@
const { spawn } = require('child_process'); const { spawn } = require('child_process');
// import TelegramBot from 'node-telegram-bot-api'; // import TelegramBot from 'node-telegram-bot-api';
import SendMessage from './send_message'; import SendMessage from './send_message';
import { IProfileConfig } from '../config/config'; import { ITaskConfig } from '../config/config';
import { ITaskOptions } from '../utils'; import { ITaskOptions } from '../utils';
import Config from '../config/config' import Config from '../config/config'
import InvalidCommandError from '../errors/invalid_command'; import InvalidCommandError from '../errors/invalid_command';
import InvalidArgumentsError from '../errors/invalid_arguments.error';
export default class TaskMessage { 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); const exec = spawn(options.command, options.args);
let errors = await promisifyExec(exec, options.command); let errors = await promisifyExec(exec, options.command);
let msg = config.task_message_template.replace('%cmd%', ` $ ${options.command} ${options.args.join(' ')}`) 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<string>, chat_id<string>, task_message_template<string>`);
if (!options.command || !options.args || !Array.isArray(options.args) || typeof options.command !== 'string') throw new InvalidArgumentsError(`Option object must have command<string>, and args<string[]>`)
}
function promisifyExec(exec, command): Promise<string> { function promisifyExec(exec, command): Promise<string> {
let errors = null; let errors = null;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View file

@ -1,20 +1,21 @@
process.env.NTBA_FIX_319 = 'junk'; process.env.NTBA_FIX_319 = 'junk';
import SendMessage from './flows/send_message'; import SendMessage from './flows/send_message';
import TaskMessage from './flows/task_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'; import { ISimpleMessageOptions, ITaskOptions } from './utils';
export default class Telme { export default class Telme {
static async SendMessage(config: IProfileConfig, options: ISimpleMessageOptions) { static async sendMessage(config: MessageConfig, msg: string) {
await SendMessage.send(config, options.message); await SendMessage.send(config, msg);
return true; return true;
} }
static async RunTask(config: IProfileConfig, options: ITaskOptions) { static async runTask(config: TaskConfig, options: ITaskOptions) {
await TaskMessage.run(config, options); await TaskMessage.run(config, options);
} }
} }
export namespace Interfaces { 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 IMessageOptions extends ISimpleMessageOptions { };
export interface ITaskMessageOptions extends ITaskOptions { }; export interface ITaskMessageOptions extends ITaskOptions { };
} }

View file

@ -1,13 +1,14 @@
#!/usr/bin/env node #!/usr/bin/env node
import Telme from './telme'; import Telme from './telme';
import { ArgParser, ERunMode, ISimpleMessageOptions, ITaskOptions, IInitProfileOptions } from './utils'; 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' import Init from './flows/init'
const { version } = require('../package.json'); const { version } = require('../package.json');
async function main() { async function main() {
const parsed = ArgParser.parse(process.argv); const parsed = ArgParser.parse(process.argv);
let config: IProfileConfig; Config.cliInit();
let config: ITaskConfig;
let options: any; let options: any;
switch (parsed.mode) { switch (parsed.mode) {
case ERunMode.VERSION: case ERunMode.VERSION:
@ -22,13 +23,13 @@ async function main() {
case ERunMode.SIMPLE_MESSAGE: case ERunMode.SIMPLE_MESSAGE:
config = await Config.getConfig(parsed.mode_data.profileName); config = await Config.getConfig(parsed.mode_data.profileName);
options = parsed.mode_data as ISimpleMessageOptions; options = parsed.mode_data as ISimpleMessageOptions;
await Telme.SendMessage(config, options); await Telme.sendMessage(config, options.message);
console.log(`[${Config.APP_NAME}]: Told Ya!`); console.log(`[${Config.APP_NAME}]: Told Ya!`);
break; break;
case ERunMode.TASK_MESSAGE: case ERunMode.TASK_MESSAGE:
config = await Config.getConfig(parsed.mode_data.profileName); config = await Config.getConfig(parsed.mode_data.profileName);
options = parsed.mode_data as ITaskOptions; options = parsed.mode_data as ITaskOptions;
await Telme.RunTask(config, options); await Telme.runTask(config, options);
console.log(`[${Config.APP_NAME}]: Told Ya!`); console.log(`[${Config.APP_NAME}]: Told Ya!`);
break; break;
} }
@ -52,15 +53,15 @@ Options:
Examples: Examples:
init: init:
\t\t '${Config.APP_NAME} --init' - init a default profile \t '${Config.APP_NAME} --init' - init a default profile
\t\t '${Config.APP_NAME} -i -p <profile-name>' - init a 'named' profile \t '${Config.APP_NAME} -i -p <profile-name>' - init a 'named' profile
tasks: tasks:
\t\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} docker-compose pull' - Send a message to default profile once the command 'docker-compose pull' is done
\t\t '${Config.APP_NAME} -p <profile-name> docker-compose pull' - Send a message to <profile-name> profile once the command 'docker-compose pull' is done \t '${Config.APP_NAME} -p <profile-name> docker-compose pull' - Send a message to <profile-name> profile once the command 'docker-compose pull' is done
messages: messages:
\t\t '${Config.APP_NAME} -m "text to send"' - Send a message to default profile \t '${Config.APP_NAME} -m "text to send"' - Send a message to default profile
\t\t '${Config.APP_NAME} -p <profile-name> -m "text to send"' - Send message to <profile-name> profile \t '${Config.APP_NAME} -p <profile-name> -m "text to send"' - Send message to <profile-name> profile
`; `;

View file

@ -1,6 +1,6 @@
{ {
"name": "node-telme", "name": "node-telme",
"version": "0.1.1", "version": "0.1.2",
"bin": { "bin": {
"telme": "dist/telme_cli.js" "telme": "dist/telme_cli.js"
}, },
@ -11,6 +11,9 @@
"type": "git", "type": "git",
"url": "https://gitlab.com/sagidayan/telme" "url": "https://gitlab.com/sagidayan/telme"
}, },
"bugs": {
"url": "https://gitlab.com/sagidayan/telme/-/issues"
},
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"node-telegram-bot-api": "^0.40.0" "node-telegram-bot-api": "^0.40.0"
@ -20,5 +23,14 @@
}, },
"scripts": { "scripts": {
"build": "tsc" "build": "tsc"
} },
"keywords": [
"telegram",
"bot",
"cli",
"telme",
"notifications",
"notification",
"automation"
]
} }