Compare commits
1 commit
e747b17141
...
c59eec9c50
Author | SHA1 | Date | |
---|---|---|---|
c59eec9c50 |
27 changed files with 681 additions and 242 deletions
|
@ -1,2 +1,3 @@
|
|||
.npmrc
|
||||
lib/
|
||||
node_modules/
|
36
README.md
36
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 <profile_name>
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
this will send the message to the `movie-club` profile chat. (By the `movie-club` bot)
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
require('../index');
|
|
@ -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} <command> <args>
|
||||
\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
|
|
@ -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;
|
|
@ -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;
|
73
index.js
73
index.js
|
@ -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} <command> <args>`);
|
||||
}
|
||||
|
||||
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(() => {});
|
22
lib/config/config.0.0.4.ts
Normal file
22
lib/config/config.0.0.4.ts
Normal file
|
@ -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;
|
||||
}
|
112
lib/config/config.ts
Normal file
112
lib/config/config.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
10
lib/errors/base_error.ts
Normal file
10
lib/errors/base_error.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
7
lib/errors/config_file_format.error.ts
Normal file
7
lib/errors/config_file_format.error.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
7
lib/errors/config_file_missing.error.ts
Normal file
7
lib/errors/config_file_missing.error.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
7
lib/errors/config_profile_missing.error.ts
Normal file
7
lib/errors/config_profile_missing.error.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
8
lib/errors/error_codes.ts
Normal file
8
lib/errors/error_codes.ts
Normal file
|
@ -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 };
|
7
lib/errors/invalid_arguments.error.ts
Normal file
7
lib/errors/invalid_arguments.error.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
7
lib/errors/invalid_command.ts
Normal file
7
lib/errors/invalid_command.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
105
lib/flows/init.ts
Normal file
105
lib/flows/init.ts
Normal file
|
@ -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} <command> <args>
|
||||
\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<string> {
|
||||
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<string> {
|
||||
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());
|
||||
});
|
||||
});
|
||||
}
|
9
lib/flows/send_message.ts
Normal file
9
lib/flows/send_message.ts
Normal file
|
@ -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' });
|
||||
}
|
||||
}
|
51
lib/flows/task_message.ts
Normal file
51
lib/flows/task_message.ts
Normal file
|
@ -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<string> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
20
lib/telme.ts
Normal file
20
lib/telme.ts
Normal file
|
@ -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 { };
|
||||
}
|
76
lib/telme_cli.ts
Normal file
76
lib/telme_cli.ts
Normal file
|
@ -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} <telme_options> <?command> <arguments>
|
||||
|
||||
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 <profile-name>' - 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 <profile-name> docker-compose pull' - Send a message to <profile-name> 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 <profile-name> -m "text to send"' - Send message to <profile-name> 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);
|
||||
});
|
139
lib/utils/index.ts
Normal file
139
lib/utils/index.ts
Normal file
|
@ -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
|
||||
}
|
10
package-lock.json
generated
10
package-lock.json
generated
|
@ -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",
|
||||
|
|
12
package.json
12
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"
|
||||
}
|
||||
}
|
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
|
@ -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"
|
||||
]
|
||||
}
|
22
tslint.json
Normal file
22
tslint.json
Normal file
|
@ -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": []
|
||||
}
|
|
@ -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"
|
||||
|
|
Reference in a new issue