From a80727e97f72be12a9ce24882c1b96c0f431e184 Mon Sep 17 00:00:00 2001 From: Untone Date: Sun, 7 Jan 2024 12:19:46 +0300 Subject: [PATCH] unbloat-code --- README.md | 1 - bot/announce.py | 55 ++++++ bot/api.py | 269 +----------------------------- bot/config.py | 2 - bot/state.py | 12 -- bot/talking.py | 16 ++ handlers/callback_unlink.py | 60 ------- handlers/callback_vouch.py | 62 ------- handlers/command_ask.py | 21 --- handlers/command_graph.py | 15 -- handlers/command_my.py | 55 ------ handlers/handle_default.py | 46 ----- handlers/handle_feedback.py | 65 -------- handlers/handle_join_request.py | 32 ++-- handlers/handle_members_change.py | 47 ------ handlers/handle_private.py | 26 +++ handlers/handle_startup.py | 45 ----- handlers/messages_routing.py | 32 ++++ handlers/routing.py | 41 ----- handlers/send_button.py | 56 ------- main.py | 110 +++--------- storage/__init__.py | 37 ---- storage/profile.py | 47 ------ utils/mention.py | 2 - 24 files changed, 179 insertions(+), 975 deletions(-) create mode 100644 bot/announce.py delete mode 100644 bot/state.py create mode 100644 bot/talking.py delete mode 100644 handlers/callback_unlink.py delete mode 100644 handlers/callback_vouch.py delete mode 100644 handlers/command_ask.py delete mode 100644 handlers/command_graph.py delete mode 100644 handlers/command_my.py delete mode 100644 handlers/handle_default.py delete mode 100644 handlers/handle_feedback.py delete mode 100644 handlers/handle_members_change.py create mode 100644 handlers/handle_private.py delete mode 100644 handlers/handle_startup.py create mode 100644 handlers/messages_routing.py delete mode 100644 handlers/routing.py delete mode 100644 handlers/send_button.py delete mode 100644 storage/__init__.py delete mode 100644 storage/profile.py diff --git a/README.md b/README.md index 4f877b4..c8bd3de 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ - `BOT_TOKEN` - токен бота созданный с помощью @BotFather - `FEEDBACK_CHAT_ID` - айди чата для обратной связи - - `REDIS_URL` ### Локальная разработка diff --git a/bot/announce.py b/bot/announce.py new file mode 100644 index 0000000..18ae18d --- /dev/null +++ b/bot/announce.py @@ -0,0 +1,55 @@ +from bot.api import telegram_api +from utils.mention import mention, userdata_extract + +import logging + + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +announces = {} + +def get_newcomer_message(msg): + lang = msg["from"].get("language_code", "ru") + r = "хочет присоединиться к нам здесь" if lang == "ru" else " wants to join us here" + _uid, identity, username = userdata_extract(msg["from"]) + if username: + r = "@" + username + " " + r + r = identity + " " + r + return r + + +async def show_announce(msg): + logger.info("showing announce with photo") + chat_id = str(msg["chat"]["id"]) + from_id = str(msg["from"]["id"]) + mid = msg.get("message_id", "") + newcomer_message = get_newcomer_message(msg) + + userphotos_response = await telegram_api("getUserphotos", user_id=from_id) + logger.debug(userphotos_response) + + file_id = "" + if userphotos_response["ok"] and userphotos_response["result"]["total_count"] > 0: + logger.info("showing button with photo") + file_id = userphotos_response["result"]["photos"][0][0]["file_id"] + + r = await telegram_api("sendPhoto", + chat_id=chat_id, + file_id=file_id, + caption=newcomer_message, + reply_to=mid + ) + logger.debug(r) + announces[from_id] = r.get("message_id") + + +async def edit_announce(msg): + logger.info("editing announce") + chat_id = str(msg["chat"]["id"]) + from_id = str(msg["from"]["id"]) + mid = msg.get("message_id", "") + caption = get_newcomer_message(msg) + msg.get("text") + announce_message_id = announces.get(from_id) + r = await telegram_api("editMessageCaption", chat_id=chat_id, message_id=announce_message_id, caption=caption) + announces[from_id] = r.get("message_id") diff --git a/bot/api.py b/bot/api.py index 8b65fb1..401afba 100644 --- a/bot/api.py +++ b/bot/api.py @@ -1,6 +1,7 @@ import aiohttp import json -from bot.config import BOT_TOKEN, WEBHOOK +from urllib.parse import urlencode +from bot.config import BOT_TOKEN import logging # Create a logger instance @@ -10,269 +11,9 @@ logging.basicConfig(level=logging.INFO) apiBase = f"https://api.telegram.org/bot{BOT_TOKEN}/" -async def register_webhook(): +async def telegram_api(endpoint: str, **kwargs): async with aiohttp.ClientSession() as session: - async with session.get(apiBase + f"setWebhook?url={WEBHOOK}") as response: + async with session.get(apiBase + f"{endpoint}?{urlencode(kwargs)}") as response: data = await response.json() - logger.info("Webhook registration response: %s", data) - return data - - -# https://core.telegram.org/bots/api#sendmessage -async def send_message(cid: str, body, reply_to=None, reply_markup=None): - if not body: - return - url = apiBase + f"sendMessage" - params = {"chat_id": cid, "text": body, "parse_mode": "HTML"} - if reply_to: - params["reply_to_message_id"] = reply_to - if reply_markup: - params["reply_markup"] = json.dumps(reply_markup) - - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Message sent to %s: %s", cid, data) - return data - - -# https://core.telegram.org/bots/api#sendchataction -async def send_chataction(cid: str, action: str): - url = apiBase + f"sendChatAction" - params = {"chat_id": cid, "action": action} - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Chat action sent to %s: %s", cid, data) - return data - - -# https://core.telegram.org/bots/api#forwardmessage -async def forward_message(cid, mid, to_chat_id): - url = apiBase + f"forwardMessage" - params = {"chat_id": to_chat_id, "from_chat_id": cid, "message_id": mid} - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Message forwarded from %s to %s: %s", cid, to_chat_id, data) - return data - - -# https://core.telegram.org/bots/api#deletemessage -async def delete_message(cid: str, mid: str): - url = apiBase + f"deleteMessage" - params = {"chat_id": cid, "message_id": mid} - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - return data - - -# https://core.telegram.org/bots/api#sendphoto -async def send_photo( - cid: str, file_id: str, caption="", reply_to=None, reply_markup=None -): - url = apiBase + f"sendPhoto" - params = { - "chat_id": cid, - "photo": file_id, - "caption": caption, - "parse_mode": "HTML", - } - if reply_to: - params["reply_to_message_id"] = reply_to - if reply_markup: - params["reply_markup"] = json.dumps(reply_markup) - - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Photo sent to %s: %s", cid, data) - return data - - -# https://core.telegram.org/bots/api#banchatmember -async def ban_member(chat_id, user_id, until_date=None): - url = apiBase + f"banChatMember" - params = {"chat_id": chat_id, "user_id": user_id} - if until_date: - params["until_date"] = until_date - - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Member banned from %s: %s", chat_id, data) - return data - - -# https://core.telegram.org/bots/api#unbanchatmember -async def unban_member(chat_id, user_id): - url = apiBase + f"unbanChatMember" - params = {"chat_id": chat_id, "user_id": user_id, "only_if_banned": 1} - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Member unbanned from %s: %s", chat_id, data) - return data - - -# https://core.telegram.org/bots/api#addchatmember -async def add_member(chat_id, user_id): - url = apiBase + f"addChatMember" - params = {"chat_id": chat_id, "user_id": user_id} - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Member added to %s: %s", chat_id, data) - return data - - -# https://core.telegram.org/bots/api#restrictchatmember -async def mute_member(chat_id, member_id): - url = apiBase + f"restrictChatMember" - params = { - "chat_id": chat_id, - "user_id": member_id, - "permissions": json.dumps({"can_send_messages": False}), - "use_independent_chat_permissions": 1, - } - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Member muted in %s: %s", chat_id, data) - return data - - -# https://core.telegram.org/bots/api#restrictchatmember -async def unmute_member(chat_id, member_id, chat_permissions=None): - if not chat_permissions: - chat_permissions = json.dumps( - { - "can_send_messages": True, - "can_send_photos": True, - "can_send_other_messages": True, - "can_send_polls": True, - "can_add_web_page_previews": True, - "can_send_audios": True, - "can_invite_users": True, - "can_send_voice_notes": True, - "can_send_video_notes": True, - "can_send_videos": True, - "can_send_documents": True, - } - ) - - url = apiBase + f"restrictChatMember" - params = { - "chat_id": chat_id, - "user_id": member_id, - "permissions": chat_permissions, - "use_independent_chat_permissions": 1, - } - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Member unmuted in %s: %s", chat_id, data) - return data - - -# https://core.telegram.org/bots/api#approvechatjoinrequest -async def approve_chat_join_request(chat_id, user_id): - url = apiBase + f"approveChatJoinRequest" - params = {"chat_id": chat_id, "user_id": user_id} - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Chat join request approved in %s: %s", chat_id, data) - return data - - -# https://core.telegram.org/bots/api#senddocument -async def send_document(chat_id, data="", filename="chart.svg"): - url = apiBase + "sendDocument" - params = {"chat_id": chat_id} - filedata = aiohttp.FormData() - filedata.add_field("document", data, filename=filename) - - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params, data=filedata) as response: - if response.status != 200: - error_text = await response.text() - print(f"Error sending document: {response.status} - {error_text}") - return None - - try: - return await response.json() - except ValueError as e: - print(f"Error decoding JSON: {e}") - return None - - -# https://core.telegram.org/bots/api#getchatadministrators -async def get_chat_administrators(chat_id): - url = apiBase + f"getChatAdministrators" - params = {"chat_id": chat_id} - async with aiohttp.ClientSession() as session: - async with session.get(url, params=params) as response: - data = await response.json() - logger.info("Chat administrators retrieved for %s: %s", chat_id, data) - return data - - -# https://core.telegram.org/bots/api#getchatmember -async def get_member(chat_id, member_id): - url = apiBase + f"getChatMember" - params = {"chat_id": chat_id, "user_id": member_id} - async with aiohttp.ClientSession() as session: - async with session.get(url, params=params) as response: - data = await response.json() - logger.info("Chat member retrieved for %s: %s", chat_id, data) - return data - - -# https://core.telegram.org/bots/api#getuserprofilephotos -async def get_userphotos(user_id): - url = apiBase + f"getUserProfilePhotos" - params = {"user_id": user_id} - async with aiohttp.ClientSession() as session: - async with session.get(url, params=params) as response: - data = await response.json() - logger.info("User profile photos retrieved for %s: %s", user_id, data) - return data - - -# https://core.telegram.org/bots/api#editmessagereplymarkup -async def edit_replymarkup(cid, mid, reply_markup): - url = apiBase + f"editMessageReplyMarkup" - params = { - "chat_id": cid, - "message_id": mid, - "reply_markup": json.dumps(reply_markup), - } - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Reply markup edited for message %s in %s: %s", mid, cid, data) - return data - - -# https://core.telegram.org/bots/api#getchat -async def get_chat(cid): - url = apiBase + f"getChat" - params = {"chat_id": cid} - async with aiohttp.ClientSession() as session: - async with session.get(url, params=params) as response: - data = await response.json() - logger.info("Chat retrieved for %s: %s", cid, data) - return data - - -# https://core.telegram.org/bots/api#banchatmember -async def kick_member(chat_id, member_id): - url = apiBase + f"banChatMember" - params = {"chat_id": chat_id, "user_id": member_id} - async with aiohttp.ClientSession() as session: - async with session.post(url, params=params) as response: - data = await response.json() - logger.info("Member kicked from %s: %s", chat_id, data) + logger.info("Telegram API response: %s", data) return data diff --git a/bot/config.py b/bot/config.py index c9cee7f..4993fa1 100644 --- a/bot/config.py +++ b/bot/config.py @@ -1,6 +1,4 @@ import os BOT_TOKEN = os.environ.get("BOT_TOKEN") or "" -WEBHOOK = os.environ.get("VERCEL_URL") or "http://localhost:8000" -REDIS_URL = os.environ.get("REDIS_URL") or "redis://localhost:6379" FEEDBACK_CHAT_ID = os.environ.get("FEEDBACK_CHAT_ID", "").replace("-", "-100") diff --git a/bot/state.py b/bot/state.py deleted file mode 100644 index 897109f..0000000 --- a/bot/state.py +++ /dev/null @@ -1,12 +0,0 @@ -class State: - def __init__(self): - self.talking = dict() - - def is_talking(self, uid): - return uid in self.talking - - def make_talking(self, uid, cid): - self.talking[uid] = cid - - def aho(self, uid): - del self.talking[uid] diff --git a/bot/talking.py b/bot/talking.py new file mode 100644 index 0000000..97c91b3 --- /dev/null +++ b/bot/talking.py @@ -0,0 +1,16 @@ +class TalkingStick: + def __init__(self): + self.chat_by_talking = dict() + self.talking_by_chat = dict() + + def holder(self, cid): + return self.talking_by_chat[cid] + + def take(self, uid, cid): + self.chat_by_talking[uid] = cid + self.talking_by_chat[cid] = uid + + def aho(self, uid): + cid = self.chat_by_talking[uid] + del self.talking_by_chat[cid] + del self.chat_by_talking[uid] diff --git a/handlers/callback_unlink.py b/handlers/callback_unlink.py deleted file mode 100644 index 18b9b49..0000000 --- a/handlers/callback_unlink.py +++ /dev/null @@ -1,60 +0,0 @@ -from bot.api import send_message, delete_message, kick_member -from handlers.command_my import handle_command_my -from handlers.callback_vouch import update_button -from utils.mention import userdata_extract -from storage import Profile -import logging - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -# remove link of callback sender -# from member vouched before -async def handle_unlink(payload, state): - logger.info("handle unlink button pressed or command, private chat only") - - from_id = str(payload["from"]["id"]) - linked_id = "" - if "data" in payload: - linked_id = str(payload["data"].replace("unlink", "")) - elif "text" in payload: - linked_id = str(payload["text"].replace("/unlink ", "")) - - # удаляем связь с потомком - actor = Profile.get(from_id, payload) - actor["children"].remove(str(linked_id)) - Profile.save(actor) - - # удаляем связь с предком - linked = Profile.get(linked_id) - linked["parents"].remove(str(from_id)) - Profile.save(linked) - - # удаляем старое сообщение с кнопками-unlink - reply_msg_id = payload["message"]["message_id"] - r = await delete_message(from_id, reply_msg_id) - logger.debug(r) - - # если ещё есть связи - посылаем новое сообщение - if len(actor["children"]) > 0: - await handle_command_my(payload, state) - - lang = payload["from"].get("language_code", "ru") - for chat_id in linked["chats"]: - # если больше никто не поручился - kick out - if len(linked["parents"]) == 0: - r = await kick_member(chat_id, linked_id) - logger.debug(r) - if r["ok"]: - _, identity, username = userdata_extract(linked["result"]["user"]) - body = ( - "Участник %s%s был удалён" - if lang == "ru" - else "Member %s%s was deleted" - ) % (identity, username) - r = await send_message(chat_id, body) - logger.debug(r) - - # обновление счётчика - await update_button(linked_id, chat_id) diff --git a/handlers/callback_vouch.py b/handlers/callback_vouch.py deleted file mode 100644 index 5fab9d2..0000000 --- a/handlers/callback_vouch.py +++ /dev/null @@ -1,62 +0,0 @@ -from bot.api import approve_chat_join_request, edit_replymarkup -from storage import Profile, storage -import logging - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -async def update_button(chat_id, member_id, text="❤️"): - button_message_id = storage.get(f"btn-{chat_id}-{member_id}") - print(f"button_message_id: {button_message_id}") - if button_message_id: - button_message_id = button_message_id.decode("utf-8") - print(f"button_message_id: {button_message_id}") - print("update reply markup") - newcomer = Profile.get(member_id) - amount = len(newcomer["parents"]) + 1 - text = f"❤️ {amount}" - rm = { - "inline_keyboard": [[{"text": text, "callback_data": "vouch" + member_id}]] - } - r = await edit_replymarkup(chat_id, button_message_id, reply_markup=rm) - print(r) - - -async def handle_button(callback_query): - # получаем профиль нажавшего кнопку - actor_id = str(callback_query["from"]["id"]) - actor = Profile.get(actor_id, callback_query) - - callback_data = callback_query["data"] - if callback_data.startswith("vouch"): - print(f"button pressed by {actor_id}") - - newcomer_id = callback_data[5:] - print(f"button pressed for {newcomer_id}") - - newcomer = Profile.get(newcomer_id) - print(f"newcomer profile {newcomer}") - if newcomer_id == actor_id: - # нажал сам, не реагируем, прописываем данные - _newcomer = Profile.get(newcomer_id, callback_query) - else: - # нажал кто-то другой - - if str(actor_id) not in newcomer["parents"]: - print(f"save parent for {newcomer_id}") - newcomer["parents"].append(str(actor_id)) - Profile.save(newcomer) - - if str(newcomer_id) not in actor["children"]: - print(f"save child for {actor_id}") - actor["children"].append(str(newcomer_id)) - Profile.save(actor) - - chat_id = str(vars(callback_query["message"]["chat"])["id"]) - - print("accept join request") - r = await approve_chat_join_request(chat_id, newcomer_id) - print(r) - - await update_button(chat_id, newcomer_id) diff --git a/handlers/command_ask.py b/handlers/command_ask.py deleted file mode 100644 index 544a322..0000000 --- a/handlers/command_ask.py +++ /dev/null @@ -1,21 +0,0 @@ -from storage import Profile -from handlers.send_button import show_request_msg -from bot.api import get_member -import logging -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -async def handle_command_ask(msg): - logger.info("handling request resend") - _cmd, chat_id, member_id = msg["text"].split(" ") - chat_id = chat_id.replace("-", "-100") - r = await get_member(chat_id, member_id) - logger.debug(r) - m = {} - if "result" in r: - m["from"] = r["result"]["user"] - m["chat"] = {"id": str(chat_id)} - await show_request_msg(m) - elif "error_code" in r: - logger.error(r) diff --git a/handlers/command_graph.py b/handlers/command_graph.py deleted file mode 100644 index 50caab1..0000000 --- a/handlers/command_graph.py +++ /dev/null @@ -1,15 +0,0 @@ -from utils.graph import generate_chart -from bot.api import send_document -from storage import storage, scan -import json - -import logging -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -async def handle_command_graph(msg): - _usr_ids, members = scan(match="usr-*", count=100) - data = generate_chart(members) - if data: - r = await send_document(msg["chat"]["id"], data, "chart.svg") - logger.debug(r) diff --git a/handlers/command_my.py b/handlers/command_my.py deleted file mode 100644 index cff976c..0000000 --- a/handlers/command_my.py +++ /dev/null @@ -1,55 +0,0 @@ -from storage import Profile, scan -from bot.api import get_member, send_message -from utils.mention import userdata_extract -import json -import logging -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -async def construct_unlink_buttons(actor): - print(f"constructing unlink buttons for {actor['children']}") - buttons = [] - for vouch in actor["children"]: - for chat_id in actor["chats"]: - r = await get_member(chat_id, vouch) - logger.debug(r) - if "result" in r: - member = r["result"]["user"] - _uid, identity, username = userdata_extract(member) - buttons.append( - {"text": f"{identity} {username}", "callback_data": "unlink" + vouch} - ) - return buttons - - -async def handle_command_my(msg, state): - logger.info("handle my command") - from_id = str(msg["from"]["id"]) - sender = Profile.get(from_id, msg) - - # генерируем кнопки для всех, за кого поручились - buttons = await construct_unlink_buttons(sender) - reply_markup = { - "inline_keyboard": [ - buttons, - ] - } - if len(buttons) == 0: - if msg["from"].get("language_code", "ru") == "ru": - body = "Вас ещё никто не узнал? Напишите, я передам нашему кругу" - else: - body = ( - "Nobody recognized you? Speak, I will pass your message to the circle" - ) - r = await send_message(from_id, body) - logger.debug(r) - chat_id = msg["chat"]["id"] - state.make_talking(from_id, chat_id) - else: - if msg["from"].get("language_code", "ru") == "ru": - body = "Нажмите кнопки ниже, чтобы удалить ваши связи" - else: - body = "Unlink your connections pressing the buttons below" - - r = await send_message(from_id, body, reply_markup=reply_markup) - print(r) diff --git a/handlers/handle_default.py b/handlers/handle_default.py deleted file mode 100644 index 1f8e565..0000000 --- a/handlers/handle_default.py +++ /dev/null @@ -1,46 +0,0 @@ -from bot.api import send_message, delete_message, get_chat_administrators -from handlers.command_my import handle_command_my -from storage import Profile, storage -from handlers.send_button import show_request_msg -import logging -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -async def handle_default(msg, state): - logger.info(f"default handler for all messages {msg}") - chat_id = str(msg["chat"]["id"]) - from_id = str(msg["from"]["id"]) - sender = Profile.get(from_id, msg) - text = msg.get("text", "") - if text.startswith("/my"): - # команда в групповом чате - logger.info("remove some messages in group chat") - - # удалить сообщение с командой /my - r = await delete_message(chat_id, msg["message_id"]) - logger.debug(r) - - # показать связи в личке - await handle_command_my(msg, state) - else: - # любое другое сообщение - if len(sender["parents"]) == 0: - # владелец чата автоматически ручается - logger.info(f"setting owner as parent for {from_id}") - r = await get_chat_administrators(chat_id) - logger.debug(r) - owner_id = "" - for admin in r["result"]: - if admin["status"] == "creator": - owner_id = admin["user"]["id"] - break - if owner_id: - sender["parents"].append(str(owner_id)) - # обновляем профиль владельца - owner = Profile.get(owner_id) - owner["children"].append(str(from_id)) - Profile.save(owner) - - # сохранить профиль отправителя - Profile.save(sender) diff --git a/handlers/handle_feedback.py b/handlers/handle_feedback.py deleted file mode 100644 index e8207e4..0000000 --- a/handlers/handle_feedback.py +++ /dev/null @@ -1,65 +0,0 @@ -import json - -from bot.api import ( - send_message, - forward_message, - get_chat_administrators, -) -from storage import storage -from bot.config import FEEDBACK_CHAT_ID -import logging - - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -async def handle_feedback(msg, state): - mid = msg["message_id"] - cid = msg["chat"]["id"] - uid = msg["from"]["id"] - if msg["text"] == "/start": - r = await send_message(cid, "Напишите своё сообщение для администрации чата") - logger.debug(r) - elif state.is_talking(uid): - r = await forward_message(cid, mid, state.talking[uid]) - logger.debug(r) - state.aho(uid) - else: - r = await forward_message(cid, mid, FEEDBACK_CHAT_ID) - logger.debug(r) - support_msg_id = r["result"]["message_id"] - # сохранение айди сообщения в приватной переписке с ботом - storage.set( - f"fbk-{support_msg_id}", - json.dumps( - {"author_id": uid, "message_id": mid, "chat_id": cid} - ), - ) - - -async def handle_answer(msg): - logger.info("handle answering feedback") - logger.debug(msg) - answered_msg = msg.get("reply_to_message") - if answered_msg: - if "from" not in answered_msg: - answered_msg["from"] = msg.get("from_user") - r = await get_chat_administrators(msg["chat"]["id"]) - logger.debug(r) - admins = [] - for a in r["result"]: - admins.append(a["user"]["id"]) - if answered_msg["from"]["is_bot"] and msg["from"]["id"] in admins: - support_msg_id = str(answered_msg["message_id"]) - # получение сохраненного информации о сообщении для ответа - stored_feedback = storage.get(f"fbk-{support_msg_id}") - if stored_feedback: - logger.info("handle an answer from feedback group") - stored_feedback = json.loads(stored_feedback) - r = await send_message( - f'{stored_feedback["chat_id"]}', - msg["text"], - reply_to=stored_feedback["message_id"], - ) - logger.debug(r) diff --git a/handlers/handle_join_request.py b/handlers/handle_join_request.py index f11aa2e..3327067 100644 --- a/handlers/handle_join_request.py +++ b/handlers/handle_join_request.py @@ -1,22 +1,28 @@ -from bot.api import approve_chat_join_request, delete_message -from handlers.send_button import show_request_msg -from storage import Profile, storage +from bot.api import telegram_api +from bot.announce import show_announce import logging + logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) + +positive_reactions = [] # TODO: set positive reaction kinds + async def handle_join_request(join_request): logger.info(f"handle join request {join_request}") - chat_id = str(join_request["chat"]["id"]) - from_id = str(join_request["from"]["id"]) - actor = Profile.get(from_id, join_request) - if len(actor["parents"]) == 0: - # показываем сообщение с кнопкой "поручиться" - await show_request_msg(join_request) - else: - # за пользователя поручились ранее - r = await approve_chat_join_request(chat_id, from_id) + # показываем сообщение с кнопкой "поручиться" + await show_announce(join_request) + + +async def handle_reaction_on_request(update): + chat_id = str(update["chat"]["id"]) + from_id = str(update["from"]["id"]) + + logger.debug(update) + reaction = None # TODO: get reaction kind from update + if reaction in positive_reactions: + # за пользователя поручились + r = await telegram_api("approveChatJoinRequest", chat_id=chat_id, from_id=from_id) logger.debug(r) - Profile.save(actor) diff --git a/handlers/handle_members_change.py b/handlers/handle_members_change.py deleted file mode 100644 index d1e3ecb..0000000 --- a/handlers/handle_members_change.py +++ /dev/null @@ -1,47 +0,0 @@ -from handlers.send_button import show_request_msg -from bot.api import delete_message -from storage import Profile, storage -from bot.config import FEEDBACK_CHAT_ID - -import logging -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -async def handle_join(msg): - chat_id = str(msg["chat"]["id"]) - from_id = str(msg["from"]["id"]) - - actor = Profile.get(from_id, msg) - - newcomer_id = str(msg["new_chat_member"]["id"]) - if from_id == newcomer_id: - if len(actor["parents"]) == 0 and str(chat_id) != FEEDBACK_CHAT_ID: - # показываем сообщение с кнопкой "поручиться" - r = await show_request_msg(msg) - logger.debug(r) - else: - # за пользователя поручились ранее - pass - else: - # пользователи приглашены другим участником - logger.info(f'{len(msg["new_chat_members"])} members were invited by {from_id}') - for m in msg["new_chat_members"]: - newcomer = Profile.get(m["id"]) - newcomer["parents"].append(str(from_id)) - Profile.save(newcomer) - actor["children"].append(str(m["id"])) - # обновляем профиль пригласившего - Profile.save(actor) - - -async def handle_left(msg): - logger.info("handling member leaving") - member_id = msg["left_chat_member"]["id"] - chat_id = msg["chat"]["id"] - - # удаление сообщения с кнопкой в этом чате - prev_msg = storage.get(f"btn-{chat_id}-{member_id}") - if prev_msg: - r = await delete_message(chat_id, prev_msg["message_id"]) - logger.debug(r) - storage.remove(f"btn-{chat_id}-{member_id}") diff --git a/handlers/handle_private.py b/handlers/handle_private.py new file mode 100644 index 0000000..fa8efa5 --- /dev/null +++ b/handlers/handle_private.py @@ -0,0 +1,26 @@ +from bot.config import FEEDBACK_CHAT_ID +from bot.announce import edit_announce +from bot.api import telegram_api +import logging + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +start_message = { + 'en': "Welcome home! You can type any message here to be passed to chat", + 'ru': "Доброе утро! Можешь напечатать здесь любое сообщение для передачи в чат" +} + +async def handle_private(msg, state): + text = msg.get("text") + sender = msg.get("from", {}) + lang = sender.get("language_code", "ru") + if lang != "ru" and lang != "en": + lang = "en" + if text.startswith("/"): + if text == '/start': + await telegram_api("sendMessage", chat_id=sender.get("id"), text=start_message[lang]) + else: + await telegram_api("forwardMessage", from_chat_id=sender.get("id"), message_id=msg.get("id"), chat_id=FEEDBACK_CHAT_ID) + else: + await edit_announce(msg) diff --git a/handlers/handle_startup.py b/handlers/handle_startup.py deleted file mode 100644 index e4766f6..0000000 --- a/handlers/handle_startup.py +++ /dev/null @@ -1,45 +0,0 @@ -from bot.config import FEEDBACK_CHAT_ID -from storage import scan, Profile -from bot.api import approve_chat_join_request, kick_member, send_message -from handlers.callback_vouch import update_button -from utils.mention import userdata_extract - -import logging - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -async def revalidate_storage(): - # поддерживает консистентность данных - btn_ids, _btns = scan(match="btn-*", count=100) - logger.info(f"storage data revalidation for {len(btn_ids)} entries") - for btnid in btn_ids: - # для каждой ранее созданной кнопки - btnid_str = btnid.decode("utf-8").replace("btn-", "") - parts = btnid_str.split("-") - logger.debug(parts) - _, chat_id, member_id = parts - chat_id = "-" + chat_id - newcomer = Profile.get(member_id) - if len(newcomer.get("parents", [])) > 0: - # принять заявку если её нажимали - r = await approve_chat_join_request(chat_id, member_id) - logger.debug(r) - await update_button(chat_id, member_id) - elif len(newcomer.get("parents", [])) == 0: - r = await kick_member(chat_id, member_id) - logger.debug(r) - if r["ok"]: - _, identity, username = userdata_extract(newcomer["result"]["user"]) - # feedback report - body = f"Участник {identity} {username} был удалён" - r = await send_message(FEEDBACK_CHAT_ID, body) - logger.debug(r) - # pm report - body = f"Вы утратили поддержку в чате и были удалены" - r = await send_message(member_id, body) - logger.debug(r) - -async def handle_startup(): - await revalidate_storage() diff --git a/handlers/messages_routing.py b/handlers/messages_routing.py new file mode 100644 index 0000000..582ccd1 --- /dev/null +++ b/handlers/messages_routing.py @@ -0,0 +1,32 @@ +from bot.api import telegram_api +from bot.config import FEEDBACK_CHAT_ID + +import logging + +from handlers.handle_private import handle_private +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +async def messages_routing(msg, state): + cid = msg["chat"]["id"] + uid = msg["from"]["id"] + text = msg.get("text") + + if cid == uid: + # сообщения в личке с ботом + logger.info("private chat message") + await handle_private(msg, state) + + elif str(cid) == FEEDBACK_CHAT_ID: + # сообщения из группы обратной связи + logger.info("feedback chat message") + logger.debug(msg) + reply_msg = msg.get("reply_to_message") + if reply_msg: + reply_chat_id = reply_msg.get("chat", {}).get("id") + if reply_chat_id != FEEDBACK_CHAT_ID: + await telegram_api("sendMessage", chat_id=reply_chat_id, text=text, reply_to=reply_msg.get("message_id")) + + else: + pass diff --git a/handlers/routing.py b/handlers/routing.py deleted file mode 100644 index 54be943..0000000 --- a/handlers/routing.py +++ /dev/null @@ -1,41 +0,0 @@ -from handlers.handle_feedback import handle_feedback, handle_answer -from handlers.handle_default import handle_default -from handlers.command_my import handle_command_my -from handlers.command_graph import handle_command_graph -from handlers.command_ask import handle_command_ask -from bot.config import FEEDBACK_CHAT_ID - -import logging -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -async def handle_routing(msg, state): - cid = msg["chat"]["id"] - uid = msg["from"]["id"] - if cid == uid: - # сообщения в личке с ботом - logger.info("private chat message") - text = msg.get("text") - if text: - if text.startswith("/my"): - await handle_command_my(msg, state) - elif text.startswith("/graph"): - await handle_command_graph(msg) - else: - await handle_feedback(msg, state) - - elif str(cid) == FEEDBACK_CHAT_ID: - # сообщения из группы обратной связи - logger.info("feedback chat message") - logger.debug(msg) - if msg.get("reply_to_message"): - await handle_answer(msg) - elif msg.get("text", "").startswith("/ask"): - await handle_command_ask(msg) - - else: - # сообщения из всех остальных групп - logger.info(f"group {cid} chat message") - text = msg.get("text", msg.get("caption")) - if text: - await handle_default(msg, state) diff --git a/handlers/send_button.py b/handlers/send_button.py deleted file mode 100644 index 52ac60f..0000000 --- a/handlers/send_button.py +++ /dev/null @@ -1,56 +0,0 @@ -from bot.api import send_message, send_photo, get_userphotos, delete_message -from utils.mention import mention, userdata_extract -from storage import storage - -import logging - - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -async def show_request_msg(msg, auto = False): - logger.info("showing request with button") - chat_id = str(msg["chat"]["id"]) - from_id = str(msg["from"]["id"]) - lang = msg["from"].get("language_code", "ru") - reply_markup = { - "inline_keyboard": [[{"text": "❤️", "callback_data": "vouch" + from_id}]] - } - newcomer_message = ( - "Нажмите, чтобы одобрить заявку " - if lang == "ru" - else "There is a newcomer, press the button if you are connected with " - ) - r = await get_userphotos(user_id=from_id) - logger.debug(r) - if r["ok"] and r["result"]["total_count"] > 0: - logger.info("showing button with photo") - file_id = r["result"]["photos"][0][0]["file_id"] - _uid, identity, username = userdata_extract(msg["from"]) - r = await send_photo( - chat_id, - file_id, - caption=newcomer_message + f"{identity}{username}", - reply_to=msg.get("message_id", ""), - reply_markup=reply_markup, - ) - else: - logger.info("showing button without photo") - r = await send_message( - chat_id, - newcomer_message + mention(msg["from"]), - reply_to=msg.get("message_id", ""), - reply_markup=reply_markup, - ) - logger.debug(r) - if "message_id" in r: - # удаляем предыдущее сообщение с кнопкой в этом чате - prevbtn = storage.get(f"btn-{chat_id}-{from_id}") - if prevbtn: - r = await delete_message(chat_id, prevbtn) - logger.debug(r) - # создаём новое - newbtn = r["message_id"] - logger.info(f"button message id: {newbtn}") - storage.set(f"btn-{chat_id}-{from_id}", newbtn) diff --git a/main.py b/main.py index 3243676..7f9714b 100644 --- a/main.py +++ b/main.py @@ -1,21 +1,17 @@ import asyncio import logging import signal +import sys from aiohttp import ClientSession -from handlers.command_my import send_message -from handlers.routing import handle_routing -from handlers.callback_unlink import handle_unlink -from handlers.callback_vouch import handle_button +from handlers.commands import handle_command +from handlers.messages_routing import messages_routing from handlers.handle_join_request import handle_join_request -from handlers.handle_startup import handle_startup -from handlers.handle_members_change import handle_join, handle_left from bot.config import BOT_TOKEN, FEEDBACK_CHAT_ID -from bot.state import State -from storage import Profile +from bot.api import send_message logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) -state = State() +state = dict() api_url = f'https://api.telegram.org/bot{BOT_TOKEN}/' @@ -25,87 +21,33 @@ async def fetch(session, url): return await response.json() -async def command_start_handler(message): - caption = "Напишите своё сообщение для нас" if message.from_user.llanguage_code == 'ru' else "Write your message for us" - await message.answer(caption) - - -async def process_callback(cbq): - try: - data = cbq.get("data") - if data.startswith("vouch"): - await handle_button(cbq) - elif data.startswith("unlink"): - await handle_unlink(cbq, state) - except Exception as e: - logger.error(f"[main.process_callback] ERROR {e}") - logger.debug(cbq) - import traceback - text = traceback.format_exc() - await send_message(FEEDBACK_CHAT_ID, text) - - -async def join_request_handler(join_request): - try: - await handle_join_request(join_request) - except Exception as e: - logger.error(f"[main.join_request_handler] ERROR {e}") - logger.debug(join_request) - import traceback - text = traceback.format_exc() - await send_message(FEEDBACK_CHAT_ID, text) - - -async def all_handler(message): - try: - await handle_routing(message, state) - except Exception as e: - logger.error(f"[main.all_handler] ERROR {e}") - logger.debug(message) - import traceback - text = traceback.format_exc() - await send_message(FEEDBACK_CHAT_ID, text) - - -async def chat_members_change(update): - try: - old = update.get("old_chat_member") - if old: - if old.get("status") == 'KICKED': - Profile.erase(update["from"]["id"]) - await handle_left(update) - elif update.get("new_chat_member"): - await handle_join(update) - else: - logger.info("unhandled members update") - - except Exception as e: - logger.error(f"[main.my_chat_member] ERROR {e}") - logger.debug(update) - import traceback - text = traceback.format_exc() - await send_message(FEEDBACK_CHAT_ID, text) - - async def main(): - # storage revalidation - await handle_startup() - async with ClientSession() as session: offset = 0 # начальное значение offset while True: updates = await fetch(session, f"{api_url}getUpdates?offset={offset}") - for update in updates["result"]: - if "message" in update: - await all_handler(update["message"]) - elif "callback_query" in update: - await process_callback(update["callback_query"]) - elif "join_chat_request" in update: - await join_request_handler(update["join_chat_request"]) - elif "my_chat_member" in update: - await chat_members_change(update["my_chat_member"]) + for update in updates.get("result", []): + try: + message = update.get("message") + join_chat_request = update.get("join_chat_request") + if message: + text = message.get("text") + if text.startswith('/'): + await handle_command(message, state) + else: + await messages_routing(message, state) - offset = update["update_id"] + 1 # обновление offset + elif join_chat_request: + await handle_join_request(join_chat_request) + + except Exception as e: + logger.error(e) + logger.debug(update) + import traceback + text = traceback.format_exc() + await send_message(FEEDBACK_CHAT_ID, text) + + offset = update["update_id"] + 1 await asyncio.sleep(1.0) diff --git a/storage/__init__.py b/storage/__init__.py deleted file mode 100644 index 74d8e23..0000000 --- a/storage/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -from redis import Redis -from storage.profile import Profile as ProfileObj -from bot.config import REDIS_URL -import json - - -# сохраняет сессии, айди кнопок в чатах для удаления и пересылаемые сообщения между перезагрузками -storage = Redis.from_url(REDIS_URL) - -# хранение необходимой информации о пользователях -Profile = ProfileObj(storage) - - -# достаёт из хранилища jsonы по маске и количеству -def scan(match="usr-*", count=100): - cursor = 0 - keys = [] - r = storage - while True: - # Scan for keys starting with in batches of - cursor, batch_keys = r.scan(cursor=cursor, match=match, count=count) - keys += batch_keys - # If the cursor is 0, then we've reached the end of the keys - if cursor == 0: - break - # Get the values of all the keys - values = r.mget(keys) - # Parse the JSON data from each value - items = [] - for value in values: - if value: - value_str = value.decode("utf-8") - i = json.loads(value_str) - items.append(i) - print(f"scan found {len(items)} items") - - return keys, items diff --git a/storage/profile.py b/storage/profile.py deleted file mode 100644 index 2664ac9..0000000 --- a/storage/profile.py +++ /dev/null @@ -1,47 +0,0 @@ -import json - - -class Profile: - def __init__(self, storage): - self.storage = storage - - def create(self, member_id, msg=None): - s = {"id": member_id, "parents": [], "children": [], "chats": []} - - if msg: - if "from" in msg: - sender = msg.get("from") - s["mention"] = sender.get("username") - s[ - "name" - ] = f"{sender['first_name']} {sender.get('last_name', '')}".strip() - - if "chat" in msg: - chat_id = str(msg["chat"]["id"]) - if chat_id not in s["chats"]: - s["chats"].append(chat_id) - - self.storage.set(f"usr-{member_id}", json.dumps(s)) - return s - - def save(self, s): - self.storage.set(f'usr-{s["id"]}', json.dumps(s)) - - def get(self, member_id, msg=None): - data = self.storage.get(f"usr-{member_id}") - if data is None: - r = self.create(member_id, msg) - else: - r = json.loads(data) - return r - - def erase(self, member_id): - data = self.storage.get(f"usr-{member_id}") - if data: - member = json.loads(data) - for child in member["children"]: - child_member = self.storage.get(f"usr-{child}") - if child_member: - child_member = json.loads(child_member) - child_member["parents"].remove(member_id) - self.storage.set(f"usr-{child_member['id']}", json.dumps(child_member)) diff --git a/utils/mention.py b/utils/mention.py index f19a197..09e7059 100644 --- a/utils/mention.py +++ b/utils/mention.py @@ -16,6 +16,4 @@ def userdata_extract(user): identity = f"{user['first_name']}{ln}" uid = user["id"] username = user.get("username", "") - if username: - username = f"(@{username})" return uid, identity, username