├── Procfile ├── UsageExample-EN.png ├── UsageExample-RU.png ├── BotCode ├── interactions │ ├── bot │ │ ├── subscriptions │ │ │ ├── all.js │ │ │ ├── commands.js │ │ │ ├── basic.js │ │ │ └── advanced.js │ │ ├── static │ │ │ ├── commandsList.json │ │ │ └── replies │ │ │ │ ├── repliesLoader.js │ │ │ │ ├── keyboards.js │ │ │ │ ├── en.json │ │ │ │ └── ru.json │ │ ├── actions │ │ │ ├── stt │ │ │ │ └── stt.js │ │ │ ├── handling │ │ │ │ ├── callbackQueries │ │ │ │ │ ├── callbackQueryCases.js │ │ │ │ │ ├── case_unsubscribe.js │ │ │ │ │ ├── case_cancel_rm.js │ │ │ │ │ ├── case_help_trello.js │ │ │ │ │ ├── case_help_back.js │ │ │ │ │ ├── case_startTZ.js │ │ │ │ │ ├── case_delete.js │ │ │ │ │ ├── callbackQueries.js │ │ │ │ │ ├── case_cancel.js │ │ │ │ │ ├── case_repeat.js │ │ │ │ │ ├── case_confirm.js │ │ │ │ │ ├── case_complete_reminder.js │ │ │ │ │ └── case_to_trello.js │ │ │ │ ├── inlineQuery.js │ │ │ │ ├── textMessage.js │ │ │ │ └── trelloCommands.js │ │ │ ├── replying.js │ │ │ ├── remindersChecking.js │ │ │ ├── remindersParsing.js │ │ │ └── technical.js │ │ └── main.js │ └── processing │ │ ├── nicknamesExtraction.js │ │ ├── utilities.js │ │ ├── remindersOperations.js │ │ ├── timeProcessing.js │ │ └── formatting.js ├── constants.json ├── keepAlive.js ├── storage │ ├── encryption │ │ └── encrypt.js │ └── dataBase │ │ ├── DataBase.js │ │ ├── copying.js │ │ ├── Functions.js │ │ ├── Connector.js │ │ ├── TablesClasses │ │ ├── Chat.js │ │ ├── User.js │ │ └── Schedule.js │ │ └── Migrations.js ├── package.json └── index.js ├── package.json ├── LICENSE.md ├── .gitignore ├── README-EN.md └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | worker: npm start --prefix BotCode -------------------------------------------------------------------------------- /UsageExample-EN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alordash/BotSmartScheduler/HEAD/UsageExample-EN.png -------------------------------------------------------------------------------- /UsageExample-RU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alordash/BotSmartScheduler/HEAD/UsageExample-RU.png -------------------------------------------------------------------------------- /BotCode/interactions/bot/subscriptions/all.js: -------------------------------------------------------------------------------- 1 | const Subscriptions = { 2 | initAdvanced: require('./advanced'), 3 | initBasic: require('./basic'), 4 | initCommands: require('./commands') 5 | } 6 | 7 | module.exports = Subscriptions; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botsmartscheduler", 3 | "version": "1.2.0", 4 | "description": "", 5 | "scripts": { 6 | "postinstall": "npm install --prefix BotCode" 7 | }, 8 | "engines": { 9 | "node": "13.12.x", 10 | "npm": "6.14.x" 11 | }, 12 | "dependencies": {} 13 | } -------------------------------------------------------------------------------- /BotCode/interactions/bot/static/commandsList.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": "help", 3 | "listSchedules": "list", 4 | "deleteSchedules": "del", 5 | "changeTimeZone": "tz", 6 | "displayStatus": "/displayStatus", 7 | "trelloInit": "trello", 8 | "trelloClear": "clear", 9 | "trelloPinBoardCommand": "/trello_pin", 10 | "trelloUnpinBoardCommand": "/trello_unpin", 11 | "trelloAddListCommand": "/tl", 12 | "trelloHelp": "trello" 13 | } -------------------------------------------------------------------------------- /BotCode/constants.json: -------------------------------------------------------------------------------- 1 | { 2 | "MaximumVoiceMessageDuration": 30, 3 | "repeatScheduleTime": 300000, 4 | "MaximumCountOfSchedules": 50, 5 | "MaxMessageLength": 4096, 6 | "MessagesUntilTzWarning": 8, 7 | "MaxShortStringLength": 128, 8 | "defaultUserLanguage": "ru", 9 | "defaultUserTimezone": 10800, 10 | "inlineQueryScheduleTimeout": 300000, 11 | "minReminderTimeDifferenceSec": 240, 12 | "maxReminderTimeDifferenceSec": 35078400 13 | } -------------------------------------------------------------------------------- /BotCode/interactions/bot/static/replies/repliesLoader.js: -------------------------------------------------------------------------------- 1 | const Languages = Object.freeze({ 2 | ru: "ru", 3 | en: "en" 4 | }); 5 | 6 | /**@param {Languages} language */ 7 | function LoadReplies(language = global.defaultUserLanguage) { 8 | if(language == null || !Languages.hasOwnProperty(language)) { 9 | language = global.defaultUserLanguage; 10 | } 11 | return require(`${__dirname}/${language}.json`); 12 | } 13 | 14 | module.exports = { 15 | Languages, 16 | LoadReplies 17 | } -------------------------------------------------------------------------------- /BotCode/keepAlive.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const utils = require('./interactions/processing/utilities'); 3 | 4 | let keepAliveUrl = process.env.SMART_SCHEDULER_KEEP_ALIVE_URL; 5 | 6 | function startKeepAliveService() { 7 | console.log("Started keep alive service"); 8 | utils.RepeatActionsWithPeriod(900000, async function () { 9 | console.log("Sending request to keep alive"); 10 | https.get(keepAliveUrl, (res) => console.log(`KeepAlive response: ${res}`)); 11 | }); 12 | } 13 | 14 | module.exports = { startKeepAliveService } -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/stt/stt.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise'); 2 | 3 | exports.speechToText = class { 4 | constructor(API_KEY, FOLDER_ID) { 5 | this.API_KEY = API_KEY; 6 | this.FOLDER_ID = FOLDER_ID; 7 | this.url = 'https://stt.api.cloud.yandex.net/speech/v1/stt:recognize?folderId='; 8 | } 9 | 10 | async recognize(body) { 11 | try { 12 | const response = await request.post({ 13 | url: `${this.url}${this.FOLDER_ID}`, 14 | headers: { 15 | 'Authorization': `Api-Key ${this.API_KEY}`, 16 | }, 17 | body 18 | }); 19 | return JSON.parse(response).result; 20 | } catch (e) { 21 | console.error(e); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /BotCode/storage/encryption/encrypt.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const iv = '1234567891113150'; 3 | const algorithm = 'aes-128-cbc'; 4 | 5 | /**@param {String} text 6 | * @param {String} key 7 | * @returns {String} 8 | */ 9 | function Encrypt(text, key) { 10 | const secret = crypto.scryptSync(key, 'salt', 16);; 11 | let cipher = crypto.createCipheriv(algorithm, secret, iv); 12 | let encrypted = cipher.update(text); 13 | encrypted = Buffer.concat([encrypted, cipher.final()]); 14 | return encrypted.toString('hex'); 15 | } 16 | 17 | /**@param {String} text 18 | * @param {String} key 19 | * @returns {String} 20 | */ 21 | function Decrypt(text, key) { 22 | const secret = crypto.scryptSync(key, 'salt', 16);; 23 | let encryptedText = Buffer.from(text, 'hex'); 24 | let decipher = crypto.createDecipheriv(algorithm, secret, iv); 25 | let decrypted = decipher.update(encryptedText); 26 | decrypted = Buffer.concat([decrypted, decipher.final()]); 27 | return decrypted.toString(); 28 | } 29 | 30 | module.exports = { 31 | Encrypt, 32 | Decrypt 33 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexey Orazov 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. -------------------------------------------------------------------------------- /BotCode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botcode", 3 | "version": "1.2.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "jasmine" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/alordash/BotSmartScheduler.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/alordash/BotSmartScheduler/issues" 16 | }, 17 | "homepage": "https://github.com/alordash/BotSmartScheduler#readme", 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "@alordash/date-parser": "^2.3.17", 22 | "@alordash/node-js-trello": "^1.1.4", 23 | "@alordash/parse-word-to-number": "^3.0.6", 24 | "dotenv": "^8.2.0", 25 | "express": "^4.17.3", 26 | "lodash": "^4.17.21", 27 | "pg": "^8.2.1", 28 | "request": "^2.88.2", 29 | "request-promise": "^4.2.6", 30 | "socks5-https-client": "^1.2.1", 31 | "telegraf": "^3.38.0" 32 | }, 33 | "engines": { 34 | "node": "13.12.x", 35 | "npm": "6.14.x" 36 | }, 37 | "devDependencies": { 38 | "jasmine": "^3.5.0", 39 | "jasmine-core": "^3.5.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /BotCode/interactions/processing/nicknamesExtraction.js: -------------------------------------------------------------------------------- 1 | const { TrelloManager } = require('@alordash/node-js-trello'); 2 | 3 | /** 4 | * @param {String} string 5 | * @returns {{nicks: Array., string: {String}} 6 | */ 7 | function ExtractNicknames(string) { 8 | let matches = [...string.matchAll(/@[\w\d_]+/g)]; 9 | for (const i in matches) { 10 | let match = matches[i]; 11 | string = string.substring(0, match.index) + string.substring(match.index + match[0].length); 12 | matches[i] = matches[i][0].substring(1); 13 | } 14 | string = string.trim() 15 | return { nicks: matches, string }; 16 | } 17 | 18 | /** 19 | * @param {Array.} nicks 20 | * @param {TrelloManager} trelloManager 21 | * @returns {Array.} 22 | */ 23 | async function GetUsersIDsFromNicknames(nicks, trelloManager) { 24 | let ids = []; 25 | for (const nick of nicks) { 26 | let user = await trelloManager.GetMember(nick); 27 | if (typeof (user) != 'undefined') { 28 | ids.push(user.id); 29 | } 30 | } 31 | return ids; 32 | } 33 | 34 | module.exports = { 35 | ExtractNicknames, 36 | GetUsersIDsFromNicknames 37 | } -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/callbackQueryCases.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const fileStart = 'case_'; 5 | 6 | class CallbackQueryCase { 7 | /**@type {String} */ 8 | name; 9 | /**@type {Function} */ 10 | callback; 11 | 12 | /** 13 | * @param {String} name 14 | * @param {Function} callback 15 | */ 16 | constructor(name, callback) { 17 | this.name = name; 18 | this.callback = callback; 19 | } 20 | } 21 | 22 | /**@type {Array.} */ 23 | let CallbackQueryCases = []; 24 | 25 | (function LoadQueryCases() { 26 | console.log('callbackQueries __dirname :>> ', __dirname); 27 | let callbackQueryFiles = fs.readdirSync(__dirname); 28 | console.log('callbackQueryFiles :>> ', callbackQueryFiles); 29 | for (const filename of callbackQueryFiles) { 30 | if (path.extname(filename) == '.js' 31 | && filename.startsWith(fileStart)) { 32 | let name = filename.substring(fileStart.length).slice(0, -3); 33 | let callback = require(`./${filename}`); 34 | CallbackQueryCases.push(new CallbackQueryCase(name, callback)); 35 | } 36 | } 37 | })(); 38 | 39 | module.exports = CallbackQueryCases; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/case_unsubscribe.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { TrelloManager } = require('@alordash/node-js-trello'); 7 | const utils = require('../../../../processing/utilities'); 8 | const { StartTimeZoneDetermination } = require('../../technical'); 9 | 10 | /** 11 | * @param {*} ctx 12 | * @param {Array.} tzPendingConfirmationUsers 13 | * @param {Array.} trelloPendingConfirmationUsers 14 | * @param {String} chatID 15 | * @param {User} user 16 | * @param {Languages} language 17 | * @param {*} replies 18 | */ 19 | async function CaseUnsubscribe(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, chatID, user, language, replies) { 20 | try { 21 | await DataBase.Users.SetUserSubscription(ctx.from.id, false); 22 | } catch (e) { 23 | console.log(e); 24 | } 25 | } 26 | 27 | module.exports = CaseUnsubscribe; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/case_cancel_rm.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { TrelloManager } = require('@alordash/node-js-trello'); 7 | const utils = require('../../../../processing/utilities'); 8 | const { StartTimeZoneDetermination } = require('../../technical'); 9 | 10 | /** 11 | * @param {*} ctx 12 | * @param {Array.} tzPendingConfirmationUsers 13 | * @param {Array.} trelloPendingConfirmationUsers 14 | * @param {String} chatID 15 | * @param {User} user 16 | * @param {Languages} language 17 | * @param {*} replies 18 | */ 19 | async function CaseCancelRm(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, chatID, user, language, replies) { 20 | await DataBase.Schedules.RemoveSchedulesByState(chatID, ScheduleStates.invalid); 21 | try { 22 | ctx.deleteMessage(); 23 | } catch (e) { 24 | console.error(e); 25 | } 26 | } 27 | 28 | module.exports = CaseCancelRm; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/case_help_trello.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { TrelloManager } = require('@alordash/node-js-trello'); 7 | const utils = require('../../../../processing/utilities'); 8 | const { StartTimeZoneDetermination } = require('../../technical'); 9 | const kbs = require('../../../static/replies/keyboards'); 10 | 11 | /** 12 | * @param {*} ctx 13 | * @param {Array.} tzPendingConfirmationUsers 14 | * @param {Array.} trelloPendingConfirmationUsers 15 | * @param {String} chatID 16 | * @param {User} user 17 | * @param {Languages} language 18 | * @param {*} replies 19 | */ 20 | async function CaseHelpTrello(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, chatID, user, language, replies) { 21 | let kb = kbs.BackKeyboard(language, 'help_back'); 22 | ctx.editMessageText(replies.trelloHelp, { parse_mode: 'HTML', reply_markup: kb.reply_markup }); 23 | ctx.answerCbQuery(); 24 | } 25 | 26 | module.exports = CaseHelpTrello; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/case_help_back.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { TrelloManager } = require('@alordash/node-js-trello'); 7 | const utils = require('../../../../processing/utilities'); 8 | const { StartTimeZoneDetermination } = require('../../technical'); 9 | const kbs = require('../../../static/replies/keyboards'); 10 | 11 | /** 12 | * @param {*} ctx 13 | * @param {Array.} tzPendingConfirmationUsers 14 | * @param {Array.} trelloPendingConfirmationUsers 15 | * @param {String} chatID 16 | * @param {User} user 17 | * @param {Languages} language 18 | * @param {*} replies 19 | */ 20 | async function CaseHelpBack(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, chatID, user, language, replies) { 21 | let kb = kbs.HelpSectionsKeyboards(language); 22 | ctx.editMessageText(replies.commands, { parse_mode: 'HTML', disable_web_page_preview: true, reply_markup: kb.reply_markup }); 23 | ctx.answerCbQuery(); 24 | } 25 | 26 | module.exports = CaseHelpBack; -------------------------------------------------------------------------------- /BotCode/storage/dataBase/DataBase.js: -------------------------------------------------------------------------------- 1 | const { ConnectorOptions, Connector } = require('./Connector'); 2 | const Chat = require('./TablesClasses/Chat'); 3 | const { Schedule } = require('./TablesClasses/Schedule'); 4 | const User = require('./TablesClasses/User'); 5 | const Migrations = require('./Migrations'); 6 | 7 | class DataBase { 8 | constructor() { 9 | if (this.constructor == DataBase) { 10 | throw new Error("DataBase class can't be instantiated."); 11 | } 12 | } 13 | 14 | static Chats = Chat; 15 | static Schedules = Schedule; 16 | static Users = User; 17 | 18 | static async InitializeDataBase() { 19 | await Migrations.InitializeTables(); 20 | 21 | await Migrations.InitializeDataBaseFunctions(); 22 | 23 | await Migrations.ExpandSchedulesTable('creator'); 24 | 25 | await Migrations.ExpandUsersIdsTable('trello_token'); 26 | 27 | await Migrations.ExpandChatsTable('trello_token'); 28 | 29 | if (process.env.SMART_SCHEDULER_ENCRYPT_SCHEDULES === 'true') { 30 | await Migrations.EncryptSchedules(); 31 | } 32 | console.log(`Data base initialization finished`); 33 | } 34 | 35 | /** 36 | * @param {ConnectorOptions} options 37 | */ 38 | static EstablishConnection(options) { 39 | Connector.Instantiate(options); 40 | } 41 | } 42 | 43 | module.exports = { 44 | DataBase, 45 | Chat, 46 | Schedule, 47 | User 48 | }; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/case_startTZ.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { TrelloManager } = require('@alordash/node-js-trello'); 7 | const utils = require('../../../../processing/utilities'); 8 | const { StartTimeZoneDetermination } = require('../../technical'); 9 | 10 | /** 11 | * @param {*} ctx 12 | * @param {Array.} tzPendingConfirmationUsers 13 | * @param {Array.} trelloPendingConfirmationUsers 14 | * @param {String} chatID 15 | * @param {User} user 16 | * @param {Languages} language 17 | * @param {*} replies 18 | */ 19 | async function CaseStartTZ(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, chatID, user, language, replies) { 20 | try { 21 | ctx.from.language_code = language; 22 | ctx.editMessageReplyMarkup(Extra.markup((m) => 23 | m.inlineKeyboard([]).removeKeyboard() 24 | )); 25 | StartTimeZoneDetermination(ctx, tzPendingConfirmationUsers); 26 | } catch (e) { 27 | console.error(e); 28 | } 29 | } 30 | 31 | module.exports = CaseStartTZ; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/case_delete.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { TrelloManager } = require('@alordash/node-js-trello'); 7 | const utils = require('../../../../processing/utilities'); 8 | const { StartTimeZoneDetermination } = require('../../technical'); 9 | const { RemoveReminders } = require('../../../../processing/remindersOperations'); 10 | 11 | /** 12 | * @param {*} ctx 13 | * @param {Array.} tzPendingConfirmationUsers 14 | * @param {Array.} trelloPendingConfirmationUsers 15 | * @param {String} chatID 16 | * @param {User} user 17 | * @param {Languages} language 18 | * @param {*} replies 19 | */ 20 | async function CaseDelete(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, chatID, user, language, replies) { 21 | let message_id = ctx.update.callback_query.message.message_id; 22 | try { 23 | let schedules = await DataBase.Schedules.GetSchedules(chatID, GetOptions.draft, message_id); 24 | await RemoveReminders(ctx, schedules); 25 | } catch (e) { 26 | console.log(e); 27 | } 28 | } 29 | 30 | module.exports = CaseDelete; -------------------------------------------------------------------------------- /BotCode/storage/dataBase/copying.js: -------------------------------------------------------------------------------- 1 | const { DataBase } = require("./DataBase"); 2 | 3 | 4 | /** 5 | * @param {String} dbUrl 6 | * @returns {{users: Array., chats: Array., schedules: Array.}} 7 | */ 8 | async function CopyDatabase(dbUrl) { 9 | if (dbUrl == undefined) { 10 | return { users: [], chats: [], schedules: [] }; 11 | } 12 | dbUrl = new URL(dbUrl); 13 | const dbOptions = { 14 | user: dbUrl.username, 15 | host: dbUrl.hostname, 16 | database: dbUrl.pathname.substring(1), 17 | password: dbUrl.password, 18 | port: parseInt(dbUrl.port), 19 | ssl: { 20 | rejectUnauthorized: false 21 | } 22 | } 23 | 24 | DataBase.EstablishConnection(dbOptions); 25 | let users = await DataBase.Users.GetAllUsers(); 26 | let chats = await DataBase.Chats.GetAllChats(); 27 | let schedules = await DataBase.Schedules.GetAllSchedules(); 28 | 29 | return { users, chats, schedules }; 30 | } 31 | 32 | /** 33 | * 34 | * @param {Array.} users 35 | * @param {Array.} chats 36 | * @param {Array.} schedules 37 | */ 38 | async function SaveDatabase(users, chats, schedules) { 39 | console.log(`Saving ${users.length} users, ${chats.length} chats, ${schedules.length} schedules`); 40 | await DataBase.Users.InsertUsers(users); 41 | await DataBase.Chats.InsertChats(chats); 42 | await DataBase.Schedules.InsertSchedules(schedules); 43 | } 44 | 45 | module.exports = { 46 | CopyDatabase, 47 | SaveDatabase 48 | } -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/callbackQueries.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, Schedule, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { TrelloManager } = require('@alordash/node-js-trello'); 6 | const utils = require('../../../../processing/utilities'); 7 | const { StartTimeZoneDetermination } = require('../../technical'); 8 | const CallbackQueryCases = require('./callbackQueryCases'); 9 | 10 | /** 11 | * @param {*} ctx 12 | * @param {Array.} tzPendingConfirmationUsers 13 | * @param {Array.} trelloPendingConfirmationUsers 14 | */ 15 | async function HandleCallbackQueries(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers) { 16 | let data = ctx.callbackQuery.data; 17 | console.log(`got callback_query, data: "${data}"`); 18 | let chatID = utils.FormatChatId(ctx.callbackQuery.message.chat.id); 19 | const user = await DataBase.Users.GetUserById(ctx.from.id); 20 | const language = user.lang; 21 | const replies = LoadReplies(language); 22 | const args = [...arguments, chatID, user, language, replies]; 23 | 24 | for(const callbackQueryCase of CallbackQueryCases) { 25 | if(data == callbackQueryCase.name) { 26 | callbackQueryCase.callback(...args); 27 | break; 28 | } 29 | } 30 | 31 | try { 32 | ctx.answerCbQuery(); 33 | } catch (e) { 34 | console.error(e); 35 | } 36 | } 37 | 38 | module.exports = HandleCallbackQueries; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/case_cancel.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { TrelloManager } = require('@alordash/node-js-trello'); 7 | const utils = require('../../../../processing/utilities'); 8 | const { StartTimeZoneDetermination } = require('../../technical'); 9 | const kbs = require('../../../static/replies/keyboards'); 10 | const { BotReply } = require('../../replying'); 11 | 12 | /** 13 | * @param {*} ctx 14 | * @param {Array.} tzPendingConfirmationUsers 15 | * @param {Array.} trelloPendingConfirmationUsers 16 | * @param {String} chatID 17 | * @param {User} user 18 | * @param {Languages} language 19 | * @param {*} replies 20 | */ 21 | async function CaseCancel(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, chatID, user, language, replies) { 22 | utils.ClearPendingConfirmation(tzPendingConfirmationUsers, trelloPendingConfirmationUsers, ctx.from.id); 23 | let text = replies.cancelReponse; 24 | if (typeof (user) == 'undefined' || typeof(user.id) == 'undefined') { 25 | text += '\r\n' + replies.tzCancelWarning; 26 | } 27 | try { 28 | let kb = await kbs.LogicalListKeyboard(language, chatID); 29 | kb.parse_mode = 'HTML'; 30 | ctx.answerCbQuery(); 31 | await ctx.deleteMessage(); 32 | BotReply(ctx, text, kb); 33 | } catch (e) { 34 | console.error(e); 35 | } 36 | } 37 | 38 | module.exports = CaseCancel; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/case_repeat.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { TrelloManager } = require('@alordash/node-js-trello'); 7 | const utils = require('../../../../processing/utilities'); 8 | const { StartTimeZoneDetermination } = require('../../technical'); 9 | 10 | /** 11 | * @param {*} ctx 12 | * @param {Array.} tzPendingConfirmationUsers 13 | * @param {Array.} trelloPendingConfirmationUsers 14 | * @param {String} chatID 15 | * @param {User} user 16 | * @param {Languages} language 17 | * @param {*} replies 18 | */ 19 | async function CaseRepeat(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, chatID, user, language, replies) { 20 | let schedules = await DataBase.Schedules.GetSchedules(chatID, GetOptions.draft, ctx.callbackQuery.message.message_id, true); 21 | if (schedules.length == 0) { 22 | return 0; 23 | } 24 | let hasCaption = typeof (ctx.callbackQuery.message.text) == 'undefined' ? true : false; 25 | let schedule = schedules[0]; 26 | let text = schedule.text; 27 | let tz = user.tz; 28 | 29 | try { 30 | await DataBase.Schedules.ConfirmSchedules([schedule]); 31 | let newText = `"${text}"\r\n${replies.remindSchedule} ${Format.FormDateStringFormat(new Date(schedule.target_date + tz * 1000), language, false)}`; 32 | if (hasCaption) { 33 | ctx.editMessageCaption(newText, { parse_mode: 'HTML' }); 34 | } else { 35 | ctx.editMessageText(newText, { parse_mode: 'HTML' }); 36 | } 37 | } catch (e) { 38 | console.error(e); 39 | } 40 | } 41 | 42 | module.exports = CaseRepeat; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/case_confirm.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { TrelloManager } = require('@alordash/node-js-trello'); 7 | const utils = require('../../../../processing/utilities'); 8 | const { StartTimeZoneDetermination } = require('../../technical'); 9 | 10 | /** 11 | * @param {*} ctx 12 | * @param {Array.} tzPendingConfirmationUsers 13 | * @param {Array.} trelloPendingConfirmationUsers 14 | * @param {String} chatID 15 | * @param {User} user 16 | * @param {Languages} language 17 | * @param {*} replies 18 | */ 19 | async function CaseConfirm(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, chatID, user, language, replies) { 20 | let message_id = ctx.update.callback_query.message.message_id; 21 | let schedules = await DataBase.Schedules.GetSchedules(chatID, GetOptions.draft, message_id); 22 | try { 23 | if (schedules.length > 0) { 24 | await DataBase.Schedules.ConfirmSchedules(schedules); 25 | schedules = await DataBase.Schedules.ReorderSchedules(chatID, true); 26 | } 27 | let text = ''; 28 | let tz = user.tz; 29 | for (let schedule of schedules) { 30 | if (schedule.message_id != message_id) { 31 | continue; 32 | } 33 | text += `${await Format.FormStringFormatSchedule(schedule, tz, language, true, true)}\r\n`; 34 | } 35 | if (text.length > 0) { 36 | ctx.editMessageText(text, { parse_mode: 'HTML' }); 37 | } 38 | ctx.editMessageReplyMarkup(Extra.markup((m) => 39 | m.inlineKeyboard([]).removeKeyboard() 40 | )); 41 | } catch (e) { 42 | console.error(e); 43 | } 44 | } 45 | 46 | module.exports = CaseConfirm; -------------------------------------------------------------------------------- /BotCode/storage/dataBase/Functions.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ReorderSchedulesFunction: `CREATE OR REPLACE FUNCTION ReorderSchedules(_chatid TEXT) RETURNS SETOF schedules AS $$ 3 | DECLARE 4 | s int; 5 | BEGIN 6 | s := 1; 7 | SELECT Max(num) FROM schedules WHERE chatid = _chatid INTO s; 8 | IF s IS NULL THEN 9 | s:= 1; 10 | END IF; 11 | FOR i IN REVERSE s..1 LOOP 12 | IF NOT EXISTS (SELECT FROM schedules WHERE chatid = _chatid AND num = i) THEN 13 | UPDATE schedules SET num = num - 1 WHERE chatid = _chatid AND num > i; 14 | END IF; 15 | END LOOP; 16 | RETURN QUERY SELECT * FROM schedules WHERE chatid = _chatid; 17 | END; 18 | $$ LANGUAGE plpgsql;`, 19 | ReorderMultipleSchedulesFunction: `CREATE OR REPLACE FUNCTION ReorderMultipleSchedules(_chatids TEXT[]) RETURNS VOID AS $$ 20 | DECLARE 21 | i TEXT; 22 | _s schedules; 23 | BEGIN 24 | FOREACH i IN ARRAY _chatids LOOP 25 | SELECT * FROM ReorderSchedules(i) INTO _s; 26 | END LOOP; 27 | END; 28 | $$ LANGUAGE plpgsql;`, 29 | ConfirmScheduleFunction: `CREATE OR REPLACE FUNCTION ConfirmSchedule(_id INT, _state TEXT, _filter_state TEXT) RETURNS VOID AS $$ 30 | DECLARE 31 | s int; 32 | _schedule schedules; 33 | BEGIN 34 | SELECT INTO _schedule * 35 | FROM schedules WHERE id = _id; 36 | SELECT Count(*) FROM schedules WHERE (chatid = _schedule.chatid AND state = _filter_state) INTO s; 37 | UPDATE schedules SET 38 | state = _state, 39 | num = s + 1 40 | WHERE id = _id; 41 | END; 42 | $$ LANGUAGE plpgsql;`/*, 43 | CutOutScheduleByStateFunction: `CREATE OR REPLACE FUNCTION CutOutScheduleByState(_chatID TEXT, _state TEXT) RETURNS schedules AS $$ 44 | DECLARE 45 | schedule schedules; 46 | BEGIN 47 | SELECT INTO schedule * 48 | FROM schedules WHERE chatid = _chatid AND state = _state; 49 | DELETE FROM schedules WHERE chatid = chatid AND state = _state; 50 | RETURN schedule; 51 | END; 52 | $$ LANGUAGE plpgsql;`*/ 53 | } -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/case_complete_reminder.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { TrelloManager } = require('@alordash/node-js-trello'); 7 | const utils = require('../../../../processing/utilities'); 8 | const { StartTimeZoneDetermination } = require('../../technical'); 9 | 10 | /** 11 | * @param {*} ctx 12 | * @param {Array.} tzPendingConfirmationUsers 13 | * @param {Array.} trelloPendingConfirmationUsers 14 | * @param {String} chatID 15 | * @param {User} user 16 | * @param {Languages} language 17 | * @param {*} replies 18 | */ 19 | async function CaseCompleteReminder(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, chatID, user, language, replies) { 20 | let schedules = await DataBase.Schedules.GetSchedules(chatID, GetOptions.valid, undefined, true); 21 | if (schedules.length == 0) 22 | return 0; 23 | 24 | let hasCaption = typeof (ctx.callbackQuery.message.text) == 'undefined' ? true : false; 25 | let text = (hasCaption ? ctx.callbackQuery.message.caption : ctx.callbackQuery.message.text); 26 | text = text.substring(text.indexOf(' ') + 1); 27 | let schedule; 28 | let found = false; 29 | for (schedule of schedules) { 30 | if (schedule.period_time > 0 && schedule.text == text) { 31 | found = true; 32 | break; 33 | } 34 | } 35 | if (!found) 36 | return 0; 37 | 38 | try { 39 | DataBase.Schedules.RemoveSchedules([schedule]); 40 | let newText = `✅ ${text}`; 41 | if (hasCaption) { 42 | ctx.editMessageCaption(newText, { parse_mode: 'HTML' }); 43 | } else { 44 | ctx.editMessageText(newText, { parse_mode: 'HTML' }); 45 | } 46 | ctx.editMessageReplyMarkup(Extra.markup((m) => 47 | m.inlineKeyboard([]).removeKeyboard() 48 | )); 49 | } catch (e) { 50 | console.error(e); 51 | } 52 | } 53 | 54 | module.exports = CaseCompleteReminder; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/callbackQueries/case_to_trello.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { Languages, LoadReplies } = require('../../../static/replies/repliesLoader'); 3 | const Format = require('../../../../processing/formatting'); 4 | const { DataBase, User, Chat } = require('../../../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { TrelloManager } = require('@alordash/node-js-trello'); 7 | const utils = require('../../../../processing/utilities'); 8 | const { StartTimeZoneDetermination } = require('../../technical'); 9 | const { AddScheduleToTrello } = require('../../../../processing/remindersOperations'); 10 | 11 | /** 12 | * @param {*} ctx 13 | * @param {Array.} tzPendingConfirmationUsers 14 | * @param {Array.} trelloPendingConfirmationUsers 15 | * @param {String} chatID 16 | * @param {User} user 17 | * @param {Languages} language 18 | * @param {*} replies 19 | */ 20 | async function CaseToTrello(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, chatID, user, language, replies) { 21 | let message_id = ctx.update.callback_query.message.message_id; 22 | let schedules = await DataBase.Schedules.GetSchedules(chatID, GetOptions.all, message_id, true); 23 | try { 24 | let unconfirmedSchedules = []; 25 | for(const si in schedules) { 26 | let schedule = schedules[si]; 27 | if(schedule.state == ScheduleStates.pending) { 28 | unconfirmedSchedules.push(schedule); 29 | schedule.state = ScheduleStates.valid; 30 | } 31 | } 32 | await DataBase.Schedules.ConfirmSchedules(unconfirmedSchedules); 33 | let text = ''; 34 | let tz = user.tz; 35 | for (let schedule of schedules) { 36 | if (schedule.message_id != message_id) { 37 | continue; 38 | } 39 | schedule = await AddScheduleToTrello(ctx, schedule); 40 | text += `${await Format.FormStringFormatSchedule(schedule, tz, language, true, true)}\r\n`; 41 | } 42 | if (text.length > 0) { 43 | ctx.editMessageText(text, { parse_mode: 'HTML' }); 44 | } 45 | ctx.editMessageReplyMarkup(Extra.markup((m) => 46 | m.inlineKeyboard([]).removeKeyboard() 47 | )); 48 | } catch (e) { 49 | console.error(e); 50 | } 51 | } 52 | 53 | module.exports = CaseToTrello; -------------------------------------------------------------------------------- /BotCode/storage/dataBase/Connector.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | class ConnectorOptions { 4 | /**@type {String} */ 5 | user; 6 | /**@type {String} */ 7 | host; 8 | /**@type {String} */ 9 | dataBasePath; 10 | /**@type {String} */ 11 | password; 12 | /**@type {Number} */ 13 | port; 14 | /**@type {Object} */ 15 | ssl; 16 | } 17 | 18 | class Connector { 19 | /**@type {Pool} */ 20 | pool; 21 | /**@type {Boolean} */ 22 | sending; 23 | 24 | /**@param {ConnectorOptions} options */ 25 | constructor(options) { 26 | this.pool = new Pool(options); 27 | this.sending = false; 28 | } 29 | 30 | async Query(query) { 31 | const client = await this.pool.connect(); 32 | let res; 33 | try { 34 | res = await client.query(query); 35 | } catch (err) { 36 | console.error(err.stack); 37 | } finally { 38 | client.release(); 39 | return res; 40 | } 41 | } 42 | 43 | async paramQuery(query, values) { 44 | const client = await this.pool.connect(); 45 | let res; 46 | try { 47 | res = await client.query({ 48 | rowMode: 'array', 49 | text: query, 50 | values 51 | }); 52 | } catch (err) { 53 | console.error(err.stack); 54 | } finally { 55 | client.release(); 56 | return res; 57 | } 58 | } 59 | 60 | /**@type {Connector} */ 61 | static instance; 62 | 63 | static get instance() { 64 | if (typeof (Connector.instance) == 'undefined') { 65 | throw "Data base connection wasn't initialized before call"; 66 | } 67 | return Connector.instance; 68 | } 69 | 70 | static get sending() { 71 | if (typeof (Connector.instance) == 'undefined') { 72 | throw "Data base connection wasn't initialized before call"; 73 | } 74 | return Connector.instance.sending; 75 | } 76 | 77 | /**@param {Boolean} state */ 78 | static set sending(state) { 79 | if (typeof (Connector.instance) == 'undefined') { 80 | throw "Data base connection wasn't initialized before call"; 81 | } 82 | Connector.instance.sending = state; 83 | } 84 | 85 | /** 86 | * @param {ConnectorOptions} options 87 | */ 88 | static Instantiate(options) { 89 | Connector.instance = new Connector(options); 90 | } 91 | } 92 | 93 | module.exports = { 94 | ConnectorOptions, 95 | Connector 96 | } -------------------------------------------------------------------------------- /BotCode/index.js: -------------------------------------------------------------------------------- 1 | const telegraf = require('telegraf'); 2 | const { DataBase } = require('./storage/dataBase/DataBase'); 3 | const botConfig = require('./interactions/bot/main'); 4 | const { CheckExpiredSchedules, CheckPendingSchedules, CheckDisplayStatueMessages } = require('./interactions/bot/actions/remindersChecking'); 5 | const utils = require('./interactions/processing/utilities'); 6 | const { CopyDatabase, SaveDatabase } = require('./storage/dataBase/copying'); 7 | const { startKeepAliveService } = require('./keepAlive'); 8 | 9 | console.log(`process.env.IS_HEROKU = ${process.env.IS_HEROKU}`); 10 | console.log(`process.env.SMART_SCHEDULER_DB_URL = ${process.env.SMART_SCHEDULER_DB_URL}`); 11 | 12 | Number.prototype.div = function (x) { 13 | return Math.floor(this / x); 14 | } 15 | 16 | var SmartSchedulerBot = new telegraf(process.env.SMART_SCHEDULER_TLGRM_API_TOKEN); 17 | 18 | let dbUrl; 19 | if (process.env.IS_HEROKU == 'true') { 20 | dbUrl = new URL(process.env.DATABASE_URL); 21 | } else { 22 | dbUrl = new URL(process.env.SMART_SCHEDULER_DB_URL); 23 | } 24 | const dbOptions = { 25 | user: dbUrl.username, 26 | host: dbUrl.hostname, 27 | database: dbUrl.pathname.substring(1), 28 | password: dbUrl.password, 29 | port: parseInt(dbUrl.port), 30 | ssl: { 31 | rejectUnauthorized: false 32 | } 33 | } 34 | 35 | async function Initialization() { 36 | // startKeepAliveService(); 37 | 38 | const now = new Date(); 39 | console.log('now.getTimezoneOffset() :>> ', now.getTimezoneOffset()); 40 | let constants = require('./constants.json'); 41 | for (let [key, value] of Object.entries(constants)) { 42 | global[key] = value; 43 | } 44 | 45 | await DataBase.InitializeDataBase(); 46 | await botConfig.InitBot(SmartSchedulerBot); 47 | 48 | if (process.env.ENABLE_SCHEDULES_CHEKING == 'true') { 49 | utils.RepeatActionsWithPeriod(60000, async function () { 50 | await CheckExpiredSchedules(SmartSchedulerBot); 51 | await CheckPendingSchedules(SmartSchedulerBot); 52 | }); 53 | utils.RepeatActionsWithPeriod(86400000, async function () { 54 | await CheckDisplayStatueMessages(SmartSchedulerBot); 55 | }); 56 | } 57 | 58 | if (process.env.ENABLE_LOGS == 'false') { 59 | console.log = function () { }; 60 | } 61 | } 62 | 63 | async function init() { 64 | 65 | // let dbCopy = await CopyDatabase(process.env.SMART_SCHEDULER_COPY_DB_URL); 66 | console.log('dbOptions :>> ', dbOptions); 67 | DataBase.EstablishConnection(dbOptions); 68 | 69 | // if (dbCopy.users.length > 0) { 70 | // await SaveDatabase(dbCopy.users, dbCopy.chats, dbCopy.schedules); 71 | // } 72 | 73 | Initialization(); 74 | } 75 | 76 | init(); -------------------------------------------------------------------------------- /BotCode/interactions/bot/main.js: -------------------------------------------------------------------------------- 1 | const Extra = require('telegraf/extra'); 2 | const { User } = require('../../storage/dataBase/DataBase'); 3 | const { LoadReplies } = require('./static/replies/repliesLoader'); 4 | const Subscriptions = require('./subscriptions/all'); 5 | 6 | /**@type {Array.} */ 7 | let tzPendingConfirmationUsers = []; 8 | /**@type {Array.} */ 9 | let trelloPendingConfirmationUsers = []; 10 | 11 | var i = 0; 12 | var errorsCount = 0; 13 | 14 | /**@param {String} inviteLink 15 | * @param {Array.} users 16 | */ 17 | function sendNotification(bot, inviteLink, users) { 18 | const length = users.length; 19 | let delay = Math.floor(Math.random() * 200) + 100; 20 | setTimeout(async function (delay, inviteLink, users) { 21 | const user = users[i]; 22 | const replies = LoadReplies(user.lang); 23 | const keyboard = Extra.markup((m) => 24 | m.inlineKeyboard([ 25 | m.callbackButton(replies.unsubscribe, `unsubscribe`) 26 | ]).oneTime() 27 | ); 28 | try { 29 | if (user.subscribed) { 30 | if (typeof (await bot.telegram.sendMessage(user.id, `${replies.invite} ${inviteLink}`, 31 | { 32 | parse_mode: 'HTML', 33 | ...keyboard 34 | })) != 'undefined') { 35 | console.log(`#${i} Success. Target :>> ${user.id}, delay :>> ${delay}`); 36 | } else { 37 | errorsCount++; 38 | console.log(`#${i} Error. Target :>> ${user.id}, delay :>> ${delay}`); 39 | } 40 | } 41 | } catch (e) { 42 | console.error(e); 43 | errorsCount++; 44 | } finally { 45 | i++; 46 | if (i < length) { 47 | sendNotification(bot, inviteLink, users); 48 | } else { 49 | try { 50 | bot.telegram.sendMessage(process.env.SMART_SCHEDULER_ADMIN, `Sent all invites, errors count: ${errorsCount}`); 51 | } catch (e) { 52 | console.error(e); 53 | } 54 | } 55 | } 56 | }, delay, delay, inviteLink, users); 57 | } 58 | 59 | /**@param {*} bot */ 60 | exports.InitBot = async function (bot) { 61 | const subscriptionsArgs = [bot, tzPendingConfirmationUsers, trelloPendingConfirmationUsers]; 62 | await Subscriptions.initCommands(...subscriptionsArgs); 63 | await Subscriptions.initAdvanced(...subscriptionsArgs); 64 | await Subscriptions.initBasic(...subscriptionsArgs); 65 | await bot.launch(); 66 | 67 | if (process.env.SMART_SCHEDULER_SEND_INVITE === 'true' 68 | && process.env.SMART_SCHEDULER_INVITE.length > 0) { 69 | sendNotification(bot, process.env.SMART_SCHEDULER_INVITE, users); 70 | } 71 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node,visualstudiocode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,visualstudiocode 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | ### VisualStudioCode ### 115 | .vscode/ 116 | !.vscode/settings.json 117 | !.vscode/tasks.json 118 | !.vscode/launch.json 119 | !.vscode/extensions.json 120 | *.code-workspace 121 | 122 | ### VisualStudioCode Patch ### 123 | # Ignore all local history of files 124 | .history 125 | 126 | # End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode 127 | -------------------------------------------------------------------------------- /BotCode/interactions/bot/subscriptions/commands.js: -------------------------------------------------------------------------------- 1 | const technicalActions = require('../actions/technical'); 2 | const { FormatChatId } = require('../../processing/utilities'); 3 | const { Composer } = require('telegraf'); 4 | const { DataBase } = require('../../../storage/dataBase/DataBase'); 5 | const cms = require('../static/commandsList'); 6 | const { BotReply } = require('../actions/replying'); 7 | const { TrelloCommand, TrelloPinCommand, TrelloUnpinCommand } = require('../actions/handling/trelloCommands'); 8 | 9 | /** 10 | * @param {Composer} bot 11 | * @param {Array.} tzPendingConfirmationUsers 12 | * @param {Array.} trelloPendingConfirmationUsers 13 | */ 14 | function InitCommandsSubscriptions(bot, tzPendingConfirmationUsers, trelloPendingConfirmationUsers) { 15 | bot.command(cms.listSchedules, async ctx => { 16 | let user = await DataBase.Users.GetUserById(ctx.from.id); 17 | let tz = user.tz; 18 | let language = user.lang; 19 | let chatID = FormatChatId(ctx.chat.id); 20 | let answers = await technicalActions.LoadSchedulesList(chatID, tz, language); 21 | for (const answer of answers) { 22 | try { 23 | BotReply(ctx, answer, { disable_web_page_preview: true }); 24 | } catch (e) { 25 | console.error(e); 26 | } 27 | } 28 | }); 29 | bot.command(cms.deleteSchedules, async ctx => { 30 | let language = await DataBase.Users.GetUserLanguage(ctx.from.id); 31 | ctx.from.language_code = language; 32 | technicalActions.DeleteSchedules(ctx); 33 | }); 34 | bot.command(cms.changeTimeZone, async ctx => { 35 | try { 36 | let language = await DataBase.Users.GetUserLanguage(ctx.from.id); 37 | ctx.from.language_code = language; 38 | technicalActions.StartTimeZoneDetermination(ctx, tzPendingConfirmationUsers); 39 | } catch (e) { 40 | console.error(e); 41 | } 42 | }); 43 | bot.command(cms.trelloInit, async ctx => { 44 | try { 45 | let user = await DataBase.Users.GetUserById(ctx.from.id); 46 | TrelloCommand(user, ctx, trelloPendingConfirmationUsers); 47 | } catch (e) { 48 | console.error(e); 49 | } 50 | }); 51 | bot.command(cms.trelloPinBoardCommand, async ctx => { 52 | try { 53 | let user = await DataBase.Users.GetUserById(ctx.from.id); 54 | TrelloPinCommand(ctx, user); 55 | } catch (e) { 56 | console.error(e); 57 | } 58 | }); 59 | bot.command(cms.trelloUnpinBoardCommand, async ctx => { 60 | try { 61 | let user = await DataBase.Users.GetUserById(ctx.from.id); 62 | TrelloUnpinCommand(ctx, user); 63 | } catch (e) { 64 | console.log(e); 65 | } 66 | }); 67 | bot.command(cms.displayStatus, async ctx => { 68 | try { 69 | technicalActions.StartDisplayingStatus(ctx); 70 | } catch (e) { 71 | console.log(e); 72 | } 73 | }); 74 | } 75 | 76 | module.exports = InitCommandsSubscriptions; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/replying.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | /**@param {*} ctx 4 | * @param {Text} text 5 | * @param {Object} option 6 | * @param {Boolean} notify 7 | * @param {String|Number} chatid 8 | */ 9 | function BotReply(ctx, text, option = {}, notify = false, chatid = ctx.chat.id) { 10 | let res; 11 | if (text == undefined || text.length == 0) { 12 | return res; 13 | } 14 | option.disable_notification = !notify; 15 | option.parse_mode = 'HTML'; 16 | try { 17 | res = ctx.telegram.sendMessage(chatid, text, option) 18 | } catch (e) { 19 | console.log(e); 20 | } finally { 21 | return res; 22 | } 23 | } 24 | 25 | /**@param {*} bot 26 | * @param {Number} chatID 27 | * @param {String} text 28 | * @param {Object} option 29 | * @param {Boolean} notify 30 | */ 31 | function BotSendMessage(bot, chatID, text, option = {}, notify = false) { 32 | option.disable_notification = !notify; 33 | let res; 34 | try { 35 | res = bot.telegram.sendMessage(chatID, text, option); 36 | } catch (e) { 37 | console.log(e); 38 | } finally { 39 | return res; 40 | } 41 | } 42 | 43 | /** 44 | * 45 | * @param {*} bot 46 | * @param {Number} chatID 47 | * @param {String} caption 48 | * @param {Number} file_id 49 | * @param {Object} option 50 | * @param {Boolean} notify 51 | */ 52 | async function BotSendAttachment(bot, chatID, caption, file_id, option = {}, notify = false) { 53 | option.disable_notification = !notify; 54 | let file_info = await bot.telegram.getFile(file_id); 55 | let file_path = path.parse(file_info.file_path); 56 | let res; 57 | try { 58 | if (file_path.dir == 'photos') { 59 | res = bot.telegram.sendPhoto(chatID, file_id, { 60 | caption, 61 | ...option 62 | }); 63 | } else if (file_path.dir == 'videos') { 64 | res = bot.telegram.sendVideo(chatID, file_id, { 65 | caption, 66 | ...option 67 | }); 68 | } else { 69 | res = bot.telegram.sendDocument(chatID, file_id, { 70 | caption, 71 | ...option 72 | }); 73 | } 74 | } catch (e) { 75 | console.log(e); 76 | } finally { 77 | return res; 78 | } 79 | } 80 | 81 | /** 82 | * @param {*} ctx 83 | * @param {Array.} replies 84 | * @param {Array.} options 85 | * @returns {Array.} 86 | */ 87 | async function BotReplyMultipleMessages(ctx, replies, options) { 88 | let results = []; 89 | if (typeof (options) == 'undefined') { 90 | options = []; 91 | } 92 | 93 | try { 94 | for (const i in replies) { 95 | let reply = replies[i]; 96 | let option = options[i] || {}; 97 | results.push(await BotReply(ctx, reply, option)); 98 | } 99 | } catch (e) { 100 | console.log(e); 101 | } 102 | return results; 103 | } 104 | 105 | module.exports = { 106 | BotReply, 107 | BotSendMessage, 108 | BotSendAttachment, 109 | BotReplyMultipleMessages 110 | }; -------------------------------------------------------------------------------- /BotCode/interactions/bot/subscriptions/basic.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise'); 2 | const { Languages, LoadReplies } = require('../static/replies/repliesLoader'); 3 | const rp = require('../static/replies/repliesLoader'); 4 | const kbs = require('../static/replies/keyboards'); 5 | const { Composer } = require('telegraf'); 6 | const { DataBase } = require('../../../storage/dataBase/DataBase'); 7 | const { speechToText } = require('../actions/stt/stt'); 8 | const stt = new speechToText(process.env.YC_API_KEY, process.env.YC_FOLDER_ID); 9 | const { BotReply } = require('../actions/replying'); 10 | const { HelpCommand, HandleTextMessage } = require('../actions/handling/textMessage'); 11 | 12 | /** 13 | * @param {Composer} bot 14 | * @param {Array.} tzPendingConfirmationUsers 15 | * @param {Array.} trelloPendingConfirmationUsers 16 | */ 17 | function InitBasicSubscriptions(bot, tzPendingConfirmationUsers, trelloPendingConfirmationUsers) { 18 | bot.start(async ctx => { 19 | const replies = LoadReplies(ctx.from.language_code); 20 | try { 21 | let inlineKeyboard = kbs.TzDeterminationOnStartInlineKeyboard(ctx.from.language_code); 22 | inlineKeyboard['disable_web_page_preview'] = true; 23 | BotReply(ctx, replies.start, inlineKeyboard); 24 | } catch (e) { 25 | console.error(e); 26 | } 27 | }); 28 | bot.help(async ctx => { 29 | try { 30 | HelpCommand(ctx); 31 | } catch (e) { 32 | console.error(e); 33 | } 34 | }); 35 | 36 | if (!!process.env.YC_FOLDER_ID && !!process.env.YC_API_KEY) { 37 | bot.on('voice', async ctx => { 38 | let fileInfo; 39 | try { 40 | fileInfo = await ctx.telegram.getFile(ctx.message.voice.file_id); 41 | } catch (e) { 42 | console.log(e); 43 | } 44 | console.log(`Received Voice msg`); 45 | if (ctx.message.voice.duration < global.MaximumVoiceMessageDuration) { 46 | let text; 47 | try { 48 | let uri = `https://api.telegram.org/file/bot${process.env.SMART_SCHEDULER_TLGRM_API_TOKEN}/${fileInfo.file_path}`; 49 | let voiceMessage = await request.get({ uri, encoding: null }); 50 | text = await stt.recognize(voiceMessage); 51 | } catch (e) { 52 | console.error(e); 53 | } 54 | if (text == '') { 55 | return; 56 | } 57 | ctx.message.text = text; 58 | let language = await DataBase.Users.GetUserLanguage(ctx.from.id); 59 | ctx.from.language_code = language; 60 | HandleTextMessage(bot, ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, 20); 61 | } else { 62 | try { 63 | BotReply(ctx, rp.voiceMessageTooBig); 64 | } catch (e) { 65 | console.error(e); 66 | } 67 | } 68 | }); 69 | } 70 | 71 | bot.on('message', async ctx => { 72 | try { 73 | let via_bot = ctx.message.via_bot; 74 | if (via_bot != undefined && via_bot.id == ctx.botInfo.id) { 75 | return; 76 | } 77 | HandleTextMessage(bot, ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers); 78 | } catch (e) { 79 | console.log(e) 80 | } 81 | }); 82 | } 83 | 84 | module.exports = InitBasicSubscriptions; -------------------------------------------------------------------------------- /BotCode/interactions/processing/utilities.js: -------------------------------------------------------------------------------- 1 | const { Languages } = require('../bot/static/replies/repliesLoader'); 2 | 3 | /** 4 | * @param {Number} id 5 | * @returns {String} 6 | */ 7 | function FormatChatId(id) { 8 | id = id.toString(10); 9 | if (id[0] == '-') { 10 | id = '_' + id.substring(1); 11 | } 12 | return id; 13 | } 14 | 15 | /** 16 | * @param {String} id 17 | * @returns {Number} 18 | */ 19 | function UnformatChatId(id) { 20 | if (id[0] == '_') { 21 | id = - +(id.substring(1)); 22 | } else { 23 | id = +id; 24 | } 25 | return id; 26 | } 27 | 28 | /** 29 | * @param {String} string 30 | * @returns {Languages} 31 | */ 32 | function DetermineLanguage(string) { 33 | let ruCount = [...string.matchAll(/[А-Яа-я]/g)].length; 34 | let enCount = [...string.matchAll(/[A-Za-z]/g)].length; 35 | let result = null; 36 | if (ruCount > enCount) { 37 | result = Languages.ru; 38 | } else if (enCount > ruCount) { 39 | result = Languages.en; 40 | } 41 | return result; 42 | } 43 | 44 | /** 45 | * @param {Array.} tz 46 | * @param {Array.} trello 47 | * @param {Number} id 48 | */ 49 | function ClearPendingConfirmation(tzs = [], trellos = [], id) { 50 | let index = tzs.indexOf(id) 51 | if (index >= 0) { 52 | tzs.splice(index, 1); 53 | } 54 | index = trellos.indexOf(id); 55 | if (index >= 0) { 56 | trellos.splice(index, 1); 57 | } 58 | } 59 | 60 | /** 61 | * @param {String} chatID 62 | * @param {Array.<{s: String, chatID: String}>} deletingIDs 63 | * @returns {Number} 64 | */ 65 | function GetDeletingIDsIndex(chatID, deletingIDs) { 66 | if (deletingIDs.length) { 67 | for (let i in deletingIDs) { 68 | if (deletingIDs[i].chatID == chatID) { 69 | return i; 70 | } 71 | } 72 | } 73 | return false; 74 | } 75 | 76 | /**@returns {String} */ 77 | function GetAttachmentId(message) { 78 | if (typeof (message.document) != 'undefined') { 79 | return message.document.file_id; 80 | } else if (typeof (message.video) != 'undefined') { 81 | return message.video.file_id; 82 | } else if (typeof (message.photo) != 'undefined' && message.photo.length > 0) { 83 | let photoes = message.photo; 84 | let file_id = photoes[0].file_id; 85 | let file_size = photoes[0].file_size; 86 | for (let i = 1; i < photoes.length; i++) { 87 | const photo = photoes[i]; 88 | if (photo.file_size > file_size) { 89 | file_size = photo.file_size; 90 | file_id = photo.file_id; 91 | } 92 | } 93 | return file_id; 94 | } 95 | return '~'; 96 | } 97 | 98 | /** 99 | * @param {Number} period 100 | * @param {Function} callback 101 | */ 102 | function RepeatActionsWithPeriod(period, callback) { 103 | const now = Date.now(); 104 | setTimeout(async function () { 105 | console.log(`Timeout expired`); 106 | setInterval(callback, period); 107 | await callback(); 108 | }, (Math.floor(now / period) + 1) * period - now); 109 | } 110 | 111 | module.exports = { 112 | FormatChatId, 113 | UnformatChatId, 114 | DetermineLanguage, 115 | ClearPendingConfirmation, 116 | GetDeletingIDsIndex, 117 | GetAttachmentId, 118 | RepeatActionsWithPeriod 119 | } -------------------------------------------------------------------------------- /BotCode/storage/dataBase/TablesClasses/Chat.js: -------------------------------------------------------------------------------- 1 | const { Connector } = require('../Connector'); 2 | 3 | class Chat { 4 | /**@type {String} */ 5 | id; 6 | /**@type {String} */ 7 | trello_board_id; 8 | /**@type {String} */ 9 | trello_list_id; 10 | /**@type {String} */ 11 | trello_token; 12 | 13 | constructor(id, trello_board_id, trello_list_id, trello_token) { 14 | this.id = id; 15 | this.trello_board_id = trello_board_id; 16 | this.trello_list_id = trello_list_id; 17 | this.trello_token = trello_token; 18 | } 19 | 20 | /** 21 | * @param {String} id 22 | * @param {String} trello_board_id 23 | */ 24 | static async AddChat(id, trello_board_id) { 25 | let query = `INSERT INTO chats VALUES ('${id}')`; 26 | if (typeof (trello_board_id) != 'undefined') { 27 | query = `INSERT INTO chats VALUES ('${id}', '${trello_board_id}')`; 28 | } 29 | return await Connector.instance.Query(query); 30 | } 31 | 32 | /** 33 | * @param {Array.} chats 34 | */ 35 | static async InsertChats(chats) { 36 | if (chats.length <= 0) { 37 | return; 38 | } 39 | let query = `INSERT INTO chats VALUES `; 40 | let i = 0; 41 | let values = []; 42 | for (const chat of chats) { 43 | query = `${query}($${++i}, $${++i}, $${++i}, $${++i}), `; 44 | values.push(chat.id, chat.trello_board_id, chat.trello_list_id, chat.trello_token); 45 | } 46 | query = query.substring(0, query.length - 2); 47 | await Connector.instance.paramQuery(query, values); 48 | } 49 | 50 | /** 51 | * @param {String} id 52 | * @returns {Chat} 53 | */ 54 | static async GetChatById(id) { 55 | if (id[0] == '_') { 56 | id = `-${id.substring(1)}`; 57 | } 58 | return (await Connector.instance.Query( 59 | `SELECT * FROM chats 60 | WHERE id = '${id}'` 61 | )).rows[0]; 62 | } 63 | 64 | /** 65 | * @returns {Array.} 66 | */ 67 | static async GetAllChats() { 68 | return (await Connector.instance.Query( 69 | `SELECT * FROM chats` 70 | )).rows; 71 | } 72 | 73 | /** 74 | * @param {String} id 75 | * @param {String} trello_board_id 76 | */ 77 | static async SetChatTrelloBoard(id, trello_board_id) { 78 | return await Connector.instance.paramQuery( 79 | `UPDATE chats 80 | SET trello_board_id = $1 81 | WHERE id = '${id}'`, 82 | [trello_board_id] 83 | ); 84 | } 85 | 86 | /** 87 | * @param {String} id 88 | * @param {String} trello_list_id 89 | */ 90 | static async SetChatTrelloList(id, trello_list_id, trello_token) { 91 | return await Connector.instance.Query( 92 | `UPDATE chats 93 | SET trello_list_id = '${trello_list_id}', 94 | trello_token = '${trello_token}' 95 | WHERE id = '${id}'` 96 | ); 97 | } 98 | 99 | /** 100 | * @param {String} id 101 | */ 102 | static async ClearChatFromTrello(id) { 103 | return await Connector.instance.Query( 104 | `UPDATE chats 105 | SET trello_board_id = NULL, 106 | trello_list_id = NULL, 107 | trello_token = NULL 108 | WHERE id = '${id}'` 109 | ); 110 | } 111 | } 112 | 113 | module.exports = Chat; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/inlineQuery.js: -------------------------------------------------------------------------------- 1 | const { wordsParseDate } = require("@alordash/date-parser"); 2 | const { arrayParseString } = require("@alordash/parse-word-to-number"); 3 | const { DataBase, Schedule } = require("../../../../storage/dataBase/DataBase"); 4 | const { ScheduleStates } = require("../../../../storage/dataBase/TablesClasses/Schedule"); 5 | const { FormStringFormatSchedule, ShortenString, FormDateStringFormat } = require("../../../processing/formatting"); 6 | const { ProcessParsedDate } = require("../../../processing/timeProcessing"); 7 | const { SimpleScheduleParse, FormParsedDates } = require("../remindersParsing"); 8 | 9 | /** 10 | * @param {*} ctx 11 | * @param {String} query 12 | */ 13 | async function TryInlineQuerySchedule(ctx, query = ctx.inlineQuery.query) { 14 | let results = []; 15 | let id = 0; 16 | 17 | let parsedDates = FormParsedDates(query, 40); 18 | console.log('parsedDates.length :>> ', parsedDates.length); 19 | if (parsedDates.length <= 0) { 20 | return; 21 | } 22 | 23 | let user = await DataBase.Users.GetUserById(ctx.from.id); 24 | console.log('user :>> ', user); 25 | const language = user.lang; 26 | 27 | let schedules = SimpleScheduleParse(parsedDates, user); 28 | console.log('schedules.length :>> ', schedules.length); 29 | if (schedules.length <= 0) { 30 | return false; 31 | } 32 | 33 | for (let schedule of schedules) { 34 | let title = ShortenString(schedule.text, 50); 35 | let description = `📝 ${FormDateStringFormat(new Date(schedule.target_date + user.tz * 1000))}`; 36 | let message_text = await FormStringFormatSchedule(schedule, user.tz, language, false, false, true); 37 | let result = { type: 'article', id: id++, description, title, input_message_content: { message_text, parse_mode: 'html' } }; 38 | results.push(result); 39 | } 40 | ctx.answerInlineQuery(results, 10); 41 | return true; 42 | } 43 | 44 | /** 45 | * @param {*} ctx 46 | */ 47 | async function InlineQuerySearch(ctx) { 48 | let query = ctx.inlineQuery.query.toLocaleLowerCase(); 49 | let results = []; 50 | let id = 0; 51 | 52 | const schedules = await DataBase.Schedules.GetSchedules(ctx.from.id, undefined, undefined, true); 53 | const user = await DataBase.Users.GetUserById(ctx.from.id); 54 | 55 | for (let schedule of schedules) { 56 | if (schedule.state != ScheduleStates.valid || !schedule.text.toLocaleLowerCase().includes(query)) 57 | continue; 58 | let title = ShortenString(schedule.text, 30); 59 | let description = `▪️ ${FormDateStringFormat(new Date(schedule.target_date + user.tz * 1000))}`; 60 | 61 | let message_text = await FormStringFormatSchedule(schedule, user.tz, user.lang, false, false); 62 | 63 | let result = { type: 'article', id: id++, title, input_message_content: { message_text, parse_mode: 'html' } }; 64 | if (true || schedule.text.length > 20) { 65 | result['description'] = description; 66 | } 67 | results.push(result); 68 | } 69 | ctx.answerInlineQuery(results, { cache_time: 5 }); 70 | } 71 | 72 | /** 73 | * @param {*} ctx 74 | */ 75 | async function HandleInlineQuery(ctx) { 76 | if (await TryInlineQuerySchedule(ctx)) 77 | return; 78 | InlineQuerySearch(ctx); 79 | } 80 | 81 | /** 82 | * @param {*} ctx 83 | * @param {String} text 84 | */ 85 | async function ConfirmInlineQuerySchedule(ctx, text) { 86 | let id = parseInt(ctx.chosenInlineResult.result_id); 87 | let schedule = SimpleScheduleParse(ctx.chosenInlineResult.query, await DataBase.Users.GetUserById(ctx.from.id), 40, id)[0]; 88 | if (schedule != undefined) 89 | DataBase.Schedules.AddSchedule(schedule); 90 | } 91 | 92 | module.exports = { HandleInlineQuery, ConfirmInlineQuerySchedule }; -------------------------------------------------------------------------------- /BotCode/interactions/bot/subscriptions/advanced.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { LoadReplies } = require('../static/replies/repliesLoader'); 4 | const kbs = require('../static/replies/keyboards'); 5 | const utils = require('../../processing/utilities'); 6 | const technicalActions = require('../actions/technical'); 7 | const { Composer } = require('telegraf'); 8 | const { DataBase, User } = require('../../../storage/dataBase/DataBase'); 9 | const Markup = require('telegraf/markup'); 10 | const { BotReply } = require('../actions/replying'); 11 | const HandleCallbackQueries = require('../actions/handling/callbackQueries/callbackQueries'); 12 | const { HandleInlineQuery, ConfirmInlineQuerySchedule } = require('../actions/handling/inlineQuery'); 13 | 14 | /** 15 | * @param {Composer} bot 16 | * @param {Array.} tzPendingConfirmationUsers 17 | * @param {Array.} trelloPendingConfirmationUsers 18 | */ 19 | function InitAdvancedSubscriptions(bot, tzPendingConfirmationUsers, trelloPendingConfirmationUsers) { 20 | console.log('__dirname :>> ', __dirname); 21 | let repliesFiles; 22 | try { 23 | repliesFiles = fs.readdirSync(__dirname.substring(0, __dirname.lastIndexOf('/')) + '/static/replies'); 24 | } catch (e) { 25 | repliesFiles = fs.readdirSync(path.join(__dirname, '..', 'static', 'replies')); 26 | } 27 | console.log('repliesFiles :>> ', repliesFiles); 28 | for (const filename of repliesFiles) { 29 | if (path.extname(filename) != '.json') { 30 | continue; 31 | } 32 | const language = path.basename(filename, '.json'); 33 | const replies = LoadReplies(language); 34 | if (typeof (replies.tzUseLocation) != 'undefined') { 35 | bot.hears(replies.tzUseLocation, ctx => { 36 | try { 37 | BotReply(ctx, replies.tzUseLocationResponse); 38 | } catch (e) { 39 | console.error(e); 40 | } 41 | }); 42 | } 43 | if (typeof (replies.cancel) != 'undefined') { 44 | bot.hears(replies.cancel, async ctx => { 45 | utils.ClearPendingConfirmation(tzPendingConfirmationUsers, trelloPendingConfirmationUsers, ctx.from.id); 46 | let reply = replies.cancelReponse; 47 | let user = await DataBase.Users.GetUserById(ctx.from.id, true); 48 | if (typeof (user) == 'undefined' || user.tz == null) { 49 | reply += '\r\n' + replies.tzCancelWarning; 50 | } 51 | try { 52 | BotReply(ctx, reply, await kbs.LogicalListKeyboard(language, utils.FormatChatId(ctx.chat.id))); 53 | } catch (e) { 54 | console.error(e); 55 | } 56 | }); 57 | } 58 | if (typeof (replies.showListAction) != 'undefined') { 59 | bot.hears(replies.showListAction, async ctx => { 60 | let chatID = utils.FormatChatId(ctx.chat.id); 61 | let tz = (await DataBase.Users.GetUserById(ctx.from.id)).tz; 62 | let answers = await technicalActions.LoadSchedulesList(chatID, tz, language); 63 | for (const answer of answers) { 64 | try { 65 | BotReply(ctx, answer, { disable_web_page_preview: true }); 66 | } catch (e) { 67 | console.error(e); 68 | } 69 | } 70 | }); 71 | } 72 | } 73 | 74 | bot.on('location', async ctx => { 75 | if (typeof (process.env.SMART_SCHEDULER_GOOGLE_API_KEY) == 'undefined') { 76 | return; 77 | } 78 | let location = ctx.message.location; 79 | technicalActions.ConfirmLocation(ctx, location.latitude, location.longitude, tzPendingConfirmationUsers); 80 | }); 81 | 82 | bot.on('callback_query', async (ctx) => { 83 | let language = await DataBase.Users.GetUserLanguage(ctx.from.id); 84 | ctx.from.language_code = language; 85 | HandleCallbackQueries(ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers); 86 | }); 87 | 88 | bot.on('inline_query', async (ctx) => { 89 | HandleInlineQuery(ctx); 90 | }); 91 | 92 | bot.on('chosen_inline_result', async (ctx) => { 93 | let res = ctx.message; 94 | console.log('res :>> ', res); 95 | ConfirmInlineQuerySchedule(ctx, ctx.chosenInlineResult.query); 96 | }) 97 | } 98 | 99 | module.exports = InitAdvancedSubscriptions; -------------------------------------------------------------------------------- /BotCode/interactions/processing/remindersOperations.js: -------------------------------------------------------------------------------- 1 | const { DataBase, Chat } = require('../../storage/dataBase/DataBase'); 2 | const { Schedule, GetOptions, ScheduleStates } = require('../../storage/dataBase/TablesClasses/Schedule'); 3 | const { TrelloManager } = require('@alordash/node-js-trello'); 4 | const { ExtractNicknames, GetUsersIDsFromNicknames } = require('./nicknamesExtraction'); 5 | const utils = require('./utilities'); 6 | const request = require('request-promise'); 7 | const path = require('path'); 8 | const { ShortenString } = require('./formatting'); 9 | 10 | /** 11 | * @param {Schedule} schedule 12 | * @param {TrelloManager} trelloManager 13 | */ 14 | async function RemoveTrelloBoard(schedule, trelloManager) { 15 | if (typeof (schedule.trello_card_id) != 'undefined' && schedule.trello_card_id != null) { 16 | trelloManager.DeleteCard(schedule.trello_card_id); 17 | } 18 | } 19 | 20 | /** 21 | * @param {*} bot 22 | * @param {Array.} schedule 23 | */ 24 | async function RemoveReminders(bot, schedules = []) { 25 | for (const schedule of schedules) { 26 | let chat = await DataBase.Chats.GetChatById(schedule.chatid); 27 | if (typeof (chat) != 'undefined' && chat.trello_list_id != null) { 28 | let trelloManager = new TrelloManager(process.env.TRELLO_TOKEN, chat.trello_token); 29 | let _schedules = await DataBase.Schedules.GetSchedules(schedule.chatid, GetOptions.all, schedule.message_id); 30 | for (const _schedule of _schedules) { 31 | RemoveTrelloBoard(_schedule, trelloManager); 32 | } 33 | } 34 | } 35 | 36 | await DataBase.Schedules.RemoveSchedules(schedules); 37 | 38 | for (const schedule of schedules) { 39 | let _chatid = utils.UnformatChatId(schedule.chatid); 40 | try { 41 | if (_chatid < 0 && schedule.state == ScheduleStates.pending) { 42 | bot.telegram.deleteMessage(_chatid, schedule.message_id); 43 | } else { 44 | bot.telegram.editMessageReplyMarkup(_chatid, schedule.message_id); 45 | } 46 | } catch (e) { 47 | console.log(e); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * @param {*} bot 54 | * @param {String} chatID 55 | * @param {Number} message_id 56 | * @returns {Boolean} 57 | */ 58 | async function RemoveInvalidRemindersMarkup(bot, chatID, message_id = null) { 59 | try { 60 | if (message_id == null) { 61 | let invalidSchedules = await DataBase.Schedules.GetSchedules(chatID, GetOptions.invalid); 62 | let invalidSchedule = invalidSchedules[0]; 63 | if (typeof (invalidSchedule) == 'undefined') { 64 | return false; 65 | } 66 | message_id = invalidSchedule.message_id; 67 | } 68 | let _chatid = utils.UnformatChatId(chatID); 69 | bot.telegram.editMessageReplyMarkup(_chatid, message_id); 70 | } catch (e) { 71 | console.error(e); 72 | return false; 73 | } 74 | return true; 75 | } 76 | 77 | /** 78 | * @param {*} ctx 79 | * @param {Schedule} schedule 80 | * @param {Chat} chat 81 | * @returns {Schedule} 82 | */ 83 | async function AddScheduleToTrello(ctx, schedule, chat = null) { 84 | if (chat == null) { 85 | chat = await DataBase.Chats.GetChatById(schedule.chatid); 86 | } 87 | let trelloManager = new TrelloManager(process.env.TRELLO_TOKEN, chat.trello_token); 88 | 89 | let nickExtractionResult = ExtractNicknames(schedule.text); 90 | let ids = await GetUsersIDsFromNicknames(nickExtractionResult.nicks, trelloManager); 91 | schedule.text = nickExtractionResult.string; 92 | 93 | 94 | let text = ShortenString(schedule.text); 95 | 96 | let card = await trelloManager.AddCard(chat.trello_list_id, text, schedule.text, 0, new Date(schedule.target_date), ids); 97 | 98 | if (schedule.file_id != undefined && schedule.file_id != '~') { 99 | let fileInfo; 100 | try { 101 | fileInfo = await ctx.telegram.getFile(schedule.file_id); 102 | let uri = `https://api.telegram.org/file/bot${process.env.SMART_SCHEDULER_TLGRM_API_TOKEN}/${fileInfo.file_path}`; 103 | let file = await request.get({ uri, encoding: null }); 104 | let fileName = path.basename(fileInfo.file_path); 105 | await trelloManager.AddAttachment(card.id, file, { name: fileName }); 106 | } catch (e) { 107 | console.log(e); 108 | } 109 | } 110 | 111 | if (typeof (card) != 'undefined') { 112 | schedule.trello_card_id = card.id; 113 | } 114 | schedule.max_date = 0; 115 | schedule.period_time = 0; 116 | await DataBase.Schedules.SetSchedule(schedule); 117 | return schedule; 118 | } 119 | 120 | module.exports = { 121 | RemoveReminders, 122 | RemoveInvalidRemindersMarkup, 123 | AddScheduleToTrello 124 | }; -------------------------------------------------------------------------------- /README-EN.md: -------------------------------------------------------------------------------- 1 | # [t.me/SmartScheduler_bot](https://t.me/SmartScheduler_bot) 2 | 3 | - [Описание на русском](README.md) 4 | 5 | ## Bot understands English and Russian languages 6 | 7 | If you need fast and handy tool to schedule your plans, Smart Scheduler Bot is the right choice. 8 | 9 | ![Usage example](UsageExample-EN.png) 10 | 11 | ## How to use 12 | 13 | [Smart Scheduler Bot works in Telegram.](https://t.me/SmartScheduler_bot) 14 | Just type task with required expiration time and Smart Scheduler will automatically find scheduling date and what task to schedule in your message. 15 | Smart Scheduler will send you notification when particular task's time expires. 16 | 17 | 18 | ## Features 19 | 20 | You do not need to follow specified date format, Smart Scheduler understands most of human date formats (e.g. «in 5 minutes, «at 5 minutes to 10 pm», «after tomorrow in half past 1»). 21 | Also it's not necessary to write perfectly. Bot can understand what you meant to say even **in words with mistakes** (e.g. «in twinty minuts» -> *«in 20 minutes»*). 22 | You can make multiple reminders in one message by dividing each with dot, comma or word "**and**". 23 | 24 | Smart Scheduler can store tasks with *minute precision*. 25 | Smart Scheduler can generate schedules from **voice messages**. 26 | Smart Scheduler supports recurring reminders (e.g. «every 5 minutes») and also supports time limits for them (e.g. «every 5 minutes until 20 hours»). 27 | Smart Scheduler supports pictures, videos and files 💾 in reminders. 28 | Smart Scheduler stores tasks **separately** for every chat and can work in conversations. 29 | 30 | Smart Scheduler can be integrated to your [Trello](https://trello.com/) board! 31 | More details: [Guide to working with Trello](https://t.me/SmartScheduler_Info/25). 32 | 33 | ### Supported commands: 34 | 35 | - 🗓 **/list** - Shows active tasks for current chat. 36 | 37 | - 🗑 **/del** _1, 2, ...N_ - Deletes tasks by id. 38 | 39 | - 🗑 **/del** _1-10, A-B_ - Deletes all tasks within range. 40 | 41 | - #️⃣ **_/N_** - Deletes N-th task. 42 | 43 | - 🌐 **_/tz_** - Configures time zone. 44 | 45 | and /start with /help of course. 46 | 47 | 48 | ## Installation 49 | 50 | This bot requires PostgreSQL data base. 51 | 52 | ### Environment variables 53 | 54 | Make sure you set following environment variables before starting the bot: 55 | 1. **ENABLE_LOGS**: "true" or "false", enables or disables logging. 56 | 2. **ENABLE_SCHEDULES_CHEKING**: "true" or "false", enables or disables checking and sending notifications. 57 | 3. **TZ**: only "GMT". 58 | 4. **DATABASE_URL** (optional): URL of your PostgreSQL data base. 59 | 5. **SMART_SCHEDULER_DB_URL** (optional): URL of your PostgreSQL data base. 60 | 6. **IS_HEROKU**: "true" or "false". If **true**, then use **DATABASE_URL** for data base URL, else **SMART_SCHEDULER_DB_URL**. 61 | 7. **SMART_SCHEDULER_TLGRM_API_TOKEN**: telegram bot token. 62 | 63 | For debugging (optional): 64 | 65 | 8. **SMART_SCHEDULER_ADMIN**: telegram-id of bot administrator user. 66 | 9. **SMART_SCHEDULER_DEBUG_MODE**: "true" or "false". If enabled, then bot will check only those reminders, which were created by **SMART_SCHEDULER_ADMIN**. 67 | 68 | For time zone definition via geolocation (optional): 69 | 70 | 10. **SMART_SCHEDULER_GOOGLE_API_KEY**: api-key for Google services. 71 | 72 | For voice messages processing (optional): 73 | 74 | 11. **YC_API_KEY**: Yandex api key. 75 | 12. **YC_FOLDER_ID**: Yandex catalog id. 76 | 77 | For integration with [Trello](https://trello.com/) (optional): 78 | 79 | 13. **TRELLO_TOKEN**: Trello api token. 80 | 81 | ### On local server 82 | 83 | ``` 84 | $ git clone https://github.com/alordash/BotSmartScheduler 85 | $ cd BotSmartScheduler 86 | $ npm install 87 | $ node ./BotCode/index.js 88 | ``` 89 | 90 | ### Deploying on heroku 91 | 92 | 1. Create [github](https://github.com/join) account. 93 | 2. Add [this](https://github.com/alordash/BotSmartScheduler) repository to yourself. 94 | 3. Create [heroku](https://signup.heroku.com/) account. 95 | 4. Create new [heroku application](https://dashboard.heroku.com/new-app). 96 | 5. Open created application. 97 | 6. Follow [this link](https://elements.heroku.com/addons/heroku-postgresql). 98 | 7. Press **Install Heroku Postgres**. 99 | 8. In the appeared line type your application name and press **Provision add-on**. 100 | 9. Go to the tab **Settings** of your application. 101 | 10. Press **Reveal Config Vars**. 102 | 11. Fill all necessary enviromental variables. 103 | 12. Go to the tab **Deploy**. 104 | 13. In the field **Deployment method** choose **GitHub**. 105 | 14. Connect your github account to heroku by pressing **Connect to GitHub**. 106 | 15. In appeared window choose *your* respository, press **connect**. 107 | 16. Press **Deploy Branch**. 108 | 17. Once loaded, go to the tab **Resources**. 109 | 18. Disable **web**, enable **worker**. 110 | Your bot is ready to work. 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [t.me/SmartScheduler_bot](https://t.me/SmartScheduler_bot) 2 | 3 | - [English description](README-EN.md) 4 | 5 | ## Бот понимает русскую и англоязычную речь 6 | 7 | Если вам нужно быстро и с удобством планировать свои задачи, бот Smart Scheduler станет незаменимым инструментом в этом деле. 8 | 9 | ![Пример использования](UsageExample-RU.png) 10 | 11 | ## Как использовать 12 | 13 | [Бот Smart Scheduler работает в Telegram.](https://t.me/SmartScheduler_bot) 14 | Просто напишите задачу и требуемое время, а Smart Scheduler автоматически найдет дату и задачу в сообщении. 15 | По истечению времени запланированной задачи бот Smart Scheduler пришлет вам уведомление. 16 | 17 | ## Особенности 18 | 19 | Данный бот распознает большинство форм представления даты человеком (например «через X минут», «в без пятнадцати десять», «послезавтра пол первого»), поэтому вам не нужно следовать определенному формату времени. 20 | Также не обязательно писать слова «абсолютно» точно. Бот способен понять что вы имели ввиду даже **в словах с ошибками** (например «чрез читыре нидели» -> *«через 4 недели»*). 21 | Вы можете создавать сразу несколько напоминаний в одном сообщении, разделяя каждое с помощью точки, запятой или союза "**и**". 22 | 23 | Smart Scheduler способен записывать время с точностью *до минуты*. 24 | Smart Scheduler может формировать напоминания из **голосовых сообщений**. 25 | Smart Scheduler поддерживает повторяющиеся напоминания («каждые 5 минут»), а также поддерживает ограничение для них («каждые 5 минут до 20 часов»). 26 | Smart Scheduler поддерживает картинки, видео или файлы 💾 в напоминаниях. 27 | Smart Scheduler хранит для каждого чата задачи **раздельно** и может функционировать в беседах. 28 | 29 | Smart Scheduler может быть интегрирован в Вашу [Trello](https://trello.com/) доску! 30 | Подробнее: [Руководство по работе с Trello](https://t.me/SmartScheduler_Info/25) 31 | 32 | ### Поддерживаемые комманды: 33 | 34 | - 🗓 **/list** - Показывает активные задачи для данного чата. 35 | 36 | - 🗑 **/del** _1, 2, ...N_ - Удаляет задачи по id. 37 | 38 | - 🗑 **/del** _1-10, A-B_ - Удаляет задачи в введенном диапазоне. 39 | 40 | - #️⃣ **_/N_** - Удаляет N-ную задачу. 41 | 42 | - 🌐 **_/tz_** - Настройка часового пояса. 43 | 44 | и, конечно же, /start и /help. 45 | 46 | ## Установка 47 | 48 | Для работы бота требуется база данных PostgreSQL. 49 | 50 | ### Переменные среды 51 | 52 | Прежде чем запускать бота, убедитесь что вы установили следующие переменные среды: 53 | 1. **ENABLE_LOGS**: "true" или "false", включает или отключает логирование. 54 | 2. **ENABLE_SCHEDULES_CHEKING**: "true" или "false", включает или отключает проверку и отправление уведомлений. 55 | 3. **TZ**: только "GMT". 56 | 4. **DATABASE_URL** (опционально): URL базы данных PostgreSQL. 57 | 5. **SMART_SCHEDULER_DB_URL** (опционально): URL базы данных PostreSQL. 58 | 6. **IS_HEROKU**: "true" или "false". Если **true**, то для базы данных используется **DATABASE_URL**, иначе **SMART_SCHEDULER_DB_URL**. 59 | 7. **SMART_SCHEDULER_TLGRM_API_TOKEN**: токен бота в телеграмме. 60 | 61 | Для дебага (опционально): 62 | 63 | 8. **SMART_SCHEDULER_ADMIN**": telegram-id пользователя-администратора бота 64 | 9. **SMART_SCHEDULER_DEBUG_MODE**: "true" или "false". При включении бот проверяет только те напоминания, которые были созданные пользователем **SMART_SCHEDULER_ADMIN**. 65 | 66 | Для определения часового пояса по геолокации (опционально): 67 | 68 | 10. **SMART_SCHEDULER_GOOGLE_API_KEY**: api-ключ для сервисов Google. 69 | 70 | Для обработки голосовых сообщений (опционально): 71 | 72 | 11. **YC_API_KEY**: Yandex api key. 73 | 12. **YC_FOLDER_ID**: Yandex catalog id. 74 | 75 | Для интеграции с [Trello](https://trello.com/) (опционально): 76 | 77 | 13. **TRELLO_TOKEN**: Trello api token. 78 | 79 | ### На локальном сервере 80 | 81 | ``` 82 | $ git clone https://github.com/alordash/BotSmartScheduler 83 | $ cd BotSmartScheduler 84 | $ npm install 85 | $ node ./BotCode/index.js 86 | ``` 87 | 88 | ### Используя heroku 89 | 90 | 1. Создайте аккаунт [github](https://github.com/join). 91 | 2. Добавьте [этот](https://github.com/alordash/BotSmartScheduler) репозиторий к себе. 92 | 3. Создайте аккаунт [heroku](https://signup.heroku.com/). 93 | 4. Создайте новое [heroku приложение](https://dashboard.heroku.com/new-app). 94 | 5. Откройте новое приложение. 95 | 6. Перейдите по [этой ссылке](https://elements.heroku.com/addons/heroku-postgresql). 96 | 7. Нажмите **Install Heroku Postgres**. 97 | 8. В появившейся строке введите название своего приложения и нажмите **Provision add-on**. 98 | 9. Перейдите во вкладку **Settings** своего приложения. 99 | 10. Нажмите **Reveal Config Vars**. 100 | 11. Заполните все необходимые переменные среды. 101 | 12. Перейдите во вкладку **Deploy**. 102 | 13. В поле **Deployment method** выберите **GitHub**. 103 | 14. Подключите свой github аккаунт к хероку нажав **Connect to GitHub**. 104 | 15. В появившемся окне выберите *ваш* репозиторий, нажмите **connect**. 105 | 16. Нажмите **Deploy Branch**. 106 | 17. После окончания загрузки перейдите во вкладку **Resources**. 107 | 18. Отключите **web**, включите **worker**. 108 | Ваш бот готов к работе. 109 | -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/textMessage.js: -------------------------------------------------------------------------------- 1 | const Markup = require('telegraf/markup'); 2 | const { Languages, LoadReplies } = require('../../static/replies/repliesLoader'); 3 | const Format = require('../../../processing/formatting'); 4 | const kbs = require('../../static/replies/keyboards'); 5 | const { DataBase, User, Chat } = require('../../../../storage/dataBase/DataBase'); 6 | const { Schedule, ScheduleStates } = require('../../../../storage/dataBase/TablesClasses/Schedule'); 7 | const { help, trelloAddListCommand, trelloHelp } = require('../../static/commandsList'); 8 | const { BotReply, BotSendAttachment } = require('../replying'); 9 | const utils = require('../../../processing/utilities'); 10 | const { ParseScheduleMessage } = require('../remindersParsing'); 11 | const { ConfrimTimeZone } = require('../technical'); 12 | const { TrelloAuthenticate, TrelloAddList } = require('./trelloCommands'); 13 | 14 | /** @param {*} ctx */ 15 | async function HelpCommand(ctx) { 16 | let language = await DataBase.Users.GetUserLanguage(ctx.from.id); 17 | const replies = LoadReplies(language); 18 | let keyboard = kbs.HelpSectionsKeyboards(language); 19 | keyboard['disable_web_page_preview'] = true; 20 | // reply = `${replies.trelloHelp}\r\n${Format.TrelloInfoLink(language, process.env.SMART_SCHEDULER_INVITE)}`; 21 | BotReply(ctx, replies.commands, keyboard); 22 | } 23 | 24 | /** 25 | * @param {*} ctx 26 | * @param {String} chatID 27 | * @param {String} msgText 28 | * @returns 29 | */ 30 | async function HandleCommandMessage(bot, ctx, chatID, msgText) { 31 | let regExp = new RegExp(`^${trelloAddListCommand}[0-9]+`); 32 | let match = msgText.match(regExp); 33 | if (match != null) { 34 | //#region ADD TRELLO LIST 35 | TrelloAddList(ctx); 36 | return; 37 | //#endregion 38 | } 39 | //#region DELETE CLICKED TASK 40 | let scheduleNum = parseInt(msgText.substring(1, msgText.length)); 41 | if (isNaN(scheduleNum)) { 42 | return; 43 | } 44 | let schedule = await DataBase.Schedules.GetScheduleByNum(chatID, scheduleNum); 45 | try { 46 | const text = Format.Deleted(scheduleNum.toString(10), false, ctx.from.language_code); 47 | if (typeof (schedule) != 'undefined') { 48 | await DataBase.Schedules.RemoveScheduleByNum(chatID, scheduleNum); 49 | await DataBase.Schedules.ReorderSchedules(chatID); 50 | if (schedule.file_id != '~' && schedule.file_id != null) { 51 | BotSendAttachment(bot, +chatID, text, schedule.file_id); 52 | return; 53 | } 54 | } 55 | BotReply(ctx, text); 56 | } catch (e) { 57 | console.error(e); 58 | } 59 | //#endregion 60 | } 61 | 62 | /** 63 | * @param {*} bot 64 | * @param {*} ctx 65 | * @param {Array.} tzPendingConfirmationUsers 66 | * @param {Array.} trelloPendingConfirmationUsers 67 | * @param {Number} prevalenceForParsing 68 | */ 69 | async function HandleTextMessage(bot, ctx, tzPendingConfirmationUsers, trelloPendingConfirmationUsers, prevalenceForParsing = 50) { 70 | console.log(`Handling text msg in chat "${ctx.chat.id}"`); 71 | let chatID = utils.FormatChatId(ctx.chat.id); 72 | let inGroup = chatID[0] === '_'; 73 | let msgText = ctx.message.text; 74 | if (typeof (msgText) == 'undefined') { 75 | msgText = ctx.message.caption; 76 | } 77 | if (typeof (msgText) == 'undefined' || (inGroup && typeof (ctx.message.forward_date) != 'undefined')) { 78 | DataBase.Schedules.RemoveSchedulesByState(chatID, ScheduleStates.invalid); 79 | return; 80 | } 81 | 82 | let user = await DataBase.Users.GetUserById(ctx.from.id, true); 83 | let language = user.lang; 84 | let determinedLanguage; 85 | if (user.id == null) { 86 | language = determinedLanguage = utils.DetermineLanguage(msgText); 87 | user = new User(ctx.from.id, undefined, language, undefined, undefined); 88 | await DataBase.Users.AddUser(user); 89 | } 90 | ctx.from.language_code = language; 91 | 92 | const mentionText = `@${ctx.me}`; 93 | const mentionIndex = msgText.indexOf(mentionText); 94 | const mentioned = mentionIndex != -1; 95 | if (mentioned) { 96 | msgText = msgText.substring(0, mentionIndex) + msgText.substring(mentionIndex + mentionText.length); 97 | if (msgText[mentionIndex - 1] == ' ' && msgText[mentionIndex] == ' ') { 98 | msgText = msgText.substring(0, mentionIndex) + msgText.substring(mentionIndex + 1); 99 | } 100 | } 101 | if (tzPendingConfirmationUsers.indexOf(ctx.from.id) >= 0) { 102 | DataBase.Schedules.RemoveSchedulesByState(chatID, ScheduleStates.invalid); 103 | ConfrimTimeZone(ctx, tzPendingConfirmationUsers); 104 | return; 105 | } 106 | if (trelloPendingConfirmationUsers.indexOf(ctx.from.id) >= 0) { 107 | DataBase.Schedules.RemoveSchedulesByState(chatID, ScheduleStates.invalid); 108 | TrelloAuthenticate(ctx, trelloPendingConfirmationUsers); 109 | return; 110 | } 111 | 112 | if (msgText[0] == '/') { 113 | await DataBase.Schedules.RemoveSchedulesByState(chatID, ScheduleStates.invalid); 114 | HandleCommandMessage(bot, ctx, chatID, msgText); 115 | return; 116 | } 117 | 118 | if (typeof (determinedLanguage) == 'undefined') { 119 | determinedLanguage = utils.DetermineLanguage(msgText); 120 | if (determinedLanguage != null) { 121 | language = determinedLanguage; 122 | } 123 | } 124 | ParseScheduleMessage(ctx, chatID, inGroup, msgText, language, mentioned, prevalenceForParsing); 125 | } 126 | 127 | module.exports = { 128 | HelpCommand, 129 | HandleTextMessage 130 | } -------------------------------------------------------------------------------- /BotCode/interactions/bot/static/replies/keyboards.js: -------------------------------------------------------------------------------- 1 | const Markup = require('telegraf/markup'); 2 | const Extra = require('telegraf/extra'); 3 | const { Languages, LoadReplies } = require('./repliesLoader'); 4 | const { DataBase, Schedule } = require('../../../../storage/dataBase/DataBase'); 5 | 6 | /** 7 | * @param {Array.} keyboards 8 | */ 9 | function MergeInlineKeyboards(...keyboards) { 10 | let main_keyboard = keyboards[0]; 11 | if (typeof (main_keyboard.reply_markup.inline_keyboard) == 'undefined') { 12 | main_keyboard.reply_markup.inline_keyboard = []; 13 | } 14 | let count = 0; 15 | for (let i = 1; i < keyboards.length; i++) { 16 | let kb = keyboards[i]; 17 | if (typeof (kb) == 'undefined') { 18 | continue; 19 | } 20 | let inline_keyboard = kb.reply_markup.inline_keyboard; 21 | if (typeof (inline_keyboard) != 'undefined' && inline_keyboard.length > 0) { 22 | count++; 23 | main_keyboard.reply_markup.inline_keyboard.push( 24 | ...inline_keyboard 25 | ); 26 | } 27 | } 28 | if (count > 0 && typeof (main_keyboard.reply_markup.keyboard) != 'undefined') { 29 | delete main_keyboard.reply_markup.keyboard; 30 | } 31 | return main_keyboard; 32 | } 33 | 34 | /**@param {Languages} language */ 35 | function ListKeyboard(language) { 36 | const replies = LoadReplies(language); 37 | return Markup.keyboard([ 38 | [{ text: replies.showListAction }] 39 | ]).removeKeyboard().resize().extra(); 40 | } 41 | 42 | /**@param {Languages} language */ 43 | function RepeatButton(language) { 44 | const replies = LoadReplies(language); 45 | return Extra.markup((m) => 46 | m.inlineKeyboard([ 47 | m.callbackButton(replies.repeatSchedule, `repeat`) 48 | ]) 49 | ); 50 | } 51 | 52 | /**@param {Languages} language */ 53 | function TzDeterminationKeyboard(language) { 54 | const replies = LoadReplies(language); 55 | return Markup 56 | .keyboard([ 57 | [ 58 | // { text: replies.tzUseLocation, request_location: true }, 59 | { text: replies.cancel }] 60 | ]).resize() 61 | .extra(); 62 | } 63 | 64 | /**@param {Languages} language */ 65 | function TzDeterminationOnStartInlineKeyboard(language) { 66 | const replies = LoadReplies(language); 67 | return Extra.markup((m) => 68 | m.inlineKeyboard([ 69 | m.callbackButton(replies.startTZ, `startTZ`) 70 | ]) 71 | ); 72 | } 73 | 74 | /**@param {Languages} language */ 75 | function CancelKeyboard(language) { 76 | const replies = LoadReplies(language); 77 | return Markup 78 | .keyboard([ 79 | [{ text: replies.cancel }] 80 | ]).oneTime() 81 | .resize() 82 | .extra(); 83 | } 84 | 85 | /**@param {Languages} language */ 86 | function CancelButton(language) { 87 | const replies = LoadReplies(language); 88 | return Extra.markup((m) => 89 | m.inlineKeyboard([ 90 | m.callbackButton(replies.cancel, 'cancel') 91 | ]) 92 | ); 93 | } 94 | 95 | /**@param {Languages} language */ 96 | function ConfirmSchedulesKeyboard(language) { 97 | const replies = LoadReplies(language); 98 | return Extra.markup((m) => 99 | m.inlineKeyboard([ 100 | m.callbackButton(replies.confirmSchedule, `confirm`), 101 | m.callbackButton(replies.declineSchedule, `delete`) 102 | ]) 103 | ); 104 | } 105 | 106 | function RemoveKeyboard() { 107 | return { reply_markup: { remove_keyboard: true } }; 108 | } 109 | 110 | /** 111 | * @param {Languages} language 112 | * @param {String} chatID 113 | * @param {Number} schedulesCount 114 | */ 115 | async function LogicalListKeyboard(language, chatID, schedulesCount = -1) { 116 | if (schedulesCount == -1) { 117 | schedulesCount = await DataBase.Schedules.GetSchedulesCount(chatID); 118 | } 119 | return schedulesCount > 0 ? ListKeyboard(language) : RemoveKeyboard(); 120 | } 121 | 122 | /** 123 | * @param {Languages} language 124 | */ 125 | function HelpSectionsKeyboards(language) { 126 | const replies = LoadReplies(language); 127 | return Extra.markup((m) => 128 | m.inlineKeyboard([ 129 | m.callbackButton(replies.trelloHelpButton, `help_trello`) 130 | ]) 131 | ); 132 | } 133 | 134 | /** 135 | * @param {Languages} language 136 | * @param {String} cb_query 137 | */ 138 | function BackKeyboard(language, cb_query) { 139 | const replies = LoadReplies(language); 140 | return Extra.markup((m) => 141 | m.inlineKeyboard([ 142 | m.callbackButton(replies.getBack, cb_query) 143 | ]) 144 | ); 145 | } 146 | 147 | /** 148 | * @param {Languages} language 149 | */ 150 | function ToTrelloKeyboard(language) { 151 | const replies = LoadReplies(language); 152 | return Extra.markup((m) => 153 | m.inlineKeyboard([ 154 | m.callbackButton(replies.toTrelloButton, 'to_trello') 155 | ]) 156 | ); 157 | } 158 | 159 | /** 160 | * @param {Languages} language 161 | */ 162 | function CompleteReminderButton(language) { 163 | const replies = LoadReplies(language); 164 | return Extra.markup((m) => 165 | m.inlineKeyboard([ 166 | m.callbackButton(replies.completeReminder, 'complete_reminder') 167 | ]) 168 | ); 169 | } 170 | 171 | module.exports = { 172 | MergeInlineKeyboards, 173 | ListKeyboard, 174 | RepeatButton, 175 | TzDeterminationKeyboard, 176 | TzDeterminationOnStartInlineKeyboard, 177 | CancelKeyboard, 178 | CancelButton, 179 | ConfirmSchedulesKeyboard, 180 | RemoveKeyboard, 181 | LogicalListKeyboard, 182 | HelpSectionsKeyboards, 183 | BackKeyboard, 184 | ToTrelloKeyboard, 185 | CompleteReminderButton 186 | } -------------------------------------------------------------------------------- /BotCode/storage/dataBase/TablesClasses/User.js: -------------------------------------------------------------------------------- 1 | const { Connector } = require('../Connector'); 2 | 3 | class User { 4 | /**@type {Number} */ 5 | id; 6 | /**@type {Number} */ 7 | tz; 8 | /**@type {String} */ 9 | lang; 10 | /**@type {Boolean} */ 11 | subscribed; 12 | /**@type {String} */ 13 | trello_token; 14 | 15 | /**@param {Number} id 16 | * @param {Number} tz 17 | * @param {String} lang 18 | * @param {Boolean} subscribed 19 | * @param {String} trello_token 20 | */ 21 | constructor(id, tz = global.defaultUserTimezone, lang = global.defaultUserLanguage, subscribed = true, trello_token = null) { 22 | if (typeof (id) == 'object' && id != null) { 23 | tz = +id.tz; 24 | lang = id.lang; 25 | subscribed = id.subscribed; 26 | trello_token = id.trello_token; 27 | id = +id.id; 28 | } 29 | this.id = id; 30 | this.tz = tz; 31 | this.lang = lang; 32 | this.subscribed = true; 33 | this.trello_token = trello_token; 34 | } 35 | 36 | 37 | /**@param {User} user */ 38 | static async AddUser(user) { 39 | return await Connector.instance.Query(`INSERT INTO userids VALUES (${user.id}, ${user.tz}, '${user.lang}', true)`); 40 | } 41 | 42 | /** 43 | * @param {Array.} users 44 | */ 45 | static async InsertUsers(users) { 46 | if (users.length <= 0) { 47 | return; 48 | } 49 | let query = `INSERT INTO userids VALUES `; 50 | let i = 0; 51 | let values = []; 52 | for (const user of users) { 53 | query = `${query}($${++i}, $${++i}, $${++i}, $${++i}, $${++i}), `; 54 | values.push(user.id, user.tz, user.lang, user.subscribed, user.trello_token); 55 | } 56 | query = query.substring(0, query.length - 2); 57 | await Connector.instance.paramQuery(query, values); 58 | } 59 | 60 | /**@param {Number} id 61 | * @param {Number} tz 62 | */ 63 | static async SetUserTz(id, tz) { 64 | return await Connector.instance.Query( 65 | `UPDATE userids 66 | SET tz = ${tz} 67 | WHERE id = ${id};` 68 | ); 69 | } 70 | 71 | /**@param {Number} id 72 | * @param {String} language 73 | */ 74 | static async SetUserLanguage(id, language) { 75 | return await Connector.instance.Query( 76 | `UPDATE userids 77 | SET lang = '${language}' 78 | WHERE id = ${id};` 79 | ); 80 | } 81 | 82 | /**@param {Number} id 83 | * @returns {String} 84 | */ 85 | static async GetUserLanguage(id) { 86 | let res = await Connector.instance.Query(`SELECT * FROM userids where id = ${id}`); 87 | if (typeof (res) != 'undefined' && res.rows.length > 0) { 88 | return res.rows[0].lang; 89 | } else { 90 | return undefined; 91 | } 92 | } 93 | 94 | /**@param {Number} id 95 | * @param {Boolean} subscribed 96 | */ 97 | static async SetUserSubscription(id, subscribed) { 98 | return await Connector.instance.Query( 99 | `UPDATE userids 100 | SET subscribed = ${subscribed} 101 | WHERE id = ${id};` 102 | ); 103 | } 104 | 105 | /**@param {Number} id 106 | * @returns {Boolean} 107 | */ 108 | static async IsUserSubscribed(id) { 109 | let res = await Connector.instance.Query(`SELECT * FROM userids where id = ${id}`); 110 | if (typeof (res) != 'undefined' && res.rows.length > 0) { 111 | return res.rows[0].subscribed; 112 | } else { 113 | return true; 114 | } 115 | } 116 | 117 | /**@returns {Array.} */ 118 | static async GetAllUsers() { 119 | let users = await Connector.instance.Query(`SELECT * FROM userids`); 120 | if (typeof (users) != 'undefined' && users.rows.length > 0) { 121 | console.log(`Picked users count: ${users.rows.length}`); 122 | return users.rows; 123 | } else { 124 | console.log(`Picked users count: 0`); 125 | return []; 126 | } 127 | } 128 | 129 | /**@param {Number} id */ 130 | static async RemoveUser(id) { 131 | return await Connector.instance.Query(`DELETE FROM userids WHERE id = ${id}`); 132 | } 133 | 134 | /**@param {Number} id 135 | * @returns {User} 136 | */ 137 | static async GetUserById(id, real = false) { 138 | let res = await Connector.instance.Query(`SELECT * FROM userids WHERE id = ${id}`); 139 | console.log(`Got user by id "${id}"`); 140 | if (typeof (res) != 'undefined' && res.rows.length > 0) { 141 | return new User(res.rows[0]); 142 | } else if (!real) { 143 | return new User(); 144 | } else { 145 | return new User(null); 146 | } 147 | } 148 | 149 | /**@param {Number} id 150 | * @returns {Boolean} 151 | */ 152 | static async HasUserID(id) { 153 | let res = await Connector.instance.Query(`SELECT * FROM userids WHERE id = ${id}`); 154 | return typeof (res) != 'undefined' && res.rows.length > 0 155 | } 156 | 157 | /** 158 | * @param {Number} id 159 | * @param {String} trello_token 160 | */ 161 | static async SetUserTrelloToken(id, trello_token) { 162 | return await Connector.instance.paramQuery( 163 | `UPDATE userids 164 | SET trello_token = $1 165 | WHERE id = ${id}`, 166 | [trello_token] 167 | ); 168 | } 169 | 170 | /** 171 | * @param {Number} id 172 | */ 173 | static async ClearUserTrelloToken(id) { 174 | return await Connector.instance.Query( 175 | `UPDATE userids 176 | SET trello_token = NULL 177 | WHERE id = ${id};` 178 | ); 179 | } 180 | 181 | static async GetSubscribedUsersCount() { 182 | return +(await Connector.instance.Query('SELECT Count(*) FROM userids WHERE subscribed = true')).rows[0].count; 183 | } 184 | } 185 | 186 | module.exports = User; -------------------------------------------------------------------------------- /BotCode/storage/dataBase/Migrations.js: -------------------------------------------------------------------------------- 1 | const { Connector } = require('./Connector'); 2 | const User = require('./TablesClasses/User'); 3 | const { Schedule, GetOptions, ScheduleStates } = require('./TablesClasses/Schedule'); 4 | const { Encrypt, Decrypt } = require('../encryption/encrypt'); 5 | const DataBaseFunctions = require('./Functions'); 6 | 7 | class Migrations { 8 | static async InitializeTables() { 9 | await Connector.instance.Query(`CREATE TABLE IF NOT EXISTS schedules (ChatID TEXT, num INTEGER, text TEXT, username TEXT, target_date BIGINT, period_time BIGINT, max_date BIGINT, file_id TEXT, trello_card_id TEXT, id SERIAL, state TEXT DEFAULT '${ScheduleStates.valid}', message_id INT, creation_date BIGINT, creator BIGINT)`); 10 | await Connector.instance.Query('CREATE TABLE IF NOT EXISTS userids (id BIGINT, tz BIGINT, lang TEXT, subscribed BOOLEAN, trello_token TEXT)'); 11 | await Connector.instance.Query('CREATE TABLE IF NOT EXISTS chats (id TEXT, trello_board_id TEXT, trello_list_id TEXT, trello_token TEXT)'); 12 | } 13 | 14 | /**@param {String} column_name */ 15 | static async ExpandSchedulesTable(column_name) { 16 | const column = await Connector.instance.Query(`SELECT column_name 17 | FROM information_schema.columns 18 | WHERE table_name='schedules' AND column_name = '${column_name}'`); 19 | if (column.rowCount > 0) { 20 | return; 21 | } 22 | 23 | await Connector.instance.Query(`ALTER TABLE schedules DROP COLUMN IF EXISTS ts`); 24 | await Connector.instance.Query(`ALTER TABLE schedules ADD COLUMN IF NOT EXISTS target_date BIGINT`); 25 | await Connector.instance.Query(`ALTER TABLE schedules ADD COLUMN IF NOT EXISTS period_time BIGINT`); 26 | await Connector.instance.Query(`ALTER TABLE schedules ADD COLUMN IF NOT EXISTS max_date BIGINT`); 27 | await Connector.instance.Query(`ALTER TABLE schedules ADD COLUMN IF NOT EXISTS file_id TEXT`); 28 | await Connector.instance.Query(`ALTER TABLE schedules ADD COLUMN IF NOT EXISTS trello_card_id TEXT`); 29 | 30 | await Connector.instance.Query(`DO $$ 31 | DECLARE 32 | num_exist boolean; 33 | BEGIN 34 | SELECT COUNT(*) > 0 INTO num_exist 35 | FROM information_schema.columns 36 | WHERE table_name='schedules' AND column_name='num'; 37 | IF NOT num_exist THEN 38 | ALTER TABLE schedules RENAME COLUMN id TO num; 39 | END IF; 40 | END$$;`); 41 | await Connector.instance.Query(`ALTER TABLE schedules ADD COLUMN IF NOT EXISTS id SERIAL`); 42 | await Connector.instance.Query(`ALTER TABLE schedules DROP COLUMN IF EXISTS pending`); 43 | await Connector.instance.Query(`ALTER TABLE schedules ADD COLUMN IF NOT EXISTS state TEXT DEFAULT '${ScheduleStates.valid}'`); 44 | await Connector.instance.Query(`ALTER TABLE schedules ADD COLUMN IF NOT EXISTS message_id INT`); 45 | await Connector.instance.Query(`ALTER TABLE schedules ADD COLUMN IF NOT EXISTS creation_date BIGINT`); 46 | await Connector.instance.Query(`ALTER TABLE schedules ADD COLUMN IF NOT EXISTS creator BIGINT`); 47 | await Connector.instance.Query(`UPDATE schedules 48 | SET 49 | creator = chatid::int 50 | WHERE creator IS NULL AND chatid ~ '^\\d+$'`); 51 | } 52 | 53 | /**@param {String} column_name */ 54 | static async ExpandUsersIdsTable(column_name) { 55 | const column = await Connector.instance.Query(`SELECT column_name 56 | FROM information_schema.columns 57 | WHERE table_name='userids' AND column_name = '${column_name}'`); 58 | if (column.rowCount > 0) { 59 | return; 60 | } 61 | 62 | let users = await User.GetAllUsers(); 63 | await Connector.instance.Query(`ALTER TABLE userids ADD COLUMN IF NOT EXISTS lang TEXT`); 64 | await Connector.instance.Query(`ALTER TABLE userids ADD COLUMN IF NOT EXISTS subscribed BOOLEAN`); 65 | await Connector.instance.Query(`ALTER TABLE userids ADD COLUMN IF NOT EXISTS trello_token TEXT`); 66 | for (let user of users) { 67 | console.log(`User "${user.id}" doesn't have '${column_name}' field`); 68 | } 69 | } 70 | 71 | /**@param {String} column_name */ 72 | static async ExpandChatsTable(column_name) { 73 | const column = await Connector.instance.Query(`SELECT column_name 74 | FROM information_schema.columns 75 | WHERE table_name='chats' AND column_name = '${column_name}'`); 76 | if (column.rowCount > 0) { 77 | return; 78 | } 79 | 80 | await Connector.instance.Query(`ALTER TABLE chats ADD COLUMN IF NOT EXISTS trello_board_id TEXT`); 81 | await Connector.instance.Query(`ALTER TABLE chats ADD COLUMN IF NOT EXISTS trello_list_id TEXT`); 82 | await Connector.instance.Query(`ALTER TABLE chats ADD COLUMN IF NOT EXISTS trello_token TEXT`); 83 | } 84 | 85 | static async EncryptSchedules() { 86 | let schedules = await Schedule.GetAllSchedules(undefined, false); 87 | for (const schedule of schedules) { 88 | let encrypted = true; 89 | const key = schedule.chatid; 90 | let text = schedule.text; 91 | try { 92 | text = Decrypt(text, key); 93 | } catch (e) { 94 | console.log(`Schedule #${schedule.num} in chat ${schedule.chatid} is not encrypted`); 95 | encrypted = false; 96 | text = schedule.text; 97 | } 98 | if (!encrypted) { 99 | const encryptedText = Encrypt(text, key); 100 | await Connector.instance.Query(`UPDATE schedules 101 | SET text = '${encryptedText}' 102 | WHERE num = ${schedule.num} AND ChatID = '${schedule.chatid}'`); 103 | console.log(`Updated not encrypted schedule "${schedule.text}"`); 104 | } 105 | } 106 | } 107 | 108 | static async InitializeDataBaseFunctions() { 109 | for(const [key, value] of Object.entries(DataBaseFunctions)) { 110 | await Connector.instance.Query(value); 111 | } 112 | } 113 | } 114 | 115 | module.exports = Migrations; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/remindersChecking.js: -------------------------------------------------------------------------------- 1 | const { Composer } = require('telegraf'); 2 | const Extra = require('telegraf/extra'); 3 | const { LoadReplies } = require('../static/replies/repliesLoader'); 4 | const { DataBase, User, Chat } = require('../../../storage/dataBase/DataBase'); 5 | const { Schedule, GetOptions, ScheduleStates } = require('../../../storage/dataBase/TablesClasses/Schedule'); 6 | const { Decrypt, Encrypt } = require('../../../storage/encryption/encrypt'); 7 | const { TrelloManager } = require('@alordash/node-js-trello'); 8 | const { BotSendMessage, BotSendAttachment } = require('./replying'); 9 | const utils = require('../../processing/utilities'); 10 | const { Connector } = require('../../../storage/dataBase/Connector'); 11 | const { RemoveReminders } = require('../../processing/remindersOperations'); 12 | const Format = require('../../processing/formatting'); 13 | const kbs = require('../static/replies/keyboards'); 14 | const { ProcessTrelloReminder } = require('./technical'); 15 | 16 | /**@param {Composer} bot */ 17 | async function CheckExpiredSchedules(bot) { 18 | let pairs = await DataBase.Schedules.GetExpiredSchedules(); 19 | console.log(`Checking ${pairs.length} expired schedules...`); 20 | const now = Date.now(); 21 | let removingSchedules = []; 22 | for (let pair of pairs) { 23 | let schedule = pair.schedule; 24 | let chatID = schedule.chatid; 25 | if (chatID[0] == '_') { 26 | chatID = `-${chatID.substring(1)}`; 27 | } 28 | if (process.env.SMART_SCHEDULER_DEBUG_MODE === 'true' && chatID != process.env.SMART_SCHEDULER_ADMIN) { 29 | continue; 30 | } 31 | const lang = pair.lang; 32 | let expired = true; 33 | let decrypted = false; 34 | //#region Trello processing 35 | let trelloProcessionResult = await ProcessTrelloReminder(schedule, chatID); 36 | expired = trelloProcessionResult.expired; 37 | decrypted = trelloProcessionResult.decrypted; 38 | //#endregion 39 | if (!expired) { 40 | continue; 41 | } 42 | if (!decrypted) { 43 | schedule.text = Decrypt(schedule.text, schedule.chatid); 44 | } 45 | let keyboardButton = schedule.period_time > 0 ? kbs.CompleteReminderButton(lang) : kbs.RepeatButton(lang); 46 | let msgText = Format.FormReminderMessage(schedule); 47 | let isBlocked = false; 48 | let msg; 49 | let shouldDelete = true; 50 | try { 51 | if (schedule.file_id != '~' && schedule.file_id != null) { 52 | msg = await BotSendAttachment(bot, +chatID, msgText, schedule.file_id, keyboardButton, true); 53 | } else { 54 | msg = await BotSendMessage(bot, +chatID, msgText, keyboardButton, true); 55 | } 56 | } catch (e) { 57 | console.error(e); 58 | isBlocked = true; 59 | console.log(`The chat "${chatID}" has blocked bot`); 60 | } 61 | if (!isBlocked) { 62 | let repeatSchedule = new Schedule(schedule.chatid, undefined, schedule.text, schedule.username, schedule.target_date + global.repeatScheduleTime, 0, 0, schedule.file_id, ScheduleStates.repeat, msg.message_id, now, schedule.creator); 63 | DataBase.Schedules.AddSchedule(repeatSchedule); 64 | let isPeriodic = schedule.period_time > 0; 65 | let isLimited = schedule.max_date > 0; 66 | if(isPeriodic || isLimited) { 67 | let extraTime = schedule.target_date + schedule.period_time; 68 | if(!isPeriodic) { 69 | extraTime = schedule.max_date; 70 | } 71 | if(extraTime <= schedule.max_date || !isLimited) { 72 | schedule.target_date = extraTime; 73 | DataBase.Schedules.SetSchedule(schedule); 74 | shouldDelete = false; 75 | } 76 | } 77 | } 78 | if(shouldDelete || isBlocked) { 79 | removingSchedules.push(schedule); 80 | } 81 | } 82 | console.log('Done checking schedules. Removing them...'); 83 | await DataBase.Schedules.RemoveSchedules(removingSchedules); 84 | console.log('Done removing schedules. Reordering them...'); 85 | await DataBase.Schedules.ReorderMultipleSchedules(removingSchedules); 86 | console.log('Done expired schedules procession'); 87 | } 88 | 89 | /**@param {Composer} bot */ 90 | async function CheckPendingSchedules(bot) { 91 | let schedules = await DataBase.Schedules.GetAllSchedules(GetOptions.draft); 92 | Connector.instance.sending = true; 93 | let now = Date.now(); 94 | let deletingSchedules = []; 95 | for (const i in schedules) { 96 | const schedule = schedules[i]; 97 | if ((now - schedule.creation_date) >= global.repeatScheduleTime) { 98 | deletingSchedules.push(schedule); 99 | } 100 | } 101 | await RemoveReminders(bot, deletingSchedules); 102 | 103 | Connector.instance.sending = false; 104 | } 105 | 106 | async function CheckDisplayStatueMessages(bot) { 107 | Connector.instance.sending = true; 108 | let schedules = await DataBase.Schedules.GetAllSchedules(GetOptions.statusDisplay); 109 | if (schedules.length <= 0) { 110 | return; 111 | } 112 | let usersCount = await DataBase.Users.GetSubscribedUsersCount(); 113 | let schedulesCount = await DataBase.Schedules.GetTotalSchedulesCount(); 114 | let deletingSchedules = []; 115 | for (const schedule of schedules) { 116 | let message_id = schedule.message_id; 117 | let text = Format.FormDisplayStatus(global.defaultUserLanguage, usersCount, schedulesCount); 118 | let chatid = utils.UnformatChatId(schedule.chatid); 119 | try { 120 | await bot.telegram.editMessageText(chatid, message_id, undefined, text, { parse_mode: 'HTML' }); 121 | } catch (e) { 122 | console.log(e); 123 | if (e.description.indexOf('message is not modified') == -1) { 124 | deletingSchedules.push(schedule); 125 | } 126 | } 127 | } 128 | await DataBase.Schedules.RemoveSchedules(deletingSchedules); 129 | Connector.instance.sending = false; 130 | } 131 | 132 | module.exports = { 133 | CheckExpiredSchedules, 134 | CheckPendingSchedules, 135 | CheckDisplayStatueMessages 136 | }; -------------------------------------------------------------------------------- /BotCode/interactions/bot/static/replies/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": "This bot will allow you to schedule your tasks easy and fast using only a messenger ⏰\r\nJust type your plans 💬 or send voice message 🗣️ and he will automatically understand scheduling date and what's to schedule.\r\n\r\nBefore we continue, please consider specifying your time zone by pressing the button below.", 3 | "startTZ": "🌐 Specify time zone", 4 | "commands": "Available commands:\r\n🗓 /list\r\n Shows active tasks for this chat.\r\n🗑 /del 1, 2, ...N\r\n Deletes tasks by their number.\r\n🗑 /del 1-10, A-B\r\n Deletes all tasks within range.\r\n#️⃣ /N\r\n Deletes N-th task.\r\n🌐 /tz\r\n Configures time zone.\r\nThis bot can duplicate reminds to 🗄 Trello.\r\nFor detailed information press the button below.", 5 | "voiceMessageTooBig": "⚠️ Voice message duration must be less than 30 seconds.", 6 | "showListAction": "🗓 Show list", 7 | "alreadyScheduled": "already scheduled at:", 8 | "scheduleDateInvalid": "⏱ When?", 9 | "scheduleTextInvalid": "🖊 What to remind?", 10 | "showList": "Show list: /list", 11 | "cleared": "Cleared all reminders.", 12 | "deleted": "Deleted reminders with number:", 13 | "deletedGreater": "Deleted reminders from number:", 14 | "invalidUseOfCommand": "Invalid use of command.", 15 | "listIsEmpty": "List of plans is empty.", 16 | "shouldRemove": "⚠️ Please remove some of your schedules.", 17 | "maximumSchedulesCount": "Maximum count of schedules:", 18 | "errorScheduling": "Can not schedule for this date.", 19 | "showKeyboard": "Opened menu.", 20 | "repeatSchedule": "🔔 Remind in 5 minutes", 21 | "remindSchedule": "🔔 Repeat:", 22 | "confirmSchedule": "Confirm ✅", 23 | "declineSchedule": "Decline ⛔️", 24 | "until": "until", 25 | "everyTime": "Repeat period: ", 26 | "tzWarning": "⚠️ Please select your time zone by typing /tz", 27 | "tzManualConfiguration": "🛠 Type your time zone in ±HH:MM format.", 28 | "tzLocationConfiguration": "🗺 Or send your geoposition. (unavailable)", 29 | "tzGroupChatConfiguration": "🛠 To configure time zone type GMT offset in ±HH:MM format.", 30 | "tzUseLocation": "🔍 Geoposition (unavailable)", 31 | "tzUseLocationResponse": "Configuring...", 32 | "tzInvalidInput": "🚫 Please enter GMT offset in ±HH:MM format,\r\nwhere ± — plus or minus, HH - hours, MM - minutes.", 33 | "tzAddress": "Address:", 34 | "tzDefined": "🌐 Your time zone: GMT", 35 | "cancel": "❌ Cancel", 36 | "cancelReponse": "🚫 Cancelled.", 37 | "tzCancelWarning": "🎯 Please note that defining time zone increases time accuracy.", 38 | "unsubscribe": "🔕 Unsubscribe", 39 | "invite": "🏵 New major update is out!\r\nList of changes:", 40 | "year": "", 41 | "getBack": "⬅️ Back", 42 | "displayStatus0": "🌐 Online bot statistics\r\n Users: ", 43 | "displayStatus1": " Reminders: ", 44 | "displayStatus2": "updates every 24 hours", 45 | "trelloAuthenticate0": "For integration with Trello open this link 🔗, then press the bottom button [ Allow ].\r\nIn opened tab copy your token 🔐 and send it in message to bot.", 47 | "trelloSavedToken": "🔒 Saved token.", 48 | "trelloRemovedToken": "🗑 Removed token.", 49 | "trelloShowBoards": "🗄 Your boards:", 50 | "trelloWrongToken": "🚫 Please enter valid token.", 51 | "trelloTooManyBoardsWarning": "⚠️ Too many pinned boards.\r\nMaximum allowed count:", 52 | "trelloBoardDoesNotExist": "⚠️ Board not found.", 53 | "trelloBoardListsList0": "🗃 The board", 54 | "trelloBoardListsList1": "has the following lists:", 55 | "trelloBoardListsListEnd": "Choose to which list 📑 all reminds will be duplicated by pressing the command near it.", 56 | "trelloNoBoardBinded": "📌 No trello board binded to this chat.", 57 | "trelloListBinded0": "📑 All schedules from this chat now can be duplicated to list", 58 | "trelloListBinded1": "of board", 59 | "trelloBoardUnbinded0": "🧹 Board", 60 | "trelloBoardUnbinded1": "unpinned from this chat.", 61 | "trelloBoardAlreadyBinded0": "🔖 Pinned board:", 62 | "trelloBoardAlreadyBinded1": "List:", 63 | "trelloHelpButton": "🗳 Trello", 64 | "trelloHelp": "⚙️ There are three steps to integrate your schedules with Trello:\r\n1. Authorize your Trello account.\r\n By sending command /trello\r\n2. Pin one of your boards to current chat.\r\n By sending command /trello_pin <id_of_board>\r\n3. Choose to which list of that board reminds will be duplicated.\r\n By clicking the command near required list.\r\n\r\nFull list of commands:\r\n🗄 /trello\r\n Authorizes user and displays all his boards with their id.\r\n🔖 /trello_pin <id_of_board>\r\n Pins entered board to current chat.\r\n📌 /trello_unpin\r\n Unpins board from current chat.\r\nTo deauthorize your Trello account send:\r\n/trello clear", 65 | "trelloInfoLink": "All features of working with Trello are described here:", 66 | "trelloClearGroupWarn": "To deauthorize your Trello account send:\"/trello clear\" to bot's PM.", 67 | "toTrelloButton": "📤 To trello", 68 | "completeReminder": "Complete ✔️", 69 | "reminderMinTimeLimit": "⚠️ Minimum time to remind - 5 minutes.", 70 | "reminderMaxTimeLimit": "⚠️ Maximum time to remind - 1 year 1 month.", 71 | "months": [ 72 | "January", 73 | "February", 74 | "March", 75 | "April", 76 | "May", 77 | "June", 78 | "July", 79 | "August", 80 | "September", 81 | "October", 82 | "November", 83 | "December" 84 | ], 85 | "timeTypes": { 86 | "minutes": "minutes", 87 | "hours": "hours", 88 | "dates": "days", 89 | "months": "months", 90 | "years": "years" 91 | }, 92 | "daysOfWeek": [ 93 | "sunday", 94 | "monday", 95 | "tuesday", 96 | "wednesday", 97 | "thursday", 98 | "friday", 99 | "saturday" 100 | ] 101 | } -------------------------------------------------------------------------------- /BotCode/interactions/bot/static/replies/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": "Этот бот позволит вам планировать ваши задачи просто и быстро, не покидая мессенджера ⏰\r\nПросто напишите свои планы 💬 или отправьте голосовое сообщение 🗣 и он автоматически поймет о чем и когда напомнить.\r\n\r\nПрежде чем мы продолжим, пожалуйста укажите свой часовой пояс, нажав на кнопку ниже.", 3 | "startTZ": "🌐 Указать часовой пояс", 4 | "commands": "Доступные комманды:\r\n🗓 /list\r\n Показывает активные напоминания для данного чата.\r\n🗑 /del 1, 2, ...N\r\n Удаляет напоминания по их номеру.\r\n🗑 /del 1-10, A-B\r\n Удаляет все напоминания в заданном диапазоне.\r\n#️⃣ /N\r\n Удаляет N-ное напоминание.\r\n🌐 /tz\r\n Конфигурация часового пояса.\r\nБот может дублировать напоминания в 🗄 Trello.\r\nДля подробной инструкции нажмите кнопку ниже.", 5 | "voiceMessageTooBig": "⚠️ Длительность голосового сообщения должна быть меньше 30 секунд.", 6 | "showListAction": "🗓 Показать список", 7 | "alreadyScheduled": "уже запланировано на:", 8 | "scheduleDateInvalid": "⏱ На какое время?", 9 | "scheduleTextInvalid": "🖊 Что напомнить?", 10 | "showList": "Показать список: /list", 11 | "cleared": "Удалены все напоминания.", 12 | "deleted": "Удалены напоминания под номером:", 13 | "deletedGreater": "Удалены напоминания начиная с номера:", 14 | "invalidUseOfCommand": "Неправильное применение команды.", 15 | "listIsEmpty": "Список напоминаний пуст.", 16 | "shouldRemove": "⚠️ Пожалуйста удалите часть ваших напоминаний.", 17 | "maximumSchedulesCount": "Максимальное число напоминаний:", 18 | "errorScheduling": "Невозможно запланировать на данную дату.", 19 | "showKeyboard": "Открыто меню.", 20 | "repeatSchedule": "🔔 Напомнить через 5 минут", 21 | "remindSchedule": "🔔 Повтор:", 22 | "confirmSchedule": "Подтвердить ✅", 23 | "declineSchedule": "Отменить ⛔️", 24 | "until": "до", 25 | "everyTime": "Период повтора: ", 26 | "tzWarning": "⚠️ Пожалуйста настройте свой часовой пояс с помощью команды /tz", 27 | "tzManualConfiguration": "🛠 Введите ваш часовой пояс в формате ±ЧЧ:ММ.", 28 | "tzLocationConfiguration": "🗺 Или отправьте свою геопозицию. (недоступно)", 29 | "tzUseLocation": "🗺 Геопозиция (недоступно)", 30 | "tzUseLocationResponse": "Конфигурация...", 31 | "tzInvalidInput": "🚫 Пожалуйста введите часовой пояс согласно формату ±ЧЧ:ММ,\r\nгде ± — плюс или минус, ЧЧ - часы, ММ - минуты.", 32 | "tzAddress": "Адрес:", 33 | "tzDefined": "🌐 Ваш часовой пояс: GMT", 34 | "cancel": "❌ Отмена", 35 | "cancelReponse": "🚫 Отменено.", 36 | "tzCancelWarning": "🎯 Настройка часового пояса повышает точность определения времени.", 37 | "unsubscribe": "🔕 Отписаться", 38 | "invite": "🏵 Вышло крупное обновление!\r\nСписок изменений:", 39 | "year": "г.", 40 | "getBack": "⬅️ Назад", 41 | "displayStatus0": "🌐 Онлайн статистика бота\r\n пользователей:  ", 42 | "displayStatus1": " напоминаний: ", 43 | "displayStatus2": "обновляется каждые 24 часа", 44 | "trelloAuthenticate0": "Для интеграции с Trello перейдите по этой ссылке 🔗, затем нажмите снизу кнопку [ Разрешить ].\r\nВ открывшейся вкладке скопируйте Ваш токен 🔐 и отправьте его в сообщении боту.", 46 | "trelloSavedToken": "🔒 Токен сохранён.", 47 | "trelloRemovedToken": "🗑 Токен удалён.", 48 | "trelloShowBoards": "🗄 Ваши доски:", 49 | "trelloWrongToken": "🚫 Пожалуйста введите верный токен.", 50 | "trelloTooManyBoardsWarning": "⚠️ Слишком много закрепленных досок.\r\nМаксимальное количество:", 51 | "trelloBoardDoesNotExist": "⚠️ Доска не найдена.", 52 | "trelloBoardListsList0": "🗃 В доске", 53 | "trelloBoardListsList1": "есть следующие списки:", 54 | "trelloBoardListsListEnd": "Выберите список 📑, в который будут дублироваться все напоминания из этого чата, нажав на команду рядом с ним.", 55 | "trelloNoBoardBinded": "📌 К этому чату не прикреплена ни одна доска.", 56 | "trelloListBinded0": "📑 Все напоминания из этого чата теперь могут дублироваться в список", 57 | "trelloListBinded1": "доски", 58 | "trelloBoardUnbinded0": "🧹 Доска", 59 | "trelloBoardUnbinded1": "откреплена от этого чата.", 60 | "trelloBoardAlreadyBinded0": "🔖 Прикрепленная доска:", 61 | "trelloBoardAlreadyBinded1": "Список:", 62 | "trelloHelpButton": "🗳 Trello", 63 | "trelloHelp": "⚙️ Для интеграции с Trello необходимо выполнить три шага:\r\n1. Авторизировать свой аккаунт в Trello.\r\n Выполняется командой /trello\r\n2. Прикрепить одну из своих досок к текущему чату.\r\n Выполняется командой /trello_pin <id_доски>\r\n3. Выбрать в какой из списков этой доски будут дублироваться напоминания.\r\n Выполняется нажатием на команду рядом с требуемым списком.\r\n\r\nПолный список команд:\r\n🗄 /trello\r\n Авторизирует пользователя и отображает список его досок вместе с их id.\r\n🔖 /trello_pin <id_доски>\r\n Прикрепляет доску к текущему чату.\r\n📌 /trello_unpin\r\n Открепляет доску от текущего чата.\r\nДля отвязки аккаунта Trello введите:\r\n/trello clear", 64 | "trelloInfoLink": "Обо всем дополнительном функционале, доступном при работе с Trello, вы можете прочитать тут:", 65 | "trelloClearGroupWarn": "Для отвязки аккаунта Trello отправьте \"/trello clear\" в личные сообщения боту.", 66 | "toTrelloButton": "📤 В трелло", 67 | "completeReminder": "Завершить ✔️", 68 | "reminderMinTimeLimit": "⚠️ Минимальное время до напоминания - 5 минут.", 69 | "reminderMaxTimeLimit": "⚠️ Максимальное время до напоминания - 1 год 1 месяц.", 70 | "months": [ 71 | "Января", 72 | "Февраля", 73 | "Марта", 74 | "Апреля", 75 | "Мая", 76 | "Июня", 77 | "Июля", 78 | "Августа", 79 | "Сентября", 80 | "Октября", 81 | "Ноября", 82 | "Декабря" 83 | ], 84 | "timeTypes": { 85 | "minutes": "минуты", 86 | "hours": "часы", 87 | "dates": "дни", 88 | "months": "месяцы", 89 | "years": "годы" 90 | }, 91 | "daysOfWeek": [ 92 | "воскресенье", 93 | "понедельник", 94 | "вторник", 95 | "среда", 96 | "четверг", 97 | "пятница", 98 | "суббота" 99 | ] 100 | } -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/handling/trelloCommands.js: -------------------------------------------------------------------------------- 1 | const Markup = require('telegraf/markup'); 2 | const { Languages, LoadReplies } = require('../../static/replies/repliesLoader'); 3 | const rp = require('../../static/replies/repliesLoader'); 4 | const Format = require('../../../processing/formatting'); 5 | const kbs = require('../../static/replies/keyboards'); 6 | const { DataBase, Schedule, User, Chat } = require('../../../../storage/dataBase/DataBase'); 7 | const { TrelloManager } = require('@alordash/node-js-trello'); 8 | const { trelloAddListCommand, trelloClear } = require('../../static/commandsList'); 9 | const { BotReply, BotReplyMultipleMessages } = require('../replying'); 10 | const utils = require('../../../processing/utilities'); 11 | 12 | /** 13 | * @param {*} ctx 14 | * @param {User} user 15 | * @param {Array.} trelloPendingConfirmationUsers 16 | */ 17 | async function TrelloCommand(user, ctx, trelloPendingConfirmationUsers) { 18 | const replies = LoadReplies(user.lang); 19 | const inGroup = ctx.chat.id < 0; 20 | let reply = ''; 21 | //#region in group 22 | let chat = await DataBase.Chats.GetChatById(ctx.chat.id); 23 | if (chat != null && chat.trello_board_id != null && chat.trello_token != null) { 24 | let boardTrelloManager = new TrelloManager(process.env.TRELLO_TOKEN, chat.trello_token); 25 | let board = await boardTrelloManager.GetBoard(chat.trello_board_id); 26 | if (board != null) { 27 | let list = board.lists.find(x => x.id == chat.trello_list_id); 28 | 29 | if (list != null) { 30 | reply = `${Format.FormAlreadyBoardBinded(board, list, user.lang)}\r\n`; 31 | } 32 | } 33 | } 34 | if (reply == '') { 35 | reply = `${replies.trelloNoBoardBinded}\r\n`; 36 | } 37 | if (!inGroup) { 38 | if (user.trello_token != null) { 39 | //#region authorized 40 | let trelloManager = new TrelloManager(process.env.TRELLO_TOKEN, user.trello_token); 41 | let owner = await trelloManager.GetTokenOwner(user.trello_token); 42 | let boardsList = []; 43 | if (owner != null) { 44 | boardsList = await trelloManager.GetUserBoards(owner.id); 45 | if (boardsList != null && boardsList.length > 0) { 46 | reply = `${reply}${Format.FormBoardsList(boardsList, user.lang)}`; 47 | } 48 | } 49 | //#endregion 50 | } else { 51 | //#region not authorized 52 | trelloPendingConfirmationUsers.push(ctx.from.id); 53 | BotReply(ctx, Format.TrelloAuthorizationMessage(process.env.TRELLO_TOKEN, process.env.SMART_SCHEDULER_BOT_NAME, user.lang), 54 | kbs.CancelKeyboard(user.lang)); 55 | //#endregion 56 | return; 57 | } 58 | } 59 | if (ctx.message.text.indexOf(trelloClear) >= 0) { 60 | if (!inGroup) { 61 | DataBase.Users.ClearUserTrelloToken(ctx.from.id); 62 | BotReply(ctx, replies.trelloRemovedToken); 63 | return; 64 | } else { 65 | reply = `${reply}${replies.trelloClearGroupWarn}\r\n`; 66 | } 67 | } 68 | 69 | if (reply.length > 0) { 70 | let answers = Format.SplitBigMessage(reply); 71 | BotReplyMultipleMessages(ctx, answers); 72 | } 73 | } 74 | 75 | /** 76 | * @param {*} ctx 77 | * @param {Array.} trelloPendingConfirmationUsers 78 | */ 79 | async function TrelloAuthenticate(ctx, trelloPendingConfirmationUsers) { 80 | let token = ctx.message.text; 81 | const replies = rp.LoadReplies(ctx.from.language_code); 82 | let match = token.match(/^([a-zA-Z0-9]){64,}$/); 83 | if (match != null) { 84 | DataBase.Users.SetUserTrelloToken(ctx.from.id, token); 85 | trelloPendingConfirmationUsers.splice(trelloPendingConfirmationUsers.indexOf(ctx.from.id), 1); 86 | 87 | let trelloManager = new TrelloManager(process.env.TRELLO_TOKEN, token); 88 | let owner = await trelloManager.GetTokenOwner(token); 89 | let boardsList = await trelloManager.GetUserBoards(owner.id); 90 | 91 | let reply = `${replies.trelloSavedToken}\r\n${Format.FormBoardsList(boardsList, ctx.from.language_code)}`; 92 | 93 | let chatID = `${ctx.chat.id}`; 94 | if (chatID[0] == '-') { 95 | chatID = `_${chatID.substring(1)}`; 96 | } 97 | let answers = Format.SplitBigMessage(reply); 98 | let options = []; 99 | options[answers.length - 1] = await kbs.LogicalListKeyboard(ctx.from.language_code, utils.FormatChatId(ctx.chat.id)); 100 | BotReplyMultipleMessages(ctx, answers, options); 101 | } else { 102 | BotReply(ctx, replies.trelloWrongToken, kbs.CancelButton(ctx.from.language_code)); 103 | } 104 | } 105 | 106 | /** 107 | * @param {*} ctx 108 | * @param {User} user 109 | */ 110 | async function TrelloPinCommand(ctx, user) { 111 | const replies = rp.LoadReplies(user.lang); 112 | let text = ctx.message.text; 113 | let match = text.match(/[a-zA-Z0-9]{24}/); 114 | if (match == null) { 115 | BotReply(ctx, replies.invalidUseOfCommand); 116 | return; 117 | } 118 | let id = match[0]; 119 | 120 | let chat = await DataBase.Chats.GetChatById(`${ctx.chat.id}`); 121 | let trelloManager = new TrelloManager(process.env.TRELLO_TOKEN, user.trello_token); 122 | let board = await trelloManager.GetBoard(id); 123 | 124 | if (typeof (board) != 'undefined') { 125 | let chatId = `${ctx.chat.id}`; 126 | if (typeof (chat) == 'undefined') { 127 | await DataBase.Chats.AddChat(chatId, id); 128 | } else { 129 | await DataBase.Chats.SetChatTrelloBoard(chatId, id); 130 | } 131 | let replies = Format.SplitBigMessage(Format.FormBoardListsList(board, user.lang)); 132 | await BotReplyMultipleMessages(ctx, replies); 133 | } else { 134 | BotReply(ctx, replies.trelloBoardDoesNotExist); 135 | } 136 | } 137 | 138 | /** @param {*} ctx */ 139 | async function TrelloAddList(ctx) { 140 | let text = ctx.message.text; 141 | let i = parseInt(text.substring(trelloAddListCommand.length)) - 1; 142 | 143 | let chatId = `${ctx.chat.id}`; 144 | let user = await DataBase.Users.GetUserById(ctx.from.id); 145 | const replies = rp.LoadReplies(user.lang); 146 | let chat = await DataBase.Chats.GetChatById(chatId); 147 | if (chat.trello_board_id == null) { 148 | BotReply(ctx, replies.trelloNoBoardBinded); 149 | return; 150 | } 151 | let trelloManager = new TrelloManager(process.env.TRELLO_TOKEN, user.trello_token); 152 | let board = await trelloManager.GetBoard(chat.trello_board_id); 153 | let target_list = board.lists[i]; 154 | await DataBase.Chats.SetChatTrelloList(chatId, target_list.id, user.trello_token); 155 | BotReply(ctx, Format.FormListBinded(board, target_list, user.lang)); 156 | } 157 | 158 | /** 159 | * @param {*} ctx 160 | * @param {User} user 161 | */ 162 | async function TrelloUnpinCommand(ctx, user) { 163 | let chat = await DataBase.Chats.GetChatById(ctx.chat.id); 164 | DataBase.Chats.ClearChatFromTrello(ctx.chat.id); 165 | if (chat.trello_token != null) { 166 | let trelloManager = new TrelloManager(process.env.TRELLO_TOKEN, chat.trello_token); 167 | let board = await trelloManager.GetBoard(chat.trello_board_id); 168 | BotReply(ctx, Format.FormBoardUnbinded(board, user.lang)); 169 | } 170 | } 171 | 172 | module.exports = { 173 | TrelloCommand, 174 | TrelloAuthenticate, 175 | TrelloPinCommand, 176 | TrelloAddList, 177 | TrelloUnpinCommand 178 | } -------------------------------------------------------------------------------- /BotCode/interactions/processing/timeProcessing.js: -------------------------------------------------------------------------------- 1 | const { ParsedDate, TimeList } = require('@alordash/date-parser'); 2 | const { isTimeType } = require('@alordash/date-parser/lib/date-cases/date-cases'); 3 | 4 | /**@param {TimeList} tl 5 | * @returns {TimeList} 6 | */ 7 | function FillMinutes(tl) { 8 | if (typeof (tl.hours) != 'undefined' 9 | && typeof (tl.minutes) == 'undefined' 10 | && !tl.isOffset) { 11 | tl.minutes = 0; 12 | } 13 | return tl; 14 | } 15 | 16 | /**@param {TimeList} tl 17 | * @param {Date} date 18 | * @returns {TimeList} 19 | */ 20 | function TimeListFromDate(tl, date) { 21 | tl.dates = date.getUTCDate(); 22 | tl.hours = date.getUTCHours(); 23 | tl.minutes = date.getUTCMinutes(); 24 | tl.months = date.getUTCMonth(); 25 | tl.seconds = 0; 26 | tl.years = date.getUTCFullYear(); 27 | return tl; 28 | } 29 | 30 | /** 31 | * @param {TimeList} timeList 32 | * @returns {Boolean} 33 | */ 34 | function TimeListIsEmpty(timeList) { 35 | return typeof (timeList.years) == 'undefined' 36 | && typeof (timeList.months) == 'undefined' 37 | && typeof (timeList.dates) == 'undefined' 38 | && typeof (timeList.hours) == 'undefined' 39 | && typeof (timeList.minutes) == 'undefined'; 40 | } 41 | 42 | /**@param {TimeList} source 43 | * @param {TimeList} destination 44 | * @returns {TimeList} 45 | */ 46 | function CopyTimeList(source, destination) { 47 | if (typeof (destination) == 'undefined') { 48 | destination = {}; 49 | } 50 | for (const timeProperty in source) { 51 | if (isTimeType(timeProperty) 52 | && typeof (destination[timeProperty]) == 'undefined') { 53 | destination[timeProperty] = source[timeProperty]; 54 | } 55 | } 56 | return destination; 57 | } 58 | 59 | /**@param {TimeList} source 60 | * @param {TimeList} destination 61 | * @returns {TimeList} 62 | */ 63 | function SetTimeList(source, destination) { 64 | for (const timeProperty in source) { 65 | if (isTimeType(timeProperty) 66 | && typeof (source[timeProperty]) == 'number') { 67 | destination[timeProperty] = source[timeProperty]; 68 | } 69 | } 70 | return destination; 71 | } 72 | 73 | /**@param {TimeList} source 74 | * @param {TimeList} destination 75 | * @returns {TimeList} 76 | */ 77 | function AddTimeList(source, destination) { 78 | for (const timeProperty in source) { 79 | if (isTimeType(timeProperty) 80 | && typeof (source[timeProperty]) == 'number') { 81 | destination[timeProperty] += source[timeProperty]; 82 | } 83 | } 84 | return destination; 85 | } 86 | 87 | /** 88 | * @param {TimeList} timeList 89 | * @param {Number} timeListDate 90 | * @returns {TimeList} 91 | */ 92 | function UpdateTime(timeList, timeListDate) { 93 | const now = new Date(); 94 | const tsNow = now.getTime().div(1000); 95 | if (timeListDate < tsNow) { 96 | if (timeListDate + 12 * 3600 > tsNow && timeList.hours <= 12 && !timeList.isFixed) { 97 | timeList.hours = (timeList.hours + 12) % 24; 98 | timeListDate += 12 * 3600; 99 | } else { 100 | let dif = tsNow - timeListDate; 101 | let difInDate = new Date(tsNow * 1000 + dif * 1000); 102 | let monthDif = now.getUTCMonth() - difInDate.getUTCMonth(); 103 | let yearDif = now.getUTCFullYear() - difInDate.getUTCFullYear(); 104 | dif = dif.div(60); 105 | if (dif < 60 && typeof (timeList.hours) == 'undefined') { 106 | timeList.hours = now.getUTCHours() + 1; 107 | } else if (dif < 1440 && typeof (timeList.dates) == 'undefined') { 108 | timeList.dates = now.getUTCDate() + 1; 109 | } else if (monthDif < 1 && yearDif == 0 && typeof (timeList.months) == 'undefined') { 110 | timeList.months = now.getUTCMonth() + 1; 111 | } else if (yearDif < 1 && typeof (timeList.years) == 'undefined') { 112 | timeList.years = now.getUTCFullYear() + 1; 113 | } else { 114 | return undefined; 115 | } 116 | } 117 | } 118 | return timeList; 119 | } 120 | 121 | /** 122 | * @param {TimeList} target_date 123 | * @returns {Boolean} 124 | */ 125 | function TodayCheck(target_date, tz) { 126 | const date = new Date(); 127 | const checkDate = new Date(date.getTime() + tz * 1000); 128 | return target_date.dates == checkDate.getDate() 129 | && target_date.months == checkDate.getMonth() 130 | && typeof (target_date.hours) == 'undefined' 131 | && typeof (target_date.minutes) == 'undefined' 132 | && typeof (target_date.years) == 'undefined'; 133 | 134 | } 135 | 136 | /** 137 | * @param {ParsedDate} parsedDate 138 | * @param {Number} tz 139 | * @param {Boolean} requireHours 140 | * @param {Boolean} ignoreLimits 141 | * @returns {{target_date: Number, period_time: Number, max_date: Number}} 142 | */ 143 | function ProcessParsedDate(parsedDate, tz, requireHours, ignoreLimits = false) { 144 | const max_date_empty = TimeListIsEmpty(parsedDate.max_date); 145 | const period_time_empty = TimeListIsEmpty(parsedDate.period_time); 146 | if (max_date_empty && period_time_empty 147 | && TimeListIsEmpty(parsedDate.target_date)) { 148 | return { 149 | target_date: 0, 150 | period_time: 0, 151 | max_date: 0 152 | }; 153 | } 154 | if (requireHours && max_date_empty && period_time_empty) 155 | if (typeof (parsedDate.target_date.hours) == 'undefined' 156 | && typeof (parsedDate.target_date.minutes) == 'undefined') 157 | return undefined; 158 | 159 | if (TodayCheck(parsedDate.target_date, tz)) 160 | parsedDate.target_date = new TimeList(); 161 | 162 | let dateValues = parsedDate.valueOf(); 163 | let target_date = dateValues.target_date.getTime().div(1000); 164 | let periodYear = dateValues.period_time.getFullYear(); 165 | if(periodYear < 1970) { 166 | console.log("Etwas"); 167 | dateValues.period_time.setFullYear(periodYear + 70); 168 | } 169 | let period_time = dateValues.period_time.getTime().div(1000); 170 | let max_date = dateValues.max_date.getTime().div(1000); 171 | 172 | const hours = Math.floor(tz / 3600); 173 | const minutes = Math.floor((tz % 3600) / 60); 174 | 175 | console.log('hours :>> ', hours); 176 | console.log('minutes :>> ', minutes); 177 | parsedDate.target_date = FillMinutes(parsedDate.target_date); 178 | parsedDate.max_date = FillMinutes(parsedDate.max_date); 179 | if (TimeListIsEmpty(parsedDate.target_date) && !TimeListIsEmpty(parsedDate.period_time)) { 180 | parsedDate.target_date = TimeListFromDate(parsedDate.target_date, dateValues.target_date); 181 | parsedDate.target_date = AddTimeList(parsedDate.period_time, parsedDate.target_date); 182 | parsedDate.target_date.isOffset = true; 183 | target_date += period_time; 184 | } else if (TimeListIsEmpty(parsedDate.target_date) && !TimeListIsEmpty(parsedDate.max_date)) { 185 | parsedDate.target_date = SetTimeList(parsedDate.max_date, parsedDate.target_date); 186 | parsedDate.max_date = new TimeList(); 187 | target_date = dateValues.target_date.getTime().div(1000); 188 | } 189 | if (!parsedDate.target_date.isOffset) { 190 | console.log(`target_date is not offset, target_date :>> ${target_date}, will be: ${target_date - tz}, tz: ${tz}`); 191 | let curTL = TimeListFromDate(new TimeList(), dateValues.target_date); 192 | if (typeof (parsedDate.target_date.hours) == 'undefined') { 193 | parsedDate.target_date.hours = curTL.hours; 194 | } else { 195 | parsedDate.target_date.hours -= hours; 196 | } 197 | if (typeof (parsedDate.target_date.minutes) == 'undefined') { 198 | parsedDate.target_date.minutes = curTL.minutes; 199 | } else { 200 | parsedDate.target_date.minutes -= minutes; 201 | } 202 | target_date -= tz; 203 | } 204 | parsedDate.target_date = UpdateTime(parsedDate.target_date, target_date); 205 | if (!TimeListIsEmpty(parsedDate.max_date)) { 206 | parsedDate.max_date = UpdateTime(parsedDate.max_date, max_date); 207 | parsedDate.max_date = CopyTimeList(parsedDate.target_date, parsedDate.max_date); 208 | max_date = parsedDate.valueOf().max_date.getTime().div(1000); 209 | if (!parsedDate.max_date.isOffset) { 210 | console.log(`max_date is not offset, max_date :>> ${max_date}, will be: ${max_date - tz}, tz: ${tz}`); 211 | if (parsedDate.max_date.hours != null) 212 | parsedDate.max_date.hours -= hours; 213 | if (parsedDate.max_date.minutes != null) 214 | parsedDate.max_date.minutes -= minutes; 215 | max_date -= tz; 216 | } 217 | } else { 218 | let zeroDate = new Date(0); 219 | parsedDate.max_date = TimeListFromDate(parsedDate.max_date, zeroDate); 220 | } 221 | if (typeof (parsedDate.target_date) == 'undefined') 222 | return undefined; 223 | 224 | dateValues = parsedDate.valueOf(); 225 | dateValues.target_date.setSeconds(0, 0); 226 | dateValues.period_time.setSeconds(0, 0); 227 | dateValues.max_date.setSeconds(0, 0); 228 | target_date = dateValues.target_date.getTime(); 229 | periodYear = dateValues.period_time.getFullYear(); 230 | if(periodYear < 1970) { 231 | console.log("Etwas"); 232 | dateValues.period_time.setFullYear(periodYear + 70); 233 | } 234 | period_time = dateValues.period_time.getTime(); 235 | max_date = dateValues.max_date.getTime(); 236 | if (!ignoreLimits) { 237 | let dif = (target_date - Date.now()).div(1000); 238 | if (minReminderTimeDifferenceSec >= dif) 239 | return { 240 | target_date: -1, 241 | period_time: 0, 242 | max_date: 0 243 | }; 244 | else if (dif >= maxReminderTimeDifferenceSec) 245 | return { 246 | target_date: -2, 247 | period_time: 0, 248 | max_date: 0 249 | } 250 | } 251 | return { 252 | target_date, 253 | period_time, 254 | max_date 255 | } 256 | } 257 | 258 | module.exports = { 259 | TimeListFromDate, 260 | TimeListIsEmpty, 261 | UpdateTime, 262 | ProcessParsedDate 263 | } -------------------------------------------------------------------------------- /BotCode/interactions/processing/formatting.js: -------------------------------------------------------------------------------- 1 | const { ParsedDate } = require('@alordash/date-parser'); 2 | const { DataBase, Schedule, User } = require('../../storage/dataBase/DataBase'); 3 | const { isTimeType } = require('@alordash/date-parser/lib/date-cases/date-cases'); 4 | const { TimeListIsEmpty } = require('./timeProcessing'); 5 | const { Language, LoadReplies } = require('../bot/static/replies/repliesLoader'); 6 | const { trelloAddListCommand } = require('../bot/static/commandsList'); 7 | const { TrelloManager } = require('@alordash/node-js-trello'); 8 | 9 | /**@param {Date} date 10 | * @param {Language} language 11 | * @param {Boolean} showDayOfWeek 12 | * @returns {String} 13 | */ 14 | function FormDateStringFormat(date, language, showDayOfWeek) { 15 | const replies = LoadReplies(language); 16 | let month = date.getMonth(); 17 | let hour = date.getHours().toString(10), 18 | minute = date.getMinutes().toString(10); 19 | if (hour.length <= 1) { 20 | hour = `0${hour}`; 21 | } 22 | if (minute.length <= 1) { 23 | minute = `0${minute}`; 24 | } 25 | let year = ''; 26 | if (date.getFullYear() != new Date().getFullYear()) { 27 | year = ` ${date.getFullYear()} ${replies.year}`; 28 | } 29 | 30 | let dayOfWeek = ''; 31 | if (showDayOfWeek && (date.getTime() - Date.now() > 24 * 60 * 60 * 1000)) { 32 | dayOfWeek = ` (${replies.daysOfWeek[date.getDay()]})`; 33 | } 34 | return `${date.getDate()} ${replies.months[month]} ${hour}:${minute}${year}${dayOfWeek}`; 35 | } 36 | 37 | /**@param {Number} period_time 38 | * @param {Language} language 39 | * @returns {String} 40 | */ 41 | function FormPeriodStringFormat(period_time, language) { 42 | let result = ''; 43 | const replies = LoadReplies(language); 44 | const minutes = Math.floor((period_time % 3600) / 60); 45 | const hours = Math.floor((period_time % (24 * 3600)) / 3600); 46 | const days = Math.floor(period_time / (24 * 3600)); 47 | if (minutes > 0) { 48 | result = `${minutes} (${replies.timeTypes.minutes}) ${result}`; 49 | } 50 | if (hours > 0) { 51 | result = `${hours} (${replies.timeTypes.hours}) ${result}`; 52 | } 53 | if (days > 0) { 54 | result = `${days} (${replies.timeTypes.dates}) ${result}`; 55 | } 56 | return result.trim(); 57 | } 58 | 59 | /** 60 | * @param {Schedule} schedule 61 | * @param {Number} tz 62 | * @param {Language} language 63 | * @param {Boolean} showDayOfWeek 64 | * @param {Boolean} showNum 65 | * @param {Boolean} showSymbol 66 | * @returns {String} 67 | */ 68 | async function FormStringFormatSchedule(schedule, tz, language, showDayOfWeek, showNum, showSymbol = false) { 69 | let period_time = schedule.period_time.div(1000); 70 | let target_date = new Date(schedule.target_date + tz * 1000); 71 | console.log(`FORMATTING target_date: ${schedule.target_date}, tz: ${tz}, will be: ${schedule.target_date + tz * 1000}`); 72 | let max_date = new Date(schedule.max_date + tz * 1000); 73 | const replies = LoadReplies(language); 74 | 75 | let until = ''; 76 | let period = ''; 77 | if (max_date.getTime() >= Date.now()) { 78 | until = `\r\n ${replies.until} ${FormDateStringFormat(max_date, language, showDayOfWeek)}`; 79 | } 80 | if (period_time >= 60) { 81 | period = `\r\n ${replies.everyTime} ${FormPeriodStringFormat(period_time, language)}`; 82 | } 83 | let username = ''; 84 | if (schedule.username != 'none' && schedule.username != null) { 85 | username = ` (${schedule.username})`; 86 | } 87 | let file = (schedule.file_id != '~' && schedule.file_id != null) ? ' 💾' : ''; 88 | 89 | let text = schedule.text; 90 | if (schedule.trello_card_id != null) { 91 | let chatID = schedule.chatid; 92 | if (chatID[0] == '_') { 93 | chatID = '-' + chatID.substring(1); 94 | } 95 | let chat = await DataBase.Chats.GetChatById(chatID); 96 | if (typeof (chat) != 'undefined' && chat.trello_token != null) { 97 | let trelloManager = new TrelloManager(process.env.TRELLO_TOKEN, chat.trello_token); 98 | let card = await trelloManager.GetCard(schedule.trello_card_id); 99 | if (typeof (card) != 'undefined') { 100 | text = `${text}`; 101 | } 102 | } 103 | } 104 | let numText = showNum ? `/${schedule.num}. ` : ''; 105 | let symbol = showSymbol ? '📆 ' : ''; 106 | return `${symbol}${numText}${FormDateStringFormat(target_date, language, showDayOfWeek)} "${text}"${file}${username}${until}${period}`; 107 | } 108 | 109 | /** 110 | * @param {*} board 111 | * @returns {String} 112 | */ 113 | function FormBoardLink(board) { 114 | return `${board.name}`; 115 | } 116 | 117 | /** 118 | * @param {Array} boardsList 119 | * @param {String} language 120 | * @returns {String} 121 | */ 122 | function FormBoardsList(boardsList, language) { 123 | const replies = LoadReplies(language); 124 | let reply = `${replies.trelloShowBoards}\r\n`; 125 | for (const board of boardsList) { 126 | reply += `• ${FormBoardLink(board)} 127 | id: ${board.id}\r\n`; 128 | } 129 | return reply; 130 | } 131 | 132 | /** 133 | * @param {*} board 134 | * @param {Languages} language 135 | * @returns {String} 136 | */ 137 | function FormBoardListsList(board, language) { 138 | const replies = LoadReplies(language); 139 | let reply = `${replies.trelloBoardListsList0} "${FormBoardLink(board)}" ${replies.trelloBoardListsList1}\r\n`; 140 | let i = 1; 141 | for (const list of board.lists) { 142 | reply += `${trelloAddListCommand}${i} | "${list.name}"\r\n`; 143 | i++; 144 | } 145 | return `${reply}${replies.trelloBoardListsListEnd}` 146 | } 147 | 148 | /** 149 | * @param {*} board 150 | * @param {*} list 151 | * @param {Language} language 152 | * @returns {String} 153 | */ 154 | function FormListBinded(board, list, language) { 155 | const replies = LoadReplies(language); 156 | return `${replies.trelloListBinded0} "${list.name}" ${replies.trelloListBinded1} "${FormBoardLink(board)}".`; 157 | } 158 | 159 | /** 160 | * @param {*} board 161 | * @param {Languages} language 162 | * @returns {String} 163 | */ 164 | function FormBoardUnbinded(board, language) { 165 | const replies = LoadReplies(language); 166 | return `${replies.trelloBoardUnbinded0} "${FormBoardLink(board)}" ${replies.trelloBoardUnbinded1}`; 167 | } 168 | 169 | function FormAlreadyBoardBinded(board, list, language) { 170 | const replies = LoadReplies(language); 171 | return `${replies.trelloBoardAlreadyBinded0} "${FormBoardLink(board)}"\r\n${replies.trelloBoardAlreadyBinded1} "${list.name}"`; 172 | } 173 | 174 | /** 175 | * @param {String} text 176 | * @returns {Array.} 177 | */ 178 | function SplitBigMessage(text) { 179 | let answers = []; 180 | while (text.length > global.MaxMessageLength) { 181 | answers.push(text.substring(0, global.MaxMessageLength - 1)); 182 | text = text.slice(global.MaxMessageLength); 183 | } 184 | answers.push(text); 185 | return answers; 186 | } 187 | 188 | /** 189 | * @param {String} str 190 | * @param {Boolean} newline 191 | * @param {Languages} language 192 | * @returns {String} 193 | */ 194 | function Deleted(str, newline, language) { 195 | const replies = LoadReplies(language); 196 | return `${replies.deleted} ${str}. ${newline === false ? replies.showList : ``}`; 197 | } 198 | 199 | /** 200 | * @param {Number} num 201 | * @param {Boolean} newline 202 | * @param {Languages} language 203 | */ 204 | function DeletedGreater(num, newline, language) { 205 | const replies = LoadReplies(language); 206 | return `${replies.deletedGreater} ${num}.${newline === false ? replies.showList : ''}`; 207 | } 208 | 209 | /** 210 | * @param {Number} hours 211 | * @param {Number} minutes 212 | * @param {Boolean} isNegative 213 | * @returns {String} 214 | */ 215 | function TzDetermined(hours, minutes, isNegative) { 216 | let s = '+' 217 | let t = ''; 218 | if (isNegative) { 219 | s = '-'; 220 | hours *= -1; 221 | } 222 | if (hours < 10) { 223 | t = '0'; 224 | } 225 | s += t + hours + ':'; 226 | t = '0'; 227 | if (minutes >= 10) { 228 | t = ''; 229 | } 230 | s += t + minutes; 231 | return s; 232 | } 233 | 234 | /** 235 | * @param {Number} tz 236 | * @returns {String} 237 | */ 238 | function TzCurrent(tz) { 239 | let negative = tz < 0; 240 | let hour = tz / 3600 | 0; 241 | let minutes = Math.abs(tz % 3600 / 60); 242 | return TzDetermined(hour, minutes, negative); 243 | } 244 | 245 | /** 246 | * @param {String} text 247 | * @param {String} myFormattedDate 248 | * @param {Languages} language 249 | * @returns {String} 250 | */ 251 | function Scheduled(text, myFormattedDate, language) { 252 | const replies = LoadReplies(language); 253 | return `"${text}" ${replies.alreadyScheduled} ${myFormattedDate}\r\n`; 254 | } 255 | 256 | /** 257 | * @param {String} trello_key 258 | * @param {String} app_name 259 | * @param {Languages} language 260 | */ 261 | function TrelloAuthorizationMessage(trello_key, app_name, language) { 262 | const replies = LoadReplies(language); 263 | let link = `https://trello.com/1/authorize?expiration=never&scope=read,write&response_type=token&name=${app_name}&key=${trello_key}`; 264 | return `${replies.trelloAuthenticate0}${link}${replies.trelloAuthenticate1}`; 265 | } 266 | 267 | /** 268 | * @param {Languages} language 269 | * @param {String} link 270 | * @returns {String} 271 | */ 272 | function TrelloInfoLink(language, link) { 273 | const replies = LoadReplies(language); 274 | return `${replies.trelloInfoLink} ${link}`; 275 | } 276 | 277 | /** 278 | * @param {Languages} language 279 | * @param {Number} usersCount 280 | * @param {Number} schedulesCount 281 | * @returns {String} 282 | */ 283 | function FormDisplayStatus(language, usersCount, schedulesCount) { 284 | const replies = LoadReplies(language); 285 | return `${replies.displayStatus0}${usersCount} 286 | ${replies.displayStatus1}${schedulesCount} 287 | ${replies.displayStatus2}`; 288 | } 289 | 290 | /** 291 | * 292 | * @param {Schedule} schedule 293 | * @returns {String} 294 | */ 295 | function FormReminderMessage(schedule) { 296 | let mention = schedule.username != 'none' ? ` @${schedule.username}` : ''; 297 | let isPeriodic = schedule.period_time > 0; 298 | let remindIcon = isPeriodic ? '🔄' : '⏰'; 299 | return `${remindIcon}${mention} ${schedule.text}`; 300 | } 301 | 302 | /** 303 | * @param {String} s 304 | * @param {Number} length 305 | * @returns 306 | */ 307 | function ShortenString(s, length = global.MaxShortStringLength, countWords = false, endFiller = '...') { 308 | if (countWords) { 309 | let words = s.split(' '); 310 | if (words.length <= length) { 311 | return words.join(' '); 312 | } 313 | words.splice(length); 314 | return `${words.join(' ')}${endFiller}`; 315 | } 316 | if (s.length > length) { 317 | s = s.substring(0, length) 318 | s = `${s.substring(0, s.lastIndexOf(' '))}${endFiller}`; 319 | } 320 | return s; 321 | } 322 | 323 | module.exports = { 324 | TimeListIsEmpty, 325 | FormDateStringFormat, 326 | FormPeriodStringFormat, 327 | FormStringFormatSchedule, 328 | FormBoardsList, 329 | FormBoardListsList, 330 | FormListBinded, 331 | FormBoardUnbinded, 332 | FormAlreadyBoardBinded, 333 | SplitBigMessage, 334 | Deleted, 335 | DeletedGreater, 336 | TzCurrent, 337 | Scheduled, 338 | TrelloAuthorizationMessage, 339 | TrelloInfoLink, 340 | FormDisplayStatus, 341 | FormReminderMessage, 342 | ShortenString 343 | } -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/remindersParsing.js: -------------------------------------------------------------------------------- 1 | const Markup = require('telegraf/markup'); 2 | const { Languages, LoadReplies } = require('../static/replies/repliesLoader'); 3 | const Format = require('../../processing/formatting'); 4 | const kbs = require('../static/replies/keyboards'); 5 | const { DataBase, User, Chat } = require('../../../storage/dataBase/DataBase'); 6 | const { Schedule, ScheduleStates, GetOptions } = require('../../../storage/dataBase/TablesClasses/Schedule'); 7 | const { arrayParseString } = require('@alordash/parse-word-to-number'); 8 | const { wordsParseDate, TimeList, ParsedDate } = require('@alordash/date-parser'); 9 | const { ProcessParsedDate } = require('../../processing/timeProcessing'); 10 | const { TrelloManager } = require('@alordash/node-js-trello'); 11 | const { ExtractNicknames, GetUsersIDsFromNicknames } = require('../../processing/nicknamesExtraction'); 12 | const { BotReplyMultipleMessages } = require('./replying'); 13 | const utils = require('../../processing/utilities'); 14 | const { RemoveInvalidRemindersMarkup } = require('../../processing/remindersOperations'); 15 | const { Decrypt } = require('../../../storage/encryption/encrypt'); 16 | 17 | /** 18 | * @param {String} text 19 | * @param {Number} prevalence 20 | * @returns {Array.} 21 | */ 22 | function FormParsedDates(text, prevalence = 50) { 23 | return wordsParseDate(arrayParseString(text, 1), 1, prevalence, text); 24 | } 25 | 26 | /** 27 | * @param {ParsedDate} parsedDate 28 | * @param {Number} tz 29 | * @param {Number} userId 30 | * @param {Number} now 31 | * @returns {Schedule} 32 | */ 33 | function FormSchedule(parsedDate, tz, userId, now = Date.now()) { 34 | let dateParams = ProcessParsedDate(parsedDate, tz, false); 35 | const dateIsValid = typeof (dateParams) != 'undefined'; 36 | const dateExists = dateIsValid && 37 | (dateParams.target_date > 0 || 38 | dateParams.period_time > 0 || 39 | dateParams.max_date > 0); 40 | const textIsValid = parsedDate.string.length > 0; 41 | if (!dateExists || !textIsValid) 42 | return undefined; 43 | 44 | return new Schedule( 45 | userId.toString(), 46 | -1, 47 | parsedDate.string, 48 | 'none', 49 | dateParams.target_date, 50 | dateParams.period_time, 51 | dateParams.max_date, 52 | undefined, 53 | undefined, 54 | undefined, 55 | now, 56 | userId); 57 | } 58 | 59 | /** 60 | * @param {String | Array.} text 61 | * @param {User} user 62 | * @param {Number} prevalence 63 | * @returns {Array.} 64 | */ 65 | function SimpleScheduleParse(text, user, prevalence = 50, specificScheduleIndex = -1) { 66 | let result = []; 67 | let parsedDates; 68 | 69 | if (text[0] instanceof ParsedDate) 70 | parsedDates = text; 71 | else 72 | parsedDates = wordsParseDate(arrayParseString(text, 1), 1, prevalence, text); 73 | 74 | 75 | if (parsedDates.length <= 0) { 76 | return []; 77 | } 78 | const now = Date.now(); 79 | const tz = user.tz; 80 | 81 | if (specificScheduleIndex >= 0) { 82 | return [FormSchedule(parsedDates[specificScheduleIndex], tz, user.id, now)]; 83 | } 84 | 85 | for (let parsedDate of parsedDates) { 86 | let schedule = FormSchedule(parsedDate, tz, user.id, now); 87 | if (schedule != undefined) 88 | result.push(schedule); 89 | } 90 | return result; 91 | } 92 | 93 | /** 94 | * @param {*} ctx 95 | * @param {String} chatID 96 | * @param {Boolean} inGroup 97 | * @param {String} msgText 98 | * @param {Languages} language 99 | * @param {Boolean} mentioned 100 | * @param {Number} prevalenceForParsing 101 | */ 102 | async function ParseScheduleMessage(ctx, chatID, inGroup, msgText, language, mentioned, prevalenceForParsing) { 103 | let reply = ''; 104 | let file_id = utils.GetAttachmentId(ctx.message); 105 | await DataBase.Users.SetUserLanguage(ctx.from.id, language); 106 | const replies = LoadReplies(language); 107 | let tz = (await DataBase.Users.GetUserById(ctx.from.id)).tz; 108 | //#region PARSE SCHEDULE 109 | let username = 'none'; 110 | if (inGroup) { 111 | username = ctx.from.username; 112 | } 113 | let parsedDates = wordsParseDate(arrayParseString(msgText, 1), 1, prevalenceForParsing, msgText); 114 | let count = 1; 115 | let shouldWarn = false; 116 | let schedulesCount = await DataBase.Schedules.GetSchedulesCount(chatID); 117 | if (parsedDates.length == 0) { 118 | parsedDates[0] = new ParsedDate(new TimeList(), new TimeList(), new TimeList(), msgText, 50, []); 119 | } 120 | let parsedDateIndex = 0; 121 | let chat = await DataBase.Chats.GetChatById(`${ctx.chat.id}`); 122 | let trelloIsOk = typeof (chat) != 'undefined' && chat.trello_list_id != null; 123 | let trelloKeyboard; 124 | let keyboard; 125 | const now = Date.now(); 126 | 127 | let invalidSchedule; 128 | let newSchedules = []; 129 | for (let parsedDate of parsedDates) { 130 | console.log('parsedDate :>> ', parsedDate); 131 | let dateParams = ProcessParsedDate(parsedDate, tz, inGroup && !mentioned, chatID == process.env.SMART_SCHEDULER_ADMIN); 132 | const dateIsValid = typeof (dateParams) != 'undefined'; 133 | if (inGroup && !dateIsValid) { 134 | continue; 135 | } 136 | const dateExists = dateIsValid && 137 | (dateParams.target_date > 0 || 138 | dateParams.period_time > 0 || 139 | dateParams.max_date > 0); 140 | if (!dateExists) 141 | if (dateParams.target_date == -1) { 142 | reply += replies.reminderMinTimeLimit; 143 | continue; 144 | } else if (dateParams.target_date == -2) { 145 | reply += replies.reminderMaxTimeLimit; 146 | continue; 147 | } 148 | 149 | let schedules = await DataBase.Schedules.GetSchedules(chatID, GetOptions.valid, undefined, true); 150 | let max_num = 0; 151 | let found = false; 152 | let i = 0; 153 | for (let j = 0; j < schedules.length; j++) { 154 | let _schedule = schedules[j]; 155 | // if (_schedule.text == parsedDate.string) { 156 | // i = j; 157 | // found = true; 158 | // } 159 | if (_schedule.num > max_num && _schedule.state == ScheduleStates.valid) { 160 | max_num = _schedule.num; 161 | } 162 | } 163 | if (found) { 164 | let schedule = schedules[i]; 165 | if (!inGroup) { 166 | reply += Format.Scheduled(schedule.text, Format.FormDateStringFormat(new Date(schedule.target_date + tz * 1000), language, true), language); 167 | } 168 | } else { 169 | if (count + schedulesCount < global.MaximumCountOfSchedules) { 170 | const textIsValid = parsedDate.string.length > 0; 171 | let newSchedule = new Schedule( 172 | chatID, 173 | max_num + parsedDateIndex + 1, 174 | parsedDate.string, 175 | username, 176 | dateParams.target_date, 177 | dateParams.period_time, 178 | dateParams.max_date, 179 | file_id, 180 | undefined, 181 | undefined, 182 | now, 183 | ctx.from.id); 184 | let proceed = dateExists && textIsValid; 185 | if (!proceed && !inGroup) { 186 | let invalidSchedules = await DataBase.Schedules.GetSchedules(chatID, GetOptions.invalid); 187 | invalidSchedule = invalidSchedules[0]; 188 | if (typeof (invalidSchedule) != 'undefined') { 189 | invalidSchedule.text = Decrypt(invalidSchedule.text, invalidSchedule.chatid); 190 | const invalidText = invalidSchedule.text.length == 0; 191 | if (invalidText && textIsValid) { 192 | invalidSchedule.text = newSchedule.text; 193 | newSchedule = invalidSchedule; 194 | newSchedule.state = ScheduleStates.valid; 195 | proceed = true; 196 | } else if (!invalidText && dateExists) { 197 | newSchedule.text = invalidSchedule.text; 198 | proceed = true; 199 | } 200 | } 201 | if (!proceed) { 202 | if (typeof (invalidSchedule) != 'undefined') { 203 | RemoveInvalidRemindersMarkup(ctx, chatID, invalidSchedule.message_id); 204 | } 205 | await DataBase.Schedules.RemoveSchedulesByState(chatID, ScheduleStates.invalid); 206 | newSchedule.state = ScheduleStates.invalid; 207 | invalidSchedule = newSchedule; 208 | if (!dateExists) { 209 | reply = `${reply}${replies.scheduleDateInvalid}\r\n`; 210 | keyboard = kbs.CancelButton(language); 211 | } else if (!textIsValid) { 212 | reply = `${reply}${replies.scheduleTextInvalid}\r\n`; 213 | keyboard = kbs.CancelButton(language); 214 | } 215 | } else { 216 | invalidSchedule = undefined; 217 | } 218 | } 219 | if (proceed) { 220 | if (trelloIsOk) { 221 | trelloKeyboard = kbs.ToTrelloKeyboard(language); 222 | } 223 | RemoveInvalidRemindersMarkup(ctx, chatID); 224 | await DataBase.Schedules.RemoveSchedulesByState(chatID, ScheduleStates.invalid); 225 | newSchedules.push(newSchedule); 226 | count++; 227 | reply += await Format.FormStringFormatSchedule(newSchedule, tz, language, true, !inGroup) + `\r\n`; 228 | } 229 | } else { 230 | reply += replies.shouldRemove + '\r\n' + replies.maximumSchedulesCount + ` ${global.MaximumCountOfSchedules}.`; 231 | } 232 | } 233 | if (!dateIsValid && !inGroup) { 234 | reply += replies.errorScheduling + '\r\n'; 235 | } 236 | if (ctx.message.id >= global.MessagesUntilTzWarning 237 | && !inGroup && !(await DataBase.Users.HasUserID(ctx.from.id))) { 238 | shouldWarn = true; 239 | } 240 | parsedDateIndex++; 241 | } 242 | //#endregion 243 | if (reply == '') { 244 | return; 245 | } 246 | if (shouldWarn) { 247 | reply += replies.tzWarning; 248 | } 249 | let answers = Format.SplitBigMessage(reply); 250 | let options = []; 251 | try { 252 | let results; 253 | let message_id; 254 | if (!mentioned && inGroup && typeof (schedule) === 'undefined' && parsedDates.length > 0) { 255 | if (typeof (newSchedules) != 'undefined' && newSchedules.length > 0) { 256 | keyboard = kbs.ConfirmSchedulesKeyboard(language); 257 | } 258 | if (typeof (keyboard) != 'undefined') { 259 | keyboard = kbs.MergeInlineKeyboards(keyboard, trelloKeyboard); 260 | options[answers.length - 1] = keyboard; 261 | } 262 | results = await BotReplyMultipleMessages(ctx, answers, options); 263 | message_id = results[results.length - 1].message_id; 264 | for (const nsi in newSchedules) { 265 | newSchedules[nsi].state = ScheduleStates.pending; 266 | } 267 | } else { 268 | let kb = await kbs.LogicalListKeyboard(language, chatID, schedulesCount); 269 | if (typeof (keyboard) != 'undefined') { 270 | keyboard.reply_markup.inline_keyboard[0][0].callback_data = 'cancel_rm'; 271 | kb = keyboard; 272 | } else { 273 | kb = kbs.MergeInlineKeyboards(kb, trelloKeyboard); 274 | } 275 | options[answers.length - 1] = kb; 276 | results = await BotReplyMultipleMessages(ctx, answers, options); 277 | if (results.length > 0) { 278 | message_id = results[results.length - 1].message_id; 279 | if (typeof (invalidSchedule) != 'undefined') { 280 | invalidSchedule.message_id = message_id; 281 | await DataBase.Schedules.AddSchedule(invalidSchedule); 282 | } 283 | } 284 | } 285 | if (typeof (newSchedules) != 'undefined' && newSchedules.length > 0) { 286 | for (const nsi in newSchedules) { 287 | newSchedules[nsi].message_id = message_id; 288 | } 289 | await DataBase.Schedules.AddSchedules(chatID, newSchedules); 290 | } 291 | } catch (e) { 292 | console.error(e); 293 | } 294 | } 295 | 296 | module.exports = { FormParsedDates, FormSchedule, ParseScheduleMessage, SimpleScheduleParse }; -------------------------------------------------------------------------------- /BotCode/interactions/bot/actions/technical.js: -------------------------------------------------------------------------------- 1 | const Markup = require('telegraf/markup'); 2 | const { Languages, LoadReplies } = require('../static/replies/repliesLoader'); 3 | const Format = require('../../processing/formatting'); 4 | const kbs = require('../static/replies/keyboards'); 5 | const { DataBase, Schedule, User, Chat } = require('../../../storage/dataBase/DataBase'); 6 | const { BotReply } = require('./replying'); 7 | const utils = require('../../processing/utilities'); 8 | const { ScheduleStates } = require('../../../storage/dataBase/TablesClasses/Schedule'); 9 | const request = require('request-promise'); 10 | const { Decrypt, Encrypt } = require('../../../storage/encryption/encrypt'); 11 | const { TrelloManager } = require('@alordash/node-js-trello'); 12 | 13 | /** 14 | * @param {String} chatID 15 | * @param {Number} tsOffset 16 | * @param {Languages} language 17 | * @returns {Array.} 18 | */ 19 | async function LoadSchedulesList(chatID, tsOffset, language) { 20 | let schedules = await DataBase.Schedules.ListSchedules(chatID); 21 | if (schedules.length > 0) { 22 | let answers = []; 23 | let answer = ``; 24 | schedules.sort((a, b) => a.target_date - b.target_date); 25 | for (let schedule of schedules) { 26 | let newAnswer = `${await Format.FormStringFormatSchedule(schedule, tsOffset, language, false, true)}\r\n`; 27 | if (answer.length + newAnswer.length > global.MaxMessageLength) { 28 | answers.push(answer); 29 | answer = newAnswer; 30 | } else { 31 | answer += newAnswer; 32 | } 33 | } 34 | if (answer.length > 0) { 35 | answers.push(answer); 36 | } 37 | return answers; 38 | } else { 39 | const replies = LoadReplies(language); 40 | return [replies.listIsEmpty]; 41 | } 42 | } 43 | 44 | /** 45 | * @param {*} ctx 46 | * @param {Languages} 47 | */ 48 | async function DeleteSchedules(ctx) { 49 | let chatID = utils.FormatChatId(ctx.chat.id) 50 | let msgText = ctx.message.text; 51 | const replies = LoadReplies(ctx.from.language_code); 52 | if (msgText.indexOf('all') == "/del ".length) { 53 | await DataBase.Schedules.ClearAllSchedules(chatID); 54 | BotReply(ctx, replies.cleared); 55 | return; 56 | } 57 | 58 | let deletePlus = msgText.match(/[0-9]+\+/g); 59 | if (deletePlus != null) { 60 | let num = parseInt(deletePlus[0]); 61 | let query = `num >= ${num}`; 62 | try { 63 | await DataBase.Schedules.RemoveSchedulesQuery(chatID, query); 64 | BotReply(ctx, Format.DeletedGreater(num, false, ctx.message.from.language_code)); 65 | } catch (e) { 66 | console.error(e); 67 | } 68 | return; 69 | } 70 | 71 | let nums = msgText.match(/[0-9]+/g); 72 | let ranges = msgText.match(/[0-9]+-[0-9]+/g); 73 | for (let i in nums) { 74 | nums[i] = parseInt(nums[i], 10); 75 | } 76 | for (let i in ranges) { 77 | let range = ranges[i]; 78 | let index = range.indexOf('-'); 79 | let leftNum = +range.substring(0, index); 80 | let rightNum = +range.substring(index + 1); 81 | if (leftNum > rightNum) { 82 | let t = leftNum; 83 | leftNum = rightNum; 84 | rightNum = t; 85 | } 86 | for (let j = leftNum; j <= rightNum && j - leftNum <= 10; j++) { 87 | nums.push(j); 88 | } 89 | } 90 | if (nums != null) { 91 | nums = nums.filter((item, pos) => { 92 | return nums.indexOf(item) == pos; 93 | }); 94 | nums.sort((a, b) => a - b); 95 | 96 | let query = ''; 97 | for (let i in nums) { 98 | let scheduleNum = nums[i]; 99 | query += `num = ${scheduleNum} OR `; 100 | } 101 | query = query.substring(0, query.length - 4); 102 | await DataBase.Schedules.RemoveSchedulesQuery(chatID, query); 103 | await DataBase.Schedules.ReorderSchedules(chatID); 104 | let end = ''; 105 | if (nums.length > 1) { 106 | end = 's'; 107 | } 108 | try { 109 | BotReply(ctx, Format.Deleted(nums.join(', '), false, ctx.message.from.language_code)); 110 | } catch (e) { 111 | console.error(e); 112 | } 113 | return; 114 | } 115 | try { 116 | BotReply(ctx, replies.invalidUseOfCommand); 117 | } catch (e) { 118 | console.error(e); 119 | } 120 | } 121 | 122 | /** 123 | * @param {*} ctx 124 | * @param {Array.} tzPendingConfirmationUsers 125 | */ 126 | async function StartTimeZoneDetermination(ctx, tzPendingConfirmationUsers) { 127 | let curTZ = (await DataBase.Users.GetUserById(ctx.from.id, true)).tz; 128 | let reply = ''; 129 | const language = await DataBase.Users.GetUserLanguage(ctx.from.id); 130 | const replies = LoadReplies(language); 131 | if (curTZ != null) { 132 | reply = replies.tzDefined + '' + Format.TzCurrent(curTZ) + '\r\n'; 133 | } 134 | let isPrivateChat = ctx.chat.id >= 0; 135 | if (tzPendingConfirmationUsers.indexOf(ctx.from.id) < 0) { 136 | tzPendingConfirmationUsers.push(ctx.from.id); 137 | } 138 | if (isPrivateChat) { 139 | reply = `${reply}${replies.tzManualConfiguration}`; 140 | try { 141 | return await BotReply(ctx, reply, kbs.TzDeterminationKeyboard(language)); 142 | } catch (e) { 143 | console.error(e); 144 | } 145 | } 146 | try { 147 | return await BotReply(ctx, replies.tzManualConfiguration); 148 | } catch (e) { 149 | console.error(e); 150 | } 151 | } 152 | 153 | /** 154 | * 155 | * @param {*} ctx 156 | * @param {Number} lat 157 | * @param {Number} lng 158 | * @param {Array.} tzPendingConfirmationUsers 159 | * @param {Array.} trelloPendingConfirmationUsers 160 | * @param {String} cityName 161 | */ 162 | async function ConfirmLocation(ctx, lat, lng, tzPendingConfirmationUsers, cityName = '') { 163 | let language = ctx.from.language_code; 164 | const replies = LoadReplies(language); 165 | let tz = JSON.parse(await request(`https://maps.googleapis.com/maps/api/timezone/json?location=${lat},${lng}×tamp=${Date.now().div(1000)}&key=${process.env.SMART_SCHEDULER_GOOGLE_API_KEY}`)); 166 | console.log(`tz = ${JSON.stringify(tz)}`); 167 | let userId = ctx.from.id; 168 | let ts = tz.rawOffset; 169 | if (!await DataBase.Users.HasUserID(userId)) { 170 | await DataBase.Users.AddUser(new User(userId, ts, global.defaultUserLanguage)); 171 | } else { 172 | await DataBase.Users.SetUserTz(userId, ts); 173 | } 174 | try { 175 | utils.ClearPendingConfirmation(tzPendingConfirmationUsers, undefined, ctx.from.id); 176 | let reply = replies.tzDefined + '' + Format.TzCurrent(ts) + ''; 177 | if (cityName != '') { 178 | reply = `${replies.tzAddress} "${cityName}"\r\n${reply}`; 179 | } 180 | BotReply(ctx, reply, await kbs.LogicalListKeyboard(language, utils.FormatChatId(ctx.chat.id))); 181 | } catch (e) { 182 | console.error(e); 183 | } 184 | } 185 | 186 | /** 187 | * @param {*} ctx 188 | * @param {Array.} tzPendingConfirmationUsers 189 | */ 190 | async function ConfrimTimeZone(ctx, tzPendingConfirmationUsers) { 191 | let userId = ctx.from.id; 192 | let matches = ctx.message.text.match(/(\+|-|–|—|)([0-9])+:([0-9])+/g); 193 | let hours, minutes, negative, ts; 194 | const replies = LoadReplies(ctx.from.language_code); 195 | if (matches != null) { 196 | //Parse tz from msg; 197 | let offset = matches[0]; 198 | let index = offset.indexOf(':'); 199 | hours = parseInt(offset.substring(0, index)); 200 | negative = offset[0].match(/-|–|—/g) != null; 201 | minutes = parseInt(offset.substring(index + 1)); 202 | console.log(`Determining tz: offset = ${offset}, hours = ${hours}, minutes = ${minutes}, ts = ${ts}`); 203 | } else { 204 | matches = ctx.message.text.match(/(\+|-|–|—|)([0-9])+/g); 205 | if (matches != null) { 206 | let offset = matches[0]; 207 | hours = parseInt(offset); 208 | minutes = 0; 209 | negative = offset[0].match(/-|–|—/g) != null; 210 | console.log(`Determining tz from only hour option: offset = ${offset}, hours = ${hours}, minutes = ${minutes}, ts = ${ts}`); 211 | } 212 | } 213 | if (matches != null) { 214 | let ts = hours * 3600; 215 | ts += minutes * 60 * (negative ? -1 : 1); 216 | if (!await DataBase.Users.HasUserID(userId)) { 217 | await DataBase.Users.AddUser(new User(userId, ts, global.defaultUserLanguage)); 218 | } else { 219 | await DataBase.Users.SetUserTz(userId, ts); 220 | } 221 | utils.ClearPendingConfirmation(tzPendingConfirmationUsers, undefined, ctx.from.id); 222 | try { 223 | let chatID = utils.FormatChatId(ctx.chat.id); 224 | BotReply(ctx, replies.tzDefined + '' + Format.TzCurrent(ts) + '\r\n', await kbs.LogicalListKeyboard(ctx.from.language_code, chatID)); 225 | } catch (e) { 226 | console.error(e); 227 | } 228 | return; 229 | } else { 230 | try { 231 | BotReply(ctx, replies.tzInvalidInput, kbs.CancelButton(ctx.from.language_code)); 232 | } catch (e) { 233 | console.error(e); 234 | } 235 | // try { 236 | // let geocodes = JSON.parse(await request(`https://maps.googleapis.com/maps/api/geocode/json?address=${ctx.message.text}&key=${process.env.SMART_SCHEDULER_GOOGLE_API_KEY}`)); 237 | // if (geocodes.results.length > 0) { 238 | // let geocode = geocodes.results[0]; 239 | // let location = geocode.geometry.location; 240 | // ConfirmLocation(ctx, location.lat, location.lng, tzPendingConfirmationUsers, geocode.formatted_address); 241 | // return; 242 | // } else { 243 | // console.log(`Can't determine tz in "${ctx.message.text}"`); 244 | // try { 245 | // BotReply(ctx, replies.tzInvalidInput, kbs.CancelButton(ctx.from.language_code)); 246 | // } catch (e) { 247 | // console.error(e); 248 | // } 249 | // } 250 | // } catch (e) { 251 | // console.log(e); 252 | // try { 253 | // BotReply(ctx, replies.tzInvalidInput, kbs.CancelButton(ctx.from.language_code)); 254 | // } catch (e) { 255 | // console.error(e); 256 | // } 257 | // } 258 | } 259 | } 260 | 261 | async function StartDisplayingStatus(ctx) { 262 | if (ctx.from.id != +process.env.SMART_SCHEDULER_ADMIN) { 263 | return; 264 | } 265 | let channelInfo; 266 | let text = ctx.message.text; 267 | let index = text.indexOf(' '); 268 | let option; 269 | if (index == -1) { 270 | channelInfo = ctx.from; 271 | option = kbs.CancelButton(ctx.from.language_code); 272 | } else { 273 | let channelId = text.substring(index + 1); 274 | try { 275 | channelInfo = await ctx.telegram.getChat(channelId); 276 | } catch (e) { 277 | console.log(e); 278 | return; 279 | } 280 | } 281 | let usersCount = await DataBase.Users.GetSubscribedUsersCount(); 282 | let schedulesCount = await DataBase.Schedules.GetTotalSchedulesCount(); 283 | text = Format.FormDisplayStatus(ctx.from.language_code, usersCount, schedulesCount); 284 | let msg = await BotReply(ctx, text, option, undefined, channelInfo.id); 285 | let schedule = new Schedule(utils.FormatChatId(channelInfo.id), -1, ctx.from.language_code, 'none', -1, -1, -1, undefined, ScheduleStates.statusDisplay, msg.message_id, Date.now(), ctx.from.id); 286 | await DataBase.Schedules.AddSchedule(schedule); 287 | } 288 | 289 | /** 290 | * @param {Schedule} schedule 291 | * @param {string} chatID 292 | * @returns {{expired: Boolean, decrypted: Boolean}} 293 | */ 294 | async function ProcessTrelloReminder(schedule, chatID) { 295 | const now = Date.now(); 296 | let expired = true; 297 | let decrypted = false; 298 | try { 299 | if (schedule.trello_card_id != null && typeof (schedule.trello_card_id) != 'undefined') { 300 | let chat = await DataBase.Chats.GetChatById(chatID); 301 | if (typeof (chat) != 'undefined' && typeof (chat.trello_token) != 'undefined') { 302 | let trelloManager = new TrelloManager(process.env.TRELLO_TOKEN, chat.trello_token); 303 | let card = await trelloManager.GetCard(schedule.trello_card_id); 304 | if (typeof (card) != 'undefined' && typeof (card.due) != 'undefined') { 305 | let dueTime = new Date(card.due).getTime(); 306 | if (now < dueTime) { 307 | expired = false; 308 | 309 | if (dueTime != schedule.target_date) { 310 | DataBase.Schedules.SetScheduleTargetDate(schedule.chatid, schedule.num, dueTime); 311 | } 312 | 313 | const cardText = card.desc; 314 | schedule.text = Decrypt(schedule.text, schedule.chatid); 315 | decrypted = true; 316 | if (cardText != schedule.text) { 317 | DataBase.Schedules.SetScheduleText(schedule.chatid, schedule.num, Encrypt(cardText, schedule.chatid)); 318 | } 319 | } 320 | } else if (typeof (card) == 'undefined') { 321 | let board = await trelloManager.GetBoard(chat.trello_board_id); 322 | if (typeof (board) == 'undefined') { 323 | DataBase.Chats.ClearChatFromTrello(chat.id); 324 | } 325 | } 326 | } 327 | } 328 | } catch (e) { 329 | console.error(e); 330 | } finally { 331 | return { expired, decrypted }; 332 | } 333 | } 334 | 335 | module.exports = { 336 | LoadSchedulesList, 337 | DeleteSchedules, 338 | StartTimeZoneDetermination, 339 | ConfirmLocation, 340 | ConfrimTimeZone, 341 | StartDisplayingStatus, 342 | ProcessTrelloReminder 343 | } -------------------------------------------------------------------------------- /BotCode/storage/dataBase/TablesClasses/Schedule.js: -------------------------------------------------------------------------------- 1 | const { Connector } = require('../Connector'); 2 | const { Encrypt, Decrypt } = require('../../encryption/encrypt'); 3 | const { Languages } = require('../../../interactions/bot/static/replies/repliesLoader'); 4 | 5 | const GetOptions = Object.freeze({ 6 | all: 0, 7 | draft: 1, 8 | valid: 2, 9 | invalid: 3, 10 | statusDisplay: 4 11 | }); 12 | 13 | const ScheduleStates = Object.freeze({ 14 | valid: 'valid', 15 | pending: 'pending', 16 | repeat: 'repeat', 17 | invalid: 'invalid', 18 | statusDisplay: 'statusDisplay' 19 | }); 20 | 21 | class Schedule { 22 | /**@type {String} */ 23 | chatid; 24 | /**@type {Number} */ 25 | num; 26 | /**@type {String} */ 27 | text; 28 | /**@type {Number} */ 29 | target_date; 30 | /**@type {Number} */ 31 | period_time; 32 | /**@type {Number} */ 33 | max_date; 34 | /**@type {String} */ 35 | username; 36 | /**@type {Number} */ 37 | file_id; 38 | /**@type {String} */ 39 | trello_card_id; 40 | /**@type {Number} */ 41 | id; 42 | /**@type {ScheduleStates} */ 43 | state; 44 | /**@type {Number} */ 45 | message_id; 46 | /**@type {Number} */ 47 | creation_date; 48 | /**@type {Number} */ 49 | creator; 50 | 51 | /**@param {String} chatid 52 | * @param {Number} num 53 | * @param {String} text 54 | * @param {String} username 55 | * @param {Number} target_date 56 | * @param {Number} period_time 57 | * @param {Number} max_date 58 | * @param {Number} file_id 59 | * @param {ScheduleStates} state 60 | * @param {Number} message_id 61 | * @param {Number} creation_date 62 | * @param {Number} creator 63 | */ 64 | constructor(chatid, num, text, username, target_date, period_time, max_date, file_id = '~', state = ScheduleStates.valid, message_id = null, creation_date = null, creator = null) { 65 | this.chatid = chatid; 66 | this.num = num; 67 | this.text = text; 68 | this.username = username; 69 | this.target_date = target_date; 70 | this.period_time = period_time; 71 | this.max_date = max_date; 72 | this.file_id = file_id; 73 | this.state = state; 74 | this.message_id = message_id; 75 | this.creation_date = creation_date; 76 | this.creator = creator; 77 | } 78 | 79 | /** 80 | * @param {Schedule} schedule 81 | * @param {Boolean} decrypt 82 | * @returns {Schedule} 83 | */ 84 | static FixSchedule(schedule, decrypt = false) { 85 | schedule.period_time = +schedule.period_time; 86 | schedule.target_date = +schedule.target_date; 87 | schedule.max_date = +schedule.max_date; 88 | schedule.creation_date = +schedule.creation_date; 89 | schedule.creator = +schedule.creator; 90 | if (schedule.trello_card_id == 'undefined') { 91 | schedule.trello_card_id = undefined; 92 | } 93 | if (decrypt) { 94 | schedule.text = Decrypt(schedule.text, schedule.chatid); 95 | } 96 | return schedule; 97 | } 98 | 99 | /** 100 | * @param {Array.} schedules 101 | * @param {Boolean} decrypt 102 | * @returns {Array.} 103 | */ 104 | static FixSchedulesRow(schedules, decrypt = false) { 105 | for (const i in schedules) { 106 | schedules[i] = Schedule.FixSchedule(schedules[i], decrypt); 107 | } 108 | return schedules; 109 | } 110 | 111 | /** 112 | * @param {String} query 113 | * @param {GetOptions} getOptions 114 | * @param {Number} message_id 115 | * @param {String} chatid 116 | * @returns {String} 117 | */ 118 | static ApplyGetOptions(query, getOptions = GetOptions.all, message_id = null, chatid = null) { 119 | let keyWord = 'WHERE'; 120 | if (query.indexOf('WHERE') != -1) { 121 | keyWord = 'AND'; 122 | } 123 | switch (getOptions) { 124 | case GetOptions.all: 125 | query = `${query} ${keyWord} state != '${ScheduleStates.statusDisplay}'`; 126 | break; 127 | 128 | case GetOptions.draft: 129 | query = `${query} ${keyWord} (state = '${ScheduleStates.repeat}' OR state = '${ScheduleStates.pending}')`; 130 | break; 131 | 132 | case GetOptions.valid: 133 | query = `${query} ${keyWord} state = '${ScheduleStates.valid}'`; 134 | break; 135 | 136 | case GetOptions.invalid: 137 | query = `${query} ${keyWord} state = '${ScheduleStates.invalid}'`; 138 | break; 139 | 140 | case GetOptions.statusDisplay: 141 | query = `${query} ${keyWord} state = '${ScheduleStates.statusDisplay}'`; 142 | break; 143 | 144 | default: 145 | break; 146 | } 147 | if (query.indexOf('WHERE') != -1) { 148 | keyWord = 'AND'; 149 | } 150 | if ((message_id != null) && (chatid != null)) { 151 | query = `${query} ${keyWord} message_id = ${message_id} AND chatid = '${chatid}'`; 152 | } 153 | return query; 154 | } 155 | 156 | /** 157 | * @param {String} chatID 158 | * @returns {Number} 159 | */ 160 | static async GetActiveSchedulesNumber(chatID) { 161 | let query = `SELECT Max(num) FROM schedules WHERE chatid = $1`; 162 | let res = await Connector.instance.paramQuery(query, [`${chatID}`]); 163 | if (typeof (res) == 'undefined' || typeof (res.rows) == 'undefined' || res.rows.length == 0 || res.rows[0].length == 0) { 164 | return 0; 165 | } 166 | return res.rows[0][0]; 167 | } 168 | 169 | /** 170 | * @param {Array.} newSchedules 171 | * @param {String} chatID 172 | */ 173 | static async AddSchedules(chatID, newSchedules) { 174 | let queryString = `INSERT INTO schedules (ChatID, num, text, username, target_date, period_time, max_date, file_id, trello_card_id, state, message_id, creation_date, creator) VALUES `; 175 | let num = await this.GetActiveSchedulesNumber(chatID) + 1; 176 | let values = []; 177 | let i = 0; 178 | for (let schedule of newSchedules) { 179 | if (schedule.chatid[0] != '_' || typeof (schedule.username) == 'undefined') { 180 | schedule.username = 'none'; 181 | } 182 | const text = Encrypt(schedule.text, schedule.chatid); 183 | queryString = `${queryString}($${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}), `; 184 | values.push(`${schedule.chatid}`, num, `${text}`, `${schedule.username}`, schedule.target_date, schedule.period_time, schedule.max_date, `${schedule.file_id}`, `${schedule.trello_card_id}`, schedule.state, schedule.message_id, schedule.creation_date, schedule.creator); 185 | num++; 186 | } 187 | queryString = queryString.substring(0, queryString.length - 2); 188 | await Connector.instance.paramQuery(queryString, values); 189 | } 190 | 191 | /** 192 | * @param {Array.} schedules 193 | */ 194 | static async InsertSchedules(schedules) { 195 | if (schedules.length <= 0) { 196 | return; 197 | } 198 | let query = `INSERT INTO schedules (ChatID, num, text, username, target_date, period_time, max_date, file_id, trello_card_id, state, message_id, creation_date, creator) VALUES `; 199 | let i = 0; 200 | let values = []; 201 | for (const schedule of schedules) { 202 | query = `${query}($${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}, $${++i}), `; 203 | values.push(schedule.chatid, schedule.num, schedule.text, schedule.username, schedule.target_date, schedule.period_time, schedule.max_date, schedule.file_id, schedule.trello_card_id, schedule.state, schedule.message_id, schedule.creation_date, schedule.creator); 204 | } 205 | query = query.substring(0, query.length - 2); 206 | await Connector.instance.paramQuery(query, values); 207 | } 208 | 209 | /** 210 | * @param {Schedule} schedule 211 | */ 212 | static async AddSchedule(schedule) { 213 | if (schedule.chatid[0] != '_' || typeof (schedule.username) == 'undefined') schedule.username = 'none'; 214 | let num; 215 | if (schedule.state == ScheduleStates.repeat) { 216 | num = -1; 217 | } else { 218 | num = await this.GetSchedulesCount(schedule.chatid) + 1; 219 | } 220 | console.log(`Target_date = ${schedule.target_date}`); 221 | const text = Encrypt(schedule.text, schedule.chatid); 222 | await Connector.instance.paramQuery(`INSERT INTO schedules (ChatID, num, text, username, target_date, period_time, max_date, file_id, trello_card_id, state, message_id, creation_date, creator) VALUES 223 | ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`, 224 | [ 225 | `${schedule.chatid}`, 226 | num, 227 | `${text}`, 228 | `${schedule.username}`, 229 | schedule.target_date, 230 | schedule.period_time, 231 | schedule.max_date, 232 | `${schedule.file_id}`, 233 | `${schedule.trello_card_id}`, 234 | schedule.state, 235 | schedule.message_id, 236 | schedule.creation_date, 237 | schedule.creator 238 | ] 239 | ); 240 | console.log(`Added "${schedule.text}" (encrypted: "${text}") to ${schedule.target_date} from chat "${schedule.chatid}"`); 241 | } 242 | 243 | /** 244 | * @param {Number} chatID 245 | * @param {Number} num 246 | * @param {Number} target_date 247 | */ 248 | static async SetScheduleTargetDate(chatID, num, target_date) { 249 | await Connector.instance.Query( 250 | `UPDATE schedules 251 | SET target_date = ${target_date} 252 | WHERE ChatID = '${chatID}' 253 | AND num = ${num};` 254 | ); 255 | } 256 | 257 | /** 258 | * @param {Number} chatID 259 | * @param {Number} num 260 | * @param {String} text 261 | */ 262 | static async SetScheduleText(chatID, num, text) { 263 | await Connector.instance.paramQuery( 264 | `UPDATE schedules 265 | SET text = $1 266 | WHERE ChatID = $2 267 | AND num = $3;`, 268 | [text, chatID, num]); 269 | } 270 | 271 | /** 272 | * @param {Schedule} schedule 273 | * @param {Number} id 274 | */ 275 | static async SetSchedule(schedule, id = schedule.id) { 276 | const text = Encrypt(schedule.text, schedule.chatid); 277 | await Connector.instance.paramQuery(`UPDATE schedules SET 278 | ChatID = $1, 279 | text = $2, 280 | username = $3, 281 | target_date = $4, 282 | period_time = $5, 283 | max_date = $6, 284 | file_id = $7, 285 | trello_card_id = $8, 286 | state = $9, 287 | message_id = $10, 288 | creation_date = $11, 289 | creator = $12 290 | WHERE id = ${id}`, 291 | [ 292 | `${schedule.chatid}`, 293 | `${text}`, 294 | `${schedule.username}`, 295 | schedule.target_date, 296 | schedule.period_time, 297 | schedule.max_date, 298 | `${schedule.file_id}`, 299 | `${schedule.trello_card_id}`, 300 | schedule.state, 301 | schedule.message_id, 302 | schedule.creation_date, 303 | schedule.creator 304 | ] 305 | ); 306 | } 307 | 308 | /** 309 | * @param {String} chatID 310 | * @param {Number} num 311 | */ 312 | static async RemoveScheduleByNum(chatID, num) { 313 | console.log(`Removing schedule \r\ChatID = "${chatID}"`); 314 | let query = `DELETE FROM schedules WHERE ChatID = '${chatID}' AND num = ${num}`; 315 | console.log(`QUERY = "${query}"`); 316 | let res = await Connector.instance.Query(query); 317 | console.log(`res = ${JSON.stringify(res.rows)}`); 318 | } 319 | 320 | /** 321 | * @param {Array.} schedules 322 | * @param {Number} message_id 323 | * @param {String} chatid 324 | */ 325 | static async RemoveSchedules(schedules = [], message_id = null, chatid = null) { 326 | let query = `DELETE FROM schedules`; 327 | if (schedules.length == 0 && message_id == null) { 328 | return; 329 | } 330 | if (schedules.length) { 331 | query = `${query} WHERE id IN(`; 332 | for (const schedule of schedules) { 333 | query = `${query}${schedule.id}, `; 334 | } 335 | query = `${query.substring(0, query.length - 2)})`; 336 | } 337 | if ((message_id != null) && (chatid != null)) { 338 | query = Schedule.ApplyGetOptions(query, GetOptions.all, message_id, chatid); 339 | } 340 | await Connector.instance.Query(query); 341 | } 342 | 343 | /** 344 | * @param {String} chatID 345 | * @param {ScheduleStates} state 346 | * @returns 347 | */ 348 | static async RemoveSchedulesByState(chatID, state) { 349 | return await Connector.instance.Query(`DELETE FROM schedules WHERE chatid = '${chatID}' AND state = '${state}'`); 350 | } 351 | 352 | /** 353 | * @param {String} chatID 354 | * @param {String} s 355 | */ 356 | static async RemoveSchedulesQuery(chatID, s) { 357 | console.log(`Removing schedule s = "${s}"\r\nChatID = "${chatID}" typeof(ChatID) = ${typeof (chatID)}`); 358 | let query = `DELETE FROM schedules WHERE ChatID = '${chatID}' AND (${s})`; 359 | console.log(`QUERY = "${query}"`); 360 | let res = await Connector.instance.Query(query); 361 | console.log(`res = ${JSON.stringify(res.rows)}`); 362 | } 363 | 364 | /** 365 | * @param {String} chatID 366 | */ 367 | static async ClearAllSchedules(chatID) { 368 | console.log(`Clearing all schedules in chat ${chatID}`); 369 | await Connector.instance.Query(`DELETE FROM schedules WHERE ChatID = '${chatID}'`); 370 | console.log(`Cleared all schedules`); 371 | } 372 | 373 | /** 374 | * @param {String} chatID 375 | * @param {Boolean} decrypt 376 | */ 377 | static async ReorderSchedules(chatID, decrypt = false) { 378 | console.log(`Reordering schedules in chat: ${chatID}`); 379 | let res = await Connector.instance.Query(`SELECT * FROM ReorderSchedules('${chatID}')`); 380 | if (typeof (res) != 'undefined' && res.rows.length > 0) { 381 | return Schedule.FixSchedulesRow(res.rows, decrypt); 382 | } else { 383 | return []; 384 | } 385 | } 386 | 387 | /** 388 | * @param {Array.} schedules 389 | */ 390 | static async ReorderMultipleSchedules(schedules) { 391 | let chats = []; 392 | let query = `SELECT ReorderMultipleSchedules (ARRAY[''`; 393 | for (const schedule of schedules) { 394 | if (chats.indexOf(schedule.chatid) == -1) { 395 | chats.push(schedule.chatid); 396 | query = `${query}, '${schedule.chatid}'`; 397 | } 398 | } 399 | query = `${query}])`; 400 | console.log(`Reordering schedules in multiple chats: ${JSON.stringify(chats)}`); 401 | let res = await Connector.instance.Query(query); 402 | return res; 403 | } 404 | 405 | /** 406 | * @param {String} chatID 407 | * @returns {Array.} 408 | */ 409 | static async ListSchedules(chatID) { 410 | if (!Connector.instance.sending) { 411 | return await this.GetSchedules(chatID, GetOptions.valid, undefined, true); 412 | } 413 | return []; 414 | } 415 | 416 | /** 417 | * @param {Number} tsNow 418 | * @param {Boolean} decrypt 419 | * @returns {Array.} 420 | */ 421 | static async CheckActiveSchedules(tsNow, decrypt = false) { 422 | let expiredSchedules = []; 423 | let schedules = await this.GetAllSchedules(GetOptions.valid, decrypt); 424 | for (let schedule of schedules) { 425 | console.log(`schedule = ${JSON.stringify(schedule)}, tsNow = ${tsNow}`); 426 | if (schedule.target_date <= tsNow || schedule.trello_card_id != null) { 427 | expiredSchedules.push(schedule); 428 | } 429 | } 430 | return expiredSchedules; 431 | } 432 | 433 | /** 434 | * @param {Boolean} decrypt 435 | * @returns {Array.<{schedule: Schedule, lang: String}>} 436 | */ 437 | 438 | static async GetExpiredSchedules(decrypt = false) { 439 | let query = Schedule.ApplyGetOptions(`SELECT chatid, num, text, username, target_date, period_time, max_date, file_id, trello_card_id, schedules.id, state, message_id, creation_date, creator, userids.lang FROM schedules 440 | LEFT JOIN userids ON schedules.creator = userids.id`, GetOptions.valid); 441 | query = `${query} AND ((extract(epoch from now()) * 1000)::bigint >= target_date OR (trello_card_id != 'undefined' AND trello_card_id IS NOT NULL)) 442 | ORDER BY schedules.id;`; 443 | let res = await Connector.instance.Query(query); 444 | if (typeof (res) == 'undefined' || res.rows.length == 0) { 445 | return []; 446 | } 447 | console.log(`Picked expired schedules, count: ${res.rows.length}`); 448 | let result = []; 449 | for (let row of res.rows) { 450 | let lang = row.lang; 451 | delete (row.lang); 452 | result.push({ schedule: Schedule.FixSchedule(row, decrypt), lang }); 453 | } 454 | return result; 455 | } 456 | 457 | /** 458 | * @param {Boolean} decrypt 459 | * @returns {Array.<{schedule: Schedule, lang: String}>} 460 | */ 461 | 462 | static async GetStatusDisplaySchedules(decrypt = false) { 463 | let query = Schedule.ApplyGetOptions(`SELECT chatid, num, text, username, target_date, period_time, max_date, file_id, trello_card_id, schedules.id, state, message_id, creation_date, creator, userids.lang FROM schedules 464 | WHERE state = ${ScheduleStates.statusDisplay}`, GetOptions.valid); 465 | let res = await Connector.instance.Query(query); 466 | if (typeof (res) == 'undefined' || res.rows.length == 0) { 467 | return []; 468 | } 469 | console.log(`Picked status display schedules, count: ${res.rows.length}`); 470 | let result = []; 471 | for (let row of res.rows) { 472 | let lang = row.lang; 473 | delete (row.lang); 474 | result.push({ schedule: Schedule.FixSchedule(row, decrypt), lang }); 475 | } 476 | return result; 477 | } 478 | 479 | /** 480 | * @param {String} chatID 481 | * @param {Number} num 482 | * @param {Boolean} decrypt 483 | * @returns {Schedule} 484 | */ 485 | static async GetScheduleByNum(chatID, num, decrypt = false) { 486 | let res = await Connector.instance.Query(`SELECT * FROM schedules WHERE num = '${num}' AND ChatID = '${chatID}'`); 487 | console.log(`Picked schedule by num ${JSON.stringify(res.rows)}`); 488 | if (typeof (res) != 'undefined' && res.rows.length > 0) { 489 | return Schedule.FixSchedule(res.rows[0], decrypt); 490 | } else { 491 | return undefined; 492 | } 493 | } 494 | 495 | /** 496 | * @param {GetOptions} getOptions 497 | * @param {Boolean} decrypt 498 | * @returns {Array.} 499 | */ 500 | static async GetAllSchedules(getOptions = GetOptions.all, decrypt = false) { 501 | let query = Schedule.ApplyGetOptions(`SELECT * FROM schedules`, getOptions); 502 | let res = await Connector.instance.Query(query); 503 | console.log(`Picked all schedules, count: ${res.rows.length}`); 504 | if (typeof (res) != 'undefined' && res.rows.length > 0) { 505 | return Schedule.FixSchedulesRow(res.rows, decrypt); 506 | } else { 507 | return []; 508 | } 509 | } 510 | 511 | /** 512 | * @param {String} chatID 513 | * @param {GetOptions} getOptions 514 | * @param {Number} message_id 515 | * @param {Boolean} decrypt 516 | * @returns {Array.} 517 | */ 518 | static async GetSchedules(chatID, getOptions = GetOptions.all, message_id = null, decrypt = false) { 519 | let query = Schedule.ApplyGetOptions(`SELECT * FROM schedules WHERE ChatID = '${chatID}'`, getOptions, message_id, chatID); 520 | let res = await Connector.instance.Query(query); 521 | if (typeof (res) != 'undefined' && res.rows.length > 0) { 522 | console.log(`Picked ${res.rows.length} schedules from chat "${chatID}"`); 523 | return Schedule.FixSchedulesRow(res.rows, decrypt); 524 | } else { 525 | return []; 526 | } 527 | } 528 | 529 | /** 530 | * @param {String} chatID 531 | * @param {GetOptions} getOptions 532 | * @returns {Number} 533 | */ 534 | static async GetSchedulesCount(chatID, getOptions = GetOptions.all) { 535 | let query = Schedule.ApplyGetOptions(`SELECT Count(*) FROM schedules WHERE ChatID = '${chatID}'`, getOptions); 536 | let res = await Connector.instance.Query(query); 537 | return +res.rows[0].count; 538 | } 539 | 540 | /**@param {Array.} schedules */ 541 | static async ConfirmSchedules(schedules) { 542 | if (schedules.length <= 0) { 543 | return; 544 | } 545 | let query = ''; 546 | for (const schedule of schedules) { 547 | query = `${query}SELECT ConfirmSchedule(${schedule.id}, '${ScheduleStates.valid}', '${ScheduleStates.valid}');\r\n`; 548 | } 549 | return await Connector.instance.Query(query); 550 | } 551 | 552 | static async GetTotalSchedulesCount() { 553 | return +(await Connector.instance.Query('SELECT Max(id) FROM schedules')).rows[0].max; 554 | } 555 | } 556 | 557 | module.exports = { Schedule, GetOptions, ScheduleStates }; --------------------------------------------------------------------------------