├── package.json ├── LICENSE ├── .gitignore ├── README.md └── index.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sticker_time_bot", 3 | "version": "1.0.0", 4 | "description": "Sticker Time Bot for Telegram", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node .", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/imxieyi/sticker_time_bot.git" 13 | }, 14 | "keywords": [ 15 | "sticker", 16 | "time", 17 | "bot", 18 | "telegram" 19 | ], 20 | "author": "imxieyi", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/imxieyi/sticker_time_bot/issues" 24 | }, 25 | "homepage": "https://github.com/imxieyi/sticker_time_bot#readme", 26 | "dependencies": { 27 | "bottleneck": "^2.19.5", 28 | "cron": "^1.4.1", 29 | "moment": "^2.22.2", 30 | "moment-timezone": "^0.5.21", 31 | "node-telegram-bot-api": "^0.51.0", 32 | "request": "^2.88.0", 33 | "request-promise-native": "^1.0.5", 34 | "winston": "^3.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Xie Yi 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | config.json 64 | data.json 65 | log.txt 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sticker Time Bot 2 | Telegram Link: [http://t.me/sticker_time_bot](http://t.me/sticker_time_bot) 3 | 4 | ## Introduction 5 | This is a [Telegram](https://telegram.org/) bot sending a sticker indicating time every hour. You can start or stop any time using commands. 6 | 7 | ## Commands 8 | **Start sending stickers:** `/start` 9 | 10 | **Stop sending stickers:** `/stop` 11 | 12 | **Set timezone:** `/timezone Asia/Shanghai` 13 | 14 | *List of timezones in tz database: [https://en.wikipedia.org/wiki/List_of_tz_database_time_zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)* 15 | 16 | **Enable/Disable auto deleting messages:** `/autodelete [on|off]` 17 | 18 | **Set sleep time:** `/sleeptime [0-23]` 19 | 20 | **Set wake time:** `/waketime [0-23]` 21 | 22 | **Delete sleep and wake time:** `/nosleep` 23 | 24 | **Add hour to alarm list:** `/addhour [0-23]` 25 | 26 | **Delete hour from alarm list:** `/delhour [0-23]` 27 | 28 | **List hours in alarm list:** `/listhours` 29 | 30 | **Clear alarm list:** `/clearhours` 31 | 32 | ## Environment 33 | - Node.js 8.0+ 34 | 35 | ## Installation 36 | ```sh 37 | npm install 38 | ``` 39 | 40 | ## Configuration 41 | Create a file config.json: 42 | ```json 43 | { 44 | "tg_bot_token": "Your Telegram bot token here", 45 | "log_file": "Log file" 46 | } 47 | ``` 48 | 49 | ## Start 50 | ```sh 51 | npm start 52 | ``` 53 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const config = require('./config'); 2 | const TelegramBot = require('node-telegram-bot-api'); 3 | const { createLogger, format, transports } = require('winston'); 4 | const { combine, timestamp, label, prettyPrint } = format; 5 | const fs = require('fs'); 6 | const moment = require('moment-timezone'); 7 | const CronJob = require('cron').CronJob; 8 | const Bottleneck = require('bottleneck'); 9 | 10 | const logger = createLogger({ 11 | level: (typeof config.level == 'undefined') ? 'info' : config.level, 12 | format: combine( 13 | timestamp(), 14 | prettyPrint() 15 | ), 16 | transports: [ 17 | new transports.Console(), 18 | new transports.File({ filename: config.log_file }) 19 | ] 20 | }); 21 | 22 | const token = config.tg_bot_token; 23 | 24 | const stickers = [ 25 | 'CAADBQAD8wADDxXNGeYW5EDuT_6aAg', 26 | 'CAADBQAD9AADDxXNGcJK3qzks8qLAg', 27 | 'CAADBQAD9QADDxXNGZ6Uniz2IyF3Ag', 28 | 'CAADBQAD9gADDxXNGRLp93L7_gSLAg', 29 | 'CAADBQAD9wADDxXNGRU9qwR8J2NjAg', 30 | 'CAADBQAD-AADDxXNGYQQoarLCyyeAg', 31 | 'CAADBQAD-QADDxXNGe5M6q3FOIs_Ag', 32 | 'CAADBQAD-gADDxXNGYlJ7FZM6M4rAg', 33 | 'CAADBQAD-wADDxXNGe8aqxEu9OCLAg', 34 | 'CAADBQAD_AADDxXNGSb44I6FN-UzAg', 35 | 'CAADBQAD_QADDxXNGYGYV17DXBbkAg', 36 | 'CAADBQAD_gADDxXNGWuj_Z6psGN4Ag' 37 | ]; 38 | 39 | const bot = new TelegramBot(token, { polling: true }); 40 | 41 | if (fs.existsSync('./data.json')) { 42 | var fdata = fs.readFileSync('./data.json', 'utf8'); 43 | var data = JSON.parse(fdata); 44 | // logger.info('Old data: ' + JSON.stringify(data)); 45 | } 46 | 47 | function saveData() { 48 | json = JSON.stringify(data); 49 | fs.writeFile('./data.json', json, 'utf8', (err) => { 50 | if (err != null) { 51 | logger.error('Failed to save data.json: ' + err); 52 | } 53 | }); 54 | } 55 | 56 | if (typeof data == 'undefined' || data == null) { 57 | logger.info('No data.json'); 58 | var data = { 59 | chatids: [], 60 | tzmap: {}, 61 | lastid: {}, 62 | autodelete: {}, 63 | timelist: {}, 64 | }; 65 | saveData(); 66 | } 67 | 68 | if (typeof data.chatids == 'undefined' || data.chatids == null) { 69 | data.chatids = []; 70 | saveData(); 71 | } 72 | 73 | if (typeof data.tzmap == 'undefined' || data.tzmap == null) { 74 | data.tzmap = {}; 75 | saveData(); 76 | } 77 | 78 | if (typeof data.lastid == 'undefined' || data.lastid == null) { 79 | data.lastid = {}; 80 | saveData(); 81 | } 82 | 83 | if (typeof data.sleeptime == 'undefined' || data.sleeptime == null) { 84 | data.sleeptime = {}; 85 | saveData(); 86 | } 87 | 88 | if (typeof data.waketime == 'undefined' || data.waketime == null) { 89 | data.waketime = {}; 90 | saveData(); 91 | } 92 | 93 | if (typeof data.autodelete == 'undefined' || data.autodelete == null) { 94 | data.autodelete = {}; 95 | saveData(); 96 | } 97 | 98 | if (typeof data.timelist == 'undefined' || data.timelist == null) { 99 | data.timelist = {}; 100 | saveData(); 101 | } 102 | 103 | bot.onText(/\/start/, (msg) => { 104 | const chatId = msg.chat.id; 105 | let index = data.chatids.indexOf(chatId); 106 | if (index > -1) { 107 | bot.sendMessage(chatId, 'Already started, chat ID: ' + chatId); 108 | return; 109 | } 110 | data.chatids.push(chatId); 111 | delete data.lastid[chatId]; 112 | saveData(); 113 | logger.info(chatId + ' started'); 114 | bot.sendMessage(chatId, 'Started, chat ID: ' + chatId); 115 | }); 116 | 117 | bot.onText(/^\/timezone(@sticker_time_bot)?(\s+([^\s]+))?$/, (msg, match) => { 118 | const chatId = msg.chat.id; 119 | if (match[3]) { 120 | if (moment.tz.zone(match[3])) { 121 | logger.info(chatId + ' set timezone to ' + match[3]); 122 | bot.sendMessage(chatId, 'Set timezone to ' + match[3]); 123 | data.tzmap[chatId] = match[3]; 124 | saveData(); 125 | } else { 126 | bot.sendMessage(chatId, 'Invalid timezone: ' + match[3]); 127 | } 128 | } else { 129 | let tz = data.tzmap[chatId]; 130 | if (tz) { 131 | bot.sendMessage(chatId, 'Current timezone: ' + data.tzmap[chatId]); 132 | } else { 133 | bot.sendMessage(chatId, 'Timezone not set, by default Asia/Shanghai.'); 134 | } 135 | } 136 | }); 137 | 138 | bot.onText(/^\/autodelete(@sticker_time_bot)?(\s+([^\s]+))?$/, (msg, match) => { 139 | const chatId = msg.chat.id; 140 | let index = data.chatids.indexOf(chatId); 141 | if (index <= -1) { 142 | bot.sendMessage(chatId, 'Not started, chat ID: ' + chatId); 143 | return; 144 | } 145 | if (match[3]) { 146 | if (match[3] === 'on') { 147 | bot.sendMessage(chatId, 'Enable auto deleting'); 148 | data.autodelete[chatId] = true; 149 | saveData(); 150 | logger.info(chatId + ' set autodelete: on'); 151 | } else if (match[3] === 'off') { 152 | bot.sendMessage(chatId, 'Disable auto deleting'); 153 | data.autodelete[chatId] = false; 154 | saveData(); 155 | logger.info(chatId + ' set autodelete: off'); 156 | } else { 157 | bot.sendMessage(chatId, 'Unknown command'); 158 | } 159 | } else { 160 | if (chatId in data.autodelete) { 161 | bot.sendMessage(chatId, 'Auto deleting status: ' + (data.autodelete[chatId] ? 'on' : 'off')); 162 | } else { 163 | bot.sendMessage(chatId, 'Auto deleting not set, by default off.'); 164 | } 165 | } 166 | }); 167 | 168 | bot.onText(/^\/sleeptime(@sticker_time_bot)?(\s+([^\s]+))?$/, (msg, match) => { 169 | const chatId = msg.chat.id; 170 | // bot.sendMessage(chatId, match[0]+' '+match[1]+' '+match[2]+' '+match[3]) 171 | if (match[3]) { 172 | var num = parseInt(match[3], 10); 173 | if (num <= 23 && num >= 0){ 174 | data.sleeptime[chatId] = num; 175 | var message = 'Set sleep time to ' + num + ':00'; 176 | if (chatId in data.timelist) { 177 | delete data.timelist[chatId]; 178 | message += ", list of hours has been deleted."; 179 | } 180 | saveData(); 181 | logger.info(chatId + ' set sleeptime to '+ num +':00'); 182 | bot.sendMessage(chatId, message); 183 | } else { 184 | bot.sendMessage(chatId, match[3]+' is an invalid time, 0-23 expected'); 185 | } 186 | } else { 187 | if (chatId in data.sleeptime) { 188 | bot.sendMessage(chatId, "Current sleep time: " + data.sleeptime[chatId]); 189 | } else { 190 | bot.sendMessage(chatId, "Sleep time not set"); 191 | } 192 | } 193 | }); 194 | 195 | bot.onText(/^\/waketime(@sticker_time_bot)?(\s+([^\s]+))?$/, (msg, match) => { 196 | const chatId = msg.chat.id; 197 | if (match[3]) { 198 | var num = parseInt(match[3], 10); 199 | if (num <= 23 && num >= 0){ 200 | data.waketime[chatId] = num; 201 | var message = "Set waketime to " + num + ":00"; 202 | if (chatId in data.timelist) { 203 | delete data.timelist[chatId]; 204 | message += ", list of hours has been deleted."; 205 | } 206 | saveData(); 207 | logger.info(chatId + ' set waketime to '+ num +':00'); 208 | bot.sendMessage(chatId, message); 209 | } else { 210 | bot.sendMessage(chatId, match[3]+' is an invalid time, 0-23 expected'); 211 | } 212 | } else { 213 | if (chatId in data.waketime) { 214 | bot.sendMessage(chatId, "Current wake time: " + data.waketime[chatId]); 215 | } else { 216 | bot.sendMessage(chatId, "Wake time not set"); 217 | } 218 | } 219 | }); 220 | 221 | bot.onText(/\/nosleep(@sticker_time_bot)?/, (msg) => { 222 | const chatId = msg.chat.id; 223 | if (!(chatId in data.sleeptime) && !(chatId in data.waketime)) { 224 | bot.sendMessage(chatId, 'Sleep time and wake time not set'); 225 | return; 226 | } 227 | if (chatId in data.sleeptime) { 228 | delete data.sleeptime[chatId]; 229 | } 230 | if (chatId in data.waketime) { 231 | delete data.waketime[chatId]; 232 | } 233 | saveData(); 234 | logger.info(chatId + ' deleted sleep time and wake time'); 235 | bot.sendMessage(chatId, 'Successfully deleted sleep time'); 236 | }); 237 | 238 | bot.onText(/^\/addhour(@sticker_time_bot)?(\s+([^\s]+))?$/, (msg, match) => { 239 | const chatId = msg.chat.id; 240 | // bot.sendMessage(chatId, match[0]+' '+match[1]+' '+match[2]+' '+match[3]) 241 | if (match[3]) { 242 | var num = parseInt(match[3], 10); 243 | if (num <= 23 && num >= 0){ 244 | if (chatId in data.timelist) { 245 | if (data.timelist[chatId].indexOf(num) > -1) { 246 | bot.sendMessage(chatId, 'Time '+ num +':00 already added'); 247 | return; 248 | } 249 | data.timelist[chatId].push(num); 250 | } else { 251 | data.timelist[chatId] = [num]; 252 | } 253 | var sleepDeleted = false; 254 | if (chatId in data.sleeptime) { 255 | delete data.sleeptime[chatId]; 256 | sleepDeleted = true; 257 | } 258 | if (chatId in data.waketime) { 259 | delete data.waketime[chatId]; 260 | sleepDeleted = true; 261 | } 262 | logger.info(chatId + ' added time '+ num +':00'); 263 | var message = 'Added time '+ num +':00'; 264 | if (sleepDeleted) { 265 | message += ', sleep time and wake time deleted'; 266 | } 267 | bot.sendMessage(chatId, message); 268 | saveData(); 269 | } else { 270 | bot.sendMessage(chatId, match[3]+' is an invalid time, 0-23 expected'); 271 | } 272 | } else { 273 | bot.sendMessage(chatId, 'Usage: /addhour [0-23]'); 274 | } 275 | }); 276 | 277 | bot.onText(/^\/delhour(@sticker_time_bot)?(\s+([^\s]+))?$/, (msg, match) => { 278 | const chatId = msg.chat.id; 279 | // bot.sendMessage(chatId, match[0]+' '+match[1]+' '+match[2]+' '+match[3]) 280 | if (match[3]) { 281 | var num = parseInt(match[3], 10); 282 | if (num <= 23 && num >= 0){ 283 | if (chatId in data.timelist) { 284 | var index = data.timelist[chatId].indexOf(num); 285 | if (index > -1) { 286 | data.timelist[chatId].splice(index, 1); 287 | var message = 'Deleted time '+ num +':00'; 288 | if (data.timelist[chatId].length == 0) { 289 | delete data.timelist[chatId]; 290 | message += ', list of hours deleted'; 291 | } 292 | logger.info(chatId + ' deleted time '+ num +':00'); 293 | bot.sendMessage(chatId, message); 294 | saveData(); 295 | return; 296 | } 297 | } 298 | bot.sendMessage(chatId, 'Time '+ num +':00 not added'); 299 | } else { 300 | bot.sendMessage(chatId, match[3]+' is an invalid time, 0-23 expected'); 301 | } 302 | } else { 303 | bot.sendMessage(chatId, 'Usage: /delhour [0-23]'); 304 | } 305 | }); 306 | 307 | bot.onText(/^\/listhours/, (msg, match) => { 308 | const chatId = msg.chat.id; 309 | if (chatId in data.timelist) { 310 | var list = data.timelist[chatId]; 311 | var str = ''; 312 | for (var i = 0; i < list.length; i++) { 313 | str += list[i] + ':00\n'; 314 | } 315 | bot.sendMessage(chatId, 'List of hours:\n'+str); 316 | } else { 317 | bot.sendMessage(chatId, 'No hours added'); 318 | } 319 | }); 320 | 321 | bot.onText(/^\/clearhours/, (msg, match) => { 322 | const chatId = msg.chat.id; 323 | if (chatId in data.timelist) { 324 | delete data.timelist[chatId]; 325 | logger.info(chatId + ' deleted list of hours'); 326 | bot.sendMessage(chatId, 'Successfully deleted list of hours'); 327 | } else { 328 | bot.sendMessage(chatId, 'No hours added'); 329 | } 330 | }); 331 | 332 | 333 | bot.onText(/\/stop/, (msg) => { 334 | const chatId = msg.chat.id; 335 | let index = data.chatids.indexOf(chatId); 336 | if (index > -1) { 337 | data.chatids.splice(index, 1); 338 | delete data.lastid[chatId]; 339 | saveData(); 340 | } else { 341 | bot.sendMessage(chatId, 'Not started, chat ID: ' + chatId); 342 | return; 343 | } 344 | logger.info(chatId + ' stopped'); 345 | bot.sendMessage(chatId, 'Stopped, chat ID: ' + chatId); 346 | }); 347 | 348 | // bot.on('sticker', (msg) => { 349 | // const chatId = msg.chat.id; 350 | // logger.info('[' + chatId + '] ' + msg.sticker.file_id); 351 | // }); 352 | 353 | bot.on('polling_error', (error) => { 354 | logger.error('[polling_error] ' + error.code); // => 'EFATAL' 355 | }); 356 | 357 | bot.on('webhook_error', (error) => { 358 | logger.error('[webhook_error] ' + error.code); // => 'EPARSE' 359 | }); 360 | 361 | const limiter = new Bottleneck({ 362 | maxConcurrent: 30, 363 | minTime: 33 364 | }); 365 | 366 | var cron = new CronJob('0 * * * *', function() { 367 | var date = new Date(); 368 | var chatsSent = 0; 369 | data.chatids.forEach(function (id) { 370 | let tz = data.tzmap[id]; 371 | if (!tz) { 372 | tz = 'Asia/Shanghai'; 373 | } 374 | let hour = moment().tz(tz).hours(); 375 | 376 | if (id in data.sleeptime && id in data.waketime) { 377 | let sleep = data.sleeptime[id]; 378 | let wake = data.waketime[id]; 379 | if (sleep < wake) { 380 | if (hour > sleep && hour < wake) return; 381 | } 382 | if (sleep > wake) { 383 | if (hour > sleep || hour < wake) return; 384 | } 385 | } 386 | if (id in data.timelist) { 387 | if (data.timelist[id].indexOf(hour) === -1) { 388 | return; 389 | } 390 | } 391 | logger.debug('Send to ' + id); 392 | limiter.schedule(() => bot.sendSticker(id, stickers[hour % 12])).then(message => { 393 | let cid = message.chat.id; 394 | let mid = message.message_id; 395 | if (data.autodelete[cid] && data.lastid[cid]) { 396 | bot.deleteMessage(cid, data.lastid[cid]); 397 | } 398 | data.lastid[cid] = mid; 399 | saveData(); 400 | }).catch(error => { 401 | let query = error.response.request.uri.query; 402 | let matches = query.match(/chat_id=(.*)&/); 403 | if (matches && matches[1]) { 404 | let cid = Number(matches[1]); 405 | if (isNaN(cid)) { 406 | // Channel name 407 | cid = matches[1]; 408 | cid = cid.replace('%40', '@'); 409 | } 410 | logger.error('[' + error.response.body.error_code + '](' + cid + ')' + error.response.body.description); // => 'ETELEGRAM' 411 | if (query && (error.response.body.error_code === 403 || error.response.body.error_code === 400) && 412 | (error.response.body.description.includes('blocked') || 413 | error.response.body.description.includes('kicked') || 414 | error.response.body.description.includes('not a member') || 415 | error.response.body.description.includes('chat not found') || 416 | error.response.body.description.includes('upgraded') || 417 | error.response.body.description.includes('deactivated') || 418 | error.response.body.description.includes('not enough rights') || 419 | error.response.body.description.includes('have no rights') || 420 | error.response.body.description.includes('initiate conversation') || 421 | error.response.body.description.includes('CHAT_SEND_STICKERS_FORBIDDEN') || 422 | error.response.body.description.includes('CHAT_RESTRICTED') || 423 | error.response.body.description.includes('was deleted') || 424 | error.response.body.description.includes('PEER_ID_INVALID') || 425 | error.response.body.description.includes('TOPIC_CLOSED'))) { 426 | logger.info('Blocked by ' + cid); 427 | let index = data.chatids.indexOf(cid); 428 | if (index > -1) { 429 | data.chatids.splice(index, 1); 430 | delete data.tzmap[cid]; 431 | delete data.lastid[cid]; 432 | delete data.autodelete[cid]; 433 | delete data.sleeptime[cid]; 434 | delete data.waketime[cid]; 435 | delete data.timelist[cid]; 436 | saveData(); 437 | } 438 | } 439 | } 440 | }); 441 | chatsSent++; 442 | }); 443 | logger.info('Cron triggered. Send stickers to ' + chatsSent + '/' + data.chatids.length + ' chats'); 444 | }, null, true, 'Asia/Shanghai'); 445 | --------------------------------------------------------------------------------