├── .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 |
--------------------------------------------------------------------------------