├── .gitattributes
├── .gitignore
├── README.md
├── chat_bot
├── info_base_example
│ ├── config.json
│ └── questions.json
├── log_example
│ ├── error.log
│ └── info.log
└── src
│ ├── index.js
│ ├── lib
│ ├── configuration.js
│ ├── constants.js
│ ├── logger.js
│ ├── moodleAPI.js
│ └── msgHandlers.js
│ ├── package-lock.json
│ └── package.json
├── chat_bot_logo.jpg
├── chat_bot_logo_old.jpg
└── documentation
├── how_to_use
├── install_guide
│ ├── en_install.txt
│ └── бг_инсталация.txt
└── user_guide
│ ├── en_guide.txt
│ ├── бг_наръчник.txt
│ └── подготовка.txt
├── idea
├── en_idea.txt
└── бг_идея.txt
└── implementation_details
├── todo.txt
└── used_technologies.txt
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | info_base/
3 | .vscode/
4 | log/
5 | chat_bot/src/.vscode/launch.json
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ChatBot_FMI_students
2 |
3 | a telegram based chat-bot application for better education and student awareness
4 | integrated with moodle e-learning system
5 |
6 | written as a project assignment during the FMI's JavaScript Advanced 17/18 course
7 |
8 | fast links :
9 | [idea](https://github.com/IvanFilipov/ChatBot_FMI_students/tree/master/documentation/idea/en_idea.txt)
10 | [how to use](http://telegra.ph/User-guide-how-to-use-the-chatbot-01-03)
11 |
12 | ## repository organization :
13 |
14 | > `/chat_bot/`
15 | >> `src` - the source for the chatbot written in JavaScript
16 | >> `info_base` - the bot's knowledge base example
17 |
18 | > `/documentation/`
19 | >> `idea` - what the idea is and what problems it solves
20 | >> `implementation_details` - information about what was used while writting the code
21 | >> `how_to_use` - guides for both bot user and bot host
22 |
23 |
24 | # Чатбот за студентите от ФМИ
25 |
26 | приложение, базирано на телеграмски чат бот, за по - добра информираност и обучение на студентите от ФМИ,
27 | по - специално тези, които взимат участие в курсовете по УП, ООП и СДП
28 | интегриран със moodle системата за електроно обучение
29 |
30 | проектно задание като част от курса "JavaScript за напреднали" 17/18 воден във ФМИ
31 |
32 | бързи връзи :
33 | [идеята](https://github.com/IvanFilipov/ChatBot_FMI_students/tree/master/documentation/idea/бг_идея.txt)
34 | [как да използваме](http://telegra.ph/Narchnik-za-izpolzvane-na-bota-01-03)
35 |
36 | ## организация на хранилището :
37 |
38 | > `/chat_bot/`
39 | >> `src` - целия код свързан с приложението
40 | >> `info_base` - базата от познания на бота
41 |
42 | > `/documentation/`
43 | >> `idea` - каква е идеята, какви проблеми решава
44 | >> `implementation_details` - техническа информация за това, което е било използвано, докато е писан кода
45 | >> `how_to_use` - наръчници за използването на бота като клиент в чат приложението и като създател и поддържащ бота
46 |
--------------------------------------------------------------------------------
/chat_bot/info_base_example/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "questionsPath" : "../info_base/questions.json",
3 | "externalLinks" : {
4 |
5 |
6 | "courseInfo" : "https://docs.google.com/document/d/1UaaONmxmHAJXpL4RcGcgMSptBVLR4guacb45CdYN770",
7 | "teamInfo" : "https://docs.google.com/document/d/1UaaONmxmHAJXpL4RcGcgMSptBVLR4guacb45CdYN770",
8 | "booksInfo" : "https://docs.google.com/document/d/1Q9P_YwHMFULFn84VK-VLggO0JOmGs3Cn-B9klSmIrJs",
9 | "themesInfo" : "https://docs.google.com/document/d/1tKRmULwk2tb_iKXIGD3jDqSNRlFidCDBv8WipIGMzyo",
10 |
11 |
12 | "helpBG" : "(http://telegra.ph/Narchnik-za-izpolzvane-na-bota-01-03)",
13 | "helpEN" : "(http://telegra.ph/User-guide-how-to-use-the-chatbot-01-03)"
14 |
15 |
16 | },
17 | "moodleConfig": {
18 | "baseUrl": "http://dev.learn.fmi.uni-sofia.bg/",
19 | "serviceUrl": "/webservice/rest/server.php?",
20 | "fun_forumDiscussions": "mod_forum_get_forum_discussions_paginated",
21 | "forumid": 2293
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/chat_bot/info_base_example/questions.json:
--------------------------------------------------------------------------------
1 | {
2 | "questions" : [
3 | [
4 | {
5 | "text" : "Memory leak is :",
6 | "answerOptions" : [
7 | "someone, who forgets a lot",
8 | "a dynamic allocated memory without a pointer to it",
9 | "C/C++ lack of garbage collector",
10 | "a null pointer"
11 | ],
12 | "correctAnswer" : 1
13 | },
14 | {
15 | "text" : "Утечка на памет наричаме",
16 | "answerOptions" : [
17 | "някого, който много забравя",
18 | "динамично заделена памет, към която не сочи никой указател",
19 | "липсата на \"събирач на боклука\" в C/C++",
20 | "указател сочещ към NULL"
21 | ],
22 | "correctAnswer" : 1
23 | }
24 | ]
25 | ]
26 | }
--------------------------------------------------------------------------------
/chat_bot/log_example/error.log:
--------------------------------------------------------------------------------
1 | 2018-1-25 / 21:11:29 / : ERROR --- EFATAL ---
2 | 2018-1-25 / 21:12:54 / : ERROR --- ---
3 | 2018-1-25 / 21:44:24 / : ERROR --- TypeError: Cannot read property 'forEach' of undefined ---
4 |
--------------------------------------------------------------------------------
/chat_bot/log_example/info.log:
--------------------------------------------------------------------------------
1 | 2018-1-25 / 02:03:01 / : INFO --- hi, i am the chatbot :) ---
2 | 2018-1-25 / 02:03:01 / : INFO --- 538534505:AAHy5BujBqmST6aqyAM4aUOg_jgYiULcqqk ---
3 | 2018-1-25 / 02:03:27 / : INFO --- TEST 481967012 OK ---
4 | 2018-1-25 / 02:03:29 / : INFO --- TEST CALLBACK 481967012 OK ---
5 | 2018-1-25 / 21:11:29 / : INFO --- hi, i am the chatbot :) ---
6 | 2018-1-25 / 21:11:29 / : INFO --- ---
7 | 2018-1-25 / 21:11:29 / : ERROR --- EFATAL ---
8 | 2018-1-25 / 21:12:46 / : INFO --- hi, i am the chatbot :) ---
9 | 2018-1-25 / 21:12:46 / : INFO --- 538534505:AAHy5BujBqmST6aqyAM4aUOg_jgYiULcqqk ---
10 | 2018-1-25 / 21:12:47 / : INFO --- GENERAL INFO 481967012 OK ---
11 | 2018-1-25 / 21:12:50 / : INFO --- GENERAL INFO 481967012 OK ---
12 | 2018-1-25 / 21:12:52 / : INFO --- GENERAL INFO 481967012 OK ---
13 | 2018-1-25 / 21:12:54 / : ERROR --- ---
14 | 2018-1-25 / 21:13:58 / : INFO --- hi, i am the chatbot :) ---
15 | 2018-1-25 / 21:13:58 / : INFO --- 538534505:AAHy5BujBqmST6aqyAM4aUOg_jgYiULcqqk ---
16 | 2018-1-25 / 21:19:21 / : INFO --- hi, i am the chatbot :) ---
17 | 2018-1-25 / 21:19:21 / : INFO --- 538534505:AAHy5BujBqmST6aqyAM4aUOg_jgYiULcqqk ---
18 | 2018-1-25 / 21:19:54 / : INFO --- GET NEWS 481967012 OK ---
19 | 2018-1-25 / 21:23:40 / : INFO --- hi, i am the chatbot :) ---
20 | 2018-1-25 / 21:23:40 / : INFO --- 538534505:AAHy5BujBqmST6aqyAM4aUOg_jgYiULcqqk ---
21 | 2018-1-25 / 21:24:24 / : INFO --- hi, i am the chatbot :) ---
22 | 2018-1-25 / 21:24:24 / : INFO --- 538534505:AAHy5BujBqmST6aqyAM4aUOg_jgYiULcqqk ---
23 | 2018-1-25 / 21:27:10 / : INFO --- hi, i am the chatbot :) ---
24 | 2018-1-25 / 21:27:10 / : INFO --- 538534505:AAHy5BujBqmST6aqyAM4aUOg_jgYiULcqqk ---
25 | 2018-1-25 / 21:30:07 / : INFO --- hi, i am the chatbot :) ---
26 | 2018-1-25 / 21:30:07 / : INFO --- 538534505:AAHy5BujBqmST6aqyAM4aUOg_jgYiULcqqk ---
27 | 2018-1-25 / 21:39:24 / : INFO --- STARTED ---
28 | 2018-1-25 / 21:41:49 / : INFO --- STARTED ---
29 | 2018-1-25 / 21:42:05 / : INFO --- GET NEWS 481967012 OK ---
30 | 2018-1-25 / 21:44:24 / : ERROR --- TypeError: Cannot read property 'forEach' of undefined ---
31 | 2018-1-25 / 21:44:26 / : INFO --- GET NEWS 508550150 OK ---
32 | 2018-1-25 / 21:44:48 / : INFO --- GET NEWS 481967012 OK ---
33 | 2018-1-25 / 21:45:01 / : INFO --- STARTED ---
34 | 2018-1-25 / 21:45:09 / : INFO --- GET NEWS 481967012 OK ---
35 | 2018-1-25 / 21:47:18 / : INFO --- GET NEWS 481967012 OK ---
36 | 2018-1-25 / 21:48:49 / : INFO --- TEST 481967012 OK ---
37 | 2018-1-25 / 21:48:52 / : INFO --- TEST CALLBACK 481967012 OK ---
38 | 2018-1-25 / 21:48:53 / : INFO --- TEST CALLBACK 481967012 OK ---
39 | 2018-1-25 / 21:49:01 / : INFO --- TEST CALLBACK 481967012 OK ---
40 | 2018-1-25 / 21:53:15 / : INFO --- GENERAL INFO 508550150 OK ---
41 | 2018-1-25 / 21:53:16 / : INFO --- GENERAL INFO 508550150 OK ---
42 | 2018-1-25 / 21:53:16 / : INFO --- TEST 508550150 OK ---
43 | 2018-1-25 / 21:53:17 / : INFO --- TEST CALLBACK 508550150 OK ---
44 | 2018-1-25 / 21:53:18 / : INFO --- GENERAL INFO 508550150 OK ---
45 | 2018-1-25 / 21:53:18 / : INFO --- GENERAL INFO 508550150 OK ---
46 | 2018-1-25 / 21:53:23 / : INFO --- GET NEWS 508550150 OK ---
47 | 2018-1-25 / 21:53:24 / : INFO --- TEST 508550150 OK ---
48 | 2018-1-25 / 21:53:31 / : INFO --- TEST CALLBACK 508550150 OK ---
49 | 2018-1-25 / 21:53:35 / : INFO --- UNKNOWN 508550150 OK ---
50 | 2018-1-25 / 21:57:16 / : INFO --- UNKNOWN 471870834 OK ---
51 | 2018-1-25 / 21:57:21 / : INFO --- GET NEWS 471870834 OK ---
52 | 2018-1-25 / 21:57:29 / : INFO --- GENERAL INFO 471870834 OK ---
53 | 2018-1-25 / 21:58:04 / : INFO --- GET NEWS 471870834 OK ---
54 |
--------------------------------------------------------------------------------
/chat_bot/src/index.js:
--------------------------------------------------------------------------------
1 | const TelegramBot = require('node-telegram-bot-api'),
2 | schedule = require('node-schedule'),
3 | msgHandlers = require('./lib/msgHandlers'),
4 | config = require('./lib/configuration'),
5 | logger = require('./lib/logger');
6 |
7 |
8 | const {
9 | BG, EN,
10 | commandList,
11 | buttonLists,
12 | enumOptions
13 | } = require('./lib/constants');
14 |
15 | logger.info('STARTED');
16 |
17 | //getting the token from the argv
18 | const botToken = config.get('bToken');
19 |
20 | //creating the bot context
21 | const bot = new TelegramBot(botToken, {polling: true});
22 |
23 | //will be used to remember language settings
24 | //chatId -> BG | EN
25 | let usersLangs = {};
26 |
27 | //will hold chatId -> correct answer
28 | //map information for Test option
29 | let callBacks = {};
30 |
31 | //will hold all bot's stickers
32 | let stickers = [];
33 |
34 | //polling error handler
35 | let online = true; //use to avoid multiple error logs
36 | bot.on('polling_error', (err) => {
37 |
38 | if(online){
39 |
40 | logger.error(err.code);
41 | online = false;
42 | }
43 | });
44 |
45 | //on first message ever
46 | bot.onText(/\/start/, (msg) => {
47 |
48 | //saving user preferences
49 | usersLangs[msg.chat.id] = BG; //default language is bulgarian
50 |
51 | msgHandlers.welcome(bot, msg)
52 | .then(() => logger.info('WELCOME ' + msg.chat.id + ' OK'))
53 | .catch(err => logger.error(err.toString()));
54 | });
55 |
56 | //handling language changes
57 | bot.onText(/\/lang_(en|bg)/, (msg, res) => {
58 | //the result parameter is
59 | //the result of executing exec on the regular expression
60 |
61 | //exec gives us an array with matched results
62 | let ln = (res[1] === 'bg') ? BG : EN;
63 |
64 | //saving the choice
65 | usersLangs[msg.chat.id] = ln;
66 |
67 | msgHandlers.langChanged(bot, msg, ln)
68 | .then(() => logger.info('CHANGE LN ' + msg.chat.id + ' OK'))
69 | .catch(err => logger.error(err.toString()));
70 | });
71 |
72 | //handling /help option
73 | bot.onText(/\/help+/, (msg) => {
74 |
75 | //will match everything starting with help
76 |
77 | //if undefined => bulgarian
78 | const ln = (usersLangs[msg.chat.id] === EN) ? EN : BG;
79 |
80 | msgHandlers.help(bot, msg, ln)
81 | .then(() => logger.info('HELP ' + msg.chat.id + ' OK'))
82 | .catch(err => logger.error(err.toString()));
83 | });
84 |
85 | //handling personal information by faculty number
86 | bot.onText(/\d+/, (msg, res) => {
87 |
88 | const ln = (usersLangs[msg.chat.id] === EN) ? EN : BG;
89 |
90 | //faculty ids are at least 5 digits,
91 | //not handled by the regexp on purpose
92 | if (res.input.length < 5 || res.input.length > 8) {
93 |
94 | msgHandlers.invalidFacultyNumber(bot, msg, ln)
95 | .then(() => logger.info('PERSONAL INFO ' + msg.chat.id + ' INVALID ID'))
96 | .catch(err => logger.error(err.toString()));
97 |
98 | return;
99 | }
100 |
101 | msgHandlers.personalInfo(bot, msg, ln)
102 | .then(() => logger.info('PERSONAL INFO ' + msg.chat.id + ' OK'))
103 | .catch(err => logger.error(err.toString()));
104 | });
105 |
106 | //handle key command
107 | bot.onText(/\/key/, (msg, res) => {
108 |
109 | const ln = (usersLangs[msg.chat.id] === EN) ? EN : BG;
110 |
111 | msgHandlers.getMoodleKey(bot, msg, ln)
112 | .then(() => logger.info('MOODLE KEY ' + msg.chat.id + ' OK'))
113 | .catch(err => logger.error(err.toString()));
114 | });
115 |
116 |
117 |
118 | bot.on('message', (msg) => {
119 |
120 | OK = true; //back online
121 |
122 | //language of communication
123 | //undefined -> BG
124 | const ln = (usersLangs[msg.chat.id] === EN) ? EN : BG;
125 |
126 | //first of all checking for callback from "test me" option
127 | let isCallback = callBacks[msg.chat.id];
128 |
129 | //!== undefined
130 | if(isCallback){
131 |
132 | msgHandlers.testCallback(bot, msg, ln, callBacks)
133 | .then(() => logger.info('TEST CALLBACK ' + msg.chat.id + ' OK'))
134 | .catch(err => logger.error(err.toString()));
135 |
136 | return;
137 | }
138 |
139 | if(msg.sticker !== undefined) {
140 |
141 | let ans_sticker = stickers.find(el => el.emoji == msg.sticker.emoji);
142 |
143 | if(ans_sticker === undefined) {
144 |
145 | if(stickers.length !== 0) {
146 |
147 | let index = Math.floor(Math.random() * Math.floor(stickers.length));
148 | ans_sticker = stickers[index];
149 | } else {
150 |
151 | ans_sticker = msg.sticker;
152 | }
153 | }
154 |
155 | bot.sendSticker(msg.chat.id, ans_sticker.file_id)
156 | .then(() => logger.info("sticker OK"))
157 | .catch(() => logger.error("sticker PROBLEM"));
158 |
159 | return;
160 | }
161 |
162 | if(msg.text === undefined) {
163 |
164 | msgHandlers.unknown(bot, msg, ln)
165 | .then(msg => logger.info('UNKNOWN ' + msg.chat.id + ' OK'))
166 | .catch(err => logger.error(err.toString()));
167 | return;
168 | }
169 |
170 | //it is a known command, it should be handled somewhere else
171 | if(commandList.find((el) => el === msg.text) !== undefined ||
172 | msg.text.indexOf('/help') !== -1 || !isNaN(msg.text))
173 | return;
174 |
175 | //searching for the option in the current language
176 | const optIndex = buttonLists[ln].indexOf(msg.text);
177 |
178 |
179 | switch (optIndex) {
180 |
181 | case enumOptions.G_INFO_INDEX:
182 |
183 | msgHandlers.getGeneralInfo(bot, msg, ln)
184 | .then(() => logger.info('GENERAL INFO ' + msg.chat.id + ' OK'))
185 | .catch(err => logger.error(err.toString()));
186 |
187 | return;
188 |
189 | case enumOptions.TEST_INDEX:
190 |
191 | msgHandlers.testMe(bot, msg, ln, callBacks)
192 | .then(() => logger.info('TEST ' + msg.chat.id + ' OK'))
193 | .catch(err => logger.error(err.toString()));
194 |
195 | return;
196 |
197 | case enumOptions.INVALID:
198 |
199 | msgHandlers.unknown(bot, msg, ln)
200 | .then(msg => logger.info('UNKNOWN ' + msg.chat.id + ' OK'))
201 | .catch(err => logger.error(err.toString()));
202 |
203 | return;
204 |
205 | case enumOptions.NEWS_INDEX:
206 |
207 | msgHandlers.getNews(bot, msg, ln)
208 | .then(msg => logger.info('GET NEWS ' + msg.chat.id + ' OK'))
209 | .catch(err => logger.error(err.toString()));
210 |
211 | return;
212 |
213 | case enumOptions.ASSIGN_INDEX:
214 |
215 | msgHandlers.getAssignments(bot, msg, ln)
216 | .then(msg => logger.info('ASSIGNMENTS ' + msg.chat.id + ' OK'))
217 | .catch(err => logger.error(err.toString()));
218 | return;
219 |
220 | case enumOptions.P_INFO_INDEX:
221 |
222 | let answer = ln ? 'моля, въведи факултетния си номер 🎓'
223 | : 'please, enter your faculty number 🎓';
224 |
225 | bot.sendMessage(msg.chat.id, answer, {
226 | reply_markup: JSON.stringify({ //hides the keyboard
227 | hide_keyboard: true
228 | })
229 | });
230 | return;
231 |
232 | default:
233 | //debug reason only...
234 | bot.sendMessage(msg.chat.id, msg.text + ' pressed!');
235 | }
236 | });
237 |
238 |
239 | // Handle callback queries from inline keyboard
240 | bot.on('callback_query', callbackQuery => {
241 |
242 | const wantedId = parseInt(callbackQuery.data);
243 | const msg = callbackQuery.message;
244 |
245 | msgHandlers.getNewsContain(bot, msg, wantedId)
246 | .then(msg => logger.info('GET NEWS CONTAIN ' + msg.chat.id + ' OK'))
247 | .catch(err => logger.error(err.toString()));
248 | });
249 |
250 |
251 | bot.getStickerSet('HackerBoyStickers')
252 | .then(res => {
253 |
254 | stickers = res.stickers;
255 | })
256 |
257 | //update all info on starting...
258 | msgHandlers.update()
259 | .then(() => logger.info('UPDATE NEWS AND ASSIGNMENTS : OK'))
260 | .catch(err => logger.error('UPDATE : ' + err.toString()));
261 |
262 |
263 | //try to update on every five minutes
264 | const updateJob = schedule.scheduleJob('*/15 * * * *', () => {
265 |
266 | msgHandlers.update()
267 | .then(() => logger.info('UPDATE NEWS AND ASSIGNMENTS : OK'))
268 | .catch(err => logger.error('UPDATE : ' + err.toString()));
269 | });
--------------------------------------------------------------------------------
/chat_bot/src/lib/configuration.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs'),
2 | path = require('path'),
3 | nconf = require('nconf');
4 |
5 | //a wrapper class for working with nconf
6 | class Config {
7 |
8 | constructor() {
9 |
10 | //everything will be 'in-memory'
11 | nconf.use('memory');
12 |
13 | //take all arguments from the command line
14 | nconf.argv();
15 |
16 | nconf.required(['pName','bToken', 'mToken', 'configFile']);
17 |
18 | process.title = nconf.get('pName'); //process rename better DevOps
19 |
20 | //loading all config data
21 | nconf.add('conf', { type: 'file', file: nconf.get('configFile')});
22 |
23 | //will throw an error if any key is missing
24 | nconf.required(['externalLinks', 'questionsPath',
25 | 'moodleConfig', "logDirectoryPath"]);
26 |
27 | //loading all questions
28 | nconf.add('quest', { type: 'file', file: nconf.get('questionsPath') });
29 | //adding it to the required fields
30 | nconf.required(['questions']);
31 |
32 | }
33 |
34 | get(key) {
35 |
36 | return nconf.get(key);
37 | }
38 | }
39 |
40 | //only one instance
41 | let config;
42 |
43 | //if one of the keys is missing
44 | //the program cannot start...
45 | try {
46 |
47 | config = new Config();
48 |
49 | } catch (err) {
50 |
51 | console.error('FATAL ERROR WHILE INITIALIZATION : ' + err.toString());
52 | process.exit(-1);
53 | }
54 |
55 | module.exports = config;
--------------------------------------------------------------------------------
/chat_bot/src/lib/constants.js:
--------------------------------------------------------------------------------
1 | const config = require('./configuration');
2 |
3 | //loading all URLs from the config file
4 | const externalLinks = config.get('externalLinks');
5 |
6 | //loading questions from file
7 | const questions = config.get('questions');
8 |
9 | module.exports = {
10 |
11 | //taken externally from a file
12 | questionsList : questions,
13 |
14 | //language number constants
15 | BG : 1,
16 | EN : 0,
17 |
18 | //all supported commands
19 | commandList : ['/lang_bg' , '/lang_en', '/start', '/help', '/key'],
20 |
21 | //all supported buttons / text messages
22 | buttonLists : [
23 |
24 | ['News','Assignments', 'Personal information','General information','Test my knowledge'],
25 |
26 | [ 'Новини', 'Задания', 'Лична информация', 'Обща информация', 'Тествай познанията ми']
27 |
28 | ],
29 |
30 | //constants for indexes of the buttons
31 | enumOptions: {
32 | NEWS_INDEX: 0,
33 | ASSIGN_INDEX: 1,
34 | P_INFO_INDEX: 2,
35 | G_INFO_INDEX: 3,
36 | TEST_INDEX: 4,
37 | INVALID : -1
38 | },
39 |
40 |
41 | //keyboardOptions[0] - > object with options in EN
42 | //keyboardOptions[1] - > with options in BG
43 | keyboardOptions : [
44 |
45 | {
46 | 'reply_markup': {
47 |
48 | 'keyboard': [
49 | ['News'],
50 | ['Assignments', 'Personal information'],
51 | ['General information'],
52 | ['Test my knowledge']
53 | ]
54 | }
55 |
56 | },
57 |
58 | {
59 |
60 | 'reply_markup': {
61 |
62 | 'keyboard': [
63 | ['Новини'],
64 | ['Задания', 'Лична информация'],
65 | ['Обща информация'],
66 | ['Тествай познанията ми']
67 | ]
68 | }
69 | }
70 |
71 | ],
72 |
73 | //test keyboard options
74 | testKeyboardOptions : [
75 |
76 | {
77 | 'reply_markup': {
78 |
79 | 'keyboard': [
80 | ['A', 'B', 'C', 'D'],
81 | ['See the answer'],
82 | ['Give me another question']
83 | ]
84 | }
85 |
86 | },
87 |
88 | {
89 |
90 | 'reply_markup': {
91 |
92 | 'keyboard': [
93 | ['А', 'Б', 'В', 'Г'],
94 | ['Виж отговора'],
95 | ['Задай ми друг въпорс']
96 | ]
97 | }
98 | }
99 |
100 | ],
101 |
102 | //creates inline keyboard
103 | //to give options for
104 | //general information links
105 | generalInfo: [
106 |
107 | {
108 | 'reply_markup': {
109 | 'inline_keyboard': [
110 |
111 | [
112 | {
113 | text: 'course info',
114 | url: externalLinks.courseInfo
115 | },
116 | {
117 | text: 'staff & contacts',
118 | url: externalLinks.teamInfo
119 | }
120 | ],
121 | [
122 | {
123 | text: 'books & links',
124 | url: externalLinks.booksInfo
125 | },
126 | {
127 | text: 'syllabus',
128 | url: externalLinks.themesInfo
129 | }
130 | ]
131 | ]
132 | }
133 | },
134 | {
135 | 'reply_markup': {
136 | 'inline_keyboard': [
137 |
138 | [
139 | {
140 | text: 'За курса',
141 | url: externalLinks.courseInfo
142 | },
143 | {
144 | text: 'екип & контакти',
145 | url: externalLinks.teamInfo
146 | }
147 | ],
148 | [
149 | {
150 | text: 'книги & връзки',
151 | url: externalLinks.booksInfo
152 | },
153 | {
154 | text: 'конспект',
155 | url: externalLinks.themesInfo
156 | }
157 | ]
158 | ]
159 | }
160 | }
161 | ],
162 |
163 | //messages for picking a choice
164 | choose : [
165 |
166 | 'Chose from below 🔗 :'
167 | ,
168 | 'Избери от опциите 🔗 :'
169 | ],
170 |
171 | //messages for unknown commands
172 | unknownCommand : [
173 |
174 | 'I don\'t understand you 😓\nI am just a chatbot a don\'t have all the answers🧐'
175 | ,
176 | 'Не те разбирам 😓\nАз съм просто чатбот нямам отговори за всичко 🧐'
177 | ],
178 |
179 | //messages for invalid test answer
180 | invalidTestAnswer : [
181 |
182 | 'Sorry, but I don\'t understand your answer 🤔\nThe next time just press a button 🤓'
183 | ,
184 | 'Съжалявам, но не мога да разбера отговора ти 🤔\nСледващия път просто натисни бутонче 🤓'
185 | ],
186 |
187 | //messages for successful language change
188 | languageChanged :[
189 |
190 | 'Now we are talking in english! 🇬🇧'
191 | ,
192 | 'Вече си говорим на български! 🇧🇬'
193 | ],
194 |
195 | //message for invalid faculty number
196 | invalidFn :[
197 |
198 | 'Invalid faculty number! ⚠️'
199 | ,
200 | 'Невалиден факултетен номер! ⚠️'
201 | ],
202 |
203 | unseenFn :[
204 |
205 | 'I can\'t see a student with such faculty number from this course! 👀'
206 | ,
207 | 'Не "виждам" студент с такъв факултетен номер в този курс! 👀'
208 | ],
209 |
210 | //message for internal error
211 | internalError :[
212 |
213 | 'Internal error, please excuse us! 🙇\nwrite us about the problem : fmichatbot@gmail.com'
214 | ,
215 | 'Вътрешна грешка, моля да ни извиниш! 🙇\nпиши ни за проблема : fmichatbot@gmail.com'
216 | ],
217 |
218 | //messages for access denied error
219 | accessDeniedEnrol :[
220 |
221 | 'Access denied : Not enrolled! ⛔'
222 | ,
223 | 'Достъпът отказан : Не си записан/а за този курс! ⛔'
224 | ],
225 |
226 | accessDeniedOtherFn :[
227 |
228 | 'Access denied : This is not your account! 🚫'
229 | ,
230 | 'Достъпът отказан : Това не е твоят профил! 🚫'
231 |
232 | ],
233 |
234 | accessDeniedMoodleConfig :[
235 |
236 | 'Access denied : Moodle profile is not configured! ⚠️'
237 | ,
238 | 'Достъпът отказан : Профилът ти в moodle не е конфигуриран! ⚠️'
239 | ],
240 |
241 | //message for get news
242 | news :[
243 |
244 | 'These are the tittles of forum news, click on a tittle to read it\'s contain 📃\n'
245 | ,
246 | 'Това са заглавията на новините от форума, кликни на някое заглавиe, за да получиш съдържанието на новината 📃\n'
247 | ],
248 |
249 | keyInfo : [
250 |
251 | 'This is your telegram key for moodle 🔑 : '
252 | ,
253 | 'Това е ключът ти за moodle 🔑 : '
254 | ],
255 |
256 | //links to instant views
257 | //urls with how to use information
258 | helpUrl : [
259 | //'[click here]' +
260 | externalLinks.helpEN
261 | ,
262 | //'[кликни тук]' +
263 | externalLinks.helpBG
264 | ]
265 | };
--------------------------------------------------------------------------------
/chat_bot/src/lib/logger.js:
--------------------------------------------------------------------------------
1 | const winston = require('winston'),
2 | config = require('./configuration'),
3 | fs = require( 'fs' ),
4 | path = require('path');
5 |
6 | const dirPath = config.get('logDirectoryPath');
7 |
8 | if ( !fs.existsSync( dirPath ) ) {
9 | // Create the directory if it does not exist
10 | fs.mkdirSync( dirPath );
11 | }
12 |
13 | //how to format the timestamp
14 | const tsFormat = () => {
15 |
16 | let date = new Date();
17 |
18 | return date.toLocaleDateString() + ' / '
19 | + date.toLocaleTimeString() + ' / ';
20 | }
21 |
22 | //how each text to be logged
23 | const formatFunc = (options) => {
24 |
25 | return tsFormat() + ' : ' +
26 | options.level.toUpperCase() + ' --- ' +
27 | (options.message ? options.message : '')
28 | + ' --- ';
29 | }
30 |
31 | const logger = new (winston.Logger)({
32 | transports: [
33 | //debugger log config
34 | new (winston.transports.Console)({
35 | level: 'debug',
36 | json: false,
37 | formatter: formatFunc
38 | }),
39 | //info logger config
40 | new (winston.transports.File)({
41 | name: 'info-file',
42 | filename: path.join(dirPath, '/infoLog.txt'),
43 | level: 'info',
44 | json: false,
45 | formatter : formatFunc
46 | }),
47 |
48 | //error logger config
49 | new (winston.transports.File)({
50 | name: 'error-file',
51 | filename: path.join(dirPath, '/errorLog.txt'),
52 | level: 'error',
53 | json: false,
54 | formatter: formatFunc
55 |
56 | })
57 | ]
58 | });
59 |
60 | //logger.add(winston.transports.Console);
61 | try{
62 | module.exports = logger;}catch(err)
63 | {console.log(11)};
--------------------------------------------------------------------------------
/chat_bot/src/lib/moodleAPI.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 |
3 | const config = require('./configuration');
4 |
5 | const moodleConfig = config.get('moodleConfig'),
6 | moodleToken = config.get('mToken');
7 |
8 | //a request template to get
9 | //a forum news from moodle
10 | const forumReq = axios.create({
11 |
12 | url : moodleConfig.serviceUrl ,
13 | baseURL: moodleConfig.baseUrl,
14 | method : 'get',
15 | timeout: 5000,
16 |
17 | params: {
18 | wstoken: moodleToken,
19 | wsfunction : moodleConfig.fun_forumDiscussions,
20 | forumid : moodleConfig.forumid,
21 | moodlewsrestformat : 'json'
22 | },
23 |
24 | });
25 |
26 | //a request template to get
27 | //a forum news from moodle
28 | const assignReq = axios.create({
29 |
30 | url : moodleConfig.serviceUrl ,
31 | baseURL: moodleConfig.baseUrl,
32 | method : 'get',
33 | timeout: 5000,
34 |
35 | params: {
36 | wstoken: moodleToken,
37 | wsfunction : moodleConfig.fun_assignments,
38 | courseids : [moodleConfig.courseid],
39 | moodlewsrestformat : 'json'
40 | },
41 |
42 | });
43 |
44 | //a request to get a user
45 | //from moodle by faculty number
46 | const userReq = axios.create({
47 |
48 | url : moodleConfig.serviceUrl ,
49 | baseURL: moodleConfig.baseUrl,
50 | method : 'get',
51 | timeout: 3000,
52 |
53 | params: {
54 | wstoken: moodleToken,
55 | wsfunction : moodleConfig.fun_user,
56 | field : moodleConfig.field,
57 | values : [0],
58 | moodlewsrestformat : 'json'
59 | },
60 |
61 | });
62 |
63 | //a request template to get
64 | //all courses that a student is enrolled to
65 | const coursesReq = axios.create({
66 |
67 | url : moodleConfig.serviceUrl ,
68 | baseURL: moodleConfig.baseUrl,
69 | method : 'get',
70 | timeout: 5000,
71 |
72 | params: {
73 | wstoken: moodleToken,
74 | wsfunction : moodleConfig.fun_courses,
75 | userid : 0,
76 | moodlewsrestformat : 'json'
77 | },
78 |
79 | });
80 |
81 | //a request template to get
82 | //all grades for a given student
83 | const gradesReq = axios.create({
84 |
85 | url : moodleConfig.serviceUrl ,
86 | baseURL: moodleConfig.baseUrl,
87 | method : 'get',
88 | timeout: 5000,
89 |
90 | params: {
91 | wstoken: moodleToken,
92 | wsfunction : moodleConfig.fun_grades,
93 | courseid : moodleConfig.courseid,
94 | userid : 0,
95 | moodlewsrestformat : 'json'
96 | },
97 |
98 | });
99 |
100 | module.exports = {
101 | forumReq, assignReq, userReq,
102 | gradesReq, coursesReq
103 | };
104 |
--------------------------------------------------------------------------------
/chat_bot/src/lib/msgHandlers.js:
--------------------------------------------------------------------------------
1 | const htmlToText = require('html-to-text');
2 |
3 | const {
4 | keyboardOptions,
5 | unknownCommand, languageChanged,
6 | helpUrl, generalInfo,
7 | choose, testKeyboardOptions,
8 | questionsList, invalidFn,
9 | internalError, news,
10 | accessDeniedEnrol, accessDeniedOtherFn,
11 | accessDeniedMoodleConfig, unseenFn,
12 | keyInfo, invalidTestAnswer,
13 | EN, BG } = require('./constants');
14 |
15 |
16 | const { forumReq, assignReq,
17 | userReq, gradesReq,
18 | coursesReq, updatesReq } = require('./moodleAPI');
19 |
20 | //each function will handle a message
21 | //received by the bot
22 | //and will return a promise
23 | module.exports = {
24 |
25 | welcome: function (bot, msg) {
26 |
27 | //first name is required
28 | let name = msg.chat.first_name;
29 |
30 | if (msg.chat.last_name !== undefined)
31 | name += ' ' + msg.chat.last_name;
32 |
33 | const answerEN = `Welcome,${name}!\nI am the FMI\'s chatbot 🤖. \
34 | \n\n${keyInfo[EN]}${msg.from.id} \
35 | \n\nuse /lang_en to change the language to english 🇬🇧 \
36 | \nthen type /help to see how to configure your moodle profile \
37 | \nand how to communicate with me 😎`;
38 |
39 |
40 | const answerBG = `Здравей,${name}!\nАз съм чатботът на ФМИ 🤖. \
41 | \n\n${keyInfo[BG]}${msg.from.id} \
42 | \nИзползвай /help за да видиш\nкак да конфигурираш своя moodle профил \
43 | \nи как най - лесно да комуникираш с мен 😎`;
44 |
45 |
46 | //because sendMessage changes its param
47 | //deep copy objects
48 | //const optEN = JSON.parse(JSON.stringify(keyboardOptions[EN]));
49 | const optBG = JSON.parse(JSON.stringify(keyboardOptions[BG]));
50 |
51 | return bot.sendMessage(msg.chat.id, answerEN)//, optEN)
52 | .then(() => bot.sendMessage(msg.chat.id, answerBG, optBG))
53 | },
54 |
55 | langChanged: function (bot, msg, ln) {
56 |
57 | const opt = JSON.parse(JSON.stringify(keyboardOptions[ln]));
58 |
59 | return bot.sendMessage(msg.chat.id, languageChanged[ln], opt);
60 | },
61 |
62 | help: function (bot, msg, ln) {
63 |
64 | return bot.sendMessage(msg.chat.id, helpUrl[ln], { parse_mode: "Markdown" });
65 | },
66 |
67 | unknown: function (bot, msg, ln) {
68 |
69 | return bot.sendMessage(msg.chat.id, unknownCommand[ln], keyboardOptions[ln]);
70 | },
71 |
72 | getGeneralInfo: function (bot, msg, ln) {
73 |
74 | return bot.sendMessage(msg.chat.id, choose[ln], generalInfo[ln]);
75 | },
76 |
77 | testCallback: function (bot, msg, ln, callBacks) {
78 |
79 | const ansOpt = ['ABCD', 'АБВГ'];
80 | let userAnswer = ansOpt[ln].indexOf(msg.text);
81 | let [questionId, correctAnswer] = callBacks[msg.chat.id];
82 |
83 | //remove from callback list
84 | delete callBacks[msg.chat.id];
85 |
86 | //see answer option
87 | if (msg.text === 'See the answer' ||
88 | msg.text === 'Виж отговора') {
89 |
90 | let answerMsg = ln ? 'Верният отговор е: \n'
91 | : 'The correct answer is :\n ';
92 |
93 | answerMsg += questionsList[questionId][ln].answerOptions[correctAnswer];
94 |
95 | const opt = JSON.parse(JSON.stringify(keyboardOptions[ln]));
96 | return bot.sendMessage(msg.chat.id, answerMsg, opt);
97 | }
98 |
99 | //another question option
100 | if (msg.text === 'Give me another question' ||
101 | msg.text === 'Задай ми друг въпорс') {
102 |
103 | return this.testMe(bot, msg, ln, callBacks);
104 | }
105 |
106 | //invalid answer
107 | if (userAnswer === -1)
108 | return bot.sendMessage(msg.chat.id, invalidTestAnswer[ln], keyboardOptions[ln]);
109 |
110 | //wrong answer
111 | if (userAnswer !== -1 && userAnswer !== correctAnswer) {
112 |
113 | let answerMsg = ln ? 'Грешен отговор 😞\nВерният отговор е :\n'
114 | :'Wrong answer 😞\nThe correct answer is :\n ';
115 |
116 | answerMsg += questionsList[questionId][ln].answerOptions[correctAnswer];
117 |
118 | const opt = JSON.parse(JSON.stringify(keyboardOptions[ln]));
119 | return bot.sendMessage(msg.chat.id, answerMsg, opt);
120 | }
121 |
122 | //correct answer
123 | let answerMsg = ln ? 'Верен отговор 👍' : 'Correct answer 👍\n';
124 | const opt = JSON.parse(JSON.stringify(keyboardOptions[ln]));
125 | return bot.sendMessage(msg.chat.id, answerMsg, opt);
126 | },
127 |
128 | testMe: function (bot, msg, ln, callBacks) {
129 |
130 | let qIndex = getRandomInt(questionsList.length);
131 |
132 | let question = questionsList[qIndex][ln];
133 |
134 | let answer = formatQuestion(question, ln);
135 |
136 | return bot.sendMessage(msg.chat.id, answer, testKeyboardOptions[ln])
137 | .then(() => callBacks[msg.chat.id] = [qIndex, question.correctAnswer]);
138 | },
139 |
140 | getNews: function (bot, msg, ln) {
141 |
142 | if (discussions.length === 0)
143 | return bot.sendMessage(msg.chat.id, internalError[ln]);
144 |
145 | let res = getTittles(discussions);
146 | return bot.sendMessage(msg.chat.id, news[ln], res);
147 | },
148 |
149 | getNewsContain : function (bot, msg, action) {
150 |
151 | let disc = discussions.find(el => el.id === action);
152 |
153 | if(disc === undefined)
154 | return bot.sendMessage(msg.chat.id, 'Currently unavailable');
155 |
156 | //only basic HTML is supported ...
157 | //all moodle discussions are formatted as HTML,
158 | //so we need to convert them to plain text
159 | let answer = htmlToText.fromString(disc.message);
160 |
161 | return bot.sendMessage(msg.chat.id, answer)//, { parse_mode: "HTML" })
162 | //.catch(() => bot.sendMessage(msg.chat.id, disc.message)); // send raw .. :(
163 | },
164 |
165 | getAssignments : function (bot, msg, ln) {
166 |
167 | if (assignments.length === 0)
168 | return bot.sendMessage(msg.chat.id, internalError[ln]);
169 |
170 | let res = getAssignmentsInfo(assignments, ln);
171 | return bot.sendMessage(msg.chat.id, res);
172 | },
173 |
174 | personalInfo: function (bot, msg, ln) {
175 |
176 | let facultyId = msg.text;
177 | let courseid = gradesReq.defaults.params['courseid'];
178 | let userid;
179 |
180 | userReq.defaults.params['values'] = [facultyId];
181 |
182 | return userReq.request()
183 | .then(response => {
184 |
185 | try {
186 | coursesReq.defaults.params['userid'] =
187 | checkUserTelegram(response.data[0], msg.from.id.toString(), ln);
188 | }
189 | catch (err) { //authentication error
190 | bot.sendMessage(msg.chat.id, err, keyboardOptions[ln]);
191 | throw err;
192 | }
193 |
194 | return coursesReq.request();
195 |
196 | })
197 | .then(response => {
198 |
199 | let courses = response.data;
200 |
201 | let ind = courses.find(el => el.id === courseid);
202 |
203 | if (ind === undefined) {
204 |
205 | bot.sendMessage(msg.chat.id, accessDeniedEnrol[ln], keyboardOptions[ln]);
206 | throw accessDeniedEnrol[ln];
207 | }
208 |
209 | gradesReq.defaults.params['userid'] =
210 | coursesReq.defaults.params['userid'];
211 |
212 | return gradesReq.request();
213 |
214 | })
215 | .then(response => formatGradesAnswer(response.data, ln))
216 | .then(res => bot.sendMessage(msg.chat.id, res, keyboardOptions[ln]))
217 | .catch(err => {
218 |
219 | if (err.code !== undefined) {
220 | bot.sendMessage(msg.chat.id, internalError[ln], keyboardOptions[ln]);
221 | throw err.code;
222 | }
223 |
224 | throw err;
225 | });
226 | },
227 |
228 | invalidFacultyNumber: function (bot, msg, ln) {
229 |
230 | return bot.sendMessage(msg.chat.id, invalidFn[ln], keyboardOptions[ln]);
231 | },
232 |
233 | getMoodleKey : function (bot, msg, ln) {
234 |
235 | return bot.sendMessage(msg.chat.id, keyInfo[ln] + msg.from.id, keyboardOptions[ln]);
236 | },
237 |
238 | update: function () {
239 |
240 | return fetchDiscussions()
241 | .then(() => { return fetchAssignments() })
242 | }
243 | };
244 |
245 | //will hold all forum's discussions
246 | let discussions = [];
247 | //will hold all assignments
248 | let assignments = [];
249 |
250 | //makes a request to moodule in order to get
251 | //all course assignments
252 | const fetchAssignments = () => {
253 |
254 | return assignReq.request()
255 | .then(response => assignments = response.data.courses[0].assignments);
256 |
257 | }
258 |
259 | //makes a request to moodule in order to get
260 | //all news from the forum
261 | const fetchDiscussions = () => {
262 |
263 | return forumReq.request()
264 | .then(response => discussions = response.data.discussions);
265 |
266 | }
267 |
268 | //a helper function to get tittles of news in forum
269 | //and create an inline keyboard from them
270 | const getTittles = (discussions) => {
271 |
272 | const opts = {
273 | reply_markup: {
274 | inline_keyboard: [
275 | ]
276 | }
277 | };
278 |
279 |
280 | discussions.forEach(el => {
281 | opts.reply_markup.inline_keyboard.push([{
282 | text : el.name,
283 | callback_data : el.id
284 | }]);
285 | });
286 |
287 | return opts;
288 | }
289 |
290 | //a helper function to check fn - telegram id
291 | //personal data of a user
292 | const checkUserTelegram = (user, fromId, ln) => {
293 |
294 | if(user === undefined)
295 | throw unseenFn[ln];
296 |
297 | if(user.customfields === undefined)
298 | throw accessDeniedMoodleConfig[ln];
299 |
300 | let telegramId = user.customfields.find(el => el.shortname === "telegramid").value;
301 |
302 | if(telegramId === undefined)
303 | throw accessDeniedMoodleConfig[ln];
304 |
305 | if(telegramId !== fromId)
306 | throw accessDeniedOtherFn[ln];
307 |
308 | return user.id;
309 | }
310 |
311 | //a helper that returns all
312 | //upcoming assignments formatted
313 | const getAssignmentsInfo = (assignments, ln) => {
314 |
315 | //UTC current time
316 | let currTime = Math.floor((new Date()).getTime() / 1000);
317 |
318 | let upcomingAssignments = assignments.filter(assign => assign.duedate > currTime);
319 |
320 | if(upcomingAssignments.length === 0){
321 |
322 | return ln ? "Нямаш предстоящи задания, можеш да си починеш 🙃"
323 | : "There aren\'t any assignments, you can have a rest 🙃"
324 | }
325 |
326 | let res = ln ? 'Предстоящите ти задания са 🗓️ : \n\n'
327 | : 'Your upcoming assignments are 🗓️ : \n\n';
328 |
329 | upcomingAssignments.forEach(assign => res += formatAssignment(assign, ln));
330 |
331 | return res;
332 | }
333 |
334 | //a helper to format a single assignment
335 | const formatAssignment = (assignment, ln) => {
336 |
337 | let helpWords = [['\n\nfrom : ', '\nto : ', '\nwhere : '],
338 | ['\n\nот : ', '\nдо : ', '\nкъде : ']];
339 |
340 | //UNIX timestamp is in seconds ... JS's is in milliseconds
341 | let timeDiff = 180 * 60, //BG is +2GMT
342 | from = (assignment.allowsubmissionsfromdate + timeDiff) * 1000,
343 | to = (assignment.duedate + timeDiff) * 1000;
344 |
345 | let fromTimeFormated = (new Date(from)).toUTCString().slice(0, -4), //removes " GMT"
346 | dueTimeFormated = (new Date(to)).toUTCString().slice(0, -4);
347 |
348 | let where = 'moodle';
349 |
350 | let start = assignment.intro.indexOf('Ще проведем');
351 |
352 | if(start >= 0){
353 |
354 | start = assignment.intro.indexOf('в ');
355 | let end = assignment.intro.indexOf('.');
356 |
357 | where = assignment.intro.slice(start, end);
358 | }
359 |
360 | return assignment.name
361 | + helpWords[ln][0] + fromTimeFormated
362 | + helpWords[ln][1] + dueTimeFormated
363 | + helpWords[ln][2] + where
364 | + '\n\n';
365 | }
366 |
367 | //a helper function used for formating the answer with
368 | //a user's grades
369 | const formatGradesAnswer = (user, ln) => {
370 |
371 | let res = ln ? "Оценките, които имаме за теб са 🏫 :\n"
372 | : "Your grades are 🏫 :\n" ;
373 |
374 | let arrGrades = user.usergrades[0].gradeitems;
375 |
376 | arrGrades.forEach(el => {
377 | //skip overall grade
378 | if (el.itemname !== null) {
379 |
380 | res += el.itemname + '\n'
381 | + el.gradeformatted + ' / '
382 | + el.grademax + '\n\n';
383 | }
384 |
385 | })
386 |
387 | return res;
388 | }
389 |
390 | //a helper function to represent a question
391 | //as a test
392 | const formatQuestion = (question, ln) => {
393 |
394 | let format = [
395 |
396 | ['\nA) ', '\nB) ', '\nC) ', '\nD) '],
397 | ['\nА) ', '\nБ) ', '\nВ) ', '\nГ) ']
398 | ];
399 |
400 | return question.text +
401 | format[ln][0] + question.answerOptions[0] +
402 | format[ln][1] + question.answerOptions[1] +
403 | format[ln][2] + question.answerOptions[2] +
404 | format[ln][3] + question.answerOptions[3] + '\n';
405 |
406 | }
407 |
408 | //a helper function to get a random index for
409 | //a question from the test
410 | const getRandomInt = (max) => {
411 |
412 | return Math.floor(Math.random() * Math.floor(max));
413 | }
414 |
415 | //used to replace substring in a given string
416 | const replaceAll = (str, find, replace) => {
417 |
418 | return str.replace(new RegExp(find, 'g'), replace);
419 | }
420 |
421 | //a helper designed to removed unsupported HTML tags
422 | //currently unused
423 | const clearTags = (str) => {
424 |
425 | let answer = replaceAll(str, '