diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d657a4..323f9ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - исправление обработки случая без фото - добавлен автомат состояний - добавлена возможность высказаться "на кругу" без одобрения заявки +- асинхронное api ## [0.0.11] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cc7d15e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.11-slim +WORKDIR /app +COPY . /app +COPY requirements.txt /app/ +RUN apt-get update && apt-get install -y git build-essential libffi-dev libssl-dev +RUN pip install --trusted-host pypi.python.org -r /app/requirements.txt +CMD ["python", "bot/main.py"] \ No newline at end of file diff --git a/Procfile b/Procfile deleted file mode 100644 index 5459f93..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: python api/webhook.py diff --git a/api/webhook.py b/api/webhook.py index 5583697..852efa0 100644 --- a/api/webhook.py +++ b/api/webhook.py @@ -1,16 +1,10 @@ -from bot.handlers.handle_feedback import handle_feedback, handle_answer -from bot.handlers.handle_members_change import handle_join, handle_left -from bot.handlers.handle_join_request import handle_join_request -from bot.handlers.handle_default import handle_default -from bot.handlers.command_my import handle_command_my -from bot.handlers.command_graph import handle_command_graph -from bot.handlers.command_ask import handle_command_ask +from bot.handlers.routing import handle_routing from bot.handlers.callback_vouch import handle_button from bot.handlers.callback_unlink import handle_unlink from bot.handlers.handle_startup import handle_startup +from bot.handlers.handle_join_request import handle_join_request from bot.api import register_webhook, send_message from bot.config import FEEDBACK_CHAT_ID -from bot.state import State from sanic.app import Sanic app = Sanic(name="welcomecenter") @@ -19,16 +13,16 @@ app.config.REGISTERED = False state = State() -@app.get('/') +@app.get("/") async def register(req): if not app.config.REGISTERED: print(register_webhook()) app.config.REGISTERED = True - handle_startup() - return text('ok') - + await handle_startup() + return text("ok") -@app.post('/') + +@app.post("/") async def handle(req): print(req) try: @@ -36,57 +30,27 @@ async def handle(req): print(update) # видимые сообщения - msg = update.get('message', update.get('edited_message')) + msg = update.get("message", update.get("edited_message")) if msg: - msg['edit'] = 'edited_message' in update - - if msg['chat']['id'] == msg['from']['id']: - # сообщения в личке с ботом - print('private chat message') - if msg['text'].startswith('/my'): - handle_command_my(msg) - elif msg['text'].startswith('/unlink'): - handle_unlink(msg) - else: - handle_feedback(msg, state) - - elif str(msg['chat']['id']) == FEEDBACK_CHAT_ID: - # сообщения из группы обратной связи - print('feedback chat message') - if 'reply_to_message' in msg: - handle_answer(msg) - elif msg['text'] == '/graph': - await handle_command_graph(msg) - elif msg['text'].startswith('/ask'): - handle_command_ask(msg) - - else: - # сообщения из всех остальных групп - cid = msg['chat']['id'] - print(f'group {cid} chat message') - if 'text' in msg: - handle_default(msg) - elif 'new_chat_member' in msg: - handle_join(msg) - elif 'left_chat_member' in msg: - handle_left(msg) + msg["edit"] = "edited_message" in update + await handle_routing(msg) # кнопки - elif 'callback_query' in update: - data = update['callback_query']['data'] - if data.startswith('vouch'): - handle_button(update['callback_query']) - elif data.startswith('unlink'): - handle_unlink(update['callback_query']) + elif "callback_query" in update: + data = update["callback_query"]["data"] + if data.startswith("vouch"): + await handle_button(update["callback_query"]) + elif data.startswith("unlink"): + await handle_unlink(update["callback_query"]) # заявки - elif 'chat_join_request' in update: - print('chat join request') - handle_join_request(update['chat_join_request']) + elif "chat_join_request" in update: + print("chat join request") + await handle_join_request(update["chat_join_request"]) - except Exception: import traceback - r = send_message(FEEDBACK_CHAT_ID, f'
\n{traceback.format_exc()}\n') + + await send_message(FEEDBACK_CHAT_ID, f"
\n{traceback.format_exc()}\n") traceback.print_exc() - return text('ok') + return text("ok") diff --git a/bot/api.py b/bot/api.py index 8392268..8b65fb1 100644 --- a/bot/api.py +++ b/bot/api.py @@ -1,131 +1,198 @@ -import requests import aiohttp import json -import os from bot.config import BOT_TOKEN, WEBHOOK +import logging +# Create a logger instance +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) apiBase = f"https://api.telegram.org/bot{BOT_TOKEN}/" -def register_webhook(): - r = requests.get(apiBase + f'setWebhook?url={WEBHOOK}') - return r.json() - - -def delete_message(cid: str, mid: str): - url = apiBase + f"deleteMessage?chat_id={cid}&message_id={mid}" - r = requests.post(url) - return r.json() +async def register_webhook(): + async with aiohttp.ClientSession() as session: + async with session.get(apiBase + f"setWebhook?url={WEBHOOK}") as response: + data = await response.json() + logger.info("Webhook registration response: %s", data) + return data # https://core.telegram.org/bots/api#sendmessage -def send_message(cid: str, body, reply_to=None, reply_markup=None): - url = f"sendMessage?chat_id={cid}&text={body}" +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: - url += f'&reply_to_message_id={reply_to}' + params["reply_to_message_id"] = reply_to if reply_markup: - reply_markup = json.dumps(reply_markup) - reply_markup = requests.utils.quote(reply_markup) - url += f'&reply_markup={reply_markup}' - url += f'&parse_mode=html' - print(url) - r = requests.post(apiBase + url) - return r.json() + 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 -def send_photo(cid: str, file_id: str, caption="", reply_to=None, reply_markup=None): - url = f"sendPhoto?chat_id={cid}&photo={file_id}&caption={caption}" +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: - url += f'&reply_to_message_id={reply_to}' + params["reply_to_message_id"] = reply_to if reply_markup: - reply_markup = json.dumps(reply_markup) - reply_markup = requests.utils.quote(reply_markup) - url += f'&reply_markup={reply_markup}' - url += f'&parse_mode=html' - print(url) - r = requests.post(apiBase + url) - return r.json() + 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 -def ban_member(chat_id, user_id, until_date=None): - url = apiBase + f"banChatMember?chat_id={chat_id}&user_id={user_id}" +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: - url += f'&until_data={until_date}' - r = requests.post(url) - return r.json() + 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 -def unban_member(chat_id, user_id): - url = apiBase + f"unbanChatMember?chat_id={chat_id}&user_id={user_id}&only_if_banned=1" - r = requests.post(url) - return r.json() +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 -def add_member(chat_id, user_id): - url = apiBase + f"addChatMember?chat_id={chat_id}&user_id={user_id}" - r = requests.post(url) - return r.json() - - -def forward_message(cid, mid, to_chat_id): - url = apiBase + f"forwardMessage?chat_id={to_chat_id}" + \ - f"&from_chat_id={cid}&message_id={mid}" - r = requests.post(url) - return r.json() +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 -def mute_member(chat_id, member_id): - chat_permissions = json.dumps({ "can_send_messages": False }) - chat_permissions = requests.utils.quote(chat_permissions) - url = apiBase + f'restrictChatMember?chat_id={chat_id}' + \ - f'&user_id={member_id}&permissions={chat_permissions}' + \ - f'&use_independent_chat_permissions=1' - r = requests.post(url) - return r.json() +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 -def unmute_member(chat_id, member_id, chat_permissions=None): +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 - }) - chat_permissions = requests.utils.quote(chat_permissions) - url = apiBase + f'restrictChatMember?chat_id={chat_id}' + \ - f'&user_id={member_id}&permissions={chat_permissions}' + \ - f'&use_independent_chat_permissions=1' - r = requests.post(url) - return r.json() + 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 -def approve_chat_join_request(chat_id, user_id): - url = apiBase + f"approveChatJoinRequest?chat_id={chat_id}" + \ - f'&user_id={user_id}' - r = requests.post(url) - return r.json() +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'): +async def send_document(chat_id, data="", filename="chart.svg"): url = apiBase + "sendDocument" - params = { "chat_id": chat_id } + params = {"chat_id": chat_id} filedata = aiohttp.FormData() - filedata.add_field('document', data, filename=filename) + filedata.add_field("document", data, filename=filename) async with aiohttp.ClientSession() as session: async with session.post(url, params=params, data=filedata) as response: @@ -142,46 +209,70 @@ async def send_document(chat_id, data='', filename='chart.svg'): # https://core.telegram.org/bots/api#getchatadministrators -def get_chat_administrators(chat_id): - url = apiBase + f"getChatAdministrators?chat_id={chat_id}" - r = requests.get(url) - return r.json() +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 -def get_member(chat_id, member_id): - url = apiBase + f"getChatMember?chat_id={member_id}&user_id={member_id}" - r = requests.get(url) - return r.json() +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 -def get_userphotos(user_id): - url = apiBase + f"getUserProfilePhotos?user_id={user_id}" - r = requests.get(url) - return r.json() +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 -def edit_replymarkup(cid, mid, reply_markup): - reply_markup = json.dumps(reply_markup) - reply_markup = requests.utils.quote(reply_markup) - url = f"editMessageReplyMarkup?chat_id={cid}&message_id={mid}&reply_markup={reply_markup}" - r = requests.post(apiBase + url) - return r.json() +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 -def get_chat(cid): - url = apiBase + f"getChat?chat_id={cid}" - r = requests.get(url) - return r.json() +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 -def kick_member(chat_id, member_id): - url = f"banChatSenderChat?chat_id={cid}&user_id={member_id}" - r = requests.post(apiBase + url) - print(r.json()) - url = f"unbanChatSenderChat?chat_id={cid}&user_id={member_id}&only_if_banned=1" - r = requests.post(apiBase + url) - return r.json() \ No newline at end of file +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) + return data diff --git a/bot/config.py b/bot/config.py index 46db746..ec39d99 100644 --- a/bot/config.py +++ b/bot/config.py @@ -1,6 +1,8 @@ import os -BOT_TOKEN = os.environ.get('BOT_TOKEN') or '5543739069:AAHCKC7C8vhFERw1KN2nCbg9olQ5UFBQWfE' -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") \ No newline at end of file +BOT_TOKEN = ( + os.environ.get("BOT_TOKEN") or "5543739069:AAHCKC7C8vhFERw1KN2nCbg9olQ5UFBQWfE" +) +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/handlers/callback_unlink.py b/bot/handlers/callback_unlink.py index 8264ad6..ba589e5 100644 --- a/bot/handlers/callback_unlink.py +++ b/bot/handlers/callback_unlink.py @@ -3,52 +3,53 @@ from bot.handlers.command_my import handle_command_my from bot.utils.mention import userdata_extract from bot.storage import Profile -# remove link of callback sender + +# remove link of callback sender # from member vouched before -def handle_unlink(payload): - print('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 ', '')) +async def handle_unlink(payload): + print("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)) + actor["children"].remove(str(linked_id)) Profile.save(actor) # удаляем связь с предком linked = Profile.get(linked_id) - linked['parents'].remove(str(from_id)) + linked["parents"].remove(str(from_id)) Profile.save(linked) - # удаляем старое сообщение с кнопками-unlink - reply_msg_id = payload['message']['message_id'] - r = delete_message(from_id, reply_msg_id) + reply_msg_id = payload["message"]["message_id"] + r = await delete_message(from_id, reply_msg_id) print(r) - - # если ещё есть связи - посылаем новое сообщение - if len(actor['children']) > 0: - handle_command_my(payload) - lang = payload['from'].get('language_code', 'ru') - for chat_id in linked['chats']: - + # если ещё есть связи - посылаем новое сообщение + if len(actor["children"]) > 0: + await handle_command_my(payload) + + lang = payload["from"].get("language_code", "ru") + for chat_id in linked["chats"]: # если больше никто не поручился - kick out - if len(linked['parents']) == 0: - r = kick_member(chat_id, linked_id) + if len(linked["parents"]) == 0: + r = await kick_member(chat_id, linked_id) print(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 = send_message(chat_id, body) + 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) print(r) # обновление счётчика update_button(linked_id, chat_id) - - \ No newline at end of file diff --git a/bot/handlers/callback_vouch.py b/bot/handlers/callback_vouch.py index cfcb0e9..8f4f803 100644 --- a/bot/handlers/callback_vouch.py +++ b/bot/handlers/callback_vouch.py @@ -1,66 +1,65 @@ -from bot.api import send_message, forward_message, delete_message, \ - approve_chat_join_request, edit_replymarkup, get_chat +from bot.api import ( + send_message, + forward_message, + delete_message, + approve_chat_join_request, + edit_replymarkup, + get_chat, +) from bot.storage import Profile, storage -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}') +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') + 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}' + amount = len(newcomer["parents"]) + 1 + text += f" {amount}" rm = { - "inline_keyboard": [ - [ - { - "text": text, - "callback_data": 'vouch' + member_id - } - ] - ] + "inline_keyboard": [[{"text": text, "callback_data": "vouch" + member_id}]] } - r = edit_replymarkup(chat_id, button_message_id, reply_markup=rm) + r = await edit_replymarkup(chat_id, button_message_id, reply_markup=rm) print(r) -def handle_button(callback_query): +async def handle_button(callback_query): # получаем профиль нажавшего кнопку - actor_id = str(callback_query['from']['id']) + 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}') + 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}') + print(f"button pressed for {newcomer_id}") newcomer = Profile.get(newcomer_id) - print(f'newcomer profile {newcomer}') + print(f"newcomer profile {newcomer}") if newcomer_id == actor_id: # нажал сам, не реагируем, прописываем данные - newcomer = Profile.get(newcomer_id, callback_query) + _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(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) + 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(callback_query['message']['chat']['id']) + chat_id = str(callback_query["message"]["chat"]["id"]) - print('accept join request') - r = approve_chat_join_request(chat_id, newcomer_id) - print(r) + print("accept join request") + r = await approve_chat_join_request(chat_id, newcomer_id) + print(r) - update_button(chat_id, newcomer_id) \ No newline at end of file + await update_button(chat_id, newcomer_id) diff --git a/bot/handlers/command_ask.py b/bot/handlers/command_ask.py index 4cc8718..55434ec 100644 --- a/bot/handlers/command_ask.py +++ b/bot/handlers/command_ask.py @@ -2,16 +2,17 @@ from bot.storage import Profile from bot.handlers.send_button import show_request_msg from bot.api import get_member + def handle_command_ask(msg): - print('handling request resend') - _cmd, chat_id, member_id = msg['text'].split(' ') - chat_id = chat_id.replace('-', '-100') - r = get_member(chat_id, member_id) + print("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) print(r) m = {} - if 'result' in r: - m['from'] = r['result']['user'] - m['chat'] = { 'id': str(chat_id) } - show_request_msg(m) - elif 'error_code' in r: - print(r) \ No newline at end of file + 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: + print(r) diff --git a/bot/handlers/command_graph.py b/bot/handlers/command_graph.py index 641f94f..4fd96d4 100644 --- a/bot/handlers/command_graph.py +++ b/bot/handlers/command_graph.py @@ -5,8 +5,8 @@ import json async def handle_command_graph(msg): - usr_ids, members = scan(match='usr-*', count=100) + _usr_ids, members = scan(match="usr-*", count=100) data = generate_chart(members) if data: - r = await send_document(msg['chat']['id'], data, 'chart.svg') + r = await send_document(msg["chat"]["id"], data, "chart.svg") print(r) diff --git a/bot/handlers/command_my.py b/bot/handlers/command_my.py index 17aedef..2379d90 100644 --- a/bot/handlers/command_my.py +++ b/bot/handlers/command_my.py @@ -3,64 +3,69 @@ from bot.api import get_member, send_message, get_chat_administrators from bot.utils.mention import userdata_extract -def construct_unlink_buttons(actor): +async def construct_unlink_buttons(actor): buttons = [] - for vouch in actor['children']: - for chat_id in actor['chats']: - r = get_member(chat_id, vouch) - member = r['result']['user'] - uid, identity, username = userdata_extract(member) - buttons.append({ - 'text': f'{identity} {username}', - 'callback_data': 'unlink' + vouch - }) + for vouch in actor["children"]: + for chat_id in actor["chats"]: + r = await get_member(chat_id, vouch) + member = r["result"]["user"] + _uid, identity, username = userdata_extract(member) + buttons.append( + {"text": f"{identity} {username}", "callback_data": "unlink" + vouch} + ) return buttons -def handle_command_my(msg, state): - print('handle my command') - from_id = str(msg['from']['id']) +async def handle_command_my(msg, state): + print("handle my command") + from_id = str(msg["from"]["id"]) sender = Profile.get(from_id, msg) - handle_command_owner_my(msg) + await handle_command_owner_my(msg) # генерируем кнопки для всех, за кого поручились - buttons = construct_unlink_buttons(sender) - reply_markup = { "inline_keyboard": [ buttons, ] } + buttons = await construct_unlink_buttons(sender) + reply_markup = { + "inline_keyboard": [ + buttons, + ] + } if len(buttons) == 0: - if msg['from'].get('language_code', 'ru') == 'ru': - body = 'Вас ещё никто не узнал? Напишите, я передам нашему кругу' + if msg["from"].get("language_code", "ru") == "ru": + body = "Вас ещё никто не узнал? Напишите, я передам нашему кругу" else: - body = 'Nobody recognized you? Speak, I will pass your message to the circle' - r = send_message(from_id, body) + body = ( + "Nobody recognized you? Speak, I will pass your message to the circle" + ) + r = await send_message(from_id, body) print(r) - chat_id = msg['chat']['id'] + chat_id = msg["chat"]["id"] state.make_talking(from_id, chat_id) else: - if msg['from'].get('language_code', 'ru') == 'ru': - body = 'Нажмите кнопки ниже, чтобы удалить ваши связи' + if msg["from"].get("language_code", "ru") == "ru": + body = "Нажмите кнопки ниже, чтобы удалить ваши связи" else: - body = 'Unlink your connections pressing the buttons below' + body = "Unlink your connections pressing the buttons below" - r = send_message(from_id, body, reply_markup=reply_markup) + r = await send_message(from_id, body, reply_markup=reply_markup) print(r) -def handle_command_owner_my(msg): - chat_id = msg['chat']['id'] - r = get_chat_administrators(chat_id) +async def handle_command_owner_my(msg): + chat_id = msg["chat"]["id"] + r = await get_chat_administrators(chat_id) print(r) - owner_id = '' - for admin in r['result']: - if admin['status'] == 'creator': - owner_id = str(admin['user']['id']) + owner_id = "" + for admin in r["result"]: + if admin["status"] == "creator": + owner_id = str(admin["user"]["id"]) break if owner_id: owner = Profile.get(owner_id, msg) _uids, members = scan() for mdata in members: - m = json.loads(mdata.decode('utf-8')) - if owner_id in m['parents']: - if str(m['id']) not in owner['children']: - owner['children'].append(str(m['id'])) - Profile.save(owner) \ No newline at end of file + m = json.loads(mdata.decode("utf-8")) + if owner_id in m["parents"]: + if str(m["id"]) not in owner["children"]: + owner["children"].append(str(m["id"])) + Profile.save(owner) diff --git a/bot/handlers/handle_default.py b/bot/handlers/handle_default.py index 5a40afd..d8b6939 100644 --- a/bot/handlers/handle_default.py +++ b/bot/handlers/handle_default.py @@ -2,18 +2,19 @@ from bot.api import send_message, delete_message, get_chat_administrators from bot.storage import Profile, storage from bot.handlers.send_button import show_request_msg -def handle_default(msg): - print('default handler for all messages') - chat_id = str(msg['chat']['id']) - from_id = str(msg['from']['id']) + +async def handle_default(msg): + print("default handler for all messages") + chat_id = str(msg["chat"]["id"]) + from_id = str(msg["from"]["id"]) sender = Profile.get(from_id, msg) - if msg['text'].startswith('/my'): + if msg["text"].startswith("/my"): # команда в групповом чате - print('remove some messages in group chat') + print("remove some messages in group chat") # удалить сообщение с командой /my - r = delete_message(chat_id, msg['message_id']) + r = await delete_message(chat_id, msg["message_id"]) print(r) # показать новое сообщение с кнопкой @@ -21,22 +22,22 @@ def handle_default(msg): show_request_msg(msg) else: # любое другое сообщение - if len(sender['parents']) == 0: + if len(sender["parents"]) == 0: # владелец чата автоматически ручается - print(f'setting owner as parent for {from_id}') - r = get_chat_administrators(chat_id) + print(f"setting owner as parent for {from_id}") + r = await get_chat_administrators(chat_id) print(r) - owner_id = '' - for admin in r['result']: - if admin['status'] == 'creator': - owner_id = admin['user']['id'] + 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)) + sender["parents"].append(str(owner_id)) # обновляем профиль владельца owner = Profile.get(owner_id) - owner['children'].append(str(from_id)) + owner["children"].append(str(from_id)) Profile.save(owner) # сохранить профиль отправителя - Profile.save(sender) \ No newline at end of file + Profile.save(sender) diff --git a/bot/handlers/handle_feedback.py b/bot/handlers/handle_feedback.py index 59a77db..ee51a73 100644 --- a/bot/handlers/handle_feedback.py +++ b/bot/handlers/handle_feedback.py @@ -1,51 +1,57 @@ import json -from bot.api import send_message, forward_message, delete_message, get_chat_administrators +from bot.api import ( + send_message, + forward_message, + delete_message, + get_chat_administrators, +) from bot.handlers.send_button import show_request_msg from bot.utils.mention import userdata_extract from bot.storage import storage, Profile from bot.config import FEEDBACK_CHAT_ID -def handle_feedback(msg, state): - mid = msg['message_id'] - cid = msg['chat']['id'] - if msg['text'] == '/start': - r = send_message(cid, 'Напишите своё сообщение для администрации чата') +async def handle_feedback(msg, state): + mid = msg["message_id"] + cid = msg["chat"]["id"] + if msg["text"] == "/start": + r = await send_message(cid, "Напишите своё сообщение для администрации чата") print(r) + elif state.is_talking(uid): + r = await forward_message(cid, mid, state.talking[uid]) + print(r) + state.aho(uid) else: - uid = msg['from']['id'] - if state.is_talking(uid): - r = forward_message(cid, mid, state.talking[uid]) - print(r) - state.aho(uid) - else: - r = forward_message(cid, mid, FEEDBACK_CHAT_ID) - print(r) - support_msg_id = r['result']['message_id'] - # сохранение айди сообщения в приватной переписке с ботом - storage.set(f'fbk-{support_msg_id}', json.dumps({ - "author_id": msg["from"]["id"], - "message_id": mid, - "chat_id": cid - })) + r = await forward_message(cid, mid, FEEDBACK_CHAT_ID) + print(r) + support_msg_id = r["result"]["message_id"] + # сохранение айди сообщения в приватной переписке с ботом + storage.set( + f"fbk-{support_msg_id}", + json.dumps( + {"author_id": msg["from"]["id"], "message_id": mid, "chat_id": cid} + ), + ) - -def handle_answer(msg): - answered_msg = msg['reply_to_message'] - r = get_chat_administrators(msg['chat']['id']) +async def handle_answer(msg): + answered_msg = msg["reply_to_message"] + r = await get_chat_administrators(msg["chat"]["id"]) print(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']) + 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}') + stored_feedback = storage.get(f"fbk-{support_msg_id}") if stored_feedback: - print('handle an answer from feedback group') + print("handle an answer from feedback group") stored_feedback = json.loads(stored_feedback) - r = send_message(f'{stored_feedback["chat_id"]}', msg['text'], reply_to=stored_feedback["message_id"]) + r = await send_message( + f'{stored_feedback["chat_id"]}', + msg["text"], + reply_to=stored_feedback["message_id"], + ) print(r) - diff --git a/bot/handlers/handle_join_request.py b/bot/handlers/handle_join_request.py index 35df7dc..74b243f 100644 --- a/bot/handlers/handle_join_request.py +++ b/bot/handlers/handle_join_request.py @@ -1,20 +1,19 @@ -from bot.api import approve_chat_join_request, delete_message +from bot.api import approve_chat_join_request, delete_message from bot.handlers.send_button import show_request_msg from bot.storage import Profile, storage -def handle_join_request(msg): - print(f'handle join request {msg}') - chat_id = str(msg['chat']['id']) - from_id = str(msg['from']['id']) +async def handle_join_request(msg): + print(f"handle join request {msg}") + chat_id = str(msg["chat"]["id"]) + from_id = str(msg["from"]["id"]) actor = Profile.get(from_id, msg) - if len(actor['parents']) == 0: + if len(actor["parents"]) == 0: # показываем сообщение с кнопкой "поручиться" - show_request_msg(msg) + await show_request_msg(msg) else: # за пользователя поручились ранее - r = approve_chat_join_request(chat_id, from_id) + r = await approve_chat_join_request(chat_id, from_id) print(r) Profile.save(actor) - diff --git a/bot/handlers/handle_members_change.py b/bot/handlers/handle_members_change.py index 56fb575..a16878b 100644 --- a/bot/handlers/handle_members_change.py +++ b/bot/handlers/handle_members_change.py @@ -3,42 +3,42 @@ from bot.api import delete_message from bot.storage import Profile, storage from bot.config import FEEDBACK_CHAT_ID -def handle_join(msg): - chat_id = str(msg['chat']['id']) - from_id = str(msg['from']['id']) + +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']) + 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: + if len(actor["parents"]) == 0 and str(chat_id) != FEEDBACK_CHAT_ID: # показываем сообщение с кнопкой "поручиться" - show_request_msg(msg) + r = await show_request_msg(msg) + print(r) else: # за пользователя поручились ранее pass else: # пользователи приглашены другим участником - print(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)) + print(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'])) + actor["children"].append(str(m["id"])) # обновляем профиль пригласившего Profile.save(actor) -def handle_left(msg): - print(f'handling member leaving') +async def handle_left(msg): + print("handling member leaving") member_id = msg["left_chat_member"]["id"] - chat_id = msg['chat']['id'] + chat_id = msg["chat"]["id"] # удаление сообщения с кнопкой в этом чате - prev_msg = storage.get(f'btn-{chat_id}-{member_id}') + prev_msg = storage.get(f"btn-{chat_id}-{member_id}") if prev_msg: - r = delete_message(chat_id, prev_msg['id']) + r = await delete_message(chat_id, prev_msg["id"]) print(r) - storage.remove(f'btn-{chat_id}-{member_id}') - - + storage.remove(f"btn-{chat_id}-{member_id}") diff --git a/bot/handlers/handle_startup.py b/bot/handlers/handle_startup.py index 6a89cea..3081af7 100644 --- a/bot/handlers/handle_startup.py +++ b/bot/handlers/handle_startup.py @@ -3,27 +3,32 @@ from bot.api import approve_chat_join_request, kick_member from bot.handlers.callback_vouch import update_button from bot.utils.mention import userdata_extract + # устанавливает соответствие данных -def handle_startup(): - btn_ids, btns = scan(match='btn-*', count=100) +async def handle_startup(): + btn_ids, _btns = scan(match="btn-*", count=100) for btnid in btn_ids: # для каждой ранее созданной кнопки btnid_str = btnid.decode("utf-8").replace("btn-", "") - parts = btnid_str.split('-') + parts = btnid_str.split("-") print(parts) _, chat_id, member_id = parts chat_id = "-" + chat_id newcomer = Profile.get(member_id) - if len(newcomer.get('parents', [])) > 0: + if len(newcomer.get("parents", [])) > 0: # принять заявку если её нажимали - r = approve_chat_join_request(chat_id, member_id) + r = await approve_chat_join_request(chat_id, member_id) print(r) update_button(chat_id, member_id) - elif len(newcomer.get('parents', [])) == 0: - r = kick_member(chat_id, member_id) + elif len(newcomer.get("parents", [])) == 0: + r = await kick_member(chat_id, member_id) print(r) - if r['ok']: - _, identity, username = userdata_extract(newcomer['result']['user']) - body = ('Участник %s%s был удалён' if lang == 'ru' else 'Member %s%s was deleted') % (identity, username) - r = send_message(chat_id, body) - print(r) \ No newline at end of file + if r["ok"]: + _, identity, username = userdata_extract(newcomer["result"]["user"]) + body = ( + "Участник %s%s был удалён" + if lang == "ru" + else "Member %s%s was deleted" + ) % (identity, username) + r = await send_message(chat_id, body) + print(r) diff --git a/bot/handlers/routing.py b/bot/handlers/routing.py new file mode 100644 index 0000000..dfd68f3 --- /dev/null +++ b/bot/handlers/routing.py @@ -0,0 +1,41 @@ +from bot.handlers.handle_feedback import handle_feedback, handle_answer +from bot.handlers.handle_members_change import handle_join, handle_left +from bot.handlers.handle_default import handle_default +from bot.handlers.command_my import handle_command_my +from bot.handlers.command_graph import handle_command_graph +from bot.handlers.command_ask import handle_command_ask +from bot.config import FEEDBACK_CHAT_ID + + +async def handle_routing(msg, state): + cid = msg["chat"]["id"] + uid = msg["from"]["id"] + if cid == uid: + # сообщения в личке с ботом + print("private chat message") + if msg["text"].startswith("/my"): + await handle_command_my(msg) + elif msg["text"].startswith("/unlink"): + await handle_unlink(msg) + else: + await handle_feedback(msg, state) + + elif str(cid) == FEEDBACK_CHAT_ID: + # сообщения из группы обратной связи + print("feedback chat message") + if "reply_to_message" in msg: + await handle_answer(msg) + elif msg["text"] == "/graph": + await handle_command_graph(msg) + elif msg["text"].startswith("/ask"): + await handle_command_ask(msg) + + else: + # сообщения из всех остальных групп + print(f"group {cid} chat message") + if "text" in msg: + await handle_default(msg) + elif "new_chat_member" in msg: + await handle_join(msg) + elif "left_chat_member" in msg: + await handle_left(msg) diff --git a/bot/handlers/send_button.py b/bot/handlers/send_button.py index 7fb6dc8..43901ce 100644 --- a/bot/handlers/send_button.py +++ b/bot/handlers/send_button.py @@ -2,52 +2,49 @@ from bot.api import send_message, send_photo, get_userphotos from bot.utils.mention import mention, userdata_extract from bot.storage import storage -def show_request_msg(msg): + +async def show_request_msg(msg): print("showing request with button") - chat_id = str(msg['chat']['id']) - from_id = str(msg['from']['id']) - lang = msg['from'].get('language_code', 'ru') + 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 - } - ] - ] + "inline_keyboard": [[{"text": "❤️", "callback_data": "vouch" + from_id}]] } - newcomer_message = "Нажмите, чтобы одобрить заявку " if lang == 'ru' \ + newcomer_message = ( + "Нажмите, чтобы одобрить заявку " + if lang == "ru" else "There is a newcomer, press the button if you are connected with " - r = get_userphotos(user_id=from_id) + ) + r = await get_userphotos(user_id=from_id) print(r) - if r['ok'] and r['result']['total_count'] > 0: - print('show button with photo') - file_id = r['result']['photos'][0][0]['file_id'] - _uid, identity, username = userdata_extract(msg['from']) + if r["ok"] and r["result"]["total_count"] > 0: + print("show button with photo") + file_id = r["result"]["photos"][0][0]["file_id"] + _uid, identity, username = userdata_extract(msg["from"]) r = send_photo( chat_id, file_id, - caption=newcomer_message + f'{identity}{username}', - reply_to=msg.get('message_id', ''), - reply_markup=reply_markup + caption=newcomer_message + f"{identity}{username}", + reply_to=msg.get("message_id", ""), + reply_markup=reply_markup, ) else: - print('show button without photo') + print("show button without photo") r = send_message( chat_id, - newcomer_message + mention(msg['from']), - reply_to=msg.get('message_id', ''), - reply_markup=reply_markup + newcomer_message + mention(msg["from"]), + reply_to=msg.get("message_id", ""), + reply_markup=reply_markup, ) print(r) - if 'message_id' in r: + if "message_id" in r: # удаляем предыдущее сообщение с кнопкой в этом чате - prevbtn = storage.get(f'btn-{chat_id}-{from_id}') + prevbtn = storage.get(f"btn-{chat_id}-{from_id}") if prevbtn: - r = delete_message(chat_id, prevbtn) + r = await delete_message(chat_id, prevbtn) print(r) # создаём новое - newbtn = r['message_id'] - print(f'button message id: {newbtn}') - storage.set(f'btn-{chat_id}-{from_id}', newbtn) + newbtn = r["message_id"] + print(f"button message id: {newbtn}") + storage.set(f"btn-{chat_id}-{from_id}", newbtn) diff --git a/bot/main.py b/bot/main.py new file mode 100644 index 0000000..1306757 --- /dev/null +++ b/bot/main.py @@ -0,0 +1,70 @@ +import asyncio +import logging +import sys +import signal # Import the signal module +from aiogram import Bot, Dispatcher, Router +from aiogram.enums import ParseMode +from aiogram.filters import CommandStart, Command +from aiogram.types import Message, ChatJoinRequest, CallbackQuery +from bot.config import BOT_TOKEN, ADMIN_ID +from bot.handlers.routing import handle_routing +from bot.state import State + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) +router = Router() +state = State() + + +@router.message(CommandStart()) +async def command_start_handler(message: Message) -> None: + await message.answer("Напишите своё сообщение для администрации чата") + + +@router.callback_query_handler(lambda x: True) +async def process_callback(callback_query: CallbackQuery): + cbq = vars(callback_query) + data = cbq["data"] + if data.startswith("vouch"): + await handle_button(cbq) + elif data.startswith("unlink"): + await handle_unlink(cbq) + + +@router.chat_join_request_handler() +async def join_request_handler(update: ChatJoinRequest) -> None: + print("chat join request") + join_request = vars(update) + await handle_join_request(join_request) + + +@router.message() +async def all_handler(message: Message) -> None: + msg = vars(message) + msg["from"] = vars(message.from_user) + msg["chat"] = vars(message.chat) + await handle_routing(msg) + await asyncio.sleep(1.0) + + +async def main() -> None: + bot = Bot(BOT_TOKEN, parse_mode=ParseMode.HTML) + dp = Dispatcher() + dp.include_router(router) + + # Start event dispatching + await dp.start_polling(bot) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, stream=sys.stdout) + + # Define a function to handle SIGTERM + def handle_sigterm(signum, frame): + logger.info("Received SIGTERM. Shutting down gracefully...") + asyncio.get_event_loop().stop() + + # Register the SIGTERM signal handler + signal.signal(signal.SIGTERM, handle_sigterm) + + asyncio.get_event_loop().run_until_complete(main()) diff --git a/bot/state.py b/bot/state.py index f1cab5f..897109f 100644 --- a/bot/state.py +++ b/bot/state.py @@ -1,10 +1,12 @@ - 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/storage/__init__.py b/bot/storage/__init__.py index 9caca15..27311be 100644 --- a/bot/storage/__init__.py +++ b/bot/storage/__init__.py @@ -10,8 +10,9 @@ storage = Redis.from_url(REDIS_URL) # хранение необходимой информации о пользователях Profile = ProfileObj(storage) + # достаёт из хранилища jsonы по маске и количеству -def scan(match='usr-*', count=100): +def scan(match="usr-*", count=100): cursor = 0 keys = [] r = storage @@ -27,9 +28,9 @@ def scan(match='usr-*', count=100): # Parse the JSON data from each value items = [] for value in values: - value_str = value.decode('utf-8') + value_str = value.decode("utf-8") i = json.loads(value_str) items.append(i) - print(f'scan found {len(items)} items') + print(f"scan found {len(items)} items") - return keys, items \ No newline at end of file + return keys, items diff --git a/bot/storage/profile.py b/bot/storage/profile.py index f29d54d..b1e8a3a 100644 --- a/bot/storage/profile.py +++ b/bot/storage/profile.py @@ -2,38 +2,33 @@ 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": [] - } + 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 "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']: + 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)) + 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}') + data = self.storage.get(f"usr-{member_id}") if data is None: r = self.create(member_id, msg) else: diff --git a/bot/utils/graph.py b/bot/utils/graph.py index 866bd67..378add6 100644 --- a/bot/utils/graph.py +++ b/bot/utils/graph.py @@ -1,14 +1,14 @@ # Define SVG code generation function with member_id parameter -def generate_chart(members, member_id = None): +def generate_chart(members, member_id=None): if not member_id: - member_id = members[0]['id'] + member_id = members[0]["id"] # Define some constants for layout node_radius = 30 node_spacing = 80 node_y = 100 parent_y_offset = 50 child_y_offset = 150 - + # Find the specified member and its descendants member = None descendants = set() @@ -17,7 +17,7 @@ def generate_chart(members, member_id = None): member = m descendants.add(member_id) break - + stack = member["children"].copy() while stack: child_id = stack.pop() @@ -33,12 +33,12 @@ def generate_chart(members, member_id = None): for i, m in enumerate(members): if m["id"] in descendants: x_positions[m["id"]] = (i * node_spacing) + node_radius - + # Start building the SVG code svg_width = (len(descendants) * node_spacing) + (2 * node_radius) svg_height = 200 svg_code = f'' - - return svg_code.encode('utf-8') \ No newline at end of file + svg_code += "" + + return svg_code.encode("utf-8") diff --git a/bot/utils/mention.py b/bot/utils/mention.py index 93935ce..af22e2d 100644 --- a/bot/utils/mention.py +++ b/bot/utils/mention.py @@ -1,6 +1,7 @@ def escape_username(username): # Replace any non-ASCII and non-alphanumeric characters with underscores - return ''.join(c if c.isalnum() or c.isspace() else '-' for c in username) + return "".join(c if c.isalnum() or c.isspace() else "-" for c in username) + # generates a mention from standard telegram web json 'from' field # using HTML markup @@ -12,9 +13,8 @@ def mention(user): def userdata_extract(user): identity = f"{user['first_name']} {user.get('last_name', '')}".strip() - uid = user['id'] - username = user.get('username', '') + uid = user["id"] + username = user.get("username", "") if username: - username = f'(@{username})' + username = f"(@{username})" return uid, identity, username - diff --git a/requirements.txt b/requirements.txt index fad73fc..f02328c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ sanic==19.6.0 -requests redis asyncio aiohttp \ No newline at end of file