diff --git a/.gitignore b/.gitignore index 64c90ba..851d7b8 100644 --- a/.gitignore +++ b/.gitignore @@ -147,4 +147,4 @@ database.db .idea/misc.xml .idea/modules.xml .idea/sqldialects.xml -.idea/vcs.xml +.idea/vcs.xml \ No newline at end of file diff --git a/BotBase/__init__.py b/BotBase/__init__.py index 1424684..fc2d656 100644 --- a/BotBase/__init__.py +++ b/BotBase/__init__.py @@ -1 +1 @@ -__version__ = (2, 0, 0) +__version__ = (2, 1, 0) diff --git a/BotBase/config.example.py b/BotBase/config.example.py deleted file mode 100644 index 2318d9a..0000000 --- a/BotBase/config.example.py +++ /dev/null @@ -1,239 +0,0 @@ -""" -Copyright 2020 Nocturn9x, alsoGAMER, CrisMystik - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -import os -import re -from collections import defaultdict - -from pyrogram import Client, filters -from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton - -# Antiflood module configuration -# The antiflood works by accumulating up to MAX_UPDATE_THRESHOLD updates (user-wise) and -# when that limit is reached, perform some checks to tell if the user is actually flooding - -BAN_TIME = 300 -# The amount of seconds the user will be banned -MAX_UPDATE_THRESHOLD = 7 -# How many updates to accumulate before starting to count -PRIVATE_ONLY = True -# If True, the antiflood will only work in private chats -FLOOD_PERCENTAGE = 75 -# The percentage (from 0 to 100) of updates that when below ANTIFLOOD_SENSIBILITY will trigger the anti flood -# Example, if FLOOD_PERCENTAGE == 75, if at least 75% of the messages from a user are marked as flood it will be blocked -ANTIFLOOD_SENSIBILITY = 1 -# The minimum amount of seconds between updates. Updates that are sent faster than this limit will trigger the antiflood -# This should not be below 1, but you can experiment if you feel bold enough -FLOOD_NOTICE = f"šŸ¤™ Hey buddy!\nšŸ• Relax! You have been banned for {BAN_TIME / 60:.1f} minutes" -# If you want the user to be notified of being flood-blocked, set this to the desired message, False to disable -FLOOD_CLEARED = "ā™»ļø Antiflood table cleaned up" -FLOOD_USER_CLEARED = "ā™»ļø Antiflood table for{user} cleaned up" -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 - -CACHE = defaultdict(lambda: ["none", 0]) -# Global cache. DO NOT TOUCH IT, really just don't -VERSION = "2.0.1a" -RELEASE_DATE = "05/12/2020" -CREDITS = "ā€šŸ’» Bot developed by @yourusernamehere in Python3.x and BotBase 2.0.1" \ - f"\nāš™ļø Version: {VERSION}\nšŸ—“ Release date: {RELEASE_DATE}" -# These will be shown in the 'Credits' section - -# Telegram client configuration - -WORKERS_NUM = 15 -# The number of worker threads that pyrogram will spawn at the startup. -# 15 workers means that the bot will process up to 15 users at the same time and then block until one worker has done -BOT_TOKEN = "BOT_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 = 123456 -# Get it at https://my.telegram.org/apps -API_HASH = "API_HASH_HERE" -# Same as above -DEVICE_MODEL = "BotBase" -# Name of the device shown in the sessions list - useless for a Bot -SYSTEM_VERSION = "2.0.1a" -# Host OS version, can be the same as VERSION - also useless for a Bot -LANG_CODE = "en_US" -# Session lang_code - -# Logging configuration -# To know more about what these options mean, check https://docs.python.org/3/library/logging.html - -LOGGING_FORMAT = "[%(levelname)s %(asctime)s] In thread '%(threadName)s', " \ - f"module %(module)s, function %(funcName)s at line %(lineno)d -> [{SESSION_NAME}] %(message)s" -DATE_FORMAT = "%d/%m/%Y %H:%M:%S %p" -LOGGING_LEVEL = 30 -bot = Client(api_id=API_ID, api_hash=API_HASH, bot_token=BOT_TOKEN, plugins=PLUGINS_ROOT, - session_name=SESSION_NAME, workers=WORKERS_NUM, device_model=DEVICE_MODEL, system_version=SYSTEM_VERSION, - lang_code=LANG_CODE) - -# Start module -# P.S.: {mention} in the GREET message will be replaced with a mention to the user, same applies for {id} and {username} - -GREET = """šŸ‘‹ Hi {mention} and welcome to BotBase.""" -# The message that will be sent as a reply to the /start command. If this string is empty the bot will not reply. -SUPPORT_BUTTON = "šŸ’­ Start Chat" -# The text for the button that triggers the live chat -BACK_BUTTON = "šŸ”™ Back" -# The text for the 'Back' button -CREDITS_BUTTON = "ā„¹ Credits" -# The text for the 'Credits' button -BUTTONS = InlineKeyboardMarkup([ - [InlineKeyboardButton(SUPPORT_BUTTON, "begin_chat"), - InlineKeyboardButton(CREDITS_BUTTON, "bot_info")]]) -# This keyboard will be sent along with GREET, feel free to add or remove buttons - -# Database configuration -# The only natively supported database is SQLite3, but you can easily tweak -# this section and the BotBase/database/query.py file to work with any DBMS -# If you do so and want to share your code feel free to open a PR on the repo! - -DB_PATH = os.path.join(os.getcwd(), f"BotBase/database/database.db") -DB_CREATE = """CREATE TABLE IF NOT EXISTS users ( - tg_id INTEGER PRIMARY KEY NOT NULL, - tg_uname TEXT UNIQUE NULL DEFAULT 'null', - date TEXT NOT NULL, - banned INTEGER NOT NULL DEFAULT 0); - """ - -DB_GET_USERS = "SELECT tg_id FROM users" -DB_GET_USER = "SELECT * FROM users where users.tg_id = ?" -DB_SET_USER = "INSERT INTO users (tg_id, tg_uname, date, banned) VALUES(?, ?, ?, ?)" -DB_BAN_USER = "UPDATE users SET banned = 1 WHERE users.tg_id = ?" -DB_UNBAN_USER = "UPDATE users SET banned = 0 WHERE users.tg_id = ?" -DB_CHECK_BANNED = "SELECT banned FROM users WHERE users.tg_id = ?" -DB_UPDATE_NAME = "UPDATE users SET tg_uname = ? WHERE users.tg_id = ?" -DB_GET_USER_BY_NAME = "SELECT * FROM users where users.tg_uname = ?" -from BotBase.database.query import check_banned - -# Admin module configuration - -ADMINS = {1234567: "Lorem Ipsum"} -# 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. -MARKED_BUSY = "šŸŽ² You're now busy, resend /busy to reset this state" -UNMARKED_BUSY = "āœ You'll now receive support requests again" -CANNOT_BAN_ADMIN = "āŒ Error: This user is an administrator" -USER_BANNED = "āœ… User banned successfully" -USER_UNBANNED = "āœ… User unbanned successfully" -YOU_ARE_UNBANNED = "āœ… You've been unbanned" -USER_NOT_BANNED = "āŒ This user isn't banned" -CLOSE_CHAT_BUTTON = "āŒ Close chat" -UPDATE_BUTTON = "šŸ”„ Update" -USER_ALREADY_BANNED = "āŒ This user is already banned" -YOU_ARE_BANNED = "āŒ You've been banned" -WHISPER_FROM = "šŸ“£ Message from {admin}: {msg}" -WHISPER_SUCCESSFUL = "āœ… Sent successfully" -NAME = "tg://user?id={}" -BYPASS_FLOOD = True -# If False, admins can be flood-blocked too, otherwise the antiflood will ignore them -USER_INFO_UPDATED = "āœ… Information updated" -USER_INFO_UNCHANGED = "āŒ I haven't detected any changes for this user" -ADMIN_ACCEPTED_CHAT = "āœ… {admin} has joined the chat with {user}" -USER_LEFT_QUEUE = "āš ļø {user} left the queue" -QUEUE_LIST = "šŸš» List of users waiting\n\n{queue}" -CHATS_LIST = "šŸ’¬ List of users in chat\n\n{chats}" -ADMIN_BUSY = "(Busy)" -USER_INFO = """ā„¹ļø User infos - -šŸ†” ID: {tg_id} -āœļø Username: {tg_uname} -šŸ—“ Registered on: {date} -āŒØļø Banned: {status} -šŸ’” Admin: {admin}""" -# The message that is sent with /getuser and /getranduser -INVALID_SYNTAX = "āŒ Invalid syntax: Use {correct}" -# This is sent when a command is used the wrong way -ERROR = "āŒ Error" -# This is sent when a command returns an error -NON_NUMERIC_ID = "The ID must be numeric!" -# This is sent if the parameter to /getuser is not a numerical ID -USERS_COUNT = "Total users: {count}" -# This is sent as a result of the /count command -NO_PARAMETERS = "āŒ {command} requires no parameters" -# Error saying that the given command takes no parameters -ID_MISSING = "The selected ID ({tg_id}) isn't in the database" -# Error when given ID is not in database -NAME_MISSING = "The selected username ({tg_uname}) isn't in the database" -# Error when given username is not in database -YES = "Yes" -NO = "No" -GLOBAL_MESSAGE_STATS = """Message Statistics - -āœļø Message: {msg} -šŸ”„ Attempts: {count} -āœ… Delivered: {success}""" -# Statistics that are sent to the admin after /global command - -# Live chat configuration - -ADMINS_LIST_UPDATE_DELAY = 30 -# How many seconds between an update and another -LIVE_CHAT_STATUSES = "Legend: šŸŸ¢ = Available, šŸ”“ = Busy\n\n" -SUPPORT_NOTIFICATION = "šŸ”” New support request!\n\n{uinfo}" -ADMIN_JOINS_CHAT = " [{admin_name}]({admin_id}) joined the chat!" -USER_CLOSES_CHAT = "šŸ”” [{user_name}]({user_id}) closed the chat" -USER_LEAVES_CHAT = "āœ… You left the chat" -USER_JOINS_CHAT = "āœ… You've joined the chat" -CHAT_BUSY = "āš ļø Another admin has already joined" -LEAVE_CURRENT_CHAT = "āš ļø Close current chat first!" -CANNOT_REQUEST_SUPPORT = "āš ļø You can't start a chat!" -STATUS_FREE = "šŸŸ¢ " -STATUS_BUSY = "šŸ”“ " -SUPPORT_REQUEST_SENT = "āœ… You're now in the queue, wait for an admin to answer you\n\n" \ - "šŸ”„ Admins available\n{queue}\nUpdated on: {date}\n\nNote: " \ - "If there are no admins available at the moment, press the 'Update' button every now and " \ - "then to find out if a seat is available!" -JOIN_CHAT_BUTTON = "ā— Join the chat" -USER_MESSAGE = "šŸ—£ [{user_name}]({user_id}): {message}" -ADMIN_MESSAGE = "šŸ§‘ā€šŸ’» [{user_name}]({user_id}): {message}" -TOO_FAST = "āœ‹ Calm down! Try again later" - - -# Custom filters - Don't touch them as well but feel free to add more! - -def check_user_banned(tg_id: int): - res = check_banned(tg_id) - if isinstance(res, Exception): - return False - else: - if not res: - return False - return bool(res[0]) - - -def callback_regex(pattern: str): - return filters.create(lambda flt, client, update: re.match(pattern, update.data)) - - -def admin_is_chatting(): - return filters.create( - lambda flt, client, update: update.from_user.id in ADMINS and CACHE[update.from_user.id][0] == "IN_CHAT") - - -def user_is_chatting(): - return filters.create( - lambda flt, client, update: update.from_user.id not in ADMINS and CACHE[update.from_user.id][0] == "IN_CHAT") - - -def user_banned(): - return filters.create(lambda flt, client, update: check_user_banned(update.from_user.id)) diff --git a/BotBase/config.sample.py b/BotBase/config.sample.py new file mode 100644 index 0000000..ec34585 --- /dev/null +++ b/BotBase/config.sample.py @@ -0,0 +1,146 @@ +""" +Copyright 2020-2021 Nocturn9x, alsoGAMER, CrisMystik + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from collections import defaultdict + +import MySQLdb +from pyrogram import Client +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton + +# region Antiflood module configuration +# The antiflood works by accumulating up to MAX_UPDATE_THRESHOLD updates (user-wise) and +# when that limit is reached, perform some checks to tell if the user is actually flooding + +BAN_TIME = 300 +# The amount of seconds the user will be banned +MAX_UPDATE_THRESHOLD = 7 +# How many updates to accumulate before starting to count +PRIVATE_ONLY = True +# If True, the antiflood will only work in private chats +FLOOD_PERCENTAGE = 75 +# The percentage (from 0 to 100) of updates that when below ANTIFLOOD_SENSIBILITY will trigger the anti flood +# Example, if FLOOD_PERCENTAGE == 75, if at least 75% of the messages from a user are marked as flood it will be blocked +ANTIFLOOD_SENSIBILITY = 1 +# The minimum amount of seconds between updates. Updates that are sent faster than this limit will trigger the antiflood +# This should not be below 1, but you can experiment if you feel bold enough +DELETE_MESSAGES = True +# Set this to false if you do not want the messages to be deleted after flood is detected +# endregion + +# region Various options and global variables + +CACHE = defaultdict(lambda: ["none", 0]) +# Global cache. DO NOT TOUCH IT, really just don't +VERSION = "" +RELEASE_DATE = "" +CREDITS = "ā€šŸ’» Bot developed by @yourusernamehere in Python3.9 and BotBase 2.1.0" \ + f"\nāš™ļø Version: {VERSION}\nšŸ—“ Release Date: {RELEASE_DATE}" +# These will be shown in the 'Credits' section +# endregion + +# region Telegram client configuration + +WORKERS_NUM = 15 +# The number of worker threads that pyrogram will spawn at the startup. +# 15 workers means that the bot will process up to 15 users at the same time and then block until one worker has done + +BOT_TOKEN = "" +# Get it with t.me/BotFather +SESSION_NAME = "" +# 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 = 000000 +# Get it at https://my.telegram.org/apps +API_HASH = "" +# Same as above +DEVICE_MODEL = "" +# Name of the device shown in the sessions list - useless for a Bot +SYSTEM_VERSION = "" +# Host OS version, can be the same as VERSION - also useless for a Bot +LANG_CODE = "en_US" +# Session lang_code +# endregion + +# region Logging configuration +# To know more about what these options mean, check https://docs.python.org/3/library/logging.html + +LOGGING_FORMAT = "[%(levelname)s %(asctime)s] In thread '%(threadName)s', " \ + f"module %(module)s, function %(funcName)s at line %(lineno)d -> [{SESSION_NAME}] %(message)s" +DATE_FORMAT = "%d/%m/%Y %H:%M:%S %p" +LOGGING_LEVEL = 30 +bot = Client(api_id=API_ID, api_hash=API_HASH, bot_token=BOT_TOKEN, plugins=PLUGINS_ROOT, + session_name=SESSION_NAME, workers=WORKERS_NUM, device_model=DEVICE_MODEL, system_version=SYSTEM_VERSION, + lang_code=LANG_CODE) +# endregion + +# region Start module +# P.S.: {mention} in the GREET message will be replaced with a mention to the user, same applies for {id} and {username} + +GREET = "šŸ‘‹ Hi {mention} and welcome to BotBase." +# The message that will be sent to the users as a reply to the /start command. If this string is empty the bot will not reply. +# endregion + +# region Database configuration +# The only natively supported database is MariaDB, but you can easily tweak +# this section and the BotBase/database/query.py file to work with any DBMS +# If you do so and want to share your code feel free to open a PR on the repo! + +DB_URL = MySQLdb.connect(host="host", user="user", passwd="passwd", db="db") +# endregion + +# region Greet Keyboard +from BotBase.strings.default_strings import SUPPORT_BUTTON, ABOUT_BUTTON +BUTTONS = InlineKeyboardMarkup([ + [InlineKeyboardButton(SUPPORT_BUTTON, "begin_chat"), + InlineKeyboardButton(ABOUT_BUTTON, "bot_about")]]) +# This keyboard will be sent along with GREET, feel free to add or remove buttons +# endregion + +# region Admin module configuration + +ADMINS = {00000000: "A Dude"} +# 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. + +NAME = "tg://user?id={}" +BYPASS_FLOOD = True +# If False, admins can be flood-blocked too, otherwise the antiflood will ignore them +USER_INFO = """ā„¹ļø User info + +šŸ†” ID: {tg_id} +āœļø Username: {tg_uname} +šŸ—“ Registered on: {date} +āŒØļø Banned: {status} +šŸ’” Admin: {admin}""" +# The message that is sent with /getuser and /getranduser +GLOBAL_MESSAGE_STATS = """Message Statistics + +āœļø Message: {msg} +šŸ”„ Attempts: {count} +āœ… Delivered: {success}""" +# Statistics that are sent to the admin after /global command +# endregion + +# region Live chat configuration + +ADMINS_LIST_UPDATE_DELAY = 30 +# How many seconds between an update and another +LIVE_CHAT_STATUSES = "Legend: šŸŸ¢ = Available, šŸ”“ = Busy\n\n" +STATUS_FREE = "šŸŸ¢ " +STATUS_BUSY = "šŸ”“ " +USER_MESSAGE = "šŸ—£ [{user_name}]({user_id}): {message}" +ADMIN_MESSAGE = "šŸ§‘ā€šŸ’» [{user_name}]({user_id}): {message}" +# endregion diff --git a/BotBase/database/__init__.py b/BotBase/database/__init__.py index 1424684..fc2d656 100644 --- a/BotBase/database/__init__.py +++ b/BotBase/database/__init__.py @@ -1 +1 @@ -__version__ = (2, 0, 0) +__version__ = (2, 1, 0) diff --git a/BotBase/database/query.py b/BotBase/database/query.py index 2afc06e..75928ec 100644 --- a/BotBase/database/query.py +++ b/BotBase/database/query.py @@ -1,5 +1,5 @@ """ -Copyright 2020 Nocturn9x, alsoGAMER, CrisMystik +Copyright 2020-2021 Nocturn9x, alsoGAMER, CrisMystik Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,150 +14,150 @@ limitations under the License. """ -import sqlite3.dbapi2 as sqlite3 -from BotBase.config import DB_GET_USERS, DB_GET_USER, DB_PATH, DB_SET_USER, DB_UPDATE_NAME, DB_BAN_USER, DB_UNBAN_USER, \ - DB_GET_USER_BY_NAME, DB_CHECK_BANNED import logging import time -import os +import MySQLdb -def create_database(path: str, query: str): - if os.path.exists(path): - logging.warning(f"Database file exists at {path}, running query") - else: - logging.warning(f"No database found, creating it at {path}") +from BotBase.config import DB_URL +from BotBase.database.raw_queries import * + + +def create_table(query: str): try: - database = sqlite3.connect(path) - except sqlite3.Error as connection_error: + database = DB_URL.cursor() + except MySQLdb.DatabaseError as connection_error: logging.error(f"An error has occurred while connecting to database: {connection_error}") else: try: with database: - database.executescript(query) + database.execute(query) + DB_URL.commit() database.close() - except sqlite3.Error as query_error: - logging.info(f"An error has occurred while executing DB_CREATE: {query_error}") + except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as query_error: + logging.info(f"An error has occurred while executing CREATE_TABLE: {query_error}") def get_user(tg_id: int): try: - database = sqlite3.connect(DB_PATH) - except sqlite3.Error as connection_error: + database = DB_URL.cursor() + except MySQLdb.DatabaseError as connection_error: logging.error(f"An error has occurred while connecting to database: {connection_error}") else: try: with database: - cursor = database.cursor() - query = cursor.execute(DB_GET_USER, (tg_id, )) - return query.fetchone() - except sqlite3.Error as query_error: + database.execute(DB_GET_USER, (tg_id,)) + return database.fetchone() + except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as query_error: logging.error(f"An error has occurred while executing DB_GET_USER query: {query_error}") return query_error def get_user_by_name(tg_uname: str): try: - database = sqlite3.connect(DB_PATH) - except sqlite3.Error as connection_error: + database = DB_URL.cursor() + except MySQLdb.DatabaseError as connection_error: logging.error(f"An error has occurred while connecting to database: {connection_error}") else: try: with database: - cursor = database.cursor() - query = cursor.execute(DB_GET_USER_BY_NAME, (tg_uname, )) - return query.fetchone() - except sqlite3.Error as query_error: + database.execute(DB_GET_USER_BY_NAME, (tg_uname,)) + return database.fetchone() + except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as query_error: logging.error(f"An error has occurred while executing DB_GET_USER_BY_NAME query: {query_error}") return query_error def update_name(tg_id: int, name: str): try: - database = sqlite3.connect(DB_PATH) - except sqlite3.Error as connection_error: + database = DB_URL.cursor() + except MySQLdb.DatabaseError as connection_error: logging.error(f"An error has occurred while connecting to database: {connection_error}") else: try: with database: database.execute(DB_UPDATE_NAME, (name, tg_id)) + DB_URL.commit() return True - except sqlite3.Error as query_error: + except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as query_error: logging.error(f"An error has occurred while executing DB_UPDATE_NAME query: {query_error}") return query_error def get_users(): try: - database = sqlite3.connect(DB_PATH) - except sqlite3.Error as connection_error: + database = DB_URL.cursor() + except MySQLdb.DatabaseError as connection_error: logging.error(f"An error has occurred while connecting to database: {connection_error}") else: try: with database: - cursor = database.cursor() - query = cursor.execute(DB_GET_USERS) - return query.fetchall() - except sqlite3.Error as query_error: + database.execute(DB_GET_USERS) + return database.fetchall() + except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as query_error: logging.error(f"An error has occurred while executing DB_GET_USERS query: {query_error}") return query_error def set_user(tg_id: int, tg_uname: str): try: - database = sqlite3.connect(DB_PATH) - except sqlite3.Error as connection_error: + database = DB_URL.cursor() + except MySQLdb.DatabaseError as connection_error: logging.error(f"An error has occurred while connecting to database: {connection_error}") else: try: with database: - database.execute(DB_SET_USER, (tg_id, tg_uname, time.strftime("%d/%m/%Y %T %p"), 0, 0, 0)) + database.execute(DB_SET_USER, + (tg_id, tg_uname, time.strftime("%d/%m/%Y %T %p"), 0)) + DB_URL.commit() return True - except sqlite3.Error as query_error: + except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as query_error: logging.error(f"An error has occurred while executing DB_SET_USER query: {query_error}") return query_error def ban_user(tg_id: int): try: - database = sqlite3.connect(DB_PATH) - except sqlite3.Error as connection_error: + database = DB_URL.cursor() + except MySQLdb.DatabaseError as connection_error: logging.error(f"An error has occurred while connecting to database: {connection_error}") else: try: with database: - database.execute(DB_BAN_USER, (tg_id, )) + database.execute(DB_BAN_USER, (tg_id,)) + DB_URL.commit() return True - except sqlite3.Error as query_error: + except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as query_error: logging.error(f"An error has occurred while executing DB_BAN_USER query: {query_error}") return query_error def unban_user(tg_id: int): try: - database = sqlite3.connect(DB_PATH) - except sqlite3.Error as connection_error: + database = DB_URL.cursor() + except MySQLdb.DatabaseError as connection_error: logging.error(f"An error has occurred while connecting to database: {connection_error}") else: try: with database: - database.execute(DB_UNBAN_USER, (tg_id, )) + database.execute(DB_UNBAN_USER, (tg_id,)) + DB_URL.commit() return True - except sqlite3.Error as query_error: + except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as query_error: logging.error(f"An error has occurred while executing DB_UNBAN_USER query: {query_error}") return query_error def check_banned(tg_id: int): try: - database = sqlite3.connect(DB_PATH) - except sqlite3.Error as connection_error: + database = DB_URL.cursor() + except MySQLdb.DatabaseError as connection_error: logging.error(f"An error has occurred while connecting to database: {connection_error}") else: try: with database: - query = database.execute(DB_CHECK_BANNED, (tg_id, )) - return query.fetchone() - except sqlite3.Error as query_error: + database.execute(DB_CHECK_BANNED, (tg_id,)) + return database.fetchone() + except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as query_error: logging.error(f"An error has occurred while executing DB_CHECK_BANNED query: {query_error}") return query_error diff --git a/BotBase/database/raw_queries.py b/BotBase/database/raw_queries.py new file mode 100644 index 0000000..531708e --- /dev/null +++ b/BotBase/database/raw_queries.py @@ -0,0 +1,34 @@ +""" +Copyright 2020-2021 Nocturn9x, alsoGAMER, CrisMystik + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# region CreateTablesQueries +CREATE_USERS_TABLE = """CREATE TABLE IF NOT EXISTS users ( + tg_id INTEGER(20) PRIMARY KEY NOT NULL, + tg_uname VARCHAR(32) UNIQUE NULL DEFAULT 'null', + date TEXT NOT NULL, + banned INTEGER NOT NULL DEFAULT 0); + """ +# endregion + + +DB_GET_USERS = "SELECT users.tg_id FROM users" +DB_GET_USER = "SELECT * FROM users WHERE users.tg_id = %s" +DB_SET_USER = "INSERT INTO users (tg_id, tg_uname, date, banned) VALUES (%s, %s, %s, %s)" +DB_BAN_USER = "UPDATE users SET users.banned = FALSE WHERE users.tg_id = %s" +DB_UNBAN_USER = "UPDATE users SET users.banned = FALSE WHERE users.tg_id = %s" +DB_CHECK_BANNED = "SELECT users.banned FROM users WHERE users.tg_id = %s" +DB_UPDATE_NAME = "UPDATE users SET users.tg_uname = %s WHERE users.tg_id = %s" +DB_GET_USER_BY_NAME = "SELECT * FROM users WHERE users.tg_uname = %s" diff --git a/BotBase/methods/__init__.py b/BotBase/methods/__init__.py index a9222bc..47c7d56 100644 --- a/BotBase/methods/__init__.py +++ b/BotBase/methods/__init__.py @@ -1,2 +1,2 @@ -__version__ = (2, 0, 0) +__version__ = (2, 1, 0) from ._wrapper import MethodWrapper diff --git a/BotBase/methods/_wrapper.py b/BotBase/methods/_wrapper.py index 241f549..c3c8480 100644 --- a/BotBase/methods/_wrapper.py +++ b/BotBase/methods/_wrapper.py @@ -1,5 +1,5 @@ """ -Copyright 2020 Nocturn9x, alsoGAMER, CrisMystik +Copyright 2020-2021 Nocturn9x, alsoGAMER, CrisMystik Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -51,4 +51,5 @@ async def wrapper(*args, **kwargs): return rpc_error else: raise AttributeError(self.instance, attribute) + return wrapper diff --git a/BotBase/methods/custom_filters.py b/BotBase/methods/custom_filters.py new file mode 100644 index 0000000..ffc167f --- /dev/null +++ b/BotBase/methods/custom_filters.py @@ -0,0 +1,50 @@ +""" +Copyright 2020-2021 Nocturn9x, alsoGAMER, CrisMystik + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import re + +from pyrogram import filters + +from BotBase.config import ADMINS, CACHE +from BotBase.database.query import check_banned + + +def check_user_banned(tg_id: int): + res = check_banned(tg_id) + if isinstance(res, Exception): + return False + else: + if not res: + return False + return bool(res[0]) + + +def callback_regex(pattern: str): + return filters.create(lambda flt, client, update: re.match(pattern, update.data)) + + +def admin_is_chatting(): + return filters.create( + lambda flt, client, update: update.from_user.id in ADMINS and CACHE[update.from_user.id][0] == "IN_CHAT") + + +def user_is_chatting(): + return filters.create( + lambda flt, client, update: update.from_user.id not in ADMINS and CACHE[update.from_user.id][0] == "IN_CHAT") + + +def user_banned(): + return filters.create(lambda flt, client, update: check_user_banned(update.from_user.id)) diff --git a/BotBase/modules/__init__.py b/BotBase/modules/__init__.py index 1424684..fc2d656 100644 --- a/BotBase/modules/__init__.py +++ b/BotBase/modules/__init__.py @@ -1 +1 @@ -__version__ = (2, 0, 0) +__version__ = (2, 1, 0) diff --git a/BotBase/modules/admin.py b/BotBase/modules/admin.py index 43f89fa..cfdab90 100644 --- a/BotBase/modules/admin.py +++ b/BotBase/modules/admin.py @@ -1,5 +1,5 @@ """ -Copyright 2020 Nocturn9x, alsoGAMER, CrisMystik +Copyright 2020-2021 Nocturn9x, alsoGAMER, CrisMystik Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,11 +21,11 @@ from pyrogram import Client, filters -from BotBase.config import ADMINS, CACHE, CANNOT_BAN_ADMIN, CHATS_LIST, ERROR, \ - GLOBAL_MESSAGE_STATS, ID_MISSING, INVALID_SYNTAX, LEAVE_CURRENT_CHAT, MARKED_BUSY, NAME, NAME_MISSING, NO, \ - NON_NUMERIC_ID, NO_PARAMETERS, QUEUE_LIST, UNMARKED_BUSY, USERS_COUNT, USER_ALREADY_BANNED, \ - USER_BANNED, USER_INFO, USER_INFO_UNCHANGED, USER_INFO_UPDATED, USER_NOT_BANNED, USER_UNBANNED, \ - WHISPER_FROM, WHISPER_SUCCESSFUL, YES, YOU_ARE_BANNED, YOU_ARE_UNBANNED, bot +from BotBase.config import ADMINS, CACHE, GLOBAL_MESSAGE_STATS, NAME, USER_INFO, bot +from BotBase.strings.default_strings import CANNOT_BAN_ADMIN, CHATS_LIST, ERROR, ID_MISSING, INVALID_SYNTAX, \ + LEAVE_CURRENT_CHAT, MARKED_BUSY, NAME_MISSING, NO, NON_NUMERIC_ID, NO_PARAMETERS, QUEUE_LIST, UNMARKED_BUSY, \ + USERS_COUNT, USER_ALREADY_BANNED, USER_BANNED, USER_INFO_UNCHANGED, USER_INFO_UPDATED, USER_NOT_BANNED, \ + USER_UNBANNED, WHISPER_FROM, WHISPER_SUCCESSFUL, YES, YOU_ARE_BANNED, YOU_ARE_UNBANNED from BotBase.database.query import ban_user, get_user, get_user_by_name, get_users, unban_user, update_name from BotBase.methods import MethodWrapper from BotBase.modules.antiflood import BANNED_USERS @@ -38,10 +38,10 @@ def format_user(user): tg_id, tg_uname, date, banned = user return USER_INFO.format( tg_id=tg_id, - tg_uname='@' + tg_uname if tg_uname else 'null', + tg_uname='@' + tg_uname if tg_uname else 'N/A', date=date, status=YES if banned else NO, - admin=YES if tg_id in ADMINS else NO + admin=YES if tg_id in ADMINS else NO, ) @@ -230,7 +230,7 @@ async def ban(_, message): else: await wrapper.send_message(message.chat.id, USER_NOT_BANNED if condition else USER_ALREADY_BANNED) else: - await wrapper.send_message(message.chat.id, f"{ERROR}: {ID_MISSING.format(uid=message.command[1])}") + await wrapper.send_message(message.chat.id, f"{ERROR}: {ID_MISSING.format(tg_id=message.command[1])}") @Client.on_message(filters.command("busy") & ADMINS_FILTER & filters.private & ~BANNED_USERS & ~filters.edited) diff --git a/BotBase/modules/antiflood.py b/BotBase/modules/antiflood.py index 73329ad..92da68e 100644 --- a/BotBase/modules/antiflood.py +++ b/BotBase/modules/antiflood.py @@ -1,5 +1,5 @@ """ -Copyright 2020 Nocturn9x, alsoGAMER, CrisMystik +Copyright 2020-2021 Nocturn9x, alsoGAMER, CrisMystik Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,15 +14,16 @@ limitations under the License. """ -from collections import defaultdict import logging import time +from collections import defaultdict from pyrogram import Client, filters -from BotBase.config import ADMINS, ANTIFLOOD_SENSIBILITY, BAN_TIME, BYPASS_FLOOD, CACHE, DELETE_MESSAGES, ERROR, \ - FLOOD_CLEARED, FLOOD_NOTICE, FLOOD_PERCENTAGE, FLOOD_USER_CLEARED, MAX_UPDATE_THRESHOLD, NON_NUMERIC_ID, \ - PRIVATE_ONLY, bot, user_banned +from BotBase.config import ADMINS, ANTIFLOOD_SENSIBILITY, BAN_TIME, BYPASS_FLOOD, CACHE, DELETE_MESSAGES, \ + FLOOD_PERCENTAGE, MAX_UPDATE_THRESHOLD, PRIVATE_ONLY, bot +from BotBase.strings.default_strings import ERROR, FLOOD_CLEARED, FLOOD_NOTICE, FLOOD_USER_CLEARED, NON_NUMERIC_ID +from BotBase.methods.custom_filters import user_banned from BotBase.methods import MethodWrapper # Some variables for runtime configuration diff --git a/BotBase/modules/livechat.py b/BotBase/modules/livechat.py index 380c0e6..22a1520 100644 --- a/BotBase/modules/livechat.py +++ b/BotBase/modules/livechat.py @@ -1,5 +1,5 @@ """ -Copyright 2020 Nocturn9x, alsoGAMER, CrisMystik +Copyright 2020-2021 Nocturn9x, alsoGAMER, CrisMystik Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,15 +20,16 @@ from pyrogram import Client, filters from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton -from BotBase.modules.antiflood import BANNED_USERS -from BotBase.modules.start import cb_start_handler -from BotBase.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, ADMIN_ACCEPTED_CHAT +from BotBase.config import CACHE, ADMINS, ADMINS_LIST_UPDATE_DELAY, LIVE_CHAT_STATUSES, STATUS_BUSY, STATUS_FREE, \ + USER_INFO, ADMIN_MESSAGE, USER_MESSAGE, NAME, bot +from BotBase.strings.default_strings import SUPPORT_REQUEST_SENT, SUPPORT_NOTIFICATION, ADMIN_JOINS_CHAT, \ + USER_CLOSES_CHAT, JOIN_CHAT_BUTTON, USER_LEAVES_CHAT, TOO_FAST, CHAT_BUSY, LEAVE_CURRENT_CHAT, USER_JOINS_CHAT, \ + CANNOT_REQUEST_SUPPORT, YES, NO, CLOSE_CHAT_BUTTON, BACK_BUTTON, UPDATE_BUTTON, ADMIN_ACCEPTED_CHAT +from BotBase.methods.custom_filters import callback_regex, admin_is_chatting, user_is_chatting, user_banned from BotBase.database.query import get_user from BotBase.methods import MethodWrapper +from BotBase.modules.antiflood import BANNED_USERS +from BotBase.modules.start import cb_start_handler ADMINS_FILTER = filters.user(list(ADMINS.keys())) BUTTONS = InlineKeyboardMarkup( @@ -54,15 +55,18 @@ async def begin_chat(_, query): else: queue += f"- {STATUS_BUSY}" queue += f"[{admin_name}]({NAME.format(admin_id)})\n" - msg = await cb_wrapper.edit_message_text(SUPPORT_REQUEST_SENT.format(queue=queue, date=time.strftime('%d/%m/%Y %T')), - reply_markup=BUTTONS) + msg = await cb_wrapper.edit_message_text( + SUPPORT_REQUEST_SENT.format(queue=queue, date=time.strftime('%d/%m/%Y %T')), + reply_markup=BUTTONS) join_chat_button = InlineKeyboardMarkup( [[InlineKeyboardButton(JOIN_CHAT_BUTTON, f"join_{query.from_user.id}")]]) user = get_user(query.from_user.id) tg_id, tg_uname, date, banned = user - text = USER_INFO.format(tg_id=tg_id, tg_uname='@' + tg_uname if tg_uname else 'null', date=date, + text = USER_INFO.format(tg_id=tg_id, + tg_uname='@' + tg_uname if tg_uname else 'N/A', + date=date, status=YES if banned else NO, - admin='N/A') + admin=YES if tg_id in ADMINS else NO) CACHE[query.from_user.id].append([]) for admin in ADMINS: status = CACHE[admin][0] @@ -88,15 +92,18 @@ async def update_admins_list(_, query): else: queue += f"- {STATUS_BUSY}" queue += f"[{admin_name}]({NAME.format(admin_id)})\n" - await cb_wrapper.edit_message_text(SUPPORT_REQUEST_SENT.format(queue=queue, date=time.strftime('%d/%m/%Y %T')), - reply_markup=BUTTONS) + await cb_wrapper.edit_message_text( + SUPPORT_REQUEST_SENT.format(queue=queue, date=time.strftime('%d/%m/%Y %T')), + reply_markup=BUTTONS) join_chat_button = InlineKeyboardMarkup( [[InlineKeyboardButton(JOIN_CHAT_BUTTON, f"join_{query.from_user.id}")]]) user = get_user(query.from_user.id) tg_id, tg_uname, date, banned = user - text = USER_INFO.format(tg_id=tg_id, tg_uname='@' + tg_uname if tg_uname else 'null', date=date, + text = USER_INFO.format(tg_id=tg_id, + tg_uname='@' + tg_uname if tg_uname else 'N/A', + date=date, status=YES if banned else NO, - admin='N/A') + admin=YES if tg_id in ADMINS else NO) for admin in ADMINS: status = CACHE[admin][0] if status != "IN_CHAT": @@ -150,7 +157,8 @@ async def close_chat(_, query): await wrapper.send_message(query.from_user.id, USER_LEAVES_CHAT) await wrapper.send_message(CACHE[user_id][1], - USER_CLOSES_CHAT.format(user_id=NAME.format(query.from_user.id), user_name=user_name)) + USER_CLOSES_CHAT.format(user_id=NAME.format(query.from_user.id), + user_name=user_name)) del CACHE[query.from_user.id] del CACHE[admin_id] else: @@ -179,7 +187,8 @@ async def forward_from_admin(_, message): if CACHE[user_id][0] == "IN_CHAT": del CACHE[user_id] await wrapper.send_message(user_id, - USER_CLOSES_CHAT.format(user_id=NAME.format(admin_id), user_name=admin_name)) + USER_CLOSES_CHAT.format(user_id=NAME.format(admin_id), + user_name=admin_name)) del CACHE[message.from_user.id] else: await wrapper.send_message(CACHE[message.from_user.id][1], @@ -187,12 +196,14 @@ async def forward_from_admin(_, message): user_id=NAME.format(message.from_user.id), message=message.text.html)) elif message.photo: - await wrapper.send_photo(CACHE[message.from_user.id][1], photo=message.photo.file_id, file_ref=message.photo.file_ref, + await wrapper.send_photo(CACHE[message.from_user.id][1], photo=message.photo.file_id, + file_ref=message.photo.file_ref, caption=ADMIN_MESSAGE.format(user_name=ADMINS[message.from_user.id], user_id=NAME.format(message.from_user.id), message=message.caption.html or '' if message.caption else '' if message.caption else '')) elif message.audio: - await wrapper.send_audio(CACHE[message.from_user.id][1], audio=message.audio.file_id, file_ref=message.audio.file_ref, + await wrapper.send_audio(CACHE[message.from_user.id][1], audio=message.audio.file_id, + file_ref=message.audio.file_ref, caption=ADMIN_MESSAGE.format(user_name=ADMINS[message.from_user.id], user_id=NAME.format(message.from_user.id), message=message.caption.html or '' if message.caption else '')) @@ -206,7 +217,8 @@ async def forward_from_admin(_, message): await wrapper.send_sticker(CACHE[message.from_user.id][1], sticker=message.sticker.file_id, file_ref=message.sticker.file_ref) elif message.video: - await wrapper.send_video(CACHE[message.from_user.id][1], video=message.video.file_id, file_ref=message.video.file_ref, + await wrapper.send_video(CACHE[message.from_user.id][1], video=message.video.file_id, + file_ref=message.video.file_ref, caption=ADMIN_MESSAGE.format(user_name=ADMINS[message.from_user.id], user_id=NAME.format(message.from_user.id), message=message.caption.html or '' if message.caption else '')) @@ -217,7 +229,8 @@ async def forward_from_admin(_, message): user_id=NAME.format(message.from_user.id), message=message.caption.html or '' if message.caption else '')) elif message.voice: - await wrapper.send_voice(CACHE[message.from_user.id][1], voice=message.voice.file_id, file_ref=message.voice.file_ref, + await wrapper.send_voice(CACHE[message.from_user.id][1], voice=message.voice.file_id, + file_ref=message.voice.file_ref, caption=ADMIN_MESSAGE.format(user_name=ADMINS[message.from_user.id], user_id=NAME.format(message.from_user.id), message=message.caption.html or '' if message.caption else '')) @@ -250,32 +263,38 @@ async def forward_from_user(_, message): USER_MESSAGE.format(user_name=name, user_id=NAME.format(message.from_user.id), message=message.text.html)) elif message.photo: - await wrapper.send_photo(CACHE[message.from_user.id][1], photo=message.photo.file_id, file_ref=message.photo.file_ref, + await wrapper.send_photo(CACHE[message.from_user.id][1], photo=message.photo.file_id, + file_ref=message.photo.file_ref, caption=USER_MESSAGE.format(user_name=name, user_id=NAME.format(message.from_user.id), message=message.caption.html or '' if message.caption else '')) elif message.audio: - await wrapper.send_audio(CACHE[message.from_user.id][1], audio=message.audio.file_id, file_ref=message.audio.file_ref, + await wrapper.send_audio(CACHE[message.from_user.id][1], audio=message.audio.file_id, + file_ref=message.audio.file_ref, caption=USER_MESSAGE.format(user_name=name, user_id=NAME.format(message.from_user.id), message=message.caption.html or '' if message.caption else '')) elif message.document: await wrapper.send_document(CACHE[message.from_user.id][1], document=message.document.file_id, file_ref=message.document.file_ref, - caption=USER_MESSAGE.format(user_name=name, user_id=NAME.format(message.from_user.id), + caption=USER_MESSAGE.format(user_name=name, + user_id=NAME.format(message.from_user.id), message=message.caption.html or '' if message.caption else '')) elif message.sticker: await wrapper.send_sticker(CACHE[message.from_user.id][1], sticker=message.sticker.file_id, file_ref=message.sticker.file_ref) elif message.video: - await wrapper.send_video(CACHE[message.from_user.id][1], video=message.video.file_id, file_ref=message.video.file_ref, + await wrapper.send_video(CACHE[message.from_user.id][1], video=message.video.file_id, + file_ref=message.video.file_ref, caption=USER_MESSAGE.format(user_name=name, user_id=NAME.format(message.from_user.id), message=message.caption.html or '' if message.caption else '')) elif message.animation: await wrapper.send_animation(CACHE[message.from_user.id][1], animation=message.animation.file_id, file_ref=message.animation.file_ref, - caption=USER_MESSAGE.format(user_name=name, user_id=NAME.format(message.from_user.id), + caption=USER_MESSAGE.format(user_name=name, + user_id=NAME.format(message.from_user.id), message=message.caption.html or '' if message.caption else '')) elif message.voice: - await wrapper.send_voice(CACHE[message.from_user.id][1], voice=message.voice.file_id, file_ref=message.voice.file_ref, + await wrapper.send_voice(CACHE[message.from_user.id][1], voice=message.voice.file_id, + file_ref=message.voice.file_ref, caption=USER_MESSAGE.format(user_name=name, user_id=NAME.format(message.from_user.id), message=message.caption.html or '' if message.caption else '')) elif message.video_note: @@ -323,8 +342,9 @@ async def join_chat(_, query): for admin in ADMINS: if CACHE[admin] != "IN_CHAT" and admin != admin_id: await wrapper.send_message(admin, - ADMIN_ACCEPTED_CHAT.format(admin=f"[{admin_name}]({NAME.format(admin)})", - user=f"[{user_name}]({NAME.format(user_id)})")) + ADMIN_ACCEPTED_CHAT.format( + admin=f"[{admin_name}]({NAME.format(admin_id)})", + 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 540b026..8e9da0e 100644 --- a/BotBase/modules/start.py +++ b/BotBase/modules/start.py @@ -1,5 +1,5 @@ """ -Copyright 2020 Nocturn9x, alsoGAMER, CrisMystik +Copyright 2020-2021 Nocturn9x, alsoGAMER, CrisMystik Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,11 +20,12 @@ from pyrogram import Client, filters from pyrogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message -from BotBase.modules.antiflood import BANNED_USERS -from BotBase.config import ADMINS, BACK_BUTTON, BUTTONS, CACHE, CREDITS, GREET, NAME, USER_LEFT_QUEUE, VERSION, bot, \ - user_banned +from BotBase.config import ADMINS, BUTTONS, CACHE, CREDITS, GREET, NAME, VERSION, bot +from BotBase.strings.default_strings import BACK_BUTTON, USER_LEFT_QUEUE +from BotBase.methods.custom_filters import user_banned from BotBase.database.query import get_users, set_user from BotBase.methods import MethodWrapper +from BotBase.modules.antiflood import BANNED_USERS wrapper = MethodWrapper(bot) @@ -86,8 +87,8 @@ async def cb_start_handler(_, message): await start_handler(_, message) -@Client.on_callback_query(filters.regex("bot_info") & ~BANNED_USERS) -async def bot_info(_, query): +@Client.on_callback_query(filters.regex("bot_about") & ~BANNED_USERS) +async def bot_about(_, query): cb_wrapper = MethodWrapper(query) await cb_wrapper.edit_message_text( text=CREDITS.format(VERSION=VERSION), diff --git a/BotBase/strings/__init__.py b/BotBase/strings/__init__.py new file mode 100644 index 0000000..fc2d656 --- /dev/null +++ b/BotBase/strings/__init__.py @@ -0,0 +1 @@ +__version__ = (2, 1, 0) diff --git a/BotBase/strings/default_strings.py b/BotBase/strings/default_strings.py new file mode 100644 index 0000000..6b9b7e4 --- /dev/null +++ b/BotBase/strings/default_strings.py @@ -0,0 +1,98 @@ +""" +Copyright 2020-2021 Nocturn9x, alsoGAMER, CrisMystik + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# region Flood Strings +from BotBase.config import BAN_TIME + +FLOOD_NOTICE = f"šŸ¤™ Hey buddy!\nšŸ• Relax! You have been banned for {BAN_TIME / 60:.1f} minutes" +# If you want the user to be notified of being flood-blocked, set this to the desired message, False to disable +FLOOD_CLEARED = "ā™»ļø Antiflood table cleaned up" +FLOOD_USER_CLEARED = "ā™»ļø Antiflood table for{user} cleaned up" +TOO_FAST = "āœ‹ Calm down! Try again later" +# endregion + +# region Greet Buttons Strings +SUPPORT_BUTTON = "šŸ’­ Support Chat" +# The text for the button that triggers the live chat +BACK_BUTTON = "šŸ”™ Back" +# The text for the 'Back' button +ABOUT_BUTTON = "ā„¹ About" +# The text for the 'About' button +# endregion + +# region Livechat Strings +MARKED_BUSY = "šŸŽ² You're now busy, resend /busy to reset this state" +UNMARKED_BUSY = "āœ You'll now receive support requests again" +CLOSE_CHAT_BUTTON = "āŒ Close chat" +UPDATE_BUTTON = "šŸ”„ Update" +ADMIN_ACCEPTED_CHAT = "āœ… {admin} has joined the chat with {user}" +USER_LEFT_QUEUE = "āš ļø {user} left the queue" +QUEUE_LIST = "šŸš» List of users waiting\n\n{queue}" +CHATS_LIST = "šŸ’¬ List of users in chat\n\n{chats}" +ADMIN_BUSY = "(Busy)" +SUPPORT_NOTIFICATION = "šŸ”” New support request!\n\n{uinfo}" +ADMIN_JOINS_CHAT = " [{admin_name}]({admin_id}) joined the chat!" +USER_CLOSES_CHAT = "šŸ”” [{user_name}]({user_id}) closed the chat" +USER_LEAVES_CHAT = "āœ… You left the chat" +USER_JOINS_CHAT = "āœ… You've joined the chat" +CHAT_BUSY = "āš ļø Another admin has already joined" +LEAVE_CURRENT_CHAT = "āš ļø Close current chat first!" +CANNOT_REQUEST_SUPPORT = "āš ļø You can't start a chat!" +SUPPORT_REQUEST_SENT = "āœ… You're now in the queue, wait for an admin to answer you\n\n" \ + "šŸ”„ Admins available\n{queue}\nUpdated on: {date}\n\nNote: " \ + "If there are no admins available at the moment, press the 'Update' button every now and " \ + "then to find out if a seat is available!" +JOIN_CHAT_BUTTON = "ā— Join the chat" +# endregion + +# region User Interaction Strings +USER_INFO_UPDATED = "āœ… Information updated" +USER_INFO_UNCHANGED = "āŒ I haven't detected any changes for this user" +USER_BANNED = "āœ… User banned successfully" +USER_UNBANNED = "āœ… User unbanned successfully" +YOU_ARE_UNBANNED = "āœ… You've been unbanned" +USER_NOT_BANNED = "āŒ This user isn't banned" +USER_ALREADY_BANNED = "āŒ This user is already banned" +YOU_ARE_BANNED = "āŒ You've been banned" +# endregion + +# region Whisper Strings +WHISPER_FROM = "šŸ“£ Message from {admin}: {msg}" +WHISPER_SUCCESSFUL = "āœ… Sent successfully" +# endregion + +# region Misc Strings +YES = "Yes" +NO = "No" +# endregion + +# region Error Strings +CANNOT_BAN_ADMIN = "āŒ Error: This user is an administrator" +INVALID_SYNTAX = "āŒ Invalid syntax: Use {correct}" +# This is sent when a command is used the wrong way +ERROR = "āŒ Error" +# This is sent when a command returns an error +NON_NUMERIC_ID = "The ID must be numeric!" +# This is sent if the parameter to /getuser is not a numerical ID +USERS_COUNT = "Total users: {count}" +# This is sent as a result of the /count command +NO_PARAMETERS = "āŒ {command} requires no parameters" +# Error saying that the given command takes no parameters +ID_MISSING = "The selected ID ({tg_id}) isn't in the database" +# Error when given ID is not in database +NAME_MISSING = "The selected username ({tg_uname}) isn't in the database" +# Error when given username is not in database +# endregion diff --git a/DATABASE.md b/DATABASE.md index b3d9c47..e8724be 100644 --- a/DATABASE.md +++ b/DATABASE.md @@ -1,14 +1,14 @@ # BotBase - Notes on database interaction -BotBase has a built-in API to interact with an SQLite3 database, located in the -`BotBase/database/query.py` module. The reason why SQLite3 was chosen among the -lots of options is that it's lightweight, has fewer security concerns (no user -and password to remember), and requires no setup at all. +BotBase has a built-in API to interact with an MySQL database, located in the +`BotBase/database/query.py` module. The reason why MySQL was chosen among the +lots of options is that it's solid enough for bots with standard load, +has enough features, and requires little to no setup at all. The configuration is hassle-free, you can keep the default values, and they'll work just fine. If you need a more complex database structure, just edit -the `DB_CREATE` SQL query to fit your needs, but do not alter the default -`users` table unless you also change all the SQL queries in the `config.py` +the `CREATE_USERS_TABLE` SQL query to fit your needs, but do not alter the default +`users` table unless you also change all the SQL queries in the `raw_queries.py` file as this would otherwise break the whole internal machinery of BotBase. ## Available methods @@ -17,10 +17,6 @@ The module `BotBase.database.query` implements the following default methods to interact with the database. All methods either return the result of a query or `True` if the operation was successful or an exception if the query errored. -Please note that the methods are **NOT** locked and that proper locking is -needed if you think that your bot might get a `sqlite3.OoerationalError: database -is locked` error when accessing the database. - All queries are performed within a `with` block and therefore rollbacked automatically if an error occurs or committed if the transaction was successful. @@ -42,7 +38,7 @@ to the database. The username parameter can be `None` - `update_user` -> Updates a user's username with the given ID -# I need MySQL/other DBMS! +# I need Sqlite/other DBMS! The API has been designed in a way that makes it easy to swap between different database managers, so if you feel in the right mood make a PR to support a new @@ -53,7 +49,7 @@ database, and it'll be reviewed ASAP. If you want to add custom methods to the API, we advise to follow the bot's convention: -- Set the SQL query as a global variable whose name starts with `DB_` in `config.py` +- Set the SQL query as a global variable whose name starts with `DB_` in `raw_queries.py` - Import it in the `BotBase.database.query` module - Create a new function that takes the required parameters whose name reflects the query name (without `DB_`) - Perform the query in a `with` context manager, close the cursor when you're done diff --git a/README.md b/README.md index 9168d0d..6f2dcde 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ To set up a project using BotBase, follow this step-by-step guide (assuming `pip - Open a terminal and type `git clone https://github.com/alsoGAMER/BotBase` - `cd` into the newly created directory and run `python3 -m pip install -r requirements.txt` - Once that is done, copy `BotBase/config.example.py` as `config.py` and edit your `config.py` module with a text editor and start changing the default settings -- The first thing you might want to do is change the `API_ID`, `API_HASH`, and `BOT_TOKEN` global variables. Check [this page](https://my.telegram.org/apps) and log in with your Telegram account to create an `API_ID`/`API_HASH` pair by registering a new application. For the bot token, just create one with [BotFather](https://telegram.me/BotFather). +- The first thing you might want to do is change the `API_ID`, `API_HASH`, and `BOT_TOKEN` global variables. Check [this page](https://my.telegram.org/apps) and log in with your Telegram account to create an `API_ID`/`API_HASH` pair by registering a new application. For the bot token, just create one with [BotFather](https://t.me/BotFather). **Note**: The configuration file is still a python file, and when it will be imported any python code that you typed inside it will be executed, so be careful. If you need to perform pre-startup operations it is advised to do them in the `if __name__ == "main":` block inside `bot.py`, before `bot.run()` @@ -52,7 +52,7 @@ This plugin works the following way: - Admins that are busy will not receive other support notifications - An admin cannot join a chat if it's already busy -Most of the working of the module is pre-defined, but you can customize the texts that the bot will use in the appropriate section of `config.py` +Most of the working of the module is pre-defined, but you can customize the texts that the bot will use in the appropriate section of `strings/default_strings.py` ### Plugins - Admin @@ -67,7 +67,7 @@ To configure this plugin, go to the appropriate section in `config.py` and chang The available commands are: -__Note__: ID can either be a Telegram User ID or a Telegram username (with or without the trailing @, case insensitive) +__Note__: ID can either be a Telegram User ID or a Telegram username (with or without the trailing @, case-insensitive) __Note__: Arguments marked with square brackets are optional @@ -80,9 +80,9 @@ __Note__: Arguments marked with square brackets are optional - `/whisper ID msg`: Send `msg` to a specific user. HTML and markdown formatting supported - `/update ID`: Updates the user's info in the database, if they've changed - `/busy`: Sets your admin status as busy/not busy to silence/unsilence support requests to you -- `/clearflood [ID]` - Clears the antiflood local storage. If an user ID is given, only that user's cache is purged, otherwise the whole module-level cache is reset- +- `/clearflood [ID]` - Clears the antiflood local storage. If a user ID is given, only that user's cache is purged, otherwise the whole module-level cache is reset- -__Warning__: Altough acting on users by their username is supported, it is not recommended. Users can change their name and the bot wouldn't detect +__Warning__: Altough acting on users by their username is supported, it is not recommended. Users can change their name, and the bot wouldn't detect this change until you send the `/update` command. A telegram user can never change its ID (without deleting his own account) so that's way more reliable! ### Plugins - Antiflood diff --git a/bot.py b/bot.py index 5de7308..315e91d 100644 --- a/bot.py +++ b/bot.py @@ -1,5 +1,5 @@ """ -Copyright 2020 Nocturn9x, alsoGAMER, CrisMystik +Copyright 2020-2021 Nocturn9x, alsoGAMER, CrisMystik Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,17 +16,18 @@ import logging -from BotBase.config import bot, LOGGING_LEVEL, LOGGING_FORMAT, DATE_FORMAT, DB_PATH, DB_CREATE -from BotBase.database.query import create_database from pyrogram.session import Session +from BotBase.config import bot, LOGGING_LEVEL, LOGGING_FORMAT, DATE_FORMAT +from BotBase.database.raw_queries import CREATE_USERS_TABLE +from BotBase.database.query import create_table if __name__ == "__main__": logging.basicConfig(format=LOGGING_FORMAT, datefmt=DATE_FORMAT, level=LOGGING_LEVEL) Session.notice_displayed = True try: - logging.warning("Running create_database()") - create_database(DB_PATH, DB_CREATE) + logging.warning("Running create_table()") + create_table(CREATE_USERS_TABLE) logging.warning("Database interaction complete") logging.warning("Starting bot") bot.run() diff --git a/requirements.txt b/requirements.txt index 05e21e8..3e0fe6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -pyrogram==1.0.7 +pyrogram tgcrypto -wheel \ No newline at end of file +wheel +mysqlclient \ No newline at end of file