├── screenshot ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg └── 5.gif ├── logo ├── transmission.png ├── transmission-bot.png ├── transmission-bot.psd └── robot-with-antenna.png ├── bot ├── config.json ├── package.json ├── notification-manager.js ├── formatter.js ├── engine.js └── bot.js ├── .gitignore ├── LICENSE └── README.md /screenshot/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaelecalza/transmission-telegram-bot/HEAD/screenshot/1.jpg -------------------------------------------------------------------------------- /screenshot/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaelecalza/transmission-telegram-bot/HEAD/screenshot/2.jpg -------------------------------------------------------------------------------- /screenshot/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaelecalza/transmission-telegram-bot/HEAD/screenshot/3.jpg -------------------------------------------------------------------------------- /screenshot/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaelecalza/transmission-telegram-bot/HEAD/screenshot/4.jpg -------------------------------------------------------------------------------- /screenshot/5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaelecalza/transmission-telegram-bot/HEAD/screenshot/5.gif -------------------------------------------------------------------------------- /logo/transmission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaelecalza/transmission-telegram-bot/HEAD/logo/transmission.png -------------------------------------------------------------------------------- /logo/transmission-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaelecalza/transmission-telegram-bot/HEAD/logo/transmission-bot.png -------------------------------------------------------------------------------- /logo/transmission-bot.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaelecalza/transmission-telegram-bot/HEAD/logo/transmission-bot.psd -------------------------------------------------------------------------------- /logo/robot-with-antenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaelecalza/transmission-telegram-bot/HEAD/logo/robot-with-antenna.png -------------------------------------------------------------------------------- /bot/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bot": { 3 | "token": "bottotoken", 4 | "users": [], 5 | "version": "1.0.0" 6 | }, 7 | "transmission": { 8 | "address": "ipaddress", 9 | "path": "/transmission/rpc", 10 | "credentials": { 11 | "username": "", 12 | "password": "" 13 | }, 14 | "port": 9091 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | /bot/test.js 39 | /bot/torrent-example.json 40 | -------------------------------------------------------------------------------- /bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transmission-telegram-bot", 3 | "version": "0.1.0", 4 | "description": "Simple telegram bot for controlling your your torrents status", 5 | "main": "bot.js", 6 | "scripts": { 7 | "bot": "node bot.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/raffaelecalza/transmission-telegram-bot.git" 12 | }, 13 | "author": "Calzà Raffaele ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/raffaelecalza/transmission-telegram-bot/issues" 17 | }, 18 | "homepage": "https://github.com/raffaelecalza/transmission-telegram-bot#readme", 19 | "dependencies": { 20 | "date-and-time": "^0.3.0", 21 | "handlebars": "^4.0.5", 22 | "node-telegram-bot-api": "^0.23.3", 23 | "prettysize": "0.0.3", 24 | "transmission": "^0.4.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Raffaele Calzà 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bot/notification-manager.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* 3 | _______ _ _ ____ _ 4 | |__ __| (_) (_) | _ \ | | 5 | | |_ __ __ _ _ __ ___ _ __ ___ _ ___ ___ _ ___ _ __ | |_) | ___ | |_ 6 | | | '__/ _` | '_ \/ __| '_ ` _ \| / __/ __| |/ _ \| '_ \ | _ < / _ \| __| 7 | | | | | (_| | | | \__ \ | | | | | \__ \__ \ | (_) | | | | | |_) | (_) | |_ 8 | |_|_| \__,_|_| |_|___/_| |_| |_|_|___/___/_|\___/|_| |_| |____/ \___/ \__| 9 | 10 | © 2016 - Calzà Raffaele (raffaelecalza4@gmail.com) 11 | Github repository: https://github.com/raffaelecalza/transmission-telegram-bot 12 | */ 13 | const fs = require('fs'); 14 | 15 | var exports = module.exports = {}; 16 | 17 | exports.fileExists = () => { 18 | return fs.existsSync(__dirname + '/user-notification.json'); 19 | } 20 | 21 | exports.loadFile = () => { 22 | return JSON.parse(fs.readFileSync(__dirname + '/user-notification.json', 'utf8')); 23 | } 24 | 25 | exports.saveFile = (obj) => { 26 | fs.writeFile(__dirname + '/user-notification.json', JSON.stringify(obj), (err) => { 27 | if(err) throw err; 28 | }); 29 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | WebTorrent 4 |
5 | Transmission Bot for Telegram 6 |
7 |

8 |

Do you like the project? Buy me a coffee or a beer 🍻

9 | So, you want to controll your torrent status from anywhere in the world, but you don't know how to do this, right? Here is the solution, this is a simple telegram bot that allows you to check the status of your torrents, add, stop, remove, briefly these are all the basic actions that you do with the program or web interface. 10 | 11 | #### Summary: 12 | * [List of available commands](#available-commands) 13 | * [How to install it](#how-to-install-it) 14 | * [Note](#note) 15 | * [Bugs, support and suggestions](#report-bugs-or-suggestions) 16 | 17 | ## Available commands 18 | * List of all torrents; 19 | * Torrent status - Get all details about a torrent; 20 | * Torrent start - Restart a torrent that is paused; 21 | * Torrent stop - Stop a torrent; 22 | * Torrent remove - Remove a torrent; 23 | * Add torrent - Add a torrent from a .torrent file or from an url; 24 | 25 | ## How to install it 26 | Clone the repository with git clone https://github.com/raffaelecalza/transmission-telegram-bot.git or download it as a zip file from [here](https://github.com/raffaelecalza/transmission-telegram-bot/releases/latest). 27 | ### 1) Register your bot name 28 | First of all you have to register your bot's name. To do this, begin a new chat to @BotFather. Send /newbot command. Then send to him the name of your bot (e.g. My Wonderful Bot). Then send a username for the bot (NOTE: this must end with the word 'bot'), e.g. MyWonderfulBot. After that the Bot Father will send you a message that contains the TOKEN for the bot. Save it because in the fourth step we'll use it.
29 | 30 | 31 | ### 2) Set bot commands (Optional) 32 | This step is optional because the bot has a custom keyboard, but I suggest you to set the commands. 33 | After you have register your bot and take the token you have to send to BotFather the full list of commands. Use the /setcommands and send this string to BotFather:
34 | ``` 35 | torrentlist - Get the list of all torrents 36 | torrentstatus - Get all details about a torrent 37 | torrentadd - Add a torrent from url 38 | torrentstart - Start a paused torrent 39 | torrentstop - Stop a torrent 40 | torrentremove - Remove a torrent 41 | settings - Set your preferences 42 | help - Get the list of available commands 43 | ``` 44 |

45 | 46 | ### 3) Install NodeJS, NPM and PM2 47 | Go to the [next step](#4-configure-your-bot) if you have already installed NodeJS, NPM and PM2. 48 | #### Install NodeJS and NPM 49 | For Windows and OSX go to NodeJS site and download the installer (https://nodejs.org/en/download/). This also will install NPM. 50 | 51 | For linux users, follow this guide (https://nodejs.org/en/download/package-manager/). 52 | #### Install all project dependencies 53 | Open up a terminal, go to your bot's folder and then run npm install. 54 | #### Install PM2 55 | For running your bot forever and as a daemon, you have to install a simple library called PM2 - Process Manager 2. So, Open a console and type sudo npm install -g pm2. If everything goes right try to type this command in the terminal pm2 status, and you should see an empty list of applications. 56 | ### 4) Configure your bot 57 | Now, open the config.json file with a text editor. Replace the token string with your bot's token. If you already know your chats id insert it in the array. 58 | In the transmission section, insert the ip address of the computer where transmission is installed (localhost if the bot runs in the same machine). Insert your username and password if you've set it, otherwise leave this fileds empty (don't delete them). The last step is to specify the number of the port (if you have changed it). 59 | ```javascript 60 | { 61 | "bot": { 62 | "token": "your bot's token", 63 | "users": [000000, 000000, 2222222] 64 | }, 65 | "transmission": { 66 | "address": "your ip address", 67 | "credentials": { 68 | "username": "leave this empty if you doesn't have a username", 69 | "password": "leave this empty if you doesn't have a password" 70 | }, 71 | "port": 9091 72 | } 73 | } 74 | ``` 75 | If you don't know your chat id, open a terminal and go to your bot repo's folder, open the bot/ folder then start your bot (after have installed all packages in the 3 step) with the command node bot.js. Try to send a message to your bot, you'll see in the console your chat id, hit Ctrl + C to stop the bot and insert your chat id in the config file.
76 | **NOTE: you must have inserted at least the bot token in the config file but i recommend you to configure also Transmission before running your bot.** 77 |

78 | 79 | ### 5) Run your bot 80 | Now you haven't to wait anymore, go to your bot folder and type pm2 start bot.js. Then your bot will start in background. 81 | 82 | ## NOTE 83 | If you shutdown or reboot your PC, PM2 will stop your application so you have to re-run your bot every time with the command pm2 start bot.js. 84 | ## Report bugs or suggestions 85 | If you have discovered a bug or if you have a suggestion, please open an issue or send me an email (raffaelecalza4@gmail.com) 86 | -------------------------------------------------------------------------------- /bot/formatter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* 3 | _______ _ _ ____ _ 4 | |__ __| (_) (_) | _ \ | | 5 | | |_ __ __ _ _ __ ___ _ __ ___ _ ___ ___ _ ___ _ __ | |_) | ___ | |_ 6 | | | '__/ _` | '_ \/ __| '_ ` _ \| / __/ __| |/ _ \| '_ \ | _ < / _ \| __| 7 | | | | | (_| | | | \__ \ | | | | | \__ \__ \ | (_) | | | | | |_) | (_) | |_ 8 | |_|_| \__,_|_| |_|___/_| |_| |_|_|___/___/_|\___/|_| |_| |____/ \___/ \__| 9 | 10 | © 2016 - Calzà Raffaele (raffaelecalza4@gmail.com) 11 | Github repository: https://github.com/raffaelecalza/transmission-telegram-bot 12 | */ 13 | 14 | const DateTime = require('date-and-time'); 15 | const Handlebars = require('handlebars'); 16 | const pretty = require('prettysize'); 17 | 18 | const torrentStatus = [ 19 | 'Stopped', 20 | 'Check wait', 21 | 'Check', 22 | 'Download wait', 23 | 'Download', 24 | 'Seed wait', 25 | 'Seed', 26 | 'Isolated' 27 | ]; 28 | 29 | var exports = module.exports = {}; 30 | 31 | // Handlebars helper 32 | Handlebars.registerHelper('getStatusType', (type) => { 33 | return torrentStatus[type] || 'Unknown'; 34 | }); 35 | Handlebars.registerHelper('torrentPercentage', (percent) => { 36 | return (percent * 100).toFixed(2) + '%'; 37 | }) 38 | Handlebars.registerHelper('getRemainingTime', (seconds) => { 39 | if (seconds < 0 || seconds >= (999 * 60 * 60)) 40 | return 'remaining time unknown'; 41 | 42 | var days = Math.floor(seconds / 86400), 43 | hours = Math.floor((seconds % 86400) / 3600), 44 | minutes = Math.floor((seconds % 3600) / 60), 45 | seconds = Math.floor(seconds % 60), 46 | d = days + ' ' + (days > 1 ? 'days' : 'day'), 47 | h = hours + ' ' + (hours > 1 ? 'hours' : 'hour'), 48 | m = minutes + ' ' + (minutes > 1 ? 'minutes' : 'minute'), 49 | s = seconds + ' ' + (seconds > 1 ? 'seconds' : 'second'); 50 | 51 | if (days) { 52 | if (days >= 4 || !hours) 53 | return d + ' remaining'; 54 | return d + ', ' + h + ' remaining'; 55 | } 56 | if (hours) { 57 | if (hours >= 4 || !minutes) 58 | return h + ' remaining'; 59 | return h + ', ' + m + ' remaining'; 60 | } 61 | if (minutes) { 62 | if (minutes >= 4 || !seconds) 63 | return m + ' remaining'; 64 | return m + ', ' + s + ' remaining'; 65 | } 66 | 67 | return s + ' remaining'; 68 | }) 69 | Handlebars.registerHelper('speed', (value) => { 70 | return pretty(value); 71 | }) 72 | Handlebars.registerHelper('parseDate', (date) => { 73 | // See #10 74 | if(date == 0) return new Date(); 75 | 76 | var mEpoch = parseInt(date); 77 | mEpoch *= 1000; 78 | return new Date(mEpoch); 79 | }) 80 | Handlebars.registerHelper('formatDate', (date, format) => { 81 | return DateTime.format(date, format); 82 | }) 83 | Handlebars.registerHelper('differenceBeetwenDates', (firstDate, secondDate) => { 84 | let seconds = DateTime.subtract(secondDate, firstDate).toSeconds(); 85 | let string = ''; 86 | let sec_num = parseInt(seconds, 10); 87 | let hours = Math.floor(sec_num / 3600); 88 | let minutes = Math.floor((sec_num - (hours * 3600)) / 60); 89 | seconds = sec_num - (hours * 3600) - (minutes * 60); 90 | 91 | if (hours > 0) 92 | string += hours + ' hours, '; 93 | if (minutes > 0) 94 | string += minutes + ' minutes, '; 95 | if (seconds > 0) 96 | string += seconds + ' seconds'; 97 | if (string.length == 0) 98 | string = 'Time not available'; 99 | return string; 100 | }) 101 | Handlebars.registerHelper('enableOrNot', (enable) => {return enable ? 'enabled' : 'not enabled'}) 102 | /* Torrents list template */ 103 | let torrentsListTemplate = `List of current torrents and their status: 104 | {{#each this}} 105 | {{id}}) {{name}} ({{getStatusType status}}) 106 | ➗ {{torrentPercentage percentDone}} 107 | ⌛️ {{getRemainingTime eta}} 108 | ⬇️ {{speed rateDownload}}/s - ⬆️ {{speed rateUpload}}/s 109 | 110 | 111 | {{/each}}`; 112 | 113 | exports.torrentsList = Handlebars.compile(torrentsListTemplate, {noEscape: true}); 114 | 115 | /* Torrent details template */ 116 | let torrentDetailsTemplate = `{{name}} 117 | 118 | Status = {{getStatusType status}} 119 | ⌛️ {{getRemainingTime eta}} 120 | ➗ {{torrentPercentage percentDone}} 121 | ⬇️ {{speed rateDownload}}/s - ⬆️ {{speed rateUpload}}/s 122 | 123 | Size: {{speed sizeWhenDone}} 124 | 📅 Added: {{formatDate (parseDate addedDate) 'dddd, DD MMMM HH:mm'}} 125 | 📂 {{downloadDir}} 126 | 👥 Peers connected: {{peersConnected}} 127 | `; 128 | 129 | exports.torrentDetails = Handlebars.compile(torrentDetailsTemplate, {noEscape: true}); 130 | 131 | /* New torrent added template */ 132 | let newTorrentTemplate = `The torrent was added succesfully 👌, here are some information about it: 133 | • ID torrent: {{id}}; 134 | • Name: {{name}} 135 | `; 136 | exports.newTorrent = Handlebars.compile(newTorrentTemplate, {noEscape: true}); 137 | 138 | exports.errorMessage = (err) => { 139 | return 'Ops there was an error 😰, here are some details:\n' + err; 140 | } 141 | 142 | /* Complete torrent template */ 143 | let completeTorrentTemplate = `Oh, a torrent has been downloaded completely 🙌\nHere are some details 👇: 144 | {{name}} 145 | 146 | 📅 {{formatDate (parseDate addedDate) 'DD/MM HH:mm'}} - {{formatDate (parseDate doneDate) 'DD/MM HH:mm'}} 147 | 🕔 {{differenceBeetwenDates (parseDate addedDate) (parseDate doneDate)}} 148 | Size: {{speed sizeWhenDone}} 149 | 150 | 📂 {{downloadDir}} 151 | `; 152 | exports.formatComplete = Handlebars.compile(completeTorrentTemplate, {noEscape: true}); 153 | 154 | /* Session details */ 155 | let sessionDetailsTemplate = `Transmission version: {{version}} 156 | Config dir:
{{config-dir}}
157 | 158 | Free space: {{speed download-dir-free-space}} 159 | Download directory:
{{download-dir}}
160 | Incomplete directory{{#if incomplete-dir-enabled}}:
{{incomplete-dir}}
{{else}} not enabled{{/if}} 161 | 162 | ⬇️ Speed limit{{#if speed-limit-down-enabled}}: {{speed-limit-down}}kB/s{{else}} not enabled{{/if}} 163 | ⬆️ Speed limit{{#if speed-limit-up-enabled}}: {{speed-limit-up}}kB/s{{else}} not enabled{{/if}} 164 | 165 | 👥 Peers limit: 166 | • Global = {{peer-limit-global}} 167 | • Per torrent = {{peer-limit-per-torrent}} 168 | 169 | Download queue {{enableOrNot download-queue-enabled}} 170 | `; 171 | exports.sessionDetails = Handlebars.compile(sessionDetailsTemplate, {noEscape: true}); -------------------------------------------------------------------------------- /bot/engine.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* 3 | _______ _ _ ____ _ 4 | |__ __| (_) (_) | _ \ | | 5 | | |_ __ __ _ _ __ ___ _ __ ___ _ ___ ___ _ ___ _ __ | |_) | ___ | |_ 6 | | | '__/ _` | '_ \/ __| '_ ` _ \| / __/ __| |/ _ \| '_ \ | _ < / _ \| __| 7 | | | | | (_| | | | \__ \ | | | | | \__ \__ \ | (_) | | | | | |_) | (_) | |_ 8 | |_|_| \__,_|_| |_|___/_| |_| |_|_|___/___/_|\___/|_| |_| |____/ \___/ \__| 9 | 10 | © 2016 - Calzà Raffaele (raffaelecalza4@gmail.com) 11 | Github repository: https://github.com/raffaelecalza/transmission-telegram-bot 12 | */ 13 | const Transmission = require('transmission'); 14 | const formatter = require('./formatter.js'); 15 | const config = require('./config.json'); 16 | 17 | console.log('Configuring transmission session'); 18 | const transmission = new Transmission({ 19 | port: config.transmission.port, 20 | host: config.transmission.address, 21 | url: config.transmission.path, 22 | username: config.transmission.credentials.username, 23 | password: config.transmission.credentials.password 24 | }); 25 | console.log(`-------- Session configured -------- 26 | IP address and port --> ${config.transmission.address}:${config.transmission.port} 27 | Username: ${config.transmission.credentials.username|| 'none'} 28 | Password: ${config.transmission.credentials.password || 'none'} 29 | `); 30 | 31 | var exports = module.exports = {}; 32 | var oldList = exports.torrents = []; 33 | 34 | exports.updateTorrentList = () => { 35 | transmission.get(function (err, arg) { 36 | if (err) 37 | console.error(err); 38 | else { 39 | oldList = exports.torrents; 40 | exports.torrents = arg.torrents; 41 | console.log('Downloaded the new list of torrents'); 42 | 43 | exports.checkCompletedTorrents(); 44 | } 45 | }); 46 | } 47 | 48 | exports.checkCompletedTorrents = () => { 49 | oldList.forEach(torrent => { 50 | // Search the torrent in the new list 51 | for (var i = 0; i < exports.torrents.length; i++) { 52 | if (torrent.name === exports.torrents[i].name && torrent.status != exports.torrents[i].status && exports.torrents[i].status === 6) 53 | exports.torrentCompleted(formatter.formatComplete(torrent)); 54 | } 55 | }); 56 | } 57 | 58 | // Create a keyboard with all torrent 59 | exports.getKeyboard = () => { 60 | var keyboard = [['Cancel']]; 61 | exports.torrents.forEach(torrent => { 62 | keyboard.push([`${torrent.id}) ${torrent.name}`]); 63 | }); 64 | return keyboard; 65 | } 66 | 67 | exports.getKeyboardActive = () => { 68 | var keyboard = [['Cancel']]; 69 | exports.torrents.forEach(torrent => { 70 | if (torrent.status > 3) 71 | keyboard.push([`${torrent.id}) ${torrent.name}`]); 72 | }); 73 | return keyboard; 74 | } 75 | 76 | exports.getKeyboardPaused = () => { 77 | var keyboard = [['Cancel']]; 78 | exports.torrents.forEach(torrent => { 79 | if (torrent.status == 0) 80 | keyboard.push([`${torrent.id}) ${torrent.name}`]); 81 | }); 82 | return keyboard; 83 | } 84 | 85 | exports.getTorrentsList = (success, error) => { 86 | transmission.get(function (err, arg) { 87 | if (err) 88 | error(formatter.errorMessage(err)); 89 | else { 90 | exports.torrents = arg.torrents; 91 | success(formatter.torrentsList(arg.torrents)); 92 | } 93 | }); 94 | } 95 | 96 | exports.getTorrentDetails = (id, success, error) => { 97 | transmission.get(parseInt(id), function (err, result) { 98 | if (err) { 99 | error(formatter.errorMessage(err)); 100 | return; 101 | } 102 | if (result.torrents.length > 0) 103 | success(formatter.torrentDetails(result.torrents[0])); 104 | }); 105 | } 106 | 107 | // Add a torrent from url 108 | exports.addTorrent = (url, success, error) => { 109 | transmission.addUrl(url, function (err, result) { 110 | if (err) { 111 | error(formatter.errorMessage(err)); 112 | return; 113 | } 114 | 115 | // Update torrent list 116 | exports.updateTorrentList(); 117 | success(formatter.newTorrent(result)); 118 | }); 119 | } 120 | 121 | exports.pauseTorrent = (id, success, error) => { 122 | transmission.stop(parseInt(id), function (err, result) { 123 | if (err) 124 | error(formatter.errorMessage(err)); 125 | else { 126 | // Update torrent list 127 | exports.updateTorrentList(); 128 | success(result); 129 | } 130 | }); 131 | } 132 | 133 | exports.startTorrent = (id, success, error) => { 134 | transmission.start(parseInt(id), function (err, result) { 135 | if (err) 136 | error(formatter.errorMessage(err)); 137 | else { 138 | // Update torrent list 139 | exports.updateTorrentList(); 140 | success(result); 141 | } 142 | }); 143 | } 144 | 145 | exports.removeTorrent = (id, success, error) => { 146 | transmission.remove(parseInt(id), function (err, result) { 147 | if (err) 148 | error(formatter.errorMessage(err)); 149 | else { 150 | // Update torrent list 151 | exports.updateTorrentList(); 152 | success(result); 153 | } 154 | }); 155 | } 156 | 157 | // Hide keyboard for bot 158 | exports.listOfCommandsKeyboard = { 159 | reply_markup: JSON.stringify({ 160 | keyboard: [ 161 | ['📋 List of all torrents'], 162 | ['📈 Status', '➕ Add torrent'], 163 | ['▶️ Start', '⏸ Pause', '❌ Remove'], 164 | ['⚙ Settings', '❔ Help'] 165 | ] 166 | }), 167 | parse_mode: 'html', 168 | disable_web_page_preview: true 169 | } 170 | 171 | exports.hideKeyboard = { 172 | reply_markup: JSON.stringify({ 173 | keyboard: [['Cancel']] 174 | }) 175 | } 176 | 177 | // Settings 178 | exports.settingsKeyboard = { 179 | reply_markup: JSON.stringify({ 180 | keyboard: [['🔙 menu'], ['🖥 Transmission info'], ['🔔 User notification'], ['📂 Set download folder']] 181 | }), 182 | parse_mode: 'html' 183 | } 184 | 185 | exports.getSessionDetails = (callback) => { 186 | transmission.session(function(err, arg) { 187 | if(err) 188 | callback(formatter.errorMessage(err)); 189 | else 190 | callback(formatter.sessionDetails(arg)); 191 | }); 192 | } 193 | exports.setSettings = (command, success, error) => { 194 | transmission.session(command, function(err, arg) { 195 | if(err) 196 | error(formatter.errorMessage(err)); 197 | else 198 | success(); 199 | }); 200 | } 201 | // End of settings 202 | 203 | // String to send when the list of torrents is empty 204 | exports.noTorrentsText = 'Mmh 😕 it seems that there isn\'t any torrent in the list...\nAdd one by using the /addtorrent command 😉'; 205 | 206 | /* 207 | * Help message 208 | */ 209 | exports.helpMsg = `Transmission Telegram Bot 210 | Available commands: 211 | • List of torrents 212 | • Torrent status 213 | • Add torrent 214 | • Start, Pause, Remove torrent 215 | • Settings 216 | 217 | If you have a suggestion or discovered a bug please report me 👉 here 218 | 🤖 Bot version: ${config.bot.version} 219 | 220 | Creator: Raffaele Calzà, buy me a coffee or a beer 🍻 click here 221 | Follow me on the socials if you like the project, thanks 😎👍`; 222 | 223 | // Download the torrent list every minute 224 | exports.updateTorrentList(); 225 | 226 | setInterval(exports.updateTorrentList, 60000); 227 | -------------------------------------------------------------------------------- /bot/bot.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* 3 | _______ _ _ ____ _ 4 | |__ __| (_) (_) | _ \ | | 5 | | |_ __ __ _ _ __ ___ _ __ ___ _ ___ ___ _ ___ _ __ | |_) | ___ | |_ 6 | | | '__/ _` | '_ \/ __| '_ ` _ \| / __/ __| |/ _ \| '_ \ | _ < / _ \| __| 7 | | | | | (_| | | | \__ \ | | | | | \__ \__ \ | (_) | | | | | |_) | (_) | |_ 8 | |_|_| \__,_|_| |_|___/_| |_| |_|_|___/___/_|\___/|_| |_| |____/ \___/ \__| 9 | 10 | © 2016 - Calzà Raffaele (raffaelecalza4@gmail.com) 11 | Github repository: https://github.com/raffaelecalza/transmission-telegram-bot 12 | */ 13 | 14 | const TelegramBot = require('node-telegram-bot-api'); 15 | const DateTime = require('date-and-time'); 16 | const engine = require('./engine.js'); 17 | const NotificationManager = require('./notification-manager.js'); 18 | 19 | const config = require('./config.json'); 20 | 21 | var userStates = {}; 22 | var userNotification; 23 | 24 | if(NotificationManager.fileExists()) 25 | userNotification = NotificationManager.loadFile(); 26 | else { 27 | userNotification = {}; 28 | // Enable notification for each user 29 | config.bot.users.forEach((user) => { 30 | userNotification[user] = true; 31 | }); 32 | NotificationManager.saveFile(userNotification); 33 | } 34 | 35 | console.log('Initializing the bot...') 36 | const bot = new TelegramBot(config.bot.token, { 37 | polling: true 38 | }); 39 | 40 | bot.getMe().then(function (info) { 41 | console.log(` 42 | ${info.first_name} is ready, the username is @${info.username} 43 | `); 44 | }); 45 | 46 | // End of configuration 47 | 48 | // Display every message in the console 49 | bot.on('message', function (msg) { 50 | console.log(` 51 | Oh... there's a new incoming message sir! 52 | -------- Here are some details -------- 53 | Authorized user: ${config.bot.users.indexOf(msg.from.id) > -1 ? 'yes' : 'no'} 54 | Date: ${DateTime.format(new Date(msg.date * 1000), 'DD/MM HH:mm')} 55 | From user: ${msg.chat.username || 'no username provided'} 56 | Chat ID: ${msg.chat.id} 57 | Name and surname: ${msg.from.first_name} ${msg.from.last_name} 58 | Message id: ${msg.message_id} 59 | Message text: ${msg.text || 'no text'} 60 | `); 61 | }); 62 | 63 | console.log('-------- Notify autorizhed users that the bot is up --------'); 64 | const wokeUpMsg = `Hey, I woke up just now 😎 and I'm ready to respond to your commands 🙌 65 | 66 | 👉 If you need help, use the /help command 67 | 68 | Anyway when a torrent finishes the download, I'll send you a notification 🔔`; 69 | 70 | config.bot.users.forEach(user => { 71 | if(userNotification[user]) 72 | bot.sendMessage(user, wokeUpMsg, engine.listOfCommandsKeyboard); 73 | }) 74 | 75 | // Start message 76 | bot.onText(/\/start/, function (msg) { 77 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 78 | var chatId = msg.chat.id; 79 | var reply = 'Hi ' + msg.chat.first_name + ' 🙌, I\'m your 🤖\nI\'ve been created to give you all the informations regarding the status of your torrents 😊. Start with /help to get a list of all available commands'; 80 | bot.sendMessage(chatId, reply, engine.listOfCommandsKeyboard); 81 | }); 82 | 83 | // Get the list of all torrents 84 | bot.onText(/\/torrentlist|List of all torrents/, function (msg) { 85 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 86 | var chatId = msg.chat.id; 87 | 88 | engine.getTorrentsList((msg) => { 89 | if (engine.torrents.length == 0) 90 | bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard); 91 | else 92 | bot.sendMessage(chatId, msg, engine.listOfCommandsKeyboard); 93 | }, (err) => { 94 | bot.sendMessage(chatId, err); 95 | }); 96 | }); 97 | 98 | // Get all details about a torrent 99 | bot.onText(/\/torrentstatus|Status/, function (msg) { 100 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 101 | var chatId = msg.chat.id; 102 | var keyb = engine.getKeyboard(); 103 | var opts = { 104 | reply_markup: JSON.stringify({ 105 | keyboard: keyb 106 | }) 107 | }; 108 | if (engine.torrents.length == 0) 109 | bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard); 110 | else { 111 | bot.sendMessage(chatId, 'Select a torrent and you\'ll receive all information about it', opts); 112 | userStates[chatId] = 'details'; 113 | } 114 | }); 115 | 116 | // Start torrent 117 | bot.onText(/\/torrentstart|▶️ Start/, function (msg) { 118 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 119 | var chatId = msg.chat.id; 120 | var keyb = engine.getKeyboardPaused(); 121 | var opts = { 122 | reply_markup: JSON.stringify({ 123 | keyboard: keyb 124 | }) 125 | }; 126 | 127 | if (engine.torrents.length == 0) 128 | bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard); 129 | else if (keyb.length == 1) 130 | bot.sendMessage(chatId, 'All torrents are in download queue', engine.listOfCommandsKeyboard); 131 | else { 132 | bot.sendMessage(chatId, 'Please send me a torrent to put in the download queue 😊', opts); 133 | userStates[chatId] = 'start'; 134 | } 135 | }); 136 | 137 | // Stop torrent 138 | bot.onText(/\/torrentstop|⏸ Pause/, function (msg) { 139 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 140 | var chatId = msg.chat.id; 141 | var keyb = engine.getKeyboardActive(); 142 | var opts = { 143 | reply_markup: JSON.stringify({ 144 | keyboard: keyb 145 | }) 146 | }; 147 | 148 | if (engine.torrents.length == 0) 149 | bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard); 150 | else if (keyb.length == 1) 151 | bot.sendMessage(chatId, "All torrents are currently paused", engine.listOfCommandsKeyboard); 152 | else { 153 | bot.sendMessage(chatId, 'Which torrent would you stop?', opts); 154 | userStates[chatId] = 'stop'; 155 | } 156 | }); 157 | 158 | // Remove torrent 159 | bot.onText(/\/torrentremove|❌ Remove/, function (msg) { 160 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 161 | var chatId = msg.chat.id; 162 | var keyb = engine.getKeyboard(); 163 | var opts = { 164 | reply_markup: JSON.stringify({ 165 | keyboard: keyb 166 | }) 167 | }; 168 | 169 | if (engine.torrents.length == 0) 170 | bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard); 171 | else { 172 | bot.sendMessage(chatId, '⚠️ Be careful! Once you remove it, you can not retrieve it\nSend me the torrent that you would remove 😊', opts); 173 | userStates[chatId] = 'remove'; 174 | } 175 | }) 176 | 177 | bot.onText(/Yes|No/, function (msg) { 178 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 179 | var chatId = msg.chat.id; 180 | 181 | var torrentId = userStates[chatId] || ''; 182 | var answer = msg.text; 183 | 184 | if (answer == 'Yes') 185 | engine.removeTorrent(torrentId, (details) => { 186 | bot.sendMessage(chatId, 'Torrent correctly removed\nUse /torrentstatus to see the updated torrents list', engine.listOfCommandsKeyboard); 187 | }, (err) => { 188 | bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard); 189 | }); 190 | else 191 | bot.sendMessage(chatId, 'The operation was canceled, narrow escape 😪', engine.listOfCommandsKeyboard); 192 | }) 193 | 194 | bot.onText(/\d+\) .+/, function (msg) { 195 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 196 | var chatId = msg.chat.id; 197 | 198 | var torrentId = msg.text.match(/\d+/)[0]; 199 | 200 | var torrentAction = userStates[chatId] || ''; 201 | 202 | if (torrentAction == 'stop') 203 | engine.pauseTorrent(torrentId, (details) => { 204 | bot.sendMessage(chatId, 'Torrent correctly stopped\nUse /torrentstatus to see the updated torrents list', engine.listOfCommandsKeyboard); 205 | }, (err) => { 206 | bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard); 207 | }); 208 | else if (torrentAction == 'details') 209 | engine.getTorrentDetails(torrentId, (details) => { 210 | bot.sendMessage(chatId, details, engine.listOfCommandsKeyboard); 211 | }, (err) => { 212 | bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard); 213 | }); 214 | else if (torrentAction == 'start') 215 | engine.startTorrent(torrentId, (details) => { 216 | bot.sendMessage(chatId, 'Torrent correctly started\nUse /torrentstatus to see the updated torrents list', engine.listOfCommandsKeyboard); 217 | }, (err) => { 218 | bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard); 219 | }); 220 | else if (torrentAction == 'remove') { 221 | userStates[chatId] = torrentId; 222 | bot.sendMessage(chatId, 'Are you sure you want to remove this torrent?', { 223 | reply_markup: JSON.stringify({ 224 | keyboard: [['Yes', 'No']] 225 | }) 226 | }); 227 | } 228 | }); 229 | 230 | // Add a torrent from url 231 | bot.onText(/\/addtorrent|Add torrent/, function (msg) { 232 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 233 | var chatId = msg.chat.id; 234 | 235 | bot.sendMessage(chatId, 'Please send me a torrent url or send me a torrent file (e.g. file.torrent)', engine.hideKeyboard); 236 | userStates[chatId] = 'add'; 237 | }); 238 | 239 | bot.onText(/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/, function (msg) { 240 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 241 | var chatId = msg.chat.id; 242 | 243 | var torrentAction = userStates[chatId] || ''; 244 | if (torrentAction == 'add') 245 | engine.addTorrent(msg.text, (details) => { 246 | bot.sendMessage(chatId, details, engine.listOfCommandsKeyboard); 247 | }, (err) => { 248 | bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard); 249 | }); 250 | }); 251 | 252 | // Cancel Operation 253 | bot.onText(/Cancel/, function (msg) { 254 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 255 | var chatId = msg.chat.id; 256 | userStates[chatId] = ''; 257 | bot.sendMessage(chatId, 'The operation was cancelled', engine.listOfCommandsKeyboard); 258 | }) 259 | 260 | // Receive a document (for add torrent command) 261 | bot.on('document', function (msg) { 262 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 263 | var chatId = msg.chat.id; 264 | var fileId = msg.document.file_id; 265 | bot.getFileLink(fileId).then((link) => { 266 | engine.addTorrent(link, (details) => { 267 | bot.sendMessage(chatId, 'The torrent was added succesfully, here are some information about it\n' + details, engine.listOfCommandsKeyboard); 268 | }, (err) => { 269 | bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard); 270 | }); 271 | }, (err) => { 272 | bot.sendMessage(chatId, 'Oops 😰, something seems to have gone wrong while trying to request the link to Telegram servers 😒... Please try again to send the file\nSome details about the error:\n' + JSON.stringify(err)); 273 | }); 274 | }); 275 | 276 | // Settings command 277 | bot.onText(/\/settings|⚙ Settings/, function (msg) { 278 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 279 | var chatId = msg.chat.id; 280 | 281 | bot.sendMessage(chatId, 'Please select one voice from the list', engine.settingsKeyboard); 282 | }) 283 | 284 | bot.onText(/🔙 menu/, function(msg) { 285 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 286 | 287 | var chatId = msg.chat.id; 288 | bot.sendMessage(chatId, 'What would you see?', engine.listOfCommandsKeyboard); 289 | }) 290 | 291 | bot.onText(/Transmission info/, function(msg) { 292 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 293 | var chatId = msg.chat.id; 294 | engine.getSessionDetails((msg) => { 295 | bot.sendMessage(chatId, msg, engine.settingsKeyboard); 296 | }); 297 | }) 298 | 299 | bot.onText(/User notification/, function(msg) { 300 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 301 | var chatId = msg.chat.id; 302 | bot.sendMessage(chatId, 'Would you enable or disable the notifications?', { 303 | reply_markup: JSON.stringify({ 304 | keyboard: [['Enable', 'Disable'], ['Cancel']] 305 | }) 306 | }); 307 | }) 308 | 309 | bot.onText(/Enable|Disable/, function(msg) { 310 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 311 | var chatId = msg.chat.id; 312 | if(msg.text == 'Enable') { 313 | userNotification[chatId] = true; 314 | bot.sendMessage(chatId, 'Notifications enabled 🔔, you\'ll receive a message when a torrent is completely downloaded', engine.settingsKeyboard); 315 | } else if(msg.text == 'Disable') { 316 | userNotification[chatId] = false; 317 | bot.sendMessage(chatId, 'Notification disabled 🔕, you\'ll not receive any notification when a torrent is downloaded completely', engine.settingsKeyboard); 318 | } 319 | NotificationManager.saveFile(userNotification); 320 | }) 321 | 322 | bot.onText(/Set download folder/, function(msg) { 323 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 324 | var chatId = msg.chat.id; 325 | userStates[chatId] = 'set-folder' || ''; 326 | bot.sendMessage(chatId, 'Please send me the new folder where next torrents will be downloaded', engine.hideKeyboard); 327 | }) 328 | 329 | bot.onText(/(\/\w+)+\//g, function(msg) { 330 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 331 | var chatId = msg.chat.id; 332 | if(userStates[chatId] == 'set-folder') 333 | engine.setSettings({'download-dir': msg.text}, () => { 334 | bot.sendMessage(chatId, 'The download 📂 was changed 👌', engine.settingsKeyboard); 335 | }, (err) => { 336 | bot.sendMessage(chatId, err, engine.settingsKeyboard); 337 | }); 338 | }) 339 | 340 | // End of settings 341 | 342 | // Help instructions 343 | bot.onText(/\/help|❔ Help/, function (msg) { 344 | if (config.bot.users.indexOf(msg.from.id) == -1) return; 345 | var chatId = msg.chat.id; 346 | 347 | bot.sendMessage(chatId, engine.helpMsg, engine.listOfCommandsKeyboard); 348 | }); 349 | 350 | // Callback when a torrent is completed 351 | engine.torrentCompleted = (msg) => { 352 | config.bot.users.forEach((userId) => { 353 | if(userNotification[userId]) 354 | bot.sendMessage(userId, msg, engine.listOfCommandsKeyboard); 355 | }); 356 | }; --------------------------------------------------------------------------------