diff --git a/CHANGELOG.md b/CHANGELOG.md index 66c2922..80867a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ +## [0.0.5] + +- добавлена возможность поручиться +- обычные сообщения в общем чате больше никак не обрабатываются +- унифицированный механизм хранения профилей пользователей + + ## [0.0.4] - управление правами на отправку сообщений - сохранение айди автора сообщения обратной связи + ## [0.0.3] - подключение независимого от перезапусков хранилища redis diff --git a/README.md b/README.md index 577c540..acf8bbe 100644 --- a/README.md +++ b/README.md @@ -14,5 +14,6 @@ - CHAT_ID - айди чата, который бот защищает (можно посмотреть в урле веб-версии клиента) - WELCOME_MSG - текст сообщения приветствия - BUTTON_OK - текст правильного ответа + - BUTTON_VOUCH - текст кнопки поручения - BUTTON_NO - текст неправильного ответа - FEEDBACK_CHAT_ID - айди чата для обратной связи diff --git a/api/webhook.py b/api/webhook.py index 1c5b0d3..6708b80 100644 --- a/api/webhook.py +++ b/api/webhook.py @@ -1,6 +1,6 @@ from tgbot.config import WEBHOOK, FEEDBACK_CHAT_ID, CHAT_ID # init storage there -from tgbot.handlers import handle_feedback, handle_answer, handle_welcome, \ - handle_left, handle_text, handle_button +from tgbot.handlers import handle_feedback, handle_answer, \ + handle_join, handle_left, handle_button from tgbot.api import register_webhook from sanic import Sanic from sanic.response import text @@ -35,11 +35,9 @@ async def handle(req): handle_answer(msg) elif str(msg['chat']['id']) == CHAT_ID: if 'new_chat_member' in msg: - handle_welcome(msg) + handle_join(msg) elif 'left_chat_member' in msg: handle_left(msg) - elif 'text' in msg: - handle_text(msg) if 'callback_query' in update: callback_query = update['callback_query'] chat_id = str(callback_query['message']['chat']['id']) diff --git a/tgbot/api.py b/tgbot/api.py index 1f9a217..5d0e74b 100644 --- a/tgbot/api.py +++ b/tgbot/api.py @@ -47,6 +47,12 @@ def unban_member(chat_id, user_id): r = requests.post(url) return r +# https://core.telegram.org/bots/api#addchatmember +def add_chatmember(chat_id, user_id): + url = apiBase + f"addChatMember?chat_id={chat_id}&user_id={user_id}" + r = requests.post(url) + return r + def forward_message(cid, mid, to_chat_id): url = apiBase + f"forwardMessage?chat_id={to_chat_id}" + \ diff --git a/tgbot/config.py b/tgbot/config.py index 2f7266d..3a5218d 100644 --- a/tgbot/config.py +++ b/tgbot/config.py @@ -3,9 +3,9 @@ import os REDIS_URL = os.environ.get('REDIS_URL') or 'redis://localhost:6379' WEBHOOK = os.environ.get('VERCEL_URL') or 'http://localhost:8000' -WELCOME_MSG = os.environ.get('WELCOME_MSG') or 'Welcome! Press the button' +WELCOME_MSG = os.environ.get('WELCOME_MSG') or "Welcome! Press the button or wait for a few others' connections" BUTTON_OK = os.environ.get('BUTTON_OK') or 'Ok' -BUTTON_OK2 = os.environ.get('BUTTON_OK2') or 'I see' +BUTTON_VOUCH = os.environ.get('BUTTON_VOUCH') or 'My connection!' BUTTON_NO = os.environ.get('BUTTON_NO') or 'No' CHAT_ID = os.environ.get('CHAT_ID').replace("-", "-100") diff --git a/tgbot/handlers.py b/tgbot/handlers.py index 72e8a31..1bd27df 100644 --- a/tgbot/handlers.py +++ b/tgbot/handlers.py @@ -1,24 +1,17 @@ from tgbot.api import send_message, forward_message, delete_message, \ - ban_member, set_chatpermissions + ban_member, unban_member, set_chatpermissions from tgbot.config import FEEDBACK_CHAT_ID, WELCOME_MSG, BUTTON_NO, \ - BUTTON_OK, CHAT_ID, BUTTON_OK2, REDIS_URL + BUTTON_OK, CHAT_ID, REDIS_URL import json import redis +from tgbot.profile import Profile as ProfileObj + # сохраняет сессии и пересылаемые сообщения между перезагрузками storage = redis.from_url(REDIS_URL) - -def user_accept(chat_id, author): - r = delete_message(CHAT_ID, author["welcome_id"]) - print(r.json()) - author["newcomer"] = False - - r = set_chatpermissions(CHAT_ID, { "can_send_messages": True }) - print(r.json()) - - # set author as not a newcomer - storage.set(f'usr-{author["id"]}', json.dumps(author)) +# хранение необходимой информации о пользователях +Profile = ProfileObj(storage) def handle_feedback(msg): @@ -26,8 +19,7 @@ def handle_feedback(msg): cid = msg['chat']['id'] r = forward_message(cid, mid, FEEDBACK_CHAT_ID).json() support_msg_id = r['result']['message_id'] - # store private chat message id - # fbk- -> : + # сохранение айди сообщения в приватной переписке с ботом storage.set(f'fbk-{support_msg_id}', json.dumps({ "author_id": msg["from"]["id"], "message_id": mid, @@ -38,26 +30,28 @@ def handle_feedback(msg): def handle_answer(msg): print(f'handle answer from support') support_msg_id = str(msg['reply_to_message']['message_id']) - # get stored private chat id + # получение сохраненного айди сообщения из личной переписки с ботом stored_feedback = storage.get(f'fbk-{support_msg_id}') stored_feedback = json.loads(stored_feedback) r = send_message(f'{stored_feedback["chat_id"]}', msg['text'], reply_to=stored_feedback["message_id"]) # notice 'u' before private chat ID print(r.json()) -def handle_welcome(msg): +def handle_join(msg): chat_id = str(msg['chat']['id']) from_id = str(msg['from']['id']) member_id = str(msg['new_chat_member']['id']) - s = {} + if from_id == member_id: - s["id"] = member_id + newcomer = Profile.get(member_id) + print(f'new self-joined member {member_id}') reply_markup = { "inline_keyboard": [ [ {"text": BUTTON_NO, "callback_data": BUTTON_NO}, - {"text": BUTTON_OK, "callback_data": BUTTON_OK} + {"text": BUTTON_OK, "callback_data": BUTTON_OK}, + {"text": BUTTON_VOUCH, "callback_data": BUTTON_VOUCH} ] ] } @@ -70,18 +64,30 @@ def handle_welcome(msg): welcome_msg_id = r.json()['result']['message_id'] print(r.json()) print(f'welcome message id: {welcome_msg_id}') - s["newcomer"] = True - s["welcome_id"] = welcome_msg_id + newcomer["newcomer"] = True + newcomer["welcome_id"] = welcome_msg_id perms = { "can_send_messages": False } r = set_chatpermissions(CHAT_ID, perms) print(r.json()) - else: - s['newcomer'] = False + # обновляем профиль новичка + Profile.save(newcomer) - # create new member session - storage.set(f'usr-{member_id}', json.dumps(s)) + elif 'new_chat_members' in msg: + # кто-то пригласил новых участников + print(f'{len(msg["new_chat_members"])} members were invited by {from_id}') + # получаем его профиль + inviter = Profile.get(from_id) + + for m in msg['new_chat_members']: + newcomer = Profile.get(m['id']) + newcomer['vouched_by'].append(from_id) + Profile.save(newcomer) + + inviter['vouched_for'].append(m['id']) + # обновляем профиль пригласившего + Profile.save(inviter) def handle_left(msg): @@ -89,85 +95,72 @@ def handle_left(msg): member_id = msg["left_chat_member"]["id"] - # read member session - s = storage.get(f'usr-{member_id}') - if s: - s = json.loads(s) - r = delete_message(CHAT_ID, s['welcome_id']) - print(r.json()) + # профиль покидающего чат + leaver = Profile.get(member_id) - # remove left member session - storage.delete(f'usr-{member_id}') + r = delete_message(CHAT_ID, leaver['welcome_id']) + print(r.json()) - -def handle_text(msg): - member_id = str(msg['from']['id']) - - # check if author is self-joined newcomer - author = storage.get(f'usr-{member_id}') - - if author: - author = json.loads(author) - if author.get("newcomer"): - print(f'new member speaks {msg["text"]}') - answer = msg['text'] - if BUTTON_OK.lower() in answer.lower() or \ - BUTTON_OK2.lower() in answer.lower(): - print('found answer, cleanup') - - user_accept(CHAT_ID, author) - - #else: - # print('remove some message') - # r = delete_message(CHAT_ID, msg['message_id']) - # print(r.json()) - else: - print(f'old member speaks {msg["text"]}') + Profile.leaving(leaver) def handle_button(callback_query): if 'reply_to_message' not in callback_query['message']: - # удаляет сообщение с кнопкой, если оно ни на что не отвечает + # удаляет сообщение с кнопкой, если оно уже ни на что не отвечает r = delete_message(CHAT_ID, callback_query['message']) print(r.json()) else: member_id = str(callback_query['from']['id']) callback_data = callback_query['data'] - reply_owner = str(callback_query['message']['reply_to_message']['from']['id']) + welcomed_member_id = str(callback_query['message']['reply_to_message']['from']['id']) welcome_msg_id = str(callback_query['message']['message_id']) enter_msg_id = str(callback_query['message']['reply_to_message']['message_id']) - if reply_owner == member_id: + + # получаем профиль нажавшего кнопку + actor = Profile.get(member_id) + + if welcomed_member_id == member_id: print(f'callback_query in {CHAT_ID}') - # read session - s = storage.get(f'usr-{member_id}') - if s: - s = json.loads(s) - else: - print('no user session found, create') - s = { - 'id': member_id, - 'newcomer': True, - 'welcome_id': welcome_msg_id - } - storage.set(f'usr-{member_id}', json.dumps(s)) - if callback_data == BUTTON_NO: print('wrong answer, cleanup') r = delete_message(CHAT_ID, enter_msg_id) print(r.json()) r = delete_message(CHAT_ID, welcome_msg_id) print(r.json()) - - # remove banned member session - storage.delete(f'usr-{member_id}') - print('ban member') r = ban_member(CHAT_ID, member_id) print(r.json()) + + # обработка профиля заблокированного пользователя + Profile.leaving(actor) + elif callback_data == BUTTON_OK: print('proper answer, cleanup') r = delete_message(CHAT_ID, welcome_msg_id) print(r.json()) - s['newcomer'] = False - user_accept(CHAT_ID, s) \ No newline at end of file + actor['newcomer'] = False + + r = delete_message(CHAT_ID, author["welcome_id"]) + print(r.json()) + + r = set_chatpermissions(CHAT_ID, { "can_send_messages": True }) + print(r.json()) + + # обновление профиля нажавшего правильную кнопку + Profile.save(actor) + + elif callback_data == BUTTON_VOUCH: + # это кнопка поручения + print(f'vouch button pressed by {member_id}') + newcomer = Profile.get(welcomed_member_id) + if welcomed_member_id not in inviter['vouched_for'] and \ + member_id not in newcomer['vouched_by']: + newcomer['vouched_by'].append(welcomed_member_id) + actor['vouched_for'].append(member_id) + Profile.save(newcomer) + Profile.save(actor) + print('vouch success, unban member') + r = unban_member(CHAT_ID, member_id) + print(r.json()) + diff --git a/tgbot/profile.py b/tgbot/profile.py new file mode 100644 index 0000000..d2decac --- /dev/null +++ b/tgbot/profile.py @@ -0,0 +1,28 @@ +import json + + +class Profile: + + def __init__(self, storage): + self.storage = storage + + def create(self, member_id): + s = { + "id": member_id, + "newcomer": True, + "welcome_id": 0, + "vouched_by": [], + "vouched_for": [] + } + self.storage.set(f'usr-{member_id}', json.dumps(s)) + return s + + def save(self, s): + self.storage.set(f'usr-{member_id}', json.dumps(s)) + + def get(self, member_id): + return json.loads(self.storage.get(f'usr-{member_id}')) or self.create_session(member_id) + + def leaving(self, s): + if len(s['vouched_by']) == 0: + self.storage.delete(f'usr-{s["id"]}')