commit 57f4d33134f411b6fe789a4d05be81cfa7a51b72 Author: Sagi Dayan Date: Tue Jun 25 23:20:05 2019 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aecc63d --- /dev/null +++ b/.gitignore @@ -0,0 +1,86 @@ +users.json +# Created by https://www.gitignore.io/api/node,linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# Yarn Lock file +yarn.lock + +# package.json Lock file +package-lock.json + +# dotenv environment variables file +.env + + +# End of https://www.gitignore.io/api/node,linux diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..2119fa8 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,3 @@ +{ + "esnext": true +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..2bec2d1 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Telegram Bot that saves photos + + +## Things you will need to do to run it + + - 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, copt and paste it into the config file. (botId) + - Make sure you have NodeJS and npm installed + - Install dependencies. + - ``` $ npm install``` + +All done :) + +## Configuration + +This bot can be configured via the config.js file. +Keep the structure of this file as is, and change the values to your needs. + +## Running the bot + +I would suggest to use [pm2](http://pm2.keymetrics.io) to manage this process. but its your call... + +Production Run: +``` +$ node index.js +``` + +Debug Run: +``` +$ node index.js --printMessage +``` +> This will print out the firs message the bot receives and will exit.
+So you can see user ID and other data attached to the message. diff --git a/botEngine.js b/botEngine.js new file mode 100644 index 0000000..bb431a8 --- /dev/null +++ b/botEngine.js @@ -0,0 +1,244 @@ +// Node imports +const fs = require('fs'); +const path = require('path'); + +// Telegram Module +const TelegramBot = require('node-telegram-bot-api'); + +// Congifuration +const config = require('./config'); + +let admin_chatId; +let authorizedUsers; +let telegram; +const SLIDESHOW_LOCATION = config.slideShowFolderPath; + +/** + * Runs the bot. + * @param {Array} args CLI arguments + * @return {void} + */ +let runBot = (args) => { + loadDB(); + telegram = new TelegramBot(config.botId, { + polling: true + }); + telegram.on('message', (msg) => { + const chatId = msg.chat.id; + if (args.indexOf('--printMessage') > 0) { + console.log(JSON.stringify(msg, null, 2)); + process.exit(0); + } + if (userAllowd(msg)) { + handleMessage(msg); + } else { + let message = config.messages.sendieResponse.unauthorizedUser; + message = message.replace('{username}', msg.from.username); + message = message.replace('{messageObj}', JSON.stringify(msg, null, 2)); + telegram.sendMessage(chatId, message); + message = config.messages.adminNotifications.unauthorizedUser; + message = message.replace('{username}', msg.from.username); + message = message.replace('{messageObj}', JSON.stringify(msg, null, 2)); + console.warn(message); + notifyAdmin(message); + } + }); + console.log('Telegram bot is alive and listening!'); +} + +/** + * Save a photo to config.slideShowFolderPath + * @param {string} fileId fileId (received from telegram) + * @param {Function} callback post save function - args: {Error} error, {string} filePath + * @return {void} + */ +function storeFile(fileId, callback) { + telegram.downloadFile(fileId, SLIDESHOW_LOCATION) + .then((filePath) => { + let message = config.messages.sendieResponse.fileSuccessfullySaved; + message = message.replace('{fileLocation}', filePath); + callback(null, filePath); + }) + .catch((err) => { + callback(err); + }); +} + +let storeFileCallback = (err, filePath, chatId, photoId) => { + let response; + if (err) { + console.error('Failed to save file...[ERROR]\n', err); + response = config.messages.sendieResponse.failedToSaveFile; + response = response.replace('{error}', err); + telegram.sendMessage(chatId, response); + response = config.messages.sendieResponse.failedToSaveFile; + response = response.replace('{error}', err); + if (admin_chatId !== chatId) notifyAdmin(response, photoId); + } else { + response = config.messages.sendieResponse.fileSuccessfullySaved; + response = response.replace('{fileLocation}', filePath); + telegram.sendMessage(chatId, response); + response = config.messages.adminNotifications.fileSuccessfullySaved; + response = response.replace('{fileLocation}', filePath); + if (admin_chatId !== chatId) notifyAdmin(response, photoId); + } +}; + +/** + * This function is called on every message from an authorized user. + * @param {JSON} msg telegram message object + * @return {void} + */ +let handleMessage = (msg) => { + const chatId = msg.chat.id; + if (msg.photo || (msg.document && msg.document.mime_type.indexOf('image/') === 0)) { // Handle Images + if (msg.photo) { + storeFile(msg.photo[msg.photo.length - 1].file_id, (err, filePath) => { + storeFileCallback(err, filePath, chatId, msg.photo[msg.photo.length - 1].file_id); + }); + } else { // document - uncompressed photo + storeFile(msg.document.file_id, (err, filePath) => { + storeFileCallback(err, filePath, chatId, msg.document.file_id); + }); + } + } else if (msg.contact) { // Handle Contact + addRemoveUser(msg.contact, msg.from); + } else { + // send a message to the chat acknowledging receipt of their message + let message = config.messages.sendieResponse.unableToDoThat; + message = message.replace('{username}', msg.from.username); + telegram.sendMessage(chatId, message); + } +}; + +/** + * Check if a user is authorized to interact with this bot + * @param {JSON} msg telegram message object + * @return {boolean} true/false + */ +let userAllowd = (msg) => { + if (msg.from.id === config.admin.id) { + admin_chatId = msg.chat.id; + return true; + } + if (config.permissions.allowAll) return true; + if (authorizedUsers.indexOf(msg.from.id) >= 0) return true; + return false; +}; + +/** + * Sends a message to admin. Only if config.admin.notifyActivity is true + * @param {string} msg message to send + * @param {string} photo_id the photo to send to admin incase config.notifyActivity.sendPhoto is true + * @return {void} + */ +let notifyAdmin = (msg, photo_id) => { + if (admin_chatId != null && config.admin.notifyActivity) { + telegram.sendMessage(admin_chatId, msg); + if (photo_id && config.admin.sendPhotoOnSave) telegram.sendPhoto(admin_chatId, photo_id); + } +}; + +/** + * Will add/remove a user from authorized users list. (Saved in a file) + * If a user is not in the list, it will add him. Else remove him. + * Will notify admin if wanted (config.admin.notifyActivity) and send the initiating user on fail/success + * @param {JSON} contact Telegram contact object. The user to add/remove + * @param {JSON} from Telegram user object. Who wanted to add/remove the contact + */ +let addRemoveUser = (contact, from) => { + if (!config.permissions.allowAddingUsers) { + // Ignore... + console.warn('Attempt to add/remove a user. this functionality is disabled.'); + return; + } else { + if (from.id != config.admin.id && !config.permissions.allowNonAdminToAddUsers) { + telegram.sendMessage(from.id, config.messages.sendieResponse.noPermissions); + console.warn('Attempt to add/remove a user from an unpermited user. id: ', from.id); + return; + } + } + let toAdd = true; + if (authorizedUsers.indexOf(contact.user_id) < 0) { + // Add user + authorizedUsers.push(contact.user_id); + } else { + // remove user + toAdd = false; + authorizedUsers.splice(authorizedUsers.indexOf(contact.user_id), 1); + } + if (saveToDB()) { + let message; + if (toAdd) message = config.messages.sendieResponse.userAdded; + else message = config.messages.sendieResponse.userRemoved; + message = message.replace('{username}', contact.first_name); + telegram.sendMessage(from.id, message); // response to the user that added the new one + // Not supported yet - a bot can't start a chat with a new user... so can't do this. + // if(toAdd) message = config.messages.userAdded.added; + // else message = config.messages.userRemoved.removed; + // message = message.replace('{username_add_rem}', contact.first_name); + // message = message.replace('{username}', from.first_name); + // telegram.sendMessage(contact.user_id, message); // Notify the new/old user + if (toAdd) message = config.messages.adminNotifications.userAdded; + else message = config.messages.adminNotifications.userRemoved; + message = message.replace('{username_add_rem}', contact.first_name); + message = message.replace('{username}', from.first_name); + if (from.id != config.admin.id) notifyAdmin(message); // notifyAdmin + } else { + console.log('Failed to save DB!!!'); + } +} + +/** + * Load authorized users list from disk (Reading a JSON file). If not exists (Usually on first run), will create the file. + * Incase the file is not in valid JSON format. will kill process with err code 1. + * @return {void} + */ +let loadDB = () => { + let dbLocation = config.DBLocation; + if (dbLocation[dbLocation.length - 1] != '/') dbLocation += '/users.json'; + else dbLocation += 'users.json'; + try { + data = fs.readFileSync(path.resolve(dbLocation)); + try { + authorizedUsers = JSON.parse(data.toString()); + console.log('DB loaded. AuthorizedUsers: ', authorizedUsers); + } catch (err) { + console.error('DB file is corrupted. Please delete the file', dbLocation, 'And rerun this service'); + process.exit(1); + } + } catch (err) { + authorizedUsers = []; + if (saveToDB()) { + console.log('DB was created. Everything is Okay.'); + } else { + console.error('Unable to read from DB. Error:\n', err); + process.exit(1); + } + } +} + +/** + * Saves the authorized users list to disk. (config.DBLocation) + * @return {boolean} true=success, false=fail + */ +let saveToDB = () => { + let dbLocation = config.DBLocation; + if (dbLocation[dbLocation.length - 1] != '/') dbLocation += '/users.json'; + else dbLocation += 'users.json'; + try { + fs.writeFileSync(dbLocation, JSON.stringify(authorizedUsers)); + return true; + } catch (err) { + console.error('Failed to save to DB... ERROR:\n', err); + return false; + } +} + +/** + * Bot engine exported interface + * @type {Object} + */ +module.exports = { + runBot: runBot +} diff --git a/config.js b/config.js new file mode 100644 index 0000000..170acef --- /dev/null +++ b/config.js @@ -0,0 +1,42 @@ +/** + * Configuration + */ +module.exports = { + botId: 'xxxxxxxxxx', // Bot token/Id - Get if from botFather + slideShowFolderPath: '/full/path/to/folder', // Where to save files + DBLocation: '.', // Location of DB file. You need to have permissions to read/write in that location + permissions: { + allowAll: false, // allow any telegram user to add photos + allowAddingUsers: true, // accept contacts as authorized users? + allowNonAdminToAddUsers: false // allow non admin users to add contacts to authorized users + }, + admin: { + notifyActivity: true, + sendPhotoOnSave: true, // send admin the photo that somone else have saved + id: 1234 // [Number] Your Telegram UserId + }, + messages: { + adminNotifications:{ + unauthorizedUser: 'Unauthorized user: {username}\nMessage:\n{messageObj}', // Plaseholers: {username}, {messageObj} + fileSuccessfullySaved: 'A photo was added.\n', // Plaseholers: {fileLocation} + failedToSaveFile: 'Psst...\nYou might want to know, that i have failed you. a fail was not saved.\nError:\n{error}', // Placeholders: {error} + userAdded: 'The user {username} added {username_add_rem} to the system', // Plaseholers: {username}, {username_add_rem} + userRemoved: 'The user {username} removed {username_add_rem} from the system' // Plaseholers: {username}, {username_add_rem} + }, + sendieResponse: { + unauthorizedUser: 'Hi {username}, \n Who the fuck are you?!\n I have notified my creator and he will hunt you down. I would suggest that you disconnect from the Internet', // Plaseholers: {username}, {messageObj} + fileSuccessfullySaved: 'Added to your slideshow. Saved at: {fileLocation}', // Plaseholers: {fileLocation} + failedToSaveFile: 'Woops,\nsome thing happend... Can\'t save your photo.\nTry Again later', // Placeholders: {error} + unableToDoThat: 'Hi {username},\nIm unable to understand you. I\'m able only to accept photos and contacts', // Placeholders: {username} + noPermissions: 'You have no permissions to do that...', + userAdded: 'Okay, {username} was added as a trusted user.', // Placeholders: {username} + userRemoved: 'Sure, I\'ll stop supporting {username}.' // Plaseholers: {username} + }, + userAdded: { // Not supported yet + added: 'Hi {username_add_rem},\n{username} added you as a trusted user. this means that you can send me photos to add to the slide show! Try it now' // Plaseholers: {username}, {username_add_rem} + }, + userRemoved:{ // Not supported yet + removed: 'Hi {username_add_rem},\n{username} removed you from my system... It\'s not you, it\'s me :(' // Plaseholers: {username}, {username_add_rem} + } + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..22cb264 --- /dev/null +++ b/index.js @@ -0,0 +1,10 @@ +/** + * A Telegram bot thats saves your photos locally + * + */ + +// Import bot engine module +const botEngine = require('./botEngine'); +console.log('Initializing bot...'); +//Run the bot. +botEngine.runBot(process.argv); diff --git a/package.json b/package.json new file mode 100644 index 0000000..1f31714 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "telegram_bot", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "node-telegram-bot-api": "^0.27.1" + } +}