Skip to content

Commit b176f29

Browse files
committed
Code restructurings step 2/5: decompose telegram front-end
Decomposed telegram front-end part, rearranged code, fixed some imports (both in telegram and core parts). The code seems to work now [but it now requires a VERY strange actions to make it run], though it has various shitty parts yet. There are things to decompose further (like the invitations system), and I'm planning to do some cosmetic changes (reorganize files themselves, rename something, reorder imports) after I'm done with the substantive part
1 parent 43033e6 commit b176f29

11 files changed

+291
-343
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,4 @@ dmypy.json
235235

236236

237237
# Project-level
238-
/config.py
238+
**config**

core/db_connector.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from psycopg2 import connect
44
from psycopg2.extensions import cursor as cursor_type
55

6-
from ..config import db_host, db_name, db_username, db_password
6+
from .config import db_host, db_name, db_username, db_password
77

88

99
@contextmanager

core/invitations.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
from .users import get_local_id, get_free_operators
88
from .conversations import get_conversing
9-
from telegram_bot.callback_helpers import contract_callback_data_and_jdump
10-
from telegram_bot.__main__ import bot # TODO: remove this terrible shit
9+
from ..telegram_bot.utils.callback_helpers import contract_callback_data_and_jdump
10+
from ..telegram_bot._bot import bot # TODO: remove this terrible shit
1111

1212

1313
# Whenever a client requests a conversation, all the <b>free</b> operators get a message which invites them to start

telegram_bot/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from ._bot import bot
2+
# Create `bot` handlers
3+
from . import message_handlers as _
4+
from . import callback_handlers as _
5+
6+
7+
__all__ = ["bot"]

telegram_bot/__main__.py

+1-339
Large diffs are not rendered by default.

telegram_bot/_bot.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import telebot
2+
3+
from .config import bot_token
4+
5+
6+
bot = telebot.TeleBot(bot_token)

telegram_bot/callback_handlers.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import telebot
2+
3+
from ..core.conversations import begin_conversation, get_conversing
4+
from ..core.invitations import conversation_starter_lock, operators_invitations_messages, clear_invitation_messages
5+
from ._bot import bot
6+
from .utils import nonfalling_handler, notify_admins
7+
from .utils.callback_helpers import jload_and_decontract_callback_data, datetime_from_local_epoch_secs
8+
9+
10+
def get_type_from_callback_data(call_data):
11+
d = jload_and_decontract_callback_data(call_data)
12+
if not isinstance(d, dict):
13+
return None
14+
return d.get('type')
15+
16+
17+
# Invalid callback query handler
18+
@bot.callback_query_handler(func=lambda call: get_type_from_callback_data(call.data) is None)
19+
@nonfalling_handler
20+
def invalid_callback_query(call: telebot.types.CallbackQuery):
21+
bot.answer_callback_query(call.id, "Действие не поддерживается или некорректные данные обратного вызова")
22+
23+
24+
@bot.callback_query_handler(func=lambda call: get_type_from_callback_data(call.data) == 'conversation_rate')
25+
@nonfalling_handler
26+
def conversation_rate_callback_query(call: telebot.types.CallbackQuery):
27+
d = jload_and_decontract_callback_data(call.data)
28+
29+
mood = d.get('mood')
30+
if mood == 'worse':
31+
operator_tg, operator_local = d['operator_ids']
32+
conversation_end = datetime_from_local_epoch_secs(d['conversation_end_moment'])
33+
notification_text = "Клиент чувствует себя хуже после беседы с оператором {}, которая завершилась в {}".format(
34+
f"[{operator_local}](tg://user?id={operator_tg})", conversation_end
35+
)
36+
notify_admins(text=notification_text, parse_mode="Markdown")
37+
38+
bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=None)
39+
40+
if mood is None:
41+
bot.answer_callback_query(call.id)
42+
else:
43+
bot.answer_callback_query(call.id, "Спасибо за вашу оценку")
44+
45+
46+
@bot.callback_query_handler(func=lambda call: get_type_from_callback_data(call.data) == 'conversation_acceptation')
47+
@nonfalling_handler
48+
def conversation_acceptation_callback_query(call: telebot.types.CallbackQuery):
49+
d = jload_and_decontract_callback_data(call.data)
50+
with conversation_starter_lock:
51+
if call.message.chat.id in operators_invitations_messages.keys():
52+
bot.answer_callback_query(call.id, "Невозможно начать беседу, пока вы ожидаете оператора")
53+
return
54+
55+
conversation_began = begin_conversation(d['client_id'], call.message.chat.id)
56+
57+
if conversation_began:
58+
clear_invitation_messages(d['client_id'])
59+
60+
(_, local_client_id), (_, local_operator_id) = get_conversing(call.message.chat.id)
61+
bot.answer_callback_query(call.id)
62+
bot.send_message(call.message.chat.id, f"Началась беседа с клиентом №{local_client_id}. Отправьте "
63+
"сообщение, и собеседник его увидит")
64+
bot.send_message(d['client_id'], f"Началась беседа с оператором №{local_operator_id}. Отправьте сообщение, "
65+
"и собеседник его увидит")
66+
else:
67+
bot.answer_callback_query(call.id, "Что-то пошло не так. Возможно, вы уже в беседе, или другой оператор принял "
68+
"это приглашение")

telegram_bot/message_handlers.py

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from datetime import datetime
2+
3+
import telebot
4+
5+
from ..core.conversations import get_conversing, end_conversation
6+
from ..core.db_connector import PrettyCursor
7+
from ..core.invitations import invite_operators, clear_invitation_messages
8+
from ..core.users import add_user
9+
from ._bot import bot
10+
from .utils import nonfalling_handler, AnyContentType
11+
from .utils.callback_helpers import seconds_since_local_epoch, contract_callback_data_and_jdump
12+
13+
14+
@bot.message_handler(commands=['start', 'help'])
15+
@nonfalling_handler
16+
def start_help_handler(message: telebot.types.Message):
17+
bot.reply_to(message, "Привет. /request_conversation, чтобы начать беседу, /end_conversation чтобы завершить")
18+
add_user(message.chat.id)
19+
20+
21+
@bot.message_handler(commands=['request_conversation'])
22+
@nonfalling_handler
23+
def request_conversation_handler(message: telebot.types.Message):
24+
(tg_client_id, _), (tg_operator_id, _) = get_conversing(message.chat.id)
25+
if tg_operator_id == message.chat.id:
26+
bot.reply_to(message, "Операторы не могут запрашивать помощь, пока помогают кому-то")
27+
elif tg_client_id == message.chat.id:
28+
bot.reply_to(message, "Вы уже в беседе с оператором. Используйте /end_conversation чтобы прекратить")
29+
else:
30+
result = invite_operators(message.chat.id)
31+
if result == 0:
32+
bot.reply_to(message, "Операторы получили запрос на присоединение. Ждем оператора...\nИспользуйте "
33+
"/end_conversation, чтобы отменить запрос")
34+
elif result == 1:
35+
bot.reply_to(message, "Вы уже ожидаете присоединения оператора. Используйте /end_conversation, чтобы "
36+
"отказаться от беседы")
37+
elif result == 2:
38+
bot.reply_to(message, "Сейчас нет свободных операторов. Пожалуйста, попробуйте позже")
39+
elif result == 3:
40+
bot.reply_to(message, "Вы уже в беседе. Используйте /end_conversation, чтобы выйти из нее")
41+
else:
42+
raise NotImplementedError("`invite_operators` returned an unexpected value")
43+
44+
45+
@bot.message_handler(commands=['end_conversation'])
46+
@nonfalling_handler
47+
def end_conversation_handler(message: telebot.types.Message):
48+
(_, client_local), (operator_tg, operator_local) = get_conversing(message.chat.id)
49+
50+
if operator_tg is None:
51+
if clear_invitation_messages(message.chat.id):
52+
bot.reply_to(message, "Ожидание операторов отменено. Используйте /request_conversation, чтобы запросить "
53+
"помощь снова")
54+
else:
55+
bot.reply_to(message, "В данный момент вы ни с кем не беседуете. Используйте /request_conversation, чтобы "
56+
"начать")
57+
elif operator_tg == message.chat.id:
58+
bot.reply_to(message, "Оператор не может прекратить беседу. Обратитесь к @kolayne для реализации такой "
59+
"возможности")
60+
else:
61+
keyboard = telebot.types.InlineKeyboardMarkup()
62+
d = {'type': 'conversation_rate', 'operator_ids': [operator_tg, operator_local],
63+
'conversation_end_moment': seconds_since_local_epoch(datetime.now())}
64+
65+
keyboard.add(
66+
telebot.types.InlineKeyboardButton("Лучше",
67+
callback_data=contract_callback_data_and_jdump({**d, 'mood': 'better'})),
68+
telebot.types.InlineKeyboardButton("Так же",
69+
callback_data=contract_callback_data_and_jdump({**d, 'mood': 'same'})),
70+
telebot.types.InlineKeyboardButton("Хуже",
71+
callback_data=contract_callback_data_and_jdump({**d, 'mood': 'worse'}))
72+
)
73+
keyboard.add(telebot.types.InlineKeyboardButton("Не хочу оценивать",
74+
callback_data=contract_callback_data_and_jdump(d)))
75+
76+
end_conversation(message.chat.id)
77+
bot.reply_to(message, "Беседа с оператором прекратилась. Хотите оценить свое самочувствие после нее? "
78+
"Вы остаетесь анонимным", reply_markup=keyboard)
79+
bot.send_message(operator_tg, f"Пользователь №{client_local} прекратил беседу")
80+
81+
82+
@bot.message_handler(content_types=['text'])
83+
@nonfalling_handler
84+
def text_message_handler(message: telebot.types.Message):
85+
(client_tg, _), (operator_tg, _) = get_conversing(message.chat.id)
86+
87+
if client_tg is None:
88+
bot.reply_to(message, "Чтобы начать общаться с оператором, нужно написать /request_conversation. Сейчас у вас "
89+
"нет собеседника")
90+
return
91+
92+
interlocutor_id = client_tg if message.chat.id != client_tg else operator_tg
93+
94+
reply_to = None
95+
if message.reply_to_message is not None:
96+
with PrettyCursor() as cursor:
97+
cursor.execute("SELECT sender_message_id FROM reflected_messages WHERE sender_chat_id=%s AND "
98+
"receiver_chat_id=%s AND receiver_message_id=%s",
99+
(interlocutor_id, message.chat.id, message.reply_to_message.message_id))
100+
try:
101+
reply_to, = cursor.fetchone()
102+
except TypeError:
103+
bot.reply_to(message, "Эта беседа уже завершилась. Вы не можете ответить на это сообщение")
104+
return
105+
106+
for entity in message.entities or []:
107+
if entity.type in ('mention', 'bot_command'):
108+
continue
109+
if entity.type == 'url' and message.text[entity.offset: entity.offset + entity.length] == entity.url:
110+
continue
111+
112+
bot.reply_to(message, "Это сообщение содержит форматирование, которое сейчас не поддерживается. Оно будет "
113+
"отправлено с потерей форматирования. Мы работаем над этим")
114+
break
115+
116+
sent = bot.send_message(interlocutor_id, message.text, reply_to_message_id=reply_to)
117+
118+
with PrettyCursor() as cursor:
119+
query = "INSERT INTO reflected_messages(sender_chat_id, sender_message_id, receiver_chat_id, " \
120+
"receiver_message_id) VALUES (%s, %s, %s, %s)"
121+
cursor.execute(query, (message.chat.id, message.message_id, sent.chat.id, sent.message_id))
122+
cursor.execute(query, (sent.chat.id, sent.message_id, message.chat.id, message.message_id))
123+
124+
125+
@bot.message_handler(content_types=AnyContentType())
126+
@nonfalling_handler
127+
def another_content_type_handler(message: telebot.types.Message):
128+
bot.reply_to(message, "Сообщения этого типа не поддерживаются. Свяжитесь с @kolayne, чтобы добавить поддержку")

telegram_bot/utils/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import callback_helpers
2+
from .misc import *
File renamed without changes.

telegram_bot/utils/misc.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from functools import wraps
2+
from sys import stderr
3+
from traceback import format_exc
4+
from typing import Callable
5+
6+
import telebot
7+
8+
from ...core.users import get_admins_ids
9+
from .._bot import bot # TODO: remove this terrible shit
10+
11+
12+
def notify_admins(**kwargs) -> bool:
13+
"""
14+
Send a text message to all the bot administrators. Any exceptions occurring inside are suppressed
15+
16+
::param kwargs: Keyword arguments to be forwarded to `bot.send_message` (shouldn't contain `chat_id`)
17+
:return: `True` if a message was successfully delivered to at least one admin (i. e. no exception occurred), `False`
18+
otherwise
19+
"""
20+
try:
21+
admins = get_admins_ids()
22+
except Exception:
23+
print("Couldn't get admins ids inside of `notify_admins`:", file=stderr)
24+
print(format_exc(), file=stderr)
25+
return False
26+
27+
sent = False
28+
29+
try:
30+
for i in admins:
31+
try:
32+
bot.send_message(chat_id=i, **kwargs)
33+
except Exception:
34+
print("Couldn't send a message to an admin inside of `notify_admins`:", file=stderr)
35+
print(format_exc(), file=stderr)
36+
else:
37+
sent = True
38+
39+
except Exception:
40+
print("Something went wrong while **iterating** throw `admins` inside of `notify_admins`:", file=stderr)
41+
print(format_exc(), file=stderr)
42+
43+
return sent
44+
45+
46+
def nonfalling_handler(func: Callable):
47+
@wraps(func)
48+
def ans(message: telebot.types.Message, *args, **kwargs):
49+
try:
50+
func(message, *args, **kwargs)
51+
except Exception:
52+
try:
53+
# For callback query handlers
54+
# (we got a `telebot.types.CallbackQuery` object instead of a `telebot.types.Message` object)
55+
if hasattr(message, 'message'):
56+
message = message.message
57+
58+
s = "Произошла ошибка"
59+
if notify_admins(text=('```' + format_exc() + '```'), parse_mode="Markdown"):
60+
s += ". Наши администраторы получили уведомление о ней"
61+
else:
62+
s += ". Свяжитесь с администрацией бота для исправления"
63+
s += ". Технические детали:\n```" + format_exc() + "```"
64+
65+
print(format_exc(), file=stderr)
66+
bot.send_message(message.chat.id, s, parse_mode="Markdown")
67+
except Exception:
68+
print("An exception while handling an exception:", file=stderr)
69+
print(format_exc(), file=stderr)
70+
71+
return ans
72+
73+
74+
class AnyContentType:
75+
def __contains__(self, item): return True

0 commit comments

Comments
 (0)