diff --git a/.gitignore b/.gitignore index 04b2945..b23e6d6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ __pycache__ .DS_Store .vercel api/payload.json -update.json \ No newline at end of file +update.json +.vscode \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b37721f..0dd197b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## [0.0.10] + +- добавлено фото к заявке пользователя, если есть +- изменена надпись на русском +- исправлены ошибки +- добавлена сервисная команда для показа потерянных заявок + +## [0.0.9] + +- исправление логики show_request_msg +- логика перепроверки на старте +- двуязычный интерфейс без переменных среды +- kick для тех, от кого отказались поручители +- bugfix: нестандартные символы в имени + ## [0.0.8] - генерация древовидного графа, с опорой на одного участника diff --git a/README.md b/README.md index f70504a..faf3216 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,4 @@ 4. Настроить переменные среды: - BOT_TOKEN - токен бота созданный с помощью @BotFather - - NEWCOMER_MSG - текст сообщения о новом пользователе, за которым следует его имя - - BUTTON_VOUCH - текст кнопки "поручиться" - FEEDBACK_CHAT_ID - айди чата для обратной связи diff --git a/api/webhook.py b/api/webhook.py index fe1746c..1b4fda9 100644 --- a/api/webhook.py +++ b/api/webhook.py @@ -1,7 +1,7 @@ from sanic import Sanic from sanic.response import text -from tgbot.config import WEBHOOK, FEEDBACK_CHAT_ID, BUTTON_VOUCH +from tgbot.config import WEBHOOK, FEEDBACK_CHAT_ID from tgbot.handlers.handle_feedback import handle_feedback, handle_answer from tgbot.handlers.handle_members_change import handle_join, handle_left @@ -9,9 +9,10 @@ from tgbot.handlers.handle_join_request import handle_join_request from tgbot.handlers.handle_default import handle_default from tgbot.handlers.command_my import handle_command_my from tgbot.handlers.command_graph import handle_command_graph +from tgbot.handlers.command_ask import handle_command_ask from tgbot.handlers.callback_vouch import handle_button from tgbot.handlers.callback_unlink import handle_unlink - +from tgbot.handlers.handle_startup import handle_startup from tgbot.api import register_webhook, send_message @@ -28,6 +29,7 @@ async def register(req): app.config.REGISTERED = True print(r) res = 'ok' + handle_startup() return text(res) @@ -43,7 +45,8 @@ async def handle(req): if msg: if 'text' in msg: if msg['chat']['id'] == msg['from']['id']: - if msg['text'] == '/my': + print('private chat message') + if msg['text'].startswith('/my'): handle_command_my(msg) else: handle_feedback(msg) @@ -52,6 +55,8 @@ async def handle(req): handle_answer(msg) elif msg['text'] == '/graph': await handle_command_graph(msg) + elif msg['text'].startswith('/ask'): + handle_command_ask(msg) else: handle_default(msg) elif 'new_chat_member' in msg: @@ -64,7 +69,7 @@ async def handle(req): # кнопки elif 'callback_query' in update: data = update['callback_query']['data'] - if data.startswith(BUTTON_VOUCH): + if data.startswith('vouch'): handle_button(update['callback_query']) elif data.startswith('unlink'): handle_unlink(update['callback_query']) @@ -81,6 +86,6 @@ async def handle(req): except Exception: import traceback - r = send_message(FEEDBACK_CHAT_ID, f'
{traceback.format_exc()}
') + r = send_message(FEEDBACK_CHAT_ID, f'
\n{traceback.format_exc()}\n
') traceback.print_exc() return text('ok') diff --git a/tgbot/api.py b/tgbot/api.py index 4b3c7d4..d7aaf30 100644 --- a/tgbot/api.py +++ b/tgbot/api.py @@ -29,9 +29,21 @@ def send_message(cid: str, body, reply_to=None, reply_markup=None): reply_markup = json.dumps(reply_markup) reply_markup = requests.utils.quote(reply_markup) url += f'&reply_markup={reply_markup}' - url += f'&parse_mode=HTML' + url += f'&parse_mode=html' + r = requests.post(url) + return r.json() + +# https://core.telegram.org/bots/api#sendphoto +def send_photo(cid: str, file_id: str, caption="", reply_to=None, reply_markup=None): + url = apiBase + f"sendPhoto?chat_id={cid}&photo={file_id}" + if reply_to: + url += f'&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' r = requests.post(url) - print(f'{url}') return r.json() @@ -69,7 +81,8 @@ 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'&user_id={member_id}&permissions={chat_permissions}' + \ + f'&use_independent_chat_permissions=1' r = requests.post(url) return r.json() @@ -79,7 +92,8 @@ def unmute_member(chat_id, member_id): chat_permissions = json.dumps({ "can_send_messages": 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'&user_id={member_id}&permissions={chat_permissions}' + \ + f'&use_independent_chat_permissions=1' r = requests.post(url) return r.json() @@ -113,29 +127,6 @@ async def send_document(chat_id, data='', filename='chart.svg'): return None - -# https://core.telegram.org/bots/api#sendphoto -async def send_photo(chat_id, img_data, filename='chart.png'): - url = apiBase + f"sendPhoto" - params = {"chat_id": chat_id } - filedata = aiohttp.FormData() - filedata.add_field('photo', img_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 photo: {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 def get_chat_administrators(chat_id): url = apiBase + f"getChatAdministrators?chat_id={chat_id}" @@ -143,8 +134,15 @@ def get_chat_administrators(chat_id): return r.json() -# # https://core.telegram.org/bots/api#getchatmember +# https://core.telegram.org/bots/api#getchatmember def get_member(chat_id, member_id): url = apiBase + f"getChatMember?chat_id={chat_id}&user_id={member_id}" r = requests.get(url) + return r.json() + + +# 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() \ No newline at end of file diff --git a/tgbot/config.py b/tgbot/config.py index fd6d6da..3dd9205 100644 --- a/tgbot/config.py +++ b/tgbot/config.py @@ -3,6 +3,4 @@ import os WEBHOOK = os.environ.get('VERCEL_URL') or 'http://localhost:8000' REDIS_URL = os.environ.get('REDIS_URL') or 'redis://localhost:6379' -NEWCOMER_MSG = os.environ.get('NEWCOMER_MSG') or "There is a newcomer, press the button if you are connected" -BUTTON_VOUCH = os.environ.get('BUTTON_VOUCH') or 'My connection!' FEEDBACK_CHAT_ID = os.environ.get('FEEDBACK_CHAT_ID').replace("-", "-100") \ No newline at end of file diff --git a/tgbot/handlers/callback_unlink.py b/tgbot/handlers/callback_unlink.py index cf87eb4..83a3573 100644 --- a/tgbot/handlers/callback_unlink.py +++ b/tgbot/handlers/callback_unlink.py @@ -1,4 +1,5 @@ from tgbot.api import send_message, delete_message +from tgbot.handlers.command_my import handle_command_my from tgbot.storage import Profile # remove link of callback sender @@ -7,17 +8,28 @@ def handle_unlink(callback_query): print('handle unlink button pressed, private chat only') from_id = str(callback_query['from']['id']) - reply_msg_id = callback_query['message']['message_id'] + linked_id = str(callback_query['data'].replace('unlink', '')) + # удаляем связь с потомком actor = Profile.get(from_id, callback_query) - actor['parents'].remove(from_id) - + actor['children'].remove(linked_id) + Profile.save(actor) + + # удаляем связь с предком + linked = Profile.get(linked_id) + linked['parents'].remove(from_id) + Profile.save(linked) + # удаляем старое сообщение с кнопками + reply_msg_id = callback_query['message']['message_id'] r = delete_message(from_id, reply_msg_id) print(r) + + # если ещё есть связи - посылаем новое сообщение + if len(actor['children']) > 0: + handle_command_my(callback_query) - # если ещё есть связи - посылаем новое сообщение - if len(actor['parents']) > 0: - body = construct_unlink_buttons(actor) - r = send_message(from_id, body) - print(r) \ No newline at end of file + # если больше никто не поручился - мьютим + if len(linked['parents']) == 0: + for chat_id in linked['chats']: + mute_member(chat_id, linked_id) \ No newline at end of file diff --git a/tgbot/handlers/callback_vouch.py b/tgbot/handlers/callback_vouch.py index 82f615d..219fb55 100644 --- a/tgbot/handlers/callback_vouch.py +++ b/tgbot/handlers/callback_vouch.py @@ -1,6 +1,5 @@ -from tgbot.api import send_message, forward_message, delete_message +from tgbot.api import send_message, forward_message, delete_message, approve_chat_join_request, unmute_member from tgbot.storage import Profile -from tgbot.config import BUTTON_VOUCH def handle_button(callback_query): @@ -9,31 +8,37 @@ def handle_button(callback_query): actor = Profile.get(actor_id, callback_query) callback_data = callback_query['data'] - if callback_data.startswith(BUTTON_VOUCH): - print(f'vouch button pressed by {actor_id}') + 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_id = callback_data[len(BUTTON_VOUCH):] newcomer = Profile.get(newcomer_id) + print(f'newcomer profile {newcomer}') if newcomer_id == actor_id: # нажал сам, не реагируем, прописываем данные newcomer = Profile.get(newcomer_id, callback_query) - elif newcomer_id not in actor['children'] and \ - actor_id not in newcomer['parents']: - newcomer['parents'].append(actor_id) - actor['children'].append(newcomer_id) - Profile.save(newcomer) - Profile.save(actor) - try: - chat_id = str(callback_query['message']['chat']['id']) + else: + # нажал кто-то другой - print('unmute newcomer') + if actor_id not in newcomer['parents']: + print(f'save parent for {newcomer_id}') + newcomer['parents'].append(actor_id) + Profile.save(newcomer) + + if newcomer_id not in actor['children']: + print(f'save child for {actor_id}') + actor['children'].append(newcomer_id) + Profile.save(actor) + + chat_id = str(callback_query['message']['chat']['id']) + + print('accept join request') + r = approve_chat_join_request(chat_id, newcomer_id) + print(r) + + if not r.get('ok'): + print('try to unmute newcomer') r = unmute_member(chat_id, newcomer_id) - print(r) - - print('accept join request') - r = approve_chat_join_request(chat_id, newcomer_id) - print(r) - - except: - pass - \ No newline at end of file + print(r) \ No newline at end of file diff --git a/tgbot/handlers/command_ask.py b/tgbot/handlers/command_ask.py new file mode 100644 index 0000000..d3d28fa --- /dev/null +++ b/tgbot/handlers/command_ask.py @@ -0,0 +1,14 @@ +from tgbot.storage import Profile +from tgbot.handlers.send_button import show_request_msg +from tgbot.api import get_member + +def handle_command_ask(msg): + print(f'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(r) + m = {} + m['from'] = r['result']['user'] + m['chat'] = { 'id': chat_id } + show_request_msg(m) \ No newline at end of file diff --git a/tgbot/handlers/command_graph.py b/tgbot/handlers/command_graph.py index 516225f..9dd0a9c 100644 --- a/tgbot/handlers/command_graph.py +++ b/tgbot/handlers/command_graph.py @@ -1,29 +1,11 @@ from tgbot.utils.graph import generate_chart from tgbot.api import send_document -from tgbot.storage import storage +from tgbot.storage import storage, scan import json async def handle_command_graph(msg): - cursor = 0 - keys = [] - r = storage - while True: - # Scan for keys starting with 'usr-*' in batches of 100 - cursor, batch_keys = r.scan(cursor=cursor, match='usr-*', count=100) - 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 - members = [] - for value in values: - value_str = value.decode('utf-8') - member = json.loads(value_str) - members.append(member) - print(f'found {len(members)} members') + usr_ids, members = scan(match='usr-*', count=100) data = generate_chart(members) if data: r = await send_document(msg['chat']['id'], data, 'chart.svg') diff --git a/tgbot/handlers/command_my.py b/tgbot/handlers/command_my.py index b8df0fe..e1ca85d 100644 --- a/tgbot/handlers/command_my.py +++ b/tgbot/handlers/command_my.py @@ -1,33 +1,26 @@ from tgbot.storage import Profile from tgbot.api import get_member, send_message +from tgbot.utils.mention import userdata_extract + def construct_unlink_buttons(actor): buttons = [] for vouch in actor['children']: - vouch_added = False for chat_id in actor['chats']: - if not vouch_added: - r = get_member(chat_id, vouch) - member = r.get('result') - if member: - try: - buttons.append({ - 'text': mention(r['result']), - 'callback_data': 'unlink' + vouch - }) - vouch_added = True - except: - print('member result error') - print(member) - else: - print(r) + 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 + }) return { "inline_keyboard": [ buttons, ] } + def handle_command_my(msg): print(f'handle my command') from_id = str(msg['from']['id']) sender = Profile.get(from_id, msg) - # генерируем кнопки для всех, за кого поручились reply_markup = construct_unlink_buttons(sender) diff --git a/tgbot/handlers/handle_default.py b/tgbot/handlers/handle_default.py index be28c74..939d310 100644 --- a/tgbot/handlers/handle_default.py +++ b/tgbot/handlers/handle_default.py @@ -17,15 +17,8 @@ def handle_default(msg): r = delete_message(chat_id, msg['message_id']) print(r) - # удалить предыдушее сообщение с кнопкой в этом чате - prev_msg_id = storage.get(f'btn-{chat_id}-{from_id}') - if prev_msg_id: - r = delete_message(chat_id, prev_msg_id) - print(r) - # показать новое сообщение с кнопкой - btn_msg_id = show_request_msg(msg) - storage.set(f'btn-{chat_id}-{from_id}', btn_msg_id) + show_request_msg(msg) else: # любое другое сообщение if len(sender['parents']) == 0: @@ -38,12 +31,12 @@ def handle_default(msg): if admin['status'] == 'creator': owner_id = admin['user']['id'] break - - sender['parents'].append(owner_id) - # обновляем профиль владельца - owner = Profile.get(owner_id) - owner['children'].append(from_id) - Profile.save(owner) + if owner_id: + sender['parents'].append(owner_id) + # обновляем профиль владельца + owner = Profile.get(owner_id) + owner['children'].append(from_id) + Profile.save(owner) # сохранить профиль отправителя Profile.save(sender) \ No newline at end of file diff --git a/tgbot/handlers/handle_feedback.py b/tgbot/handlers/handle_feedback.py index 57b0c26..fcf069b 100644 --- a/tgbot/handlers/handle_feedback.py +++ b/tgbot/handlers/handle_feedback.py @@ -1,6 +1,8 @@ import json from tgbot.api import send_message, forward_message, delete_message +from tgbot.handlers.send_button import show_request_msg +from tgbot.utils.mention import userdata_extract from tgbot.storage import storage, Profile from tgbot.config import FEEDBACK_CHAT_ID diff --git a/tgbot/handlers/handle_join_request.py b/tgbot/handlers/handle_join_request.py index 8c5ed07..a23e498 100644 --- a/tgbot/handlers/handle_join_request.py +++ b/tgbot/handlers/handle_join_request.py @@ -11,13 +11,7 @@ def handle_join_request(msg): if len(actor['parents']) == 0: # показываем сообщение с кнопкой "поручиться" - btn_msg_id = show_request_msg(msg) - # удаляем предыдущее сообщение с кнопкой в этом чате - prev_msg_id = storage.get(f'btn-{chat_id}-{from_id}') - if prev_msg_id: - r = delete_message(chat_id, prev_msg_id) - print(r) - storage.set(f'btn-{chat_id}-{from_id}', btn_msg_id) + show_request_msg(msg) else: # за пользователя поручились ранее r = approve_chat_join_request(chat_id, from_id) diff --git a/tgbot/handlers/handle_members_change.py b/tgbot/handlers/handle_members_change.py index 6d13ddb..6722f16 100644 --- a/tgbot/handlers/handle_members_change.py +++ b/tgbot/handlers/handle_members_change.py @@ -12,8 +12,7 @@ def handle_join(msg): if from_id == newcomer_id: if len(actor['parents']) == 0: # показываем сообщение с кнопкой "поручиться" - btn_msg_id = show_request_msg(msg) - storage.set(f'btn-{chat_id}-{from_id}', btn_msg_id) + show_request_msg(msg) # до одобрения - мьют r = mute_member(chat_id, newcomer_id) @@ -43,9 +42,9 @@ def handle_left(msg): chat_id = msg['chat']['id'] # удаление сообщения с кнопкой в этом чате - prev_msg_id = storage.get(f'btn-{chat_id}-{member_id}') + prev_msg = storage.get(f'btn-{chat_id}-{member_id}') if prev_msg_id: - r = delete_message(chat_id, prev_msg_id) + r = delete_message(chat_id, prev_msg['id']) print(r) storage.remove(f'btn-{chat_id}-{member_id}') diff --git a/tgbot/handlers/handle_startup.py b/tgbot/handlers/handle_startup.py new file mode 100644 index 0000000..8dfda91 --- /dev/null +++ b/tgbot/handlers/handle_startup.py @@ -0,0 +1,18 @@ +from tgbot.storage import scan, Profile + +# устанавливает соответствие данных +def handle_startup(): + btn_ids, btns = scan(match='btn-*', count=100) + for btnid in btn_ids: + # для каждой ранее созданной кнопки + try: + btnid_str = btnid.decode("utf-8") + chat_id, member_id = btnid_str[3:].split('-') + + newcomer = Profile.get(member_id) + if len(newcomer.get('parents', [])) > 0: + # принять заявку если её нажимали + r = approve_chat_join_request(chat_id, member_id) + print(r) + except: + print(f'error {btnid}') \ No newline at end of file diff --git a/tgbot/handlers/send_button.py b/tgbot/handlers/send_button.py index b847272..45d370f 100644 --- a/tgbot/handlers/send_button.py +++ b/tgbot/handlers/send_button.py @@ -1,25 +1,49 @@ -from tgbot.api import send_message -from tgbot.config import BUTTON_VOUCH, NEWCOMER_MSG +from tgbot.api import send_message, send_photo, get_userphotos from tgbot.utils.mention import mention +from tgbot.storage import storage def show_request_msg(msg): + chat_id = str(msg['chat']['id']) + from_id = str(msg['from']['id']) + lang = msg['from'].get('language_code', 'ru') reply_markup = { "inline_keyboard": [ [ { - "text": BUTTON_VOUCH, - "callback_data": BUTTON_VOUCH + str(msg['from']['id']) + "text": 'Моё одобрение' if lang == 'ru' else 'My connection', + "callback_data": 'vouch' + from_id } ] ] } - - r = send_message( - msg['chat']['id'], - NEWCOMER_MSG + mention(msg['from']), - reply_to=msg.get('message_id', ''), - reply_markup=reply_markup - ) - btn_msg_id = r['result']['message_id'] - print(f'request message id: {btn_msg_id}') - return btn_msg_id + 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) + print(r) + if r['ok'] and r['result']['total_count'] > 0: + file_id = r['result']['photos'][0][0]['file_id'] + r = send_photo( + chat_id, + file_id, + caption=newcomer_message + mention(msg['from']), + reply_to=msg.get('message_id', ''), + reply_markup=reply_markup + ) + else: + r = send_photo( + chat_id, + newcomer_message + mention(msg['from']), + reply_to=msg.get('message_id', ''), + reply_markup=reply_markup + ) + print(r) + if 'message_id' in r: + # удаляем предыдущее сообщение с кнопкой в этом чате + prevbtn = storage.get(f'btn-{chat_id}-{from_id}') + if prevbtn: + r = 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) diff --git a/tgbot/storage/__init__.py b/tgbot/storage/__init__.py index c82adfa..a263b38 100644 --- a/tgbot/storage/__init__.py +++ b/tgbot/storage/__init__.py @@ -1,9 +1,35 @@ import redis from tgbot.storage.profile import Profile as ProfileObj from tgbot.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: + value_str = value.decode('utf-8') + i = json.loads(value_str) + items.append(i) + print(f'scan found {len(items)} items') + + return keys, items \ No newline at end of file diff --git a/tgbot/storage/profile.py b/tgbot/storage/profile.py index 2099180..f29d54d 100644 --- a/tgbot/storage/profile.py +++ b/tgbot/storage/profile.py @@ -14,15 +14,17 @@ class Profile: "chats": [] } - if msg.get('from'): - sender = msg.get('from') - s["mention"] = sender.get('username') - s["name"] = f"{sender['first_name']} {sender.get('last_name', '')}".strip() + 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 msg.get('chat'): - chat_id = str(msg['chat']['id']) - if chat_id not in s['chats']: - s["chats"].append(chat_id) + 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 diff --git a/tgbot/utils/mention.py b/tgbot/utils/mention.py index 4e85c1e..93935ce 100644 --- a/tgbot/utils/mention.py +++ b/tgbot/utils/mention.py @@ -1,9 +1,20 @@ +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) + # generates a mention from standard telegram web json 'from' field # using HTML markup def mention(user): + uid, identity, username = userdata_extract(user) + identity = escape_username(identity) + return f'{identity} {username}' + + +def userdata_extract(user): identity = f"{user['first_name']} {user.get('last_name', '')}".strip() uid = user['id'] username = user.get('username', '') if username: username = f'(@{username})' - return f'{identity}{username}' + return uid, identity, username +