From 52b18b0b9320f9c671febd829ca521b50898c3d4 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Sun, 6 Sep 2020 18:18:12 +0200 Subject: [PATCH] Added /queue, /chats, /clearflood and /close (admin only). Improved general chat experience --- BotBase/config.example.py | 18 ++++++++++------ BotBase/database/query.py | 2 +- BotBase/modules/admin.py | 34 ++++++++++++++++++++++++++++-- BotBase/modules/antiflood.py | 31 ++++++++++++++++++++++----- BotBase/modules/livechat.py | 41 ++++++++++++++++++++++++++++++++---- BotBase/modules/start.py | 14 +++++++----- 6 files changed, 116 insertions(+), 24 deletions(-) diff --git a/BotBase/config.example.py b/BotBase/config.example.py index 6e96e6b..dd9a9cc 100644 --- a/BotBase/config.example.py +++ b/BotBase/config.example.py @@ -21,6 +21,8 @@ FLOOD_PERCENTAGE = 75 ANTIFLOOD_SENSIBILITY = 1 # If you want the user to be notified of being flood-blocked, set this to the desired message, False to disable FLOOD_NOTICE = f"🤙 **Hey amico**!\n🕐 Rilassati! Sei stato bloccato per {BAN_TIME / 60:.1f} minuti" +FLOOD_CLEARED = "♻️ Tabella antiflood svuotata" +FLOOD_USER_CLEARED = "♻️ Tabella antiflood ripulita per `{user}`" DELETE_MESSAGES = True # Set this to false if you do not want the messages to be deleted after flood is detected # Various options and global variables @@ -38,8 +40,8 @@ WORKERS_NUM = 15 # The number of worker threads that pyrogram will spawn at st BOT_TOKEN = "TOKEN HERE" # Get it with t.me/BotFather SESSION_NAME = "BotBase" # The name of the Telegram Session that the bot will have, will be visible from Telegram PLUGINS_ROOT = {"root": f"BotBase/modules"} # Do not change this unless you know what you're doing -API_ID = 1234567 # Get it at https://my.telegram.org/apps -API_HASH = "abcdef123456" # Same as above +API_ID = 123467 # Get it at https://my.telegram.org/apps +API_HASH = "abcdef1234567" # Same as above # Logging configuration # To know more about what these options mean, check https://docs.python.org/3/library/logging.html @@ -92,7 +94,7 @@ from .database.query import get_user # Admin module configuration # Edit this dict adding the ID:NAME pair of the admin that you want to add. You can add as many admins as you want -ADMINS = {669152898: "Matt-sama :3", 836296867: "αℓѕσƓAMƐR"} +ADMINS = {123456: "Sample Name"} MARKED_BUSY = "🎲 Ora sei impegnato, invia nuovamente /busy per resettare questo stato" UNMARKED_BUSY = "✍ Da ora riceverai nuovamente le richieste di assistenza" CANNOT_BAN_ADMIN = "❌ L'utente é un amministratore" @@ -110,6 +112,11 @@ NAME = "tg://user?id={}" BYPASS_FLOOD = True # If False, admins can be flood-blocked too, otherwise the antiflood will ignore them USER_INFO_UPDATED = "✅ Informazioni aggiornate" USER_INFO_UNCHANGED = "❌ Non ho rilevato cambiamenti per questo utente" +ADMIN_ACCEPTED_CHAT = "✅ {admin} ha preso in carico la chat con {user}" +USER_LEFT_QUEUE = "⚠️ {user} ha lasciato la coda" +QUEUE_LIST = "🚻 Lista utenti in attesa\n\n{queue}" +CHATS_LIST = "💬 Lista utenti in chat\n\n{chats}" +ADMIN_BUSY = "(Occupato)" USER_INFO = """**ℹ️ Informazioni** 🆔 **ID**: `{uid}` @@ -165,10 +172,7 @@ def check_user_banned(tg_id: int): else: if not res: return False - if res[-1]: - return True - else: - return False + return res[-1] def callback_regex(pattern: str): diff --git a/BotBase/database/query.py b/BotBase/database/query.py index 18943d6..3696c57 100644 --- a/BotBase/database/query.py +++ b/BotBase/database/query.py @@ -97,7 +97,7 @@ def set_user(tg_id: int, uname: str): database.execute(DB_SET_USER, (None, tg_id, uname, time.strftime("%d/%m/%Y %T %p"), 0)) return True except sqlite3.Error as query_error: - logging.error(f"An error has occurred while executing DB_GET_USERS query: {query_error}") + logging.error(f"An error has occurred while executing DB_SET_USER query: {query_error}") return query_error diff --git a/BotBase/modules/admin.py b/BotBase/modules/admin.py index 0be00e0..ea45e4a 100644 --- a/BotBase/modules/admin.py +++ b/BotBase/modules/admin.py @@ -1,7 +1,8 @@ from ..config import ADMINS, USER_INFO, INVALID_SYNTAX, ERROR, NONNUMERIC_ID, USERS_COUNT, \ NO_PARAMETERS, ID_MISSING, GLOBAL_MESSAGE_STATS, NAME, WHISPER_FROM, USER_INFO_UPDATED, USER_INFO_UNCHANGED, \ USER_BANNED, USER_UNBANNED, CANNOT_BAN_ADMIN, USER_ALREADY_BANNED, USER_NOT_BANNED, YOU_ARE_BANNED, YOU_ARE_UNBANNED, \ - MARKED_BUSY, UNMARKED_BUSY, CACHE, YES, NO, NAME_MISSING, bot, WHISPER_SUCCESSFUL + MARKED_BUSY, UNMARKED_BUSY, CACHE, YES, NO, NAME_MISSING, bot, WHISPER_SUCCESSFUL, LEAVE_CURRENT_CHAT, \ + QUEUE_LIST, CHATS_LIST, ADMIN_BUSY from pyrogram import Client, Filters from ..database.query import get_user, get_users, update_name, ban_user, unban_user, get_user_by_name from .antiflood import BANNED_USERS @@ -226,10 +227,39 @@ def busy(client, message): if len(message.command) > 1: wrapper.send_message(message.chat.id, f"{NO_PARAMETERS.format(command='/busy')}") else: - if CACHE[message.from_user.id][0] == "none": + if CACHE[message.from_user.id][0] == "IN_CHAT" and CACHE[message.from_user.id][1] != 1234567: + wrapper.send_message(message.from_user.id, LEAVE_CURRENT_CHAT) + elif CACHE[message.from_user.id][0] == "none": wrapper.send_message(message.chat.id, MARKED_BUSY) CACHE[message.from_user.id] = ["IN_CHAT", 1234567] else: if message.from_user.id in CACHE: del CACHE[message.from_user.id] wrapper.send_message(message.chat.id, UNMARKED_BUSY) + + +@Client.on_message(Filters.command("chats") & ADMINS_FILTER & Filters.private & ~BANNED_USERS & ~Filters.edited) +def chats(client, message): + logging.warning(f"{ADMINS[message.from_user.id]} [{message.from_user.id}] sent /chats") + if len(message.command) > 1: + wrapper.send_message(message.chat.id, f"{NO_PARAMETERS.format(command='/chats')}") + else: + text = "" + for user in CACHE: + if CACHE[user][0] == "IN_CHAT" and user not in ADMINS: + admin_id = CACHE[user][1] + admin_name = ADMINS[admin_id] + text += f"- 👤 [User]({NAME.format(user)}) -> 👨‍💻 [{admin_name}]({NAME.format(admin_id)})\n" + wrapper.send_message(message.chat.id, CHATS_LIST.format(chats=text)) + +@Client.on_message(Filters.command("queue") & ADMINS_FILTER & Filters.private & ~BANNED_USERS & ~Filters.edited) +def queue(client, message): + logging.warning(f"{ADMINS[message.from_user.id]} [{message.from_user.id}] sent /queue") + if len(message.command) > 1: + wrapper.send_message(message.chat.id, f"{NO_PARAMETERS.format(command='/queue')}") + else: + text = "" + for user in CACHE: + if CACHE[user][0] == "AWAITING_ADMIN": + text += f"- 👤 [User]({NAME.format(user)})\n" + wrapper.send_message(message.chat.id, QUEUE_LIST.format(queue=text)) diff --git a/BotBase/modules/antiflood.py b/BotBase/modules/antiflood.py index 111c4ac..6ea03a1 100644 --- a/BotBase/modules/antiflood.py +++ b/BotBase/modules/antiflood.py @@ -1,24 +1,27 @@ from pyrogram import Client, Filters from ..config import MAX_UPDATE_THRESHOLD, ANTIFLOOD_SENSIBILITY, BAN_TIME, ADMINS, BYPASS_FLOOD, FLOOD_NOTICE, \ - FLOOD_PERCENTAGE, CACHE, PRIVATE_ONLY, DELETE_MESSAGES, bot, user_banned + FLOOD_PERCENTAGE, CACHE, PRIVATE_ONLY, DELETE_MESSAGES, bot, user_banned, FLOOD_CLEARED, FLOOD_USER_CLEARED, \ + ERROR, NONNUMERIC_ID, check_user_banned from collections import defaultdict import logging import time from ..methods import MethodWrapper -from ..config import check_user_banned # Some variables for runtime configuration MESSAGES = defaultdict(list) # Internal variable for the antiflood module BANNED_USERS = Filters.user() # Filters where the antiflood will put banned users BYPASS_USERS = Filters.user(list(ADMINS.keys())) if BYPASS_FLOOD else Filters.user() +ADMINS = Filters.user(list(ADMINS.keys())) FILTER = Filters.private if PRIVATE_ONLY else ~Filters.user() wrapper = MethodWrapper(bot) def is_flood(updates: list): - """Calculates if a sequence of - updates corresponds to a flood""" + """ + Calculates if a sequence of + updates corresponds to a flood + """ genexpr = [i <= ANTIFLOOD_SENSIBILITY for i in ((updates[i + 1] - timestamp) if i < (MAX_UPDATE_THRESHOLD - 1) else (timestamp - updates[i - 1]) for @@ -40,7 +43,7 @@ def anti_flood(client, update): logging.warning(f"{user_id} has waited at least {BAN_TIME} seconds in {chat} and can now text again") BANNED_USERS.remove(user_id) del MESSAGES[user_id] - elif len(MESSAGES[user_id]) >= MAX_UPDATE_THRESHOLD: + elif len(MESSAGES[user_id]) >= MAX_UPDATE_THRESHOLD - 1: # -1 to avoid acting on the next update MESSAGES[user_id].append({chat: (date, message_id)}) logging.info(f"MAX_UPDATE_THRESHOLD ({MAX_UPDATE_THRESHOLD}) Reached for {user_id}") user_data = MESSAGES.pop(user_id) @@ -61,3 +64,21 @@ def anti_flood(client, update): del MESSAGES[user_id] else: MESSAGES[user_id].append({chat: (date, message_id)}) + + +@Client.on_message(FILTER & ADMINS & ~Filters.edited & Filters.command("clearflood")) +def clear_flood(_, message): + if len(message.command) == 1: + global MESSAGES # Ew... + MESSAGES = defaultdict(list) + for user in BANNED_USERS.copy(): + BANNED_USERS.remove(user) + wrapper.send_message(message.chat.id, FLOOD_CLEARED) + else: + for user in message.command[1:]: + if not user.isdigit(): + wrapper.send_message(message.chat.id, f"{ERROR}: {NONNUMERIC_ID}") + return + BANNED_USERS.discard(int(user)) + MESSAGES.pop(int(user), None) + wrapper.send_message(message.chat.id, FLOOD_USER_CLEARED.format(user=", ".join((f"{usr}" for usr in message.command[1:])))) diff --git a/BotBase/modules/livechat.py b/BotBase/modules/livechat.py index 9a84a7d..4ef8571 100644 --- a/BotBase/modules/livechat.py +++ b/BotBase/modules/livechat.py @@ -2,7 +2,8 @@ from pyrogram import Client, Filters, InlineKeyboardMarkup, InlineKeyboardButton from ..config import CACHE, ADMINS, ADMINS_LIST_UPDATE_DELAY, callback_regex, admin_is_chatting, \ user_is_chatting, LIVE_CHAT_STATUSES, STATUS_BUSY, STATUS_FREE, SUPPORT_REQUEST_SENT, SUPPORT_NOTIFICATION, \ ADMIN_JOINS_CHAT, USER_CLOSES_CHAT, JOIN_CHAT_BUTTON, USER_INFO, USER_LEAVES_CHAT, ADMIN_MESSAGE, USER_MESSAGE, \ - TOO_FAST, CHAT_BUSY, LEAVE_CURRENT_CHAT, USER_JOINS_CHAT, NAME, CANNOT_REQUEST_SUPPORT, YES, NO, user_banned, CLOSE_CHAT_BUTTON, BACK_BUTTON, UPDATE_BUTTON, bot + TOO_FAST, CHAT_BUSY, LEAVE_CURRENT_CHAT, USER_JOINS_CHAT, NAME, CANNOT_REQUEST_SUPPORT, YES, NO, user_banned, CLOSE_CHAT_BUTTON, BACK_BUTTON, UPDATE_BUTTON, bot, \ + ADMIN_ACCEPTED_CHAT import time from ..database.query import get_user from .antiflood import BANNED_USERS @@ -20,7 +21,7 @@ BUTTONS = InlineKeyboardMarkup( wrapper = MethodWrapper(bot) -@Client.on_callback_query(Filters.callback_data("sos") & ~BANNED_USERS & ~user_banned()) +@Client.on_callback_query(Filters.regex("sos") & ~BANNED_USERS & ~user_banned()) def begin_chat(client, query): cb_wrapper = MethodWrapper(query) if query.from_user.id in ADMINS: @@ -53,7 +54,7 @@ def begin_chat(client, query): CACHE[query.from_user.id][-1].append((msg.chat.id, msg.message_id)) -@Client.on_callback_query(Filters.callback_data("update_admins_list") & ~BANNED_USERS & ~user_banned()) +@Client.on_callback_query(Filters.regex("update_admins_list") & ~BANNED_USERS & ~user_banned()) def update_admins_list(_, query): cb_wrapper = MethodWrapper(query) if CACHE[query.from_user.id][0] == "AWAITING_ADMIN": @@ -135,7 +136,26 @@ def close_chat(_, query): def forward_from_admin(client, message): if message.text: logging.warning(f"Admin {ADMINS[message.from_user.id]} [{message.from_user.id}] says to {CACHE[message.from_user.id][1]}: {message.text.html}") - wrapper.send_message(CACHE[message.from_user.id][1], + if message.text == "/close": + admin_id = message.from_user.id + user_id = CACHE[admin_id][1] + data = CACHE[user_id][-1] + if isinstance(data, list): + data.append((message.from_user.id, message.message_id)) + for chatid, message_ids in data: + wrapper.delete_messages(chatid, message_ids) + status = CACHE[message.from_user.id][0] + if status == "IN_CHAT": + wrapper.send_message(message.from_user.id, USER_LEAVES_CHAT) + admin_id, admin_name = message.from_user.id, ADMINS[message.from_user.id] + logging.warning(f"{ADMINS[admin_id]} [{admin_id}] has terminated the chat with user {CACHE[admin_id][1]}") + if CACHE[user_id][0] == "IN_CHAT": + del CACHE[user_id] + wrapper.send_message(user_id, + USER_CLOSES_CHAT.format(user_id=NAME.format(admin_id), user_name=admin_name)) + del CACHE[message.from_user.id] + else: + wrapper.send_message(CACHE[message.from_user.id][1], ADMIN_MESSAGE.format(user_name=ADMINS[message.from_user.id], user_id=NAME.format(message.from_user.id), message=message.text.html)) @@ -205,6 +225,15 @@ def join_chat(_, query): cb_wrapper = MethodWrapper(query) if CACHE[query.from_user.id][0] != "IN_CHAT": user_id = int(query.data.split("_")[1]) + user = wrapper.get_users(user_id) + if isinstance(user, Exception): + user_name = "Anonymous" + elif user.first_name: + user_name = user.first_name + elif user.username: + user_name = user.username + else: + user_name = "Anonymous" if CACHE[user_id][0] != "AWAITING_ADMIN": cb_wrapper.answer(CHAT_BUSY) else: @@ -215,8 +244,12 @@ def join_chat(_, query): message = wrapper.send_message(query.from_user.id, USER_JOINS_CHAT, reply_markup=buttons) admin_joins = wrapper.send_message(user_id, ADMIN_JOINS_CHAT.format(admin_name=admin_name, admin_id=NAME.format(admin_id)), reply_markup=buttons) + logging.warning(f"{admin_name} [{admin_id}] has joined a chat with user {user_name} [{user_id}]") for chatid, message_ids in CACHE[user_id][-1]: wrapper.delete_messages(chatid, message_ids) + for admin in ADMINS: + if CACHE[admin] != "IN_CHAT" and admin != admin_id: + wrapper.send_message(admin, ADMIN_ACCEPTED_CHAT.format(admin=f"[{admin_name}]({NAME.format(admin)})", user=f"[{user_name}]({NAME.format(user_id)})")) CACHE[user_id][-1].append((message.chat.id, message.message_id)) CACHE[user_id][-1].append((admin_joins.chat.id, admin_joins.message_id)) else: diff --git a/BotBase/modules/start.py b/BotBase/modules/start.py index 91be3e0..58a74a6 100644 --- a/BotBase/modules/start.py +++ b/BotBase/modules/start.py @@ -1,6 +1,6 @@ from pyrogram import Client, Filters, InlineKeyboardButton, InlineKeyboardMarkup from .antiflood import BANNED_USERS -from ..config import GREET, BUTTONS, CREDITS, CACHE, bot, VERSION, RELEASE_DATE, user_banned, BACK_BUTTON +from ..config import GREET, BUTTONS, CREDITS, CACHE, bot, VERSION, RELEASE_DATE, user_banned, BACK_BUTTON, USER_LEFT_QUEUE, ADMINS, NAME from ..database.query import get_users, set_user import logging import itertools @@ -33,14 +33,14 @@ def start_handler(client, message): ) -@Client.on_callback_query(Filters.callback_data("info") & ~BANNED_USERS) +@Client.on_callback_query(Filters.regex("info") & ~BANNED_USERS) def bot_info(_, query): cb_wrapper = MethodWrapper(query) buttons = InlineKeyboardMarkup([[InlineKeyboardButton(BACK_BUTTON, "back_start")]]) cb_wrapper.edit_message_text(CREDITS.format(VERSION=VERSION, RELEASE_DATE=RELEASE_DATE), reply_markup=buttons) -@Client.on_callback_query(Filters.callback_data("back_start") & ~BANNED_USERS) +@Client.on_callback_query(Filters.regex("back_start") & ~BANNED_USERS) def back_start(_, query): cb_wrapper = MethodWrapper(query) if query.from_user.first_name: @@ -52,8 +52,12 @@ def back_start(_, query): if CACHE[query.from_user.id][0] == "AWAITING_ADMIN": data = CACHE[query.from_user.id][-1] if isinstance(data, list): - for chatid, message_ids in data[:-2]: + for chatid, message_ids in data: wrapper.delete_messages(chatid, message_ids) - cb_wrapper.edit_message_text(GREET.format(mention=f"[{name}](tg://user?id={query.from_user.id})", id=query.from_user.id, + for admin in ADMINS: + wrapper.send_message(admin, USER_LEFT_QUEUE.format(user=f"[{name}]({NAME.format(query.from_user.id)})")) + + wrapper.send_message(query.from_user.id, GREET.format(mention=f"[{name}](tg://user?id={query.from_user.id})", id=query.from_user.id, username=query.from_user.username), reply_markup=BUTTONS) + del CACHE[query.from_user.id]