Changed from js to ts - Added implementation for profiles - Can also be used as a node_module
This commit is contained in:
parent
79399e09d3
commit
e747b17141
27 changed files with 664 additions and 233 deletions
|
@ -1,2 +1,3 @@
|
||||||
.npmrc
|
.npmrc
|
||||||
|
lib/
|
||||||
node_modules/
|
node_modules/
|
10
README.md
10
README.md
|
@ -16,6 +16,16 @@
|
||||||
Simply run `$ telme --init` and follow the easy steps. You will need the bot token at this stage.
|
Simply run `$ telme --init` and follow the 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.
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
```
|
||||||
|
$ telme --init --profile <profile_name>
|
||||||
|
```
|
||||||
|
|
||||||
## Examples:
|
## Examples:
|
||||||
###### Simple message
|
###### Simple message
|
||||||
```
|
```
|
||||||
|
|
|
@ -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",
|
"name": "node-telme",
|
||||||
"version": "1.0.0",
|
"version": "0.0.4",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"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": {
|
"ajv": {
|
||||||
"version": "6.12.0",
|
"version": "6.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
|
||||||
|
|
12
package.json
12
package.json
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "node-telme",
|
"name": "node-telme",
|
||||||
"version": "0.0.4",
|
"version": "0.1.0",
|
||||||
"bin": {
|
"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",
|
"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",
|
"author": "Sagi Dayan",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -14,5 +14,11 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-telegram-bot-api": "^0.40.0"
|
"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
|
# 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:
|
ajv@^6.5.5:
|
||||||
version "6.12.0"
|
version "6.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7"
|
||||||
|
|
Loading…
Reference in a new issue