├── .gitignore ├── DialogClasses.py ├── DialogStatesDefinition.py ├── LICENSE ├── README.md ├── Sqlighter.py ├── config.py ├── quizzes ├── QuizClasses.py └── googleFormParser.py ├── requirements.txt ├── run.py ├── universal_reply.py └── utilities.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /DialogClasses.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Callable 2 | import universal_reply 3 | from telebot import types 4 | from collections import defaultdict 5 | from Sqlighter import SQLighter 6 | import logging 7 | from collections import OrderedDict 8 | import dill 9 | import config 10 | 11 | 12 | class State: 13 | def __init__(self, name: str, 14 | triggers_out: OrderedDict = None, 15 | hidden_states: Dict = None, 16 | welcome_msg: str = None, 17 | row_width=1, 18 | load=config.load_states, 19 | handler_welcome: Callable = None, 20 | *args): 21 | """ 22 | :param name: name of state object; 23 | :param triggers_out: dict like {state_out1_name:{'phrases':['some_string1', 'str2', etc], 24 | 'content_type':'text'}}; 25 | :param hidden_states: list of state names that have to be reachable from this state 26 | but they don't have to be shown on the keyboard; 27 | :param welcome_msg: what does the State have to say to usr after welcome_handler() 28 | :param handler_welcome: function that handle income message 29 | 30 | """ 31 | self.name = name 32 | self.hidden_states = hidden_states 33 | self.welcome_msg = welcome_msg 34 | self.triggers_out = triggers_out 35 | self.handler_welcome = handler_welcome 36 | self.row_width = row_width 37 | self.load = load 38 | self.reply_markup = self.make_reply_markup() 39 | self.additional_init(*args) 40 | if load: 41 | self.load_current_states() 42 | print('STATE {} obj has been initialized\n'.format(self.name)) 43 | 44 | def dump_current_states(self): 45 | pass 46 | 47 | def load_current_states(self): 48 | pass 49 | 50 | def additional_init(self, *args): 51 | pass 52 | 53 | def make_reply_markup(self): 54 | 55 | markup = types.ReplyKeyboardMarkup(row_width=self.row_width, resize_keyboard=True) 56 | is_markup_filled = False 57 | tmp_buttons = [] 58 | for state_name, attrib in self.triggers_out.items(): 59 | hidden_flag = ((self.hidden_states is not None) 60 | and (state_name != self.hidden_states['state_name'])) \ 61 | or (self.hidden_states is None) 62 | if len(attrib['phrases']) > 0 and hidden_flag: 63 | for txt in attrib['phrases']: 64 | tmp_buttons.append(txt) 65 | is_markup_filled = True 66 | 67 | markup.add(*(types.KeyboardButton(button) for button in tmp_buttons)) 68 | if not is_markup_filled: 69 | markup = types.ReplyKeyboardRemove() 70 | return markup 71 | 72 | def welcome_handler(self, bot, message, sqldb: SQLighter): 73 | if self.handler_welcome is not None: 74 | res = self.handler_welcome(bot, message, sqldb) 75 | else: 76 | res = None 77 | if self.welcome_msg: 78 | bot.send_message(message.chat.id, self.welcome_msg, 79 | reply_markup=self.reply_markup, parse_mode='Markdown') 80 | return res 81 | 82 | def default_out_handler(self, bot, message): 83 | if message.text == '/start': 84 | return 'MAIN_MENU' 85 | 86 | bot.send_message(message.chat.id, universal_reply.DEFAULT_ANS) 87 | return None 88 | 89 | def out_handler(self, bot, message, sqldb: SQLighter): 90 | """ 91 | Default handler manage text messages and couldn't handle any photo/documents; 92 | It just apply special handler if it is not None; 93 | If message couldn't be handled None is returned; 94 | :param message: 95 | :param bot: 96 | :param sqldb: 97 | :return: name of the new state; 98 | 99 | """ 100 | any_text_state = None 101 | for state_name, attribs in self.triggers_out.items(): 102 | if message.content_type != 'text': 103 | if message.content_type == attribs['content_type']: 104 | return state_name 105 | 106 | elif message.text in attribs['phrases']: 107 | if self.hidden_states is None: 108 | return state_name 109 | elif state_name == self.hidden_states['state_name']: 110 | if message.chat.username in self.hidden_states['users_file']: 111 | return state_name 112 | else: 113 | return state_name 114 | 115 | # the case when any text message should route to state_name 116 | elif (len(attribs['phrases']) == 0) and (attribs['content_type'] == 'text'): 117 | any_text_state = state_name 118 | 119 | if any_text_state is not None: 120 | return any_text_state 121 | 122 | return self.default_out_handler(bot, message) 123 | 124 | 125 | class DialogGraph: 126 | def __init__(self, bot, root_state: str, nodes: List[State], sqldb: SQLighter, logger: logging, 127 | dump_path: str = config.dump_graph_path, load_from_dump: bool = config.load_graph): 128 | """ 129 | Instance of this class manages all the dialog flow; 130 | :param bot: telebot.TeleBot(token); 131 | :param nodes: list of instances of State class; 132 | :param root_state: name of the root of dialog states. 133 | when new user appeared he/she has this state; 134 | root state doesn't have "welcome" method; 135 | """ 136 | self.bot = bot 137 | self.root_state = root_state 138 | self.nodes = self.make_nodes_dict(nodes) 139 | self.usr_states = defaultdict(dict) 140 | self.sqldb = sqldb 141 | self.logger = logger 142 | self.dump_path = dump_path 143 | self.load = load_from_dump 144 | 145 | def make_nodes_dict(self, nodes): 146 | return {state.name: state for state in nodes} 147 | 148 | def dump_current_states(self): 149 | with open(self.dump_path, 'wb') as fout: 150 | dill.dump({'states': self.usr_states}, fout) 151 | 152 | def load_current_states(self): 153 | try: 154 | with open(self.dump_path, 'rb') as fin: 155 | unpickled = dill.load(fin) 156 | self.usr_states = unpickled['usr_states'] 157 | except FileNotFoundError: 158 | pass 159 | 160 | def run(self, message): 161 | 162 | if message.chat.username is None: 163 | self.bot.send_message(message.chat.id, universal_reply.NO_USERNAME_WARNING) 164 | self.logger.debug("NONAME USR JOINED TELEBOT!!!") 165 | return 166 | 167 | if message.text is not None: 168 | self.logger.debug("USR: " + message.chat.username + " SAID: " + message.text) 169 | else: 170 | self.logger.debug("USR: " + message.chat.username + " SEND: " + message.content_type) 171 | if message.chat.id not in self.usr_states: 172 | self.logger.debug("NEW USR: " + message.chat.username) 173 | self.usr_states[message.chat.id]['current_state'] = self.root_state 174 | 175 | curr_state_name = self.usr_states[message.chat.id]['current_state'] 176 | new_state_name = self.nodes[curr_state_name].out_handler(self.bot, message, self.sqldb) 177 | 178 | if new_state_name is not None: 179 | self.logger.debug("USR: " + message.chat.username + " NEW STATE: " + new_state_name) 180 | self.usr_states[message.chat.id]['current_state'] = new_state_name 181 | signal = self.nodes[new_state_name].welcome_handler(self.bot, message, self.sqldb) 182 | if signal == 'BACKUP_NOW': 183 | self.dump_current_states() 184 | for name_node, node in self.nodes.items(): 185 | try: 186 | node.dump_current_states() 187 | print(name_node + ' has been dumped') 188 | except Exception as e: 189 | print( 190 | "-- ERROR DUMP: {} with exception {}:\n {}".format(name_node, e.__class__.__name__, str(e))) 191 | continue 192 | 193 | self.bot.send_message(text="All_dumped", chat_id=message.chat.id) 194 | -------------------------------------------------------------------------------- /DialogStatesDefinition.py: -------------------------------------------------------------------------------- 1 | from DialogClasses import * 2 | from Sqlighter import SQLighter 3 | import universal_reply 4 | import config 5 | import random 6 | import pandas as pd 7 | from quizzes.QuizClasses import Quiz 8 | from tabulate import tabulate 9 | from collections import OrderedDict 10 | from telebot import util 11 | import dill 12 | 13 | wait_usr_interaction = State(name='WAIT_USR_INTERACTION', 14 | triggers_out=OrderedDict(MAIN_MENU={'phrases': ['/start'], 'content_type': 'text'})) 15 | # ---------------------------------------------------------------------------- 16 | 17 | main_menu = State(name='MAIN_MENU', 18 | row_width=2, 19 | triggers_out=OrderedDict( 20 | TAKE_QUIZ={'phrases': [universal_reply.quiz_enter], 'content_type': 'text'}, 21 | PASS_HW_NUM_SELECT={'phrases': [universal_reply.hw_enter], 'content_type': 'text'}, 22 | CHECK_QUIZ={'phrases': [universal_reply.quiz_check], 'content_type': 'text'}, 23 | CHECK_HW_NUM_SELECT={'phrases': [universal_reply.hw_check], 'content_type': 'text'}, 24 | QUIZ_MARK_NUM_SELECT={'phrases': [universal_reply.quiz_estimates], 'content_type': 'text'}, 25 | GET_MARK={'phrases': [universal_reply.hw_estimates], 'content_type': 'text'}, 26 | ASK_QUESTION_START={'phrases': [universal_reply.ask_question], 'content_type': 'text'}, 27 | ADMIN_MENU={'phrases': [universal_reply.ADMIN_KEY_PHRASE], 'content_type': 'text'}), 28 | hidden_states={'state_name': 'ADMIN_MENU', 'users_file': config.admins}, 29 | welcome_msg='Выберите доступное действие, пожалуйста') 30 | 31 | 32 | # ---------------------------------------------------------------------------- 33 | 34 | 35 | class QuizState(State): 36 | def additional_init(self): 37 | self.quiz = Quiz(config.current_quiz_name, quiz_json_path=config.quiz_path, 38 | next_global_state_name='MAIN_MENU') 39 | # TODO: do smth to provide arguments in the right way 40 | self.dump_path = config.dump_quiz_path 41 | 42 | def dump_current_states(self): 43 | with open(self.dump_path, 'wb') as fout: 44 | dill.dump({'usersteps': self.quiz.usersteps, 45 | 'submitted': self.quiz.usr_submitted, 46 | 'paused': self.quiz.paused, 47 | 'usr_buttons': {q.name: q.usr_buttons for q in self.quiz.questions}, 48 | 'usr_answers': {q.name: q.usr_answers for q in self.quiz.questions} 49 | }, fout) 50 | print('---- QUIZ dumped') 51 | 52 | def load_current_states(self): 53 | try: 54 | with open(self.dump_path, 'rb') as fin: 55 | unpickled = dill.load(fin) 56 | self.quiz.usersteps = unpickled['usersteps'] 57 | self.quiz.usr_submitted = unpickled['submitted'] 58 | self.quiz.paused = unpickled['paused'] 59 | for q in self.quiz.questions: 60 | q.usr_answers = unpickled['usr_answers'][q.name] 61 | q.usr_buttons = unpickled['usr_buttons'][q.name] 62 | except FileNotFoundError: 63 | print('Quiz Load: FileNotFoundError') 64 | pass 65 | 66 | def make_reply_markup(self): 67 | pass 68 | 69 | def welcome_handler(self, bot, message, sqldb: SQLighter): 70 | result = self.quiz.run(bot, message, sqldb) 71 | if result == self.quiz.next_global_state_name: 72 | if not hasattr(self, 'back_keyboard'): 73 | self.back_keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True) 74 | self.back_keyboard.add(types.KeyboardButton('Назад')) 75 | if config.quiz_closed: 76 | bot.send_message(text='Quiz closed', chat_id=message.chat.id, reply_markup=self.back_keyboard) 77 | else: 78 | bot.send_message(chat_id=message.chat.id, 79 | text="Sorry, you have already submitted {} ~_~\"".format(self.quiz.name), 80 | reply_markup=self.back_keyboard) 81 | 82 | def out_handler(self, bot, message, sqldb: SQLighter): 83 | if message.content_type != 'text': 84 | return None 85 | if message.text == 'Назад': 86 | return self.quiz.next_global_state_name 87 | new_state = self.quiz.run(bot, message, sqldb) 88 | return new_state 89 | 90 | 91 | take_quiz = QuizState(name='TAKE_QUIZ') 92 | 93 | # ---------------------------------------------------------------------------- 94 | 95 | check_quiz = State(name='CHECK_QUIZ', 96 | triggers_out=OrderedDict( 97 | SEND_QQUESTION_TO_CHECK={'phrases': config.quizzes_possible_to_check, 'content_type': 'text'}, 98 | MAIN_MENU={'phrases': ['Назад'], 'content_type': 'text'}), 99 | welcome_msg='Пожалуйста, выберите номер квиза для проверки:') 100 | 101 | 102 | # ---------------------------------------------------------------------------- 103 | 104 | def send_qquestion(bot, message, sqldb): 105 | if message.text not in config.quizzes_possible_to_check: 106 | quiz_name = sqldb.get_latest_quiz_name(message.chat.username) 107 | else: 108 | quiz_name = message.text 109 | if quiz_name is None: 110 | bot.send_message("SMTH WENT WRONG..") 111 | return 112 | 113 | num_checked = sqldb.get_number_checked_for_one_quiz(user_id=message.chat.username, 114 | quiz_name=quiz_name) 115 | arr = sqldb.get_quiz_question_to_check(quiz_name=quiz_name, 116 | user_id=message.chat.username) 117 | if len(arr) > 0: 118 | q_id, q_name, q_text, q_user_ans, _ = arr 119 | sqldb.make_fake_db_record_quiz(q_id, message.chat.username) 120 | text = 'You have checked: {}/{}\n'.format(num_checked, config.quizzes_need_to_check) \ 121 | + q_text + '\n' + 'USER_ANSWER:\n' + q_user_ans 122 | bot.send_message(chat_id=message.chat.id, 123 | text=text, ) 124 | else: 125 | # TODO: do smth with empty db; 126 | bot.send_message(text='К сожалению проверить пока нечего. Нажмите, пожалуйста, кнопку "Назад".', 127 | chat_id=message.chat.id) 128 | 129 | 130 | send_quiz_question_to_check = State(name='SEND_QQUESTION_TO_CHECK', 131 | row_width=2, 132 | triggers_out=OrderedDict(SAVE_MARK={'phrases': ['Верю', 'Не верю']}, 133 | MAIN_MENU={'phrases': ['Назад'], 134 | 'content_type': 'text'}), 135 | handler_welcome=send_qquestion, 136 | welcome_msg='🌻 Правильно или нет ответил пользователь?\n' 137 | 'Нажмите кнопку, чтобы оценить ответ.') 138 | 139 | 140 | # ---------------------------------------------------------------------------- 141 | 142 | def mark_saving_quiz(bot, message, sqldb): 143 | is_right = int(message.text == 'Верю') 144 | sqldb.save_mark_quiz(message.chat.username, is_right) 145 | bot.send_message(text='Оценка сохранена. Спасибо.', chat_id=message.chat.id) 146 | 147 | 148 | save_mark_quiz = State(name='SAVE_MARK', 149 | row_width=2, 150 | triggers_out=OrderedDict(SEND_QQUESTION_TO_CHECK={'phrases': ['Продолжить проверку']}, 151 | CHECK_QUIZ={'phrases': ['Назад']}), 152 | handler_welcome=mark_saving_quiz, 153 | welcome_msg='🌻 Желаете ли еще проверить ответы из того же квиза?') 154 | 155 | # ---------------------------------------------------------------------------- 156 | 157 | ask_question_start = State(name='ASK_QUESTION_START', 158 | triggers_out=OrderedDict(MAIN_MENU={'phrases': ['Назад'], 'content_type': 'text'}, 159 | SAVE_QUESTION={'phrases': [], 'content_type': 'text'}), 160 | welcome_msg='Сформулируйте вопрос к семинаристу и отправьте его одним сообщением 🐠.') 161 | 162 | 163 | # ---------------------------------------------------------------------------- 164 | 165 | def save_question_handler(bot, message, sqldb): 166 | sqldb.write_question(message.chat.username, message.text) 167 | 168 | 169 | save_question = State(name='SAVE_QUESTION', 170 | triggers_out=OrderedDict(MAIN_MENU={'phrases': ['Назад'], 'content_type': 'text'}, 171 | SAVE_QUESTION={'phrases': [], 'content_type': 'text'}), 172 | handler_welcome=save_question_handler, 173 | welcome_msg='Спасибо за вопрос. Хорошего дня 🐯 :)\n' 174 | 'Если желаете задать еще вопрос, напишите его сразу следующим сообщением.' 175 | 'Если у вас нет такого желания, воспользуйтесь кнопкой "Назад".') 176 | 177 | # ---------------------------------------------------------------------------- 178 | 179 | welcome_to_pass_msg = 'Пожалуйста, выберите номер задания для сдачи.' 180 | welcome_to_return_msg = 'Доступные для сдачи задания отсутствуют.' 181 | pass_hw_num_selection = State(name='PASS_HW_NUM_SELECT', 182 | row_width=2, 183 | triggers_out=OrderedDict(PASS_HW_CHOSEN_NUM={'phrases': config.hw_possible_to_pass, 184 | 'content_type': 'text'}, 185 | MAIN_MENU={'phrases': ['Назад'], 'content_type': 'text'}), 186 | welcome_msg=welcome_to_pass_msg if len(config.hw_possible_to_pass) > 0 187 | else welcome_to_return_msg) 188 | 189 | 190 | # ---------------------------------------------------------------------------- 191 | 192 | def make_fake_db_record(bot, message, sqldb): 193 | sqldb.make_fake_db_record(message.chat.username, message.text) 194 | 195 | 196 | pass_hw_chosen_num = State(name='PASS_HW_CHOSEN_NUM', 197 | triggers_out=OrderedDict(PASS_HW_UPLOAD={'phrases': [], 'content_type': 'document'}, 198 | PASS_HW_NUM_SELECT={'phrases': ['Назад'], 'content_type': 'text'}), 199 | handler_welcome=make_fake_db_record, 200 | welcome_msg='Пришлите файл **(один архив или один Jupyter notebook)** весом не более 20 Мб.') 201 | 202 | 203 | # ---------------------------------------------------------------------------- 204 | 205 | class HwUploadState(State): 206 | def welcome_handler(self, bot, message, sqldb: SQLighter): 207 | username = message.chat.username 208 | if not message.document.file_name.endswith(config.available_hw_resolutions): 209 | tmp_markup = types.ReplyKeyboardMarkup(resize_keyboard=True) 210 | tmp_markup.add(types.KeyboardButton('Меню')) 211 | bot.send_message(message.chat.id, "🚫 {}, очень жаль но файлик не сдается в нашу систему...\n" 212 | "Возможны следующие расширения файлов: {}.\n" 213 | "Напоминаю, что дз сдается в виде одного файла архива" 214 | " или одного Jupyter ноутбука." 215 | .format(username.title(), str(config.available_hw_resolutions)), reply_markup=tmp_markup) 216 | else: 217 | sqldb.upd_homework(user_id=username, file_id=message.document.file_id) 218 | bot.send_message(message.chat.id, 219 | 'Уважаемый *{}*, ваш файлик был заботливо сохранен 🐾\n' 220 | .format(username.title()), 221 | reply_markup=self.reply_markup, parse_mode='Markdown') 222 | 223 | def out_handler(self, bot, message, sqldb: SQLighter): 224 | for state_name, attribs in self.triggers_out.items(): 225 | if message.content_type == 'document': 226 | return self.welcome_handler(bot, message, sqldb) 227 | 228 | elif (message.content_type == 'text') and (message.text in attribs['phrases']): 229 | return state_name 230 | return self.default_out_handler(bot, message) 231 | 232 | 233 | pass_hw_upload = HwUploadState(name='PASS_HW_UPLOAD', 234 | triggers_out=OrderedDict( 235 | PASS_HW_NUM_SELECT={'phrases': ['Сдать еще одно дз'], 'content_type': 'text'}, 236 | MAIN_MENU={'phrases': ['Меню'], 'content_type': 'text'})) 237 | 238 | 239 | # ---------------------------------------------------------------------------- 240 | 241 | 242 | def show_marks_table(bot, message, sqldb): 243 | num_checked = sqldb.get_num_checked(message.chat.username) 244 | if len(num_checked) == 0: 245 | bot.send_message(message.chat.id, 'Уважаемый *{}*, ' 246 | 'вам нужно проверить как минимум 3 работы' 247 | ' из каждого сданного вами задания, ' 248 | 'чтобы узнать свою оценку по данному заданию. ' 249 | 'На текущий момент вы не проверили ни одно задание :(.'.format( 250 | message.chat.username.title()), 251 | parse_mode='Markdown') 252 | else: 253 | may_be_shown = [] 254 | for num, count in num_checked: 255 | if count < 3: 256 | bot.send_message(message.chat.id, '👻 Для того чтобы узнать оценку по заданию {}' 257 | ' вам нужно проверить еще вот столько [{}]' 258 | ' заданий этого семинара.'.format(num, 3 - count)) 259 | else: 260 | may_be_shown.append(num) 261 | 262 | if len(may_be_shown) == 0: 263 | return 264 | 265 | marks = sqldb.get_marks(message.chat.username) 266 | if len(marks) < 1: 267 | bot.send_message(message.chat.id, 'Уважаемый *{}*, ' 268 | 'ваши работы еще не были проверены ни одним разумным существом.\n' 269 | 'Остается надеяться и верить в лучшее 🐸'.format( 270 | message.chat.username.title()), 271 | parse_mode='Markdown') 272 | else: 273 | count_what_show = 0 274 | ans = '_Ваши оценки следующие_\n' 275 | for hw_num, date, mark in marks: 276 | if hw_num in may_be_shown: 277 | count_what_show += 1 278 | ans += '🐛 Для работы *' + hw_num + '*, загруженной *' + date + '* оценка: *' + str( 279 | round(mark, 2)) + '*\n' 280 | if count_what_show > 0: 281 | bot.send_message(message.chat.id, ans, parse_mode='Markdown') 282 | bot.send_message(message.chat.id, 'Если какой-то работы нет в списке, значит ее еще не проверяли.') 283 | else: 284 | bot.send_message(message.chat.id, 'Уважаемый *{}*, ' 285 | 'ваши работы по проверенным вами заданиям еще не были проверены' 286 | ' ни одним разумным существом.\n' 287 | 'Остается надеяться и верить в лучшее 🐸 ' 288 | '(_или написать оргам и заставить их проверить_)'.format( 289 | message.chat.username.title()), 290 | parse_mode='Markdown') 291 | 292 | 293 | get_mark = State(name='GET_MARK', 294 | triggers_out=OrderedDict(MAIN_MENU={'phrases': ['Назад'], 'content_type': 'text'}), 295 | handler_welcome=show_marks_table, 296 | welcome_msg='Такие дела)') 297 | 298 | # ---------------------------------------------------------------------------- 299 | 300 | welcome_to_quiz_selection = 'Выберите интересующий вас квиз, чтобы узнать оценку.' 301 | return_from_quiz_selection = 'Нет проверенных квизов. Возвращайтесь позже.' 302 | 303 | quiz_mark_num_select = State(name='QUIZ_MARK_NUM_SELECT', 304 | row_width=2, 305 | triggers_out=OrderedDict(GET_QUIZ_MARK={'phrases': config.quizzes_possible_to_check, 306 | 'content_type': 'text'}, 307 | MAIN_MENU={'phrases': ['Назад'], 'content_type': 'text'}), 308 | welcome_msg=welcome_to_quiz_selection if len(config.quizzes_possible_to_check) > 0 309 | else return_from_quiz_selection) 310 | 311 | 312 | # ---------------------------------------------------------------------------- 313 | 314 | def get_marks_table_quiz(bot, message, sqldb): 315 | quiz_name = message.text 316 | num_checked = sqldb.get_number_checked_quizzes(message.chat.username, quiz_name) 317 | 318 | if num_checked < config.quizzes_need_to_check: 319 | bot.send_message(chat_id=message.chat.id, 320 | text='🌳🌻 Вы проверили {} квизов для {}. ' 321 | 'Необходимо проверить еще {} квизов,' 322 | ' чтобы узнать свою оценку по этому квизу.'.format(num_checked, quiz_name, 323 | config.quizzes_need_to_check - num_checked)) 324 | return 325 | df = sqldb.get_marks_quiz(user_id=message.chat.username, quiz_name=quiz_name) 326 | if df.empty: 327 | bot.send_message(chat_id=message.chat.id, 328 | text="Пока никто не проверил ваш квиз {} или вы его вообще не сдавали.\n" 329 | "Возвращайтесь позже.🌳🌻 ".format(quiz_name)) 330 | return 331 | finals = defaultdict(list) 332 | for i, row in df.iterrows(): 333 | text = '*' + quiz_name + '*\n' + '=' * 20 + '\n' 334 | text += row.QuestionText + '\n' + '=' * 20 + '\n' + '*Your Answer: *\n' \ 335 | + str(row.YourAnswer) + '\n*Score: *' + str(row.Score) 336 | if not pd.isna(row.NumChecks): 337 | text += '\n*Checked for [{}] times*'.format(row.NumChecks) 338 | bot.send_message(text=text, chat_id=message.chat.id, parse_mode='Markdown') 339 | final_score = int(sum(df.Score)) if (not pd.isna(sum(df.Score))) else 0 340 | mark = '{}/{}'.format(final_score, len(df)) 341 | finals['quiz'].append(quiz_name) 342 | finals['mark'].append(mark) 343 | bot.send_message(text='' + tabulate(finals, headers='keys', tablefmt="fancy_grid") + '', 344 | chat_id=message.chat.id, parse_mode='html') 345 | 346 | 347 | get_quiz_mark = State(name='GET_QUIZ_MARK', 348 | triggers_out=OrderedDict(QUIZ_MARK_NUM_SELECT={'phrases': ['Назад'], 'content_type': 'text'}), 349 | handler_welcome=get_marks_table_quiz, 350 | welcome_msg='Good Luck:)') 351 | 352 | # ---------------------------------------------------------------------------- 353 | 354 | welcome_check_hw = 'Выберите номер задания для проверки' if len(config.hw_possible_to_check) > 0 \ 355 | else 'Нет доступных для проверки заданий. Выпейте чаю, отдохните.' 356 | check_hw_num_selection = State(name='CHECK_HW_NUM_SELECT', triggers_out=OrderedDict( 357 | CHECK_HW_SEND={'phrases': config.hw_possible_to_check, 'content_type': 'text'}, 358 | MAIN_MENU={'phrases': ['Назад'], 'content_type': 'text'}), 359 | welcome_msg=welcome_check_hw, 360 | row_width=2) 361 | 362 | 363 | # ---------------------------------------------------------------------------- 364 | 365 | def choose_file_and_send(bot, message, sqldb): 366 | # TODO: do smth to fix work with empty hw set; 367 | # TODO: OH MY GOD! people should check only work that they have done!!!! 368 | hw_num = message.text 369 | file_id = sqldb.get_file_ids(hw_num=hw_num, 370 | user_id=message.chat.username) 371 | if len(file_id) > 0: 372 | sqldb.write_check_hw_ids(message.chat.username, file_id) 373 | bot.send_message(chat_id=message.chat.id, 374 | text='Этот файл предоставлен вам на проверку.') 375 | bot.send_document(message.chat.id, file_id) 376 | bot.send_message(chat_id=message.chat.id, 377 | text='Следующий файл предоставлен вам в качестве примера хорошо выполненного задания.') 378 | example_file = sqldb.get_example_hw_id(hw_num=hw_num) 379 | if len(example_file) > 0: 380 | bot.send_document(message.chat.id, example_file) 381 | else: 382 | bot.send_message(chat_id=message.chat.id, 383 | text='Ой нет. Я пошутил. Никакого примера на этот раз.') 384 | else: 385 | print("ERROR! empty sequence") 386 | bot.send_message(chat_id=message.chat.id, 387 | text="Что-то пошло не так.. Напишите об этом @fogside") 388 | 389 | 390 | check_hw_send = State(name='CHECK_HW_SEND', 391 | triggers_out=OrderedDict(CHECK_HW_SAVE_MARK={'phrases': config.marks, 392 | 'content_type': 'text'}, 393 | MAIN_MENU={'phrases': ['Меню'], 'content_type': 'text'}), 394 | handler_welcome=choose_file_and_send, 395 | row_width=3, 396 | welcome_msg="Пожалуйста, оцените работу.") 397 | 398 | 399 | # ---------------------------------------------------------------------------- 400 | 401 | def save_mark(bot, message, sqldb): 402 | sqldb.save_mark(message.chat.username, message.text) 403 | 404 | 405 | check_hw_save_mark = State(name='CHECK_HW_SAVE_MARK', 406 | triggers_out=OrderedDict(CHECK_HW_NUM_SELECT={'phrases': ['Проверить еще одну работу'], 407 | 'content_type': 'text'}, 408 | MAIN_MENU={'phrases': ['Меню'], 'content_type': 'text'}), 409 | welcome_msg='Спасибо за проверенную работу:)', 410 | handler_welcome=save_mark) 411 | 412 | # ---------------------------------------------------------------------------- 413 | 414 | admin_menu = State(name='ADMIN_MENU', 415 | row_width=3, 416 | triggers_out=OrderedDict(KNOW_NEW_QUESTIONS={'phrases': ['Questions'], 'content_type': 'text'}, 417 | SEE_HW_STAT={'phrases': ['Homeworks'], 'content_type': 'text'}, 418 | SEE_QUIZZES_STAT={'phrases': ['Quizzes'], 'content_type': 'text'}, 419 | MAIN_MENU={'phrases': ['MainMenu'], 'content_type': 'text'}, 420 | MAKE_BACKUP={'phrases': ['MakeBackup'], 'content_type': 'text'}), 421 | welcome_msg='Добро пожаловать, о Великий Одмен!') 422 | 423 | 424 | # ---------------------------------------------------------------------------- 425 | 426 | def make_backup_now(bot, message, sqldb): 427 | return 'BACKUP_NOW' 428 | 429 | 430 | make_backup = State(name='MAKE_BACKUP', 431 | triggers_out=OrderedDict(ADMIN_MENU={'phrases': ['Назад в админку'], 432 | 'content_type': 'text'}), 433 | handler_welcome=make_backup_now, 434 | welcome_msg='Working on pickling objects...') 435 | 436 | 437 | # ---------------------------------------------------------------------------- 438 | 439 | def get_quizzes_stat(bot, message, sqldb): 440 | for quiz_name in config.quizzes_possible_to_check: 441 | quizzes_stat = sqldb.get_quizzes_stat(quiz_name) 442 | bot.send_message(text="*FOR {}*".format(quiz_name), 443 | chat_id=message.chat.id, 444 | parse_mode='Markdown') 445 | bot.send_message( 446 | text='' + tabulate(pd.DataFrame(quizzes_stat, index=[0]).T, tablefmt="fancy_grid") + '', 447 | chat_id=message.chat.id, parse_mode='html') 448 | 449 | 450 | see_quizzes_stat = State(name='SEE_QUIZZES_STAT', 451 | triggers_out=OrderedDict(ADMIN_MENU={'phrases': ['Назад в админку'], 452 | 'content_type': 'text'}), 453 | handler_welcome=get_quizzes_stat, 454 | welcome_msg='Это все 👽') 455 | 456 | 457 | # ---------------------------------------------------------------------------- 458 | 459 | def get_questions(bot, message, sqldb): 460 | questions = sqldb.get_questions_last_week() 461 | if len(questions) > 0: 462 | res = '*Questions for the last week*\n' 463 | for user_id, question, date in questions: 464 | res += '👽 User: *' + user_id + '* asked at *' + date + '*:\n' + question + '\n\n' 465 | # Split the text each 3000 characters. 466 | # split_string returns a list with the splitted text. 467 | splitted_text = util.split_string(res, 3000) 468 | for text in splitted_text: 469 | bot.send_message(message.chat.id, text) 470 | else: 471 | bot.send_message(message.chat.id, '_Нет ничего новенького за последние 7 дней, к сожалению_:(', 472 | parse_mode='Markdown') 473 | 474 | 475 | know_new_questions = State(name='KNOW_NEW_QUESTIONS', 476 | triggers_out=OrderedDict(ADMIN_MENU={'phrases': ['Назад в админку'], 477 | 'content_type': 'text'}), 478 | handler_welcome=get_questions, 479 | welcome_msg='Это все 👽') 480 | 481 | 482 | # ---------------------------------------------------------------------------- 483 | 484 | def get_hw_stat(bot, message, sqldb): 485 | hw_stat = sqldb.get_checked_works_stat() 486 | if len(hw_stat) == 0: 487 | bot.send_message(message.chat.id, "Нет проверенных домашек совсем:( Грусть печаль.") 488 | else: 489 | ans = '_Количество проверенных работ на каждое задание_\n' 490 | for sem, count in hw_stat: 491 | ans += sem + '\t' + str(count) + '\n' 492 | bot.send_message(message.chat.id, ans, parse_mode='Markdown') 493 | 494 | 495 | see_hw_stat = State(name='SEE_HW_STAT', 496 | triggers_out=OrderedDict(ADMIN_MENU={'phrases': ['Назад в админку'], 'content_type': 'text'}), 497 | handler_welcome=get_hw_stat, 498 | welcome_msg='Это все что есть проверенного.\nЕсли какого номера тут нет, значит его не проверили.') 499 | 500 | # ---------------------------------------------------------------------------- 501 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NLPCourseBot 2 | Telegram-bot for NLP/RL courses 3 | 4 | ## It can work with quizzes, homeworks and student questions 5 | 6 | * receive questions from students 7 | * receive homeworks (in any archive or .ipynb form) 8 | * provide cross-check interface for homeworks (students check homeworks from other students) 9 | * provide statistics for homeworks and quizzes in the admin menu 10 | 11 | ### Quizzes 12 | * loading quizzes from json and provide interface for them 13 | * quiz json also could be obtained from Google Forms (using url or downloaded html) 14 | * accepted question types are following: 15 | * tests with one right answer 16 | * tests with multiple right answers 17 | * question with any possible text answer 18 | * all questions could contain pictures 19 | * autocheck for test questions 20 | * provide cross-check interface for quiz questions with an arbitrary text answer 21 | -------------------------------------------------------------------------------- /Sqlighter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sqlite3 4 | from sqlite3 import Error 5 | import pandas as pd 6 | import config 7 | import os 8 | 9 | 10 | class SQLighter: 11 | def __init__(self, database_file_path): 12 | if not os.path.exists(database_file_path): 13 | try: 14 | self.connection = sqlite3.connect(database_file_path) 15 | except Error as e: 16 | print(e) 17 | self.cursor = self.connection.cursor() 18 | self.cursor.execute("CREATE TABLE Questions ( user_id TEXT NOT NULL, " 19 | "date_added INTEGER NOT NULL, question TEXT );") 20 | 21 | self.cursor.execute("CREATE TABLE hw ( user_id TEXT, hw_num TEXT, " 22 | "date_added INTEGER, file_id TEXT );") 23 | 24 | self.cursor.execute("CREATE TABLE hw_checking ( file_id TEXT, user_id TEXT, mark INTEGER, " 25 | "date_checked INTEGER, date_started INTEGER );") 26 | 27 | self.cursor.execute("CREATE TABLE quizzes ( " 28 | "user_id TEXT, " 29 | "question_name TEXT, " 30 | "quiz_name TEXT, " 31 | "question_text TEXT, " 32 | "true_ans TEXT, " 33 | "is_right INTEGER, " 34 | "usr_answer TEXT, " 35 | "date_added INTEGER, " 36 | "id INTEGER PRIMARY KEY AUTOINCREMENT );") 37 | self.cursor.execute("CREATE TABLE quizzes_checking ( " 38 | "checker_user_id TEXT, " 39 | "id_quizzes INTEGER, " 40 | "date_started INTEGER, " 41 | "date_checked INTEGER, " 42 | "is_right INTEGER);") 43 | self.connection.commit() 44 | self.connection.isolation_level = None 45 | else: 46 | self.connection = sqlite3.connect(database_file_path) 47 | self.connection.isolation_level = None 48 | self.cursor = self.connection.cursor() 49 | 50 | def get_questions_last_week(self): 51 | """ Get only fresh questions from last n days """ 52 | return self.cursor.execute("SELECT user_id, question, datetime(q.date_added, 'unixepoch', 'localtime') " 53 | "FROM questions q " 54 | "WHERE (q.date_added >= strftime('%s','now','-7 day'))").fetchall() 55 | 56 | def write_question(self, user_id, question): 57 | """ Insert question into BD """ 58 | self.cursor.execute("INSERT INTO Questions (user_id, question, date_added)" 59 | " VALUES (?, ?, strftime('%s','now'))", (user_id, question)) 60 | 61 | def make_fake_db_record(self, user_id, hw_number): 62 | """ Make empty record for this hw_number """ 63 | self.cursor.execute("INSERT INTO hw (user_id, hw_num, date_added)" 64 | " VALUES (?, ?, strftime('%s','now'))", (user_id, hw_number)) 65 | 66 | def make_fake_db_record_quiz(self, q_id, user_id): 67 | """ Make empty record for this quiz_name & user_id""" 68 | self.cursor.execute("INSERT INTO quizzes_checking (checker_user_id, id_quizzes, date_started)" 69 | " VALUES (?, ?, strftime('%s','now'))", (user_id, q_id)) 70 | self.connection.commit() 71 | 72 | def upd_homework(self, user_id, file_id): 73 | """ UPD the latest record of user_id with file_id """ 74 | self.cursor.execute("UPDATE hw SET file_id = ?, date_added = strftime('%s','now') " 75 | "WHERE user_id = ? AND hw_num = " 76 | "(SELECT hw_num FROM hw WHERE user_id = ? ORDER BY date_added DESC LIMIT 1)", 77 | (file_id, user_id, user_id)) 78 | 79 | def write_check_hw_ids(self, user_id, file_id): 80 | return self.cursor.execute("INSERT INTO hw_checking (file_id, user_id, date_started) " 81 | "VALUES (?, ?, strftime('%s','now'))", (file_id, user_id)) 82 | 83 | def get_file_ids(self, hw_num, user_id): 84 | array = self.cursor.execute( 85 | "SELECT hw.file_id, count(hw_checking.file_id) checks " 86 | "FROM hw " 87 | "LEFT JOIN hw_checking " 88 | "ON hw.file_id = hw_checking.file_id " 89 | "AND hw_checking.user_id = :usr_id " 90 | "WHERE hw.file_id IS NOT NULL " 91 | "AND hw_checking.user_id is null " 92 | "AND hw.hw_num = :hw_num " 93 | "GROUP BY hw.file_id ORDER BY checks ASC LIMIT 1", 94 | {'hw_num': hw_num, 'usr_id': user_id}).fetchall() 95 | 96 | if len(array) > 0: 97 | return array[0][0] 98 | return '' 99 | 100 | def get_example_hw_id(self, hw_num): 101 | file_id = self.cursor.execute("SELECT file_id " 102 | "FROM hw_examples " 103 | "WHERE hw_name=?", (hw_num,)).fetchall() 104 | if len(file_id) > 0: 105 | return file_id[0][0] 106 | return '' 107 | 108 | def get_latest_quiz_name(self, user_id): 109 | result = self.cursor.execute("SELECT quizzes.quiz_name " 110 | "FROM quizzes JOIN quizzes_checking " 111 | "ON quizzes.id = quizzes_checking.id_quizzes " 112 | "WHERE quizzes_checking.checker_user_id = ? " 113 | "AND quizzes_checking.is_right IS NOT NULL " 114 | "ORDER BY quizzes_checking.date_checked DESC LIMIT 1", (user_id,)).fetchall() 115 | if len(result) > 0: 116 | return result[0][0] 117 | 118 | def get_number_checked_quizzes(self, user_id, quiz_name): 119 | result = self.cursor.execute("SELECT count(quizzes_checking.id_quizzes) " 120 | "FROM quizzes_checking JOIN quizzes ON quizzes.id=quizzes_checking.id_quizzes " 121 | "WHERE checker_user_id = ? " 122 | "AND quizzes_checking.is_right IS NOT NULL " 123 | "AND quizzes.quiz_name = ?" 124 | , (user_id, quiz_name)).fetchall() 125 | if len(result) > 0: 126 | return result[0][0] 127 | return 0 128 | 129 | def get_number_checked_for_one_quiz(self, user_id, quiz_name): 130 | result = self.cursor.execute("SELECT count(quizzes_checking.id_quizzes) " 131 | "FROM quizzes_checking JOIN quizzes ON quizzes.id=quizzes_checking.id_quizzes " 132 | "WHERE checker_user_id = ? AND quiz_name = ?" 133 | "AND quizzes_checking.is_right IS NOT NULL " 134 | , (user_id, quiz_name,)).fetchall() 135 | if len(result) > 0: 136 | return result[0][0] 137 | return 0 138 | 139 | def get_quiz_question_to_check(self, quiz_name, user_id): 140 | # TODO: fix processing of '' user answers; 141 | array = self.cursor.execute( 142 | "SELECT quizzes.id, quizzes.question_name," 143 | "quizzes.question_text, quizzes.usr_answer, " 144 | "count(quizzes_checking.id_quizzes) checks " 145 | "FROM quizzes " 146 | "LEFT JOIN quizzes_checking " 147 | "ON quizzes.id = quizzes_checking.id_quizzes " 148 | "AND quizzes_checking.checker_user_id = ? " 149 | "WHERE quizzes.user_id != ? " 150 | "AND quizzes.quiz_name = ? " 151 | "AND quizzes.usr_answer IS NOT NULL " 152 | "AND quizzes.true_ans IS NULL " 153 | "AND quizzes_checking.checker_user_id IS NULL " 154 | "GROUP BY quizzes.id ORDER BY checks ASC LIMIT 1", 155 | (user_id, user_id, quiz_name)).fetchall() 156 | if len(array) > 0: 157 | return array[0] 158 | return array 159 | 160 | def save_mark(self, user_id, mark): 161 | self.cursor.execute("UPDATE hw_checking SET mark = ?, date_checked=strftime('%s','now') " 162 | "WHERE user_id = ? AND file_id = " 163 | "(SELECT file_id FROM hw_checking " 164 | "WHERE user_id = ? ORDER BY date_started DESC LIMIT 1)", (mark, user_id, user_id)) 165 | self.connection.commit() 166 | 167 | def save_mark_quiz(self, user_id, mark): 168 | self.cursor.execute("UPDATE quizzes_checking SET is_right = ?, date_checked=strftime('%s','now') " 169 | "WHERE checker_user_id = ? AND id_quizzes = " 170 | "(SELECT id_quizzes FROM quizzes_checking " 171 | "WHERE checker_user_id = ? ORDER BY date_started DESC LIMIT 1)", 172 | (mark, user_id, user_id)) 173 | self.connection.commit() 174 | 175 | def get_num_checked(self, user_id): 176 | return self.cursor.execute("SELECT hw.hw_num, count(hw_checking.file_id) checks_count " 177 | "FROM hw LEFT JOIN hw_checking ON hw.file_id = hw_checking.file_id " 178 | "WHERE hw.file_id IS NOT NULL AND hw_checking.mark IS NOT NULL " 179 | "AND hw_checking.user_id = ?" 180 | "GROUP BY hw.hw_num ORDER BY checks_count ", (user_id,)).fetchall() 181 | 182 | def get_marks(self, user_id): 183 | return self.cursor.execute( 184 | "SELECT hw.hw_num, datetime(hw.date_added, 'unixepoch', 'localtime'), avg(hw_checking.mark) avg_mark " 185 | "FROM hw LEFT JOIN hw_checking ON hw.file_id = hw_checking.file_id " 186 | "WHERE hw.user_id = ? " 187 | "AND hw.file_id IS NOT NULL AND hw_checking.mark IS NOT NULL " 188 | "GROUP BY hw.date_added, hw.hw_num ORDER BY avg_mark", (user_id,)).fetchall() 189 | 190 | def get_marks_quiz(self, user_id, quiz_name): 191 | automarks = self.cursor.execute( 192 | "SELECT " 193 | "question_name, " 194 | "quizzes.question_text, " 195 | "usr_answer, " 196 | "is_right " 197 | # "datetime(quizzes.date_added, 'unixepoch', 'localtime') " 198 | "FROM quizzes WHERE user_id = ? " 199 | "AND true_ans IS NOT NULL " 200 | "AND quiz_name = ?", (user_id, quiz_name)).fetchall() 201 | cross_marks = self.cursor.execute( 202 | "SELECT " 203 | "quizzes.question_name," 204 | "quizzes.question_text, " 205 | "quizzes.usr_answer, " 206 | "round(avg(quizzes_checking.is_right),0), " 207 | "count(quizzes_checking.is_right) " 208 | # " datetime(quizzes.date_added, 'unixepoch', 'localtime') " 209 | "FROM quizzes LEFT JOIN quizzes_checking ON quizzes.id = quizzes_checking.id_quizzes " 210 | "WHERE quizzes.user_id = ? " 211 | "AND quizzes.true_ans IS NULL " 212 | "AND (quizzes_checking.is_right IS NOT NULL OR quizzes.is_right = 0)" 213 | "AND quizzes.quiz_name = ? " 214 | "GROUP BY quizzes.id", (user_id, quiz_name)).fetchall() 215 | 216 | automarks.extend(cross_marks) 217 | if len(automarks) < 4: 218 | # in case of nothing checked: 219 | return pd.DataFrame() 220 | 221 | marks = pd.DataFrame(automarks, columns=['Question', 'QuestionText', 222 | 'YourAnswer', 'Score', 'NumChecks']) 223 | marks.sort_values(by=['Question'], inplace=True) 224 | marks['Question'] = marks['Question'].apply(lambda x: x[-1:]) 225 | return marks 226 | 227 | def get_quizzes_stat(self, quiz_name): 228 | unique_people_passed = self.cursor.execute("SELECT count(DISTINCT quizzes.user_id) " 229 | "FROM quizzes WHERE quiz_name = ?", (quiz_name,)).fetchall() 230 | unique_people_passed = unique_people_passed[0][0] if len(unique_people_passed) > 0 else 0 231 | 232 | quizzes_more3_checked = self.cursor.execute("SELECT quizzes.user_id, " 233 | "avg(B.check_nums), count(B.check_nums) " 234 | "FROM quizzes INNER JOIN " 235 | "(SELECT quizzes_checking.id_quizzes, " 236 | "count(quizzes_checking.date_checked) check_nums " 237 | "FROM quizzes_checking " 238 | "GROUP BY quizzes_checking.id_quizzes) B " 239 | "ON quizzes.id = B.id_quizzes " 240 | "WHERE quizzes.quiz_name = ? " 241 | "AND B.check_nums >= 3 GROUP BY quizzes.user_id", 242 | (quiz_name,)).fetchall() 243 | 244 | quizzes_checked_all = self.cursor.execute("SELECT quizzes.user_id, " 245 | "avg(B.check_nums), count(B.check_nums) " 246 | "FROM quizzes INNER JOIN " 247 | "(SELECT quizzes_checking.id_quizzes, " 248 | "count(quizzes_checking.date_checked) check_nums " 249 | "FROM quizzes_checking " 250 | "GROUP BY quizzes_checking.id_quizzes) B " 251 | "ON quizzes.id = B.id_quizzes " 252 | "WHERE quizzes.quiz_name = ? " 253 | "AND B.check_nums >= 1 GROUP BY quizzes.user_id", 254 | (quiz_name,)).fetchall() 255 | 256 | num_people_checked_one_question = self.cursor.execute("SELECT count(DISTINCT quizzes_checking.checker_user_id) " 257 | "FROM quizzes_checking LEFT JOIN quizzes " 258 | "ON quizzes.id = quizzes_checking.id_quizzes " 259 | "WHERE quizzes.quiz_name = ? " 260 | "AND quizzes_checking.is_right IS NOT NULL", 261 | (quiz_name,)).fetchall() 262 | num_people_checked_one_question = num_people_checked_one_question[0][0] if len( 263 | num_people_checked_one_question) > 0 else 0 264 | 265 | return {'num_unique_people': unique_people_passed, 266 | 'num_truly_checked_quizzes': len(quizzes_more3_checked), 267 | 'num_all_checked_quizzes': len(quizzes_checked_all), 268 | 'num_people_checkers': num_people_checked_one_question} 269 | 270 | def get_checked_works_stat(self): 271 | return self.cursor.execute("SELECT hw.hw_num, count(hw_checking.file_id) checks_count " 272 | "FROM hw LEFT JOIN hw_checking ON hw.file_id = hw_checking.file_id " 273 | "WHERE hw.file_id IS NOT NULL AND hw_checking.mark IS NOT NULL " 274 | "GROUP BY hw.hw_num ORDER BY checks_count ").fetchall() 275 | 276 | def get_checks_for_every_work(self): 277 | return self.cursor.execute("SELECT hw.hw_num, hw.user_id, hw.file_id, " 278 | "count(hw_checking.file_id) checks, avg(hw_checking.mark) mark_avg " 279 | "FROM hw LEFT OUTER JOIN hw_checking ON hw.file_id = hw_checking.file_id " 280 | "WHERE hw.file_id IS NOT NULL " # AND hw_checking.mark IS NOT NULL " 281 | "GROUP BY hw.file_id ORDER BY checks DESC ").fetchall() 282 | 283 | def write_quiz_ans(self, user_id: str, 284 | quiz_name: str, question_name: str, 285 | is_right: int, usr_ans: str, 286 | question_text: str, true_ans: str): 287 | # check existence: 288 | record = self.cursor.execute("SELECT * FROM quizzes " 289 | "WHERE quiz_name = ? AND question_name = ? AND user_id = ?", 290 | (quiz_name, question_name, user_id)).fetchall() 291 | # update if exists: 292 | if len(record) > 0: 293 | return self.cursor.execute("UPDATE quizzes SET is_right=?, usr_answer=?, " 294 | "date_added=strftime('%s','now')" 295 | " WHERE user_id=? AND quiz_name = ? AND question_name = ?", 296 | (is_right, usr_ans, user_id, quiz_name, question_name)) 297 | else: 298 | return self.cursor.execute("INSERT INTO quizzes (user_id, quiz_name," 299 | " question_name, is_right, usr_answer," 300 | " question_text, true_ans, date_added) " 301 | "VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%s','now'))", 302 | (user_id, quiz_name, question_name, 303 | is_right, usr_ans, question_text, true_ans)) 304 | 305 | def close(self): 306 | self.connection.close() 307 | 308 | 309 | if __name__ == '__main__': 310 | sql = SQLighter(config.bd_name) 311 | # lol = sql.get_marks_quiz('fogside', 'quiz 5') 312 | # print(lol) 313 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | token = os.environ['TOKEN'] 5 | 6 | hw_possible_to_pass = ['hw1','hw2'] 7 | hw_possible_to_check = [] 8 | quizzes_possible_to_check = ['quiz 1','quiz 2'] 9 | admins = ['fogside', 'madrugado'] 10 | current_quiz_name = 'quiz 3' 11 | quiz_path = './quizzes/quiz3.json' 12 | quizzes_need_to_check = 1 13 | quiz_closed = False 14 | pics_path = './quizzes/pics' 15 | dump_graph_path = './backup/graph.dump' 16 | dump_quiz_path = './backup/quiz.dump' 17 | load_graph = False 18 | load_states = False 19 | 20 | marks = [str(i) for i in range(1, 6)] 21 | available_hw_resolutions = ('zip', 'rar', '7z', 'tar', 'tar.bz2', 'tar.gz', 'tar.xz', 'ipynb') 22 | bd_name = '/home/fogside/Projects/NLPCourseBot/questions.db' 23 | 24 | WEBHOOKS_AVAIL = False 25 | WEBHOOK_HOST = '' 26 | PORT = 8444 27 | WEBHOOK_LISTEN = '127.0.0.1' 28 | -------------------------------------------------------------------------------- /quizzes/QuizClasses.py: -------------------------------------------------------------------------------- 1 | import json 2 | from telebot import types 3 | from collections import defaultdict 4 | from utilities import download_picture 5 | import config 6 | import os 7 | import universal_reply as ureply 8 | from copy import deepcopy 9 | 10 | 11 | class QuizQuestion: 12 | def __init__(self, name, question_dict, last=False, first=False, parse_mode='Markdown', tick_symbol='💎️'): 13 | self.name = name 14 | self.question_text = question_dict['text'] 15 | self.true_ans = question_dict['true_ans'] 16 | 17 | self.grids = question_dict['grids'] 18 | self.variants_one = question_dict['variants'] if len(question_dict['variants']) > 0 else None 19 | self.variants_multiple = question_dict['several_poss_vars'] if len( 20 | question_dict['several_poss_vars']) > 0 else None 21 | self.ask_written = False if (self.variants_one or self.variants_multiple or self.grids) else True 22 | 23 | assert self.ask_written or (self.true_ans is not None), \ 24 | "true_ans must be specified if not ask_written!" 25 | 26 | self.usr_answers = dict() if self.variants_one else defaultdict(list) 27 | self.is_last = last 28 | self.is_first = first 29 | 30 | self.img_path = question_dict['img'] if len(question_dict['img']) > 0 else None 31 | if self.img_path: 32 | self._check_img_url() 33 | 34 | self.parse_mode = parse_mode 35 | if self.parse_mode == 'Markdown': 36 | self._edit_markdown_ans() 37 | self.tick_symbol = tick_symbol 38 | self.create_text_and_buttons() 39 | self.usr_buttons = defaultdict(lambda: deepcopy(self.default_buttons)) 40 | 41 | def _check_img_url(self): 42 | if 'https' in self.img_path: 43 | new_path = os.path.join(config.pics_path, './img_{}'.format(self.name)) 44 | download_picture(self.img_path, new_path) 45 | self.img_path = new_path 46 | else: 47 | self.img_path = os.path.join('./quizzes/', self.img_path) 48 | 49 | def _edit_markdown_ans(self): 50 | if self.variants_multiple: 51 | for i in range(len(self.variants_multiple)): 52 | self.variants_multiple[i] = self.variants_multiple[i].replace('*', '×') 53 | elif self.variants_one: 54 | for i in range(len(self.variants_one)): 55 | self.variants_one[i] = self.variants_one[i].replace('*', '×') 56 | 57 | def create_text_and_buttons(self): 58 | self.text = '*' + self.name + '*' + '\n' + self.question_text + '\n\n' 59 | if self.ask_written: 60 | self.text += '*Please, write an answer by yourself.*' + '\n\n' 61 | self.default_buttons = None 62 | 63 | elif self.variants_one: 64 | self.text += '*Please, choose only one right answer.*' + '\n\n' 65 | for i, v in enumerate(self.variants_one): 66 | self.text += str(i) + ') ' + v + '\n' 67 | self.default_buttons = [str(i) for i in range(0, len(self.variants_one))] 68 | 69 | elif self.variants_multiple: 70 | self.text += '*Please, mark all correct statements.*' + '\n\n' 71 | for i, v in enumerate(self.variants_multiple): 72 | self.text += str(i) + ') ' + v + '\n' 73 | self.default_buttons = [str(i) for i in range(0, len(self.variants_multiple))] 74 | 75 | elif self.grids: 76 | self.text += '*Please, choose only one right answer.*' + '\n\n' 77 | self.default_buttons = [str(i) for i in self.grids] 78 | 79 | if self.img_path: 80 | self.text += 'See the picture below.\n' 81 | 82 | if self.is_last: 83 | self.text += '*Attention! After submitting nothing can be changed.*' 84 | 85 | def create_inline_kb(self, arr_text=None, row_width=5): 86 | 87 | keyboard = types.ReplyKeyboardMarkup(row_width=row_width, resize_keyboard=True) 88 | if arr_text: 89 | keyboard.add( 90 | *[types.KeyboardButton(text=n) for n in arr_text]) 91 | 92 | next_button = types.KeyboardButton(text=ureply.quiz_next_button) 93 | back_button = types.KeyboardButton(text=ureply.quiz_back_button) 94 | main_menu = types.KeyboardButton(text=ureply.quiz_main_menu_button) 95 | 96 | if self.is_last: 97 | keyboard.add(types.KeyboardButton(text=ureply.quiz_submit_button), 98 | types.KeyboardButton(text=ureply.quiz_show_ans_button)) 99 | keyboard.add(back_button) 100 | keyboard.add(main_menu) 101 | return keyboard 102 | 103 | if self.is_first: 104 | keyboard.add(next_button) 105 | keyboard.add(main_menu) 106 | return keyboard 107 | 108 | keyboard.add(*[back_button, next_button]) 109 | keyboard.add(main_menu) 110 | return keyboard 111 | 112 | def tick_ans_in_kb(self, ans, chat_id, remove=False): 113 | """ 114 | Add ✔️ to ans; Just change self.buttons_text 115 | :param multiple: 116 | :param ones: 117 | :return: 118 | """ 119 | if not remove: 120 | self.usr_buttons[chat_id][int(ans)] += self.tick_symbol 121 | else: 122 | self.usr_buttons[chat_id][int(ans)] = self.usr_buttons[chat_id][int(ans)].replace(self.tick_symbol, '') 123 | 124 | def show_asking(self, bot, chat_id): 125 | """ 126 | Send self.text + ans variants to chat with id = msg.chat.id 127 | :param bot: 128 | :param msg: 129 | :return: 130 | """ 131 | if chat_id not in self.usr_answers: 132 | # add new user to dict with buttons... 133 | _ = self.usr_buttons[chat_id] # ...just by adding element to defaultdict 134 | self.usr_answers[chat_id] = None 135 | if self.variants_multiple: 136 | self.usr_answers[chat_id] = [] 137 | 138 | if self.ask_written and self.usr_answers[chat_id]: 139 | bot.send_message(chat_id, 140 | self.text + '\n' + '-' * 20 + '\nYOUR CURRENT ANSWER:\n' + self.usr_answers[chat_id], 141 | reply_markup=self.create_inline_kb(self.usr_buttons[chat_id]), 142 | parse_mode=self.parse_mode) 143 | else: 144 | bot.send_message(chat_id, self.text, 145 | reply_markup=self.create_inline_kb(self.usr_buttons[chat_id]), 146 | parse_mode=self.parse_mode) 147 | 148 | if self.img_path: 149 | bot.send_message(chat_id=chat_id, 150 | text='Picture for the _{}_'.format(self.name), 151 | parse_mode=self.parse_mode) 152 | with open(self.img_path, 'rb') as photo: 153 | # TODO: insert in DB file id to send it quicker to others 154 | bot.send_photo(chat_id=chat_id, photo=photo) 155 | 156 | def show_current(self, bot, chat_id): 157 | """ 158 | Show current version of question answered by usr 159 | :param bot: 160 | :param msg: 161 | :return: 162 | """ 163 | add = str(self.usr_answers[chat_id]) if self.usr_answers[chat_id] else 'None' 164 | answers = self.text + '\n' + '🍭 Your answer: ' + add 165 | bot.send_message(chat_id, answers) 166 | 167 | def callback_handler(self, bot, message): 168 | """ 169 | Handle callbacks data from all users 170 | and update self.usr_dict 171 | :return: 172 | """ 173 | ans = message.text.replace(self.tick_symbol, '') 174 | chat_id = message.chat.id 175 | 176 | if self.variants_one or self.grids: 177 | if self.grids: 178 | ans = str(self.default_buttons.index(ans)) 179 | if self.usr_answers[chat_id] != ans: 180 | if self.usr_answers[chat_id] is not None: 181 | self.tick_ans_in_kb(self.usr_answers[chat_id], chat_id, remove=True) 182 | 183 | self.tick_ans_in_kb(ans, chat_id, remove=False) 184 | self.usr_answers[chat_id] = ans 185 | 186 | elif self.variants_multiple: 187 | if ans in self.usr_answers[chat_id]: 188 | self.usr_answers[chat_id].remove(ans) 189 | self.tick_ans_in_kb(ans, chat_id, remove=True) 190 | else: 191 | self.usr_answers[chat_id].append(ans) 192 | self.tick_ans_in_kb(ans, chat_id, remove=False) 193 | 194 | keyboard = self.create_inline_kb(self.usr_buttons[chat_id]) 195 | bot.send_message(chat_id=chat_id, text='Your answer has been saved', reply_markup=keyboard) 196 | 197 | def save_written_answer(self, text, chat_id): 198 | self.usr_answers[chat_id] = text 199 | 200 | def get_ans(self, chat_id): 201 | """ 202 | Return data for chat_id 203 | :return: question_name, is_right, usr_ans, question_text, true_ans 204 | """ 205 | if chat_id not in self.usr_answers: 206 | true = str(self.true_ans) if self.true_ans else None 207 | return self.name, None, None, self.text, true 208 | 209 | ans = self.usr_answers[chat_id] 210 | is_right = None 211 | if self.ask_written: 212 | return self.name, is_right, ans, self.text, self.true_ans 213 | 214 | if self.variants_multiple: 215 | is_right = frozenset([int(a) for a in self.true_ans]) == frozenset([int(a) for a in ans]) if ans else False 216 | else: 217 | is_right = int(self.true_ans) == int(ans) if ans else False 218 | ans = str(ans) if ans else None 219 | return self.name, int(is_right), ans, self.text, str(self.true_ans) 220 | 221 | 222 | class Quiz: 223 | def __init__(self, name, quiz_json_path, next_global_state_name, self_state_name=None): 224 | self.reinitialize(quiz_json_path) 225 | self.name = name 226 | self.next_global_state_name = next_global_state_name 227 | self.self_state_name = self_state_name 228 | 229 | def reinitialize(self, new_quiz_json_path): 230 | with open(new_quiz_json_path) as q: 231 | self.json_array = json.load(q)[1:] 232 | self.q_num = len(self.json_array) 233 | self.questions = [ 234 | QuizQuestion(name="Question {}".format(i), 235 | question_dict=d, first=(i == 0), 236 | last=(i == self.q_num - 1)) 237 | for i, d in enumerate(self.json_array)] 238 | self.usersteps = dict() 239 | self.usr_submitted = defaultdict(bool) 240 | self.paused = defaultdict(bool) 241 | 242 | def get_usr_step(self, chat_id): 243 | if chat_id not in self.usersteps: 244 | # set usr to the first step 245 | self.usersteps[chat_id] = 0 246 | return self.usersteps[chat_id] 247 | 248 | def set_usr_step(self, chat_id, num: int): 249 | self.usersteps[chat_id] = num 250 | 251 | def collect_to_db(self, user_id, chat_id, sqlighter): 252 | """ 253 | collect all question answers for chat_id and write them to db 254 | :return: None 255 | """ 256 | for q in self.questions: 257 | question_name, is_right, usr_ans, question_text, true_ans = q.get_ans(chat_id) 258 | if q.ask_written and ((usr_ans is None) or (usr_ans == '')): 259 | is_right = 0 260 | usr_ans = None 261 | sqlighter.write_quiz_ans(user_id=user_id, 262 | quiz_name=self.name, 263 | question_name=question_name, 264 | is_right=is_right, 265 | usr_ans=usr_ans, 266 | question_text=question_text, 267 | true_ans=true_ans) 268 | 269 | def run(self, bot, message, sqlighter): 270 | """ 271 | Handle all messages including callbacks 272 | :param message: 273 | :return: 'end' or 'continue' 274 | """ 275 | 276 | if config.quiz_closed: 277 | return self.next_global_state_name 278 | 279 | chat_id = message.chat.id 280 | 281 | if self.usr_submitted[chat_id]: 282 | return self.next_global_state_name 283 | 284 | if (message.text == ureply.quiz_enter) and self.paused[chat_id]: 285 | self.questions[self.usersteps[chat_id]].show_asking(bot, chat_id) 286 | return self.self_state_name 287 | 288 | if chat_id not in self.usersteps: 289 | usr_step = self.get_usr_step(chat_id) 290 | bot.send_message(text='🌜 Welcome to *' + self.name + '* 🌛', 291 | chat_id=message.chat.id, 292 | parse_mode='Markdown') 293 | self.questions[usr_step].show_asking(bot, chat_id) 294 | return self.self_state_name 295 | else: 296 | usr_step = self.get_usr_step(chat_id) 297 | if message.text == ureply.quiz_main_menu_button: 298 | self.paused[chat_id] = True 299 | self.collect_to_db(user_id=message.chat.username, 300 | chat_id=message.chat.id, 301 | sqlighter=sqlighter) 302 | bot.send_message(text="You current state has been saved.", 303 | chat_id=chat_id) 304 | return self.next_global_state_name 305 | 306 | elif message.text == ureply.quiz_next_button: 307 | self.set_usr_step(chat_id, usr_step + 1) 308 | self.questions[usr_step + 1].show_asking(bot, chat_id) 309 | 310 | elif message.text == ureply.quiz_back_button: 311 | self.set_usr_step(chat_id, usr_step - 1) 312 | self.questions[usr_step - 1].show_asking(bot, chat_id) 313 | 314 | elif message.text == ureply.quiz_submit_button: 315 | self.collect_to_db(user_id=message.chat.username, 316 | chat_id=message.chat.id, 317 | sqlighter=sqlighter) 318 | self.usr_submitted[message.from_user.id] = True 319 | bot.send_message(chat_id=message.from_user.id, 320 | text='💫 Thank you! The quiz was successfully submitted! 🌝') 321 | return self.next_global_state_name 322 | 323 | elif message.text == ureply.quiz_show_ans_button: 324 | for q in self.questions: 325 | q.show_current(bot, chat_id) 326 | 327 | elif self.questions[usr_step].ask_written: 328 | self.questions[usr_step].save_written_answer(message.text, chat_id) 329 | bot.send_message(chat_id=chat_id, text='Your answer has been saved! ✨') 330 | elif message.text.replace(self.questions[0].tick_symbol, '') in self.questions[usr_step].default_buttons: 331 | self.questions[usr_step].callback_handler(bot, message) 332 | return self.self_state_name 333 | -------------------------------------------------------------------------------- /quizzes/googleFormParser.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import urllib 3 | from pprint import pprint 4 | import json 5 | 6 | 7 | class GoogleFormParser: 8 | """ 9 | Takes url of the Google form with Quiz 10 | or takes html file path on the file system 11 | and return quiz tasks as an array of dicts. 12 | e.g: each task is a dict which contains such fields: 13 | 1. 'text' -- is a question text; 14 | 2. 'variants' -- is an array of ans-variants; only one right; 15 | 3. 'several_poss_vars' -- is an array of ans-variants; several may be right; 16 | 4. 'grids' -- same as variants, but only numbers (from 1 to len(grids), only one ans right) 17 | 5. 'img' -- image path on file sys or url 18 | """ 19 | 20 | def __init__(self, url: str = None, file_path: str = None): 21 | 22 | assert (url is not None) ^ (file_path is not None), "Only one of 2 arguments must be specified!" 23 | 24 | if url is not None: 25 | try: 26 | self.url = url 27 | self.html = urllib.request.urlopen(url).read() 28 | except: 29 | print("Smth went wrong with opening url!") 30 | exit(1) 31 | if file_path is not None: 32 | with open(file_path, 'r') as fn: 33 | self.html = fn.read() 34 | self.soup = BeautifulSoup(self.html, "html5lib") 35 | 36 | def get_tasks_json(self): 37 | 38 | def get_text_from_lists(tag_lists): 39 | return [f.getText() if len(f) > 0 else '' for f in tag_lists] 40 | 41 | tasks = [] 42 | blist = self.soup.find_all("div", class_="freebirdFormviewerViewItemsItemItem") 43 | 44 | for bl in blist: 45 | task = dict() 46 | task['text'] = bl.find_all("div", class_="freebirdCustomFont")[0].getText() 47 | 48 | grids = get_text_from_lists(bl.find_all("label", class_="freebirdMaterialScalecontentColumn")) 49 | variants = get_text_from_lists(bl.find_all("label", class_="freebirdFormviewerViewItemsRadioChoice")) 50 | several_poss_vars = get_text_from_lists( 51 | bl.find_all("label", class_="freebirdFormviewerViewItemsCheckboxContainer")) 52 | imgs = bl.find_all("img", class_="freebirdFormviewerViewItemsEmbeddedobjectImage") 53 | if len(imgs) > 0: 54 | imgs = imgs[0]['src'] # img address on the file system or in the internet; 55 | else: 56 | imgs = '' 57 | 58 | task['grids'] = grids 59 | task['variants'] = variants 60 | task['several_poss_vars'] = several_poss_vars 61 | task['img'] = imgs 62 | task['true_ans'] = None 63 | tasks.append(task) 64 | 65 | return tasks 66 | 67 | def save_json(self, path_file): 68 | with open(path_file, 'w') as fn: 69 | json.dump(self.get_tasks_json(), fn) 70 | print("json saved to: {}".format(path_file)) 71 | 72 | 73 | if __name__ == "__main__": 74 | # test url: 75 | # gf = GoogleFormParser( 76 | # url="https://docs.google.com/forms/d/e/1FAIpQLScrVP6urS02qm7bOAkbpwqSXBFJOSgvUi8J9X727j_zc8tacw/viewform#start=openform") 77 | # # pprint(gf.get_tasks_json()) 78 | # gf.save_json("./quiz6.json") 79 | 80 | gf = GoogleFormParser(file_path='NLP.Quiz2.html') 81 | gf.save_json("./quiz2.json") 82 | 83 | # test loading 84 | # with open("./quiz6.json") as f: 85 | # lol = json.load(f) 86 | # pprint(lol) 87 | 88 | # test file: 89 | # gf2 = GoogleFormParser(file_path="NLP._Quiz6.html") 90 | # pprint(gf2.get_tasks_json()) 91 | 92 | # test empty arguments: 93 | # gf3 = GoogleFormParser() 94 | 95 | # test full arguments: 96 | # gf4 = GoogleFormParser(url="sdfgs", file_path="sdfgsd") 97 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=0.12.2 2 | pyTelegramBotAPI>=3.2.1 3 | dill==0.2.7.1 4 | beautifulsoup4==4.6.0 5 | pandas==0.22.0 6 | requests==2.18.4 7 | tabulate==0.8.2 -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import DialogStatesDefinition 2 | from DialogClasses import DialogGraph 3 | import telebot 4 | import config 5 | from flask import Flask, request 6 | from Sqlighter import SQLighter 7 | 8 | ############################################### 9 | ########## LOGGING CLASS SETTINGS ############ 10 | ############################################### 11 | 12 | import logging.handlers 13 | 14 | class DummyLogger: 15 | def debug(self, _): 16 | pass 17 | 18 | def info(self, _): 19 | pass 20 | 21 | def error(self, e): 22 | print(e) 23 | 24 | 25 | telebot.logger = DummyLogger() 26 | logging.getLogger("requests").setLevel(logging.WARNING) 27 | 28 | f = logging.Formatter(fmt='%(levelname)s:%(name)s: %(message)s ' 29 | '(%(asctime)s; %(filename)s:%(lineno)d)', 30 | datefmt="%Y-%m-%d %H:%M:%S") 31 | 32 | handlers = [ 33 | logging.handlers.RotatingFileHandler('usr_log.txt', encoding='utf8', 34 | maxBytes=100000, backupCount=1) 35 | ] 36 | 37 | root_logger = logging.Logger('root_logger', level=logging.DEBUG) 38 | for h in handlers: 39 | h.setFormatter(f) 40 | h.setLevel(logging.DEBUG) 41 | root_logger.addHandler(h) 42 | 43 | ############################## 44 | #### END LOGGING SETTINGS #### 45 | ############################## 46 | 47 | bot = telebot.TeleBot(config.token, threaded=False) 48 | nodes = [DialogStatesDefinition.main_menu, 49 | 50 | DialogStatesDefinition.take_quiz, 51 | DialogStatesDefinition.check_quiz, 52 | DialogStatesDefinition.quiz_mark_num_select, 53 | DialogStatesDefinition.get_quiz_mark, 54 | DialogStatesDefinition.save_mark_quiz, 55 | DialogStatesDefinition.send_quiz_question_to_check, 56 | 57 | DialogStatesDefinition.ask_question_start, 58 | DialogStatesDefinition.save_question, 59 | 60 | DialogStatesDefinition.admin_menu, 61 | DialogStatesDefinition.know_new_questions, 62 | DialogStatesDefinition.see_hw_stat, 63 | DialogStatesDefinition.see_quizzes_stat, 64 | DialogStatesDefinition.make_backup, 65 | 66 | DialogStatesDefinition.pass_hw_num_selection, 67 | DialogStatesDefinition.pass_hw_chosen_num, 68 | DialogStatesDefinition.pass_hw_upload, 69 | 70 | DialogStatesDefinition.get_mark, 71 | 72 | DialogStatesDefinition.check_hw_num_selection, 73 | DialogStatesDefinition.check_hw_save_mark, 74 | DialogStatesDefinition.check_hw_send] 75 | 76 | sqldb = SQLighter(config.bd_name) 77 | 78 | dialogGraph = DialogGraph(bot, root_state='MAIN_MENU', nodes=nodes, sqldb=sqldb, logger=root_logger) 79 | 80 | 81 | @bot.message_handler(content_types=['text', 'document', 'photo']) 82 | def handler(message): 83 | dialogGraph.run(message=message) 84 | 85 | 86 | if __name__ == '__main__': 87 | if config.WEBHOOKS_AVAIL: 88 | 89 | WEBHOOK_HOST = config.WEBHOOK_HOST 90 | PORT = config.PORT 91 | WEBHOOK_LISTEN = config.WEBHOOK_LISTEN 92 | 93 | server = Flask(__name__) 94 | 95 | 96 | @server.route("/webhook", methods=['POST']) 97 | def getMessage(): 98 | bot.process_new_updates([telebot.types.Update.de_json(request.stream.read().decode("utf-8"))]) 99 | return "!", 200 100 | 101 | 102 | server.run(host=WEBHOOK_LISTEN, port=PORT) 103 | 104 | bot.remove_webhook() 105 | bot.set_webhook(url=WEBHOOK_HOST) 106 | else: 107 | bot.delete_webhook() 108 | bot.polling(none_stop=True) 109 | -------------------------------------------------------------------------------- /universal_reply.py: -------------------------------------------------------------------------------- 1 | ADMIN_KEY_PHRASE = 'WINTERMUTE' 2 | NO_USERNAME_WARNING = 'Для продолжения работы с ботом, пожалуйста, запилите себе username в настройках телеграм!' 3 | DEFAULT_ANS = 'Я вас не понимаю.\nНажмите /start чтобы начать жизнь с чистого листа ☘️' 4 | quiz_next_button = 'next question ➡️' 5 | quiz_back_button = '⬅️ previous question' 6 | quiz_submit_button = 'submit quiz' 7 | quiz_main_menu_button = 'main menu' 8 | quiz_show_ans_button = 'show current answers' 9 | quiz_enter = '🐟 Сдать квиз 🐠' 10 | quiz_check = '🐌 Проверить квиз 🐌' 11 | quiz_estimates = '🐝 Оценки за квизы 🐝' 12 | 13 | hw_enter = '🐟 Сдать дз 🐠' 14 | hw_check = '🐌 Проверить дз 🐌' 15 | hw_estimates = '🐝 Оценки за дз 🐝' 16 | 17 | ask_question = '🦉 Задать вопрос 🦉' -------------------------------------------------------------------------------- /utilities.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import config 4 | import telebot 5 | 6 | 7 | def download_file(bot, file_id, folder_name, filename): 8 | file_info = bot.get_file(file_id) 9 | file = requests.get('https://api.telegram.org/file/bot{0}/{1}'.format(config.token, file_info.file_path), 10 | stream=True) 11 | local_filename = os.path.join(folder_name, filename) 12 | if not os.path.exists(folder_name): 13 | os.mkdir(folder_name) 14 | 15 | with open(local_filename, 'wb') as f: 16 | for chunk in file.iter_content(chunk_size=1024): 17 | if chunk: # filter out keep-alive new chunks 18 | f.write(chunk) 19 | 20 | 21 | def download_picture(pic_url, pic_path): 22 | with open(pic_path, 'wb') as handle: 23 | response = requests.get(pic_url, stream=True) 24 | 25 | if not response.ok: 26 | print(response) 27 | 28 | for block in response.iter_content(1024): 29 | if not block: 30 | break 31 | handle.write(block) 32 | 33 | if __name__ == '__main__': 34 | bot = telebot.TeleBot(config.token) 35 | file_id = "" 36 | folder_name = '/home/fogside/tmp/' 37 | file_name = '.ipynb' 38 | download_file(bot=bot, file_id=file_id, folder_name=folder_name, filename=file_name) 39 | --------------------------------------------------------------------------------