├── niscoin ├── .gitignore ├── __init__.py ├── constants.py ├── misc.py ├── configuration.py ├── conversation.py ├── messages.py ├── main.py ├── chat.py ├── help.py ├── handlers.py └── commands.py ├── entrypoint.sh ├── .dockerignore ├── docker ├── docker-compose.pickle.yml ├── Dockerfile.pickle ├── Dockerfile.postgres └── docker-compose.postgres.yml ├── config.json.example ├── README.md ├── help.json ├── .github └── workflows │ └── build.yml ├── .gitignore └── LICENSE /niscoin/.gitignore: -------------------------------------------------------------------------------- 1 | *.db -------------------------------------------------------------------------------- /niscoin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -x 4 | 5 | exec python3 niscoin/main.py --database /app/database -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.github 2 | .gitignore 3 | README.md 4 | LICENSE 5 | .vscode 6 | config.json.example 7 | docker-compose.yml 8 | logs 9 | database 10 | test -------------------------------------------------------------------------------- /docker/docker-compose.pickle.yml: -------------------------------------------------------------------------------- 1 | # Simple docker-compose file for self hosting 2 | 3 | version: "3.8" 4 | 5 | services: 6 | niscoin: 7 | image: nischaypro/niscoin-pickle:latest 8 | container_name: niscoin 9 | volumes: 10 | - ./database:/app/database 11 | - ./config.json:/app/config.json 12 | restart: unless-stopped -------------------------------------------------------------------------------- /docker/Dockerfile.pickle: -------------------------------------------------------------------------------- 1 | ARG BASE=alpine:3.16 2 | 3 | FROM ${BASE} 4 | 5 | LABEL maintainer="Nischay Mamidi " 6 | 7 | COPY entrypoint.sh /entrypoint.sh 8 | 9 | COPY . /app 10 | 11 | WORKDIR /app 12 | 13 | RUN apk add --no-cache python3 py-pip &&\ 14 | pip3 install --upgrade gtts &&\ 15 | pip3 install --upgrade python-telegram-bot --pre 16 | 17 | VOLUME [ "/app/database" , "/app/config.json" ] 18 | 19 | ENTRYPOINT [ "/entrypoint.sh" ] 20 | -------------------------------------------------------------------------------- /docker/Dockerfile.postgres: -------------------------------------------------------------------------------- 1 | ARG BASE=alpine:3.16 2 | 3 | FROM ${BASE} 4 | 5 | LABEL maintainer="Nischay Mamidi " 6 | 7 | COPY entrypoint.sh /entrypoint.sh 8 | 9 | COPY . /app 10 | 11 | WORKDIR /app 12 | 13 | RUN apk add --no-cache python3 py-pip git &&\ 14 | pip3 install --upgrade gtts psycopg[binary] &&\ 15 | pip3 install --upgrade python-telegram-bot &&\ 16 | pip3 install git+https://github.com/Nischay-Pro/telegres 17 | 18 | VOLUME [ "/app/config.json" ] 19 | 20 | ENTRYPOINT [ "/entrypoint.sh" ] 21 | -------------------------------------------------------------------------------- /niscoin/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Award(Enum): 5 | FIRST = "🥇" 6 | SECOND = "🥈" 7 | THIRD = "🥉" 8 | 9 | def __str__(self): 10 | return str(self.value) 11 | 12 | 13 | class Reputation(Enum): 14 | MININEGATIVE = -0.5 15 | NEGATIVE = -1 16 | MEGANEGATIVE = -2 17 | MINIPOSITIVE = 0.5 18 | POSITIVE = 1 19 | MEGAPOSITIVE = 2 20 | 21 | 22 | class CliStoreType(Enum): 23 | ODDS = "odds" 24 | MULTIPLIER = "multiplier" 25 | EXCHANGE = "exchange" 26 | TRANSLATION = "translation" 27 | -------------------------------------------------------------------------------- /docker/docker-compose.postgres.yml: -------------------------------------------------------------------------------- 1 | # Simple docker-compose file for self hosting 2 | 3 | version: "3.8" 4 | 5 | services: 6 | niscoin: 7 | image: nischaypro/niscoin-postgres:latest 8 | container_name: niscoin 9 | volumes: 10 | - ./config.json:/app/config.json 11 | restart: unless-stopped 12 | networks: 13 | - niscoin_network 14 | db: 15 | image: postgres:16 16 | container_name: db 17 | environment: 18 | POSTGRES_USER: niscoin 19 | POSTGRES_PASSWORD: niscoin 20 | POSTGRES_DB: telegres 21 | volumes: 22 | - ./data:/var/lib/postgresql/data 23 | restart: unless-stopped 24 | networks: 25 | - niscoin_network 26 | 27 | networks: 28 | niscoin_network: -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "bot_token": "", 3 | "lottery":{ 4 | "enabled": true, 5 | "odds": 200, 6 | "multiplier": 10 7 | }, 8 | "reputation":{ 9 | "minipositive": [".+", "miniplus", "mini+", "dotplus", "dp"], 10 | "positive": ["+", "+1", "plus"], 11 | "megapositive": ["++", "+2", "megaplus", "mega+"], 12 | "mininegative": [".-", "miniminus", "mini-", "dotminus", "dm"], 13 | "negative": ["-", "-1", "minus"], 14 | "meganegative": ["--", "-2", "megaminus", "mega-"], 15 | "ignorelist": [] 16 | }, 17 | "persistence":{ 18 | "backend": "postgres", 19 | "postgres_host": "localhost", 20 | "postgres_port": 5432, 21 | "postgres_username": "niscoin", 22 | "postgres_password": "niscoin", 23 | "postgres_database": "telegres", 24 | "postgres_schema": "niscoin" 25 | } 26 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Telegram XP Bot 2 | [![Issues](https://img.shields.io/github/issues/Nischay-Pro/python-telegram-xp)](https://github.com/Nischay-Pro/python-telegram-xp/issues) 3 | [![Forks](https://img.shields.io/github/forks/Nischay-Pro/python-telegram-xp)](https://github.com/Nischay-Pro/python-telegram-xp/network) 4 | [![Stars](https://img.shields.io/github/stars/Nischay-Pro/python-telegram-xp)](https://github.com/Nischay-Pro/python-telegram-xp/stargazers) 5 | 6 | ## What can it do? 7 | Currently, Python Telegram XP Bot can: 8 | 9 | * Award XP and levels to users automatically! 10 | * Manage XP and reputation of users in the group. 11 | * XP based Level Up system. 12 | * Telegram Support. 13 | 14 | ## Requirements 15 | 16 | TODO 17 | 18 | 19 | ## Instructions 20 | 21 | TODO 22 | 23 | ## Download 24 | 25 | TODO 26 | 27 | ## Special Thanks 28 | 29 | 1. [Suchit Kar](https://github.com/diddypod) for helping me debug and test the bot. 30 | 31 | ## Help 32 | 33 | Please raise an issue [here](https://github.com/python-telegram-xp/issues/new) if you have any problems. 34 | -------------------------------------------------------------------------------- /help.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": 3 | { 4 | "start": "Start the bot.", 5 | "reset": "Reset the bot to default settings (Wipes bot data!).", 6 | "topxp": "Display a table containing users with the most XP and level they current are.", 7 | "toplvl": "Alias to topxp.", 8 | "topcoins": "Display a table containing users with the highest number of coins.", 9 | "toprep": "Display a table containing users with the highest reputation.", 10 | "setxp": "Sets the XP of a user (Restricted).", 11 | "setrep": "Sets the reputation of a user (Restricted).", 12 | "setcoins": "Sets the coins of a user (Restricted).", 13 | "exchange": "Exchange XP for coins (Self).", 14 | "getcoins": "Gets the number of coins (Self).", 15 | "play": "Spend coins to TTS a custom provided message.", 16 | "give": "Give coins to a user.", 17 | "debug": "Access bot debug info (Restricted).", 18 | "about": "Display the about information for this bot.", 19 | "getxp": "Get the current XP and level of a user (Self).", 20 | "getlvl": "Alias to getxp.", 21 | "getrep": "Get the current reputation of a user (Self).", 22 | "getfilters": "Get the list of reputation filter words", 23 | "statistics": "Displays the general statistics of Niscoin." 24 | } 25 | } -------------------------------------------------------------------------------- /niscoin/misc.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from typing import Any 3 | from telegram import Update 4 | from bisect import bisect_left 5 | from re import match 6 | 7 | 8 | @functools.lru_cache(maxsize=500) 9 | def genLevels(x): 10 | if x == 0: 11 | return x 12 | elif x == 1: 13 | return 100 14 | 15 | if x % 2 == 0: 16 | xp = 2 * genLevels(x - 1) - genLevels(x - 2) + 35 17 | else: 18 | xp = 2 * genLevels(x - 1) - genLevels(x - 2) + 135 19 | 20 | return xp 21 | 22 | 23 | def get_level_from_xp(xp: int, levels: list) -> int: 24 | return bisect_left(levels, xp) 25 | 26 | 27 | async def configure_levels(update: Update, context: Any) -> None: 28 | if "configuration" in context.bot_data.keys(): 29 | try: 30 | levels = context.bot_data["configuration"]["levels"] 31 | except KeyError: 32 | levels = [genLevels(i) for i in range(1, 501)] 33 | context.bot_data["configuration"]["levels"] = levels 34 | else: 35 | levels = [genLevels(i) for i in range(1, 501)] 36 | context.bot_data["configuration"] = {"levels": levels} 37 | 38 | 39 | def booleanify(value: str) -> bool: 40 | if value.lower() in ["true", "yes", "on", "1"]: 41 | return True 42 | elif value.lower() in ["false", "no", "off", "0"]: 43 | return False 44 | else: 45 | raise ValueError 46 | 47 | 48 | def boolean_to_user(value: bool) -> str: 49 | if value: 50 | return "Enabled" 51 | else: 52 | return "Disabled" 53 | 54 | 55 | def match_only_alphanumeric(value: str) -> bool: 56 | return match(r"^[a-zA-Z0-9]+$", value) is not None 57 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: docker-build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | docker: 10 | strategy: 11 | matrix: 12 | backend: [pickle, postgres] 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v2 18 | - 19 | name: Docker meta 20 | id: meta 21 | uses: docker/metadata-action@v3 22 | with: 23 | # list of Docker images to use as base name for tags 24 | images: | 25 | nischaypro/niscoin-{{ matrix.backend }} 26 | ghcr.io/Nischay-Pro/niscoin-{{ matrix.backend }} 27 | # generate Docker tags based on the following events/attributes 28 | tags: | 29 | type=semver,suffix={{ matrix.backend }},pattern={{version}} 30 | type=semver,suffix={{ matrix.backend }},pattern={{major}}.{{minor}} 31 | - 32 | name: Set up QEMU 33 | uses: docker/setup-qemu-action@v1 34 | - 35 | name: Set up Docker Buildx 36 | uses: docker/setup-buildx-action@v1 37 | - 38 | name: Login to DockerHub 39 | uses: docker/login-action@v1 40 | with: 41 | username: ${{ secrets.DOCKERHUB_USERNAME }} 42 | password: ${{ secrets.DOCKERHUB_TOKEN }} 43 | - 44 | name: Login to GitHub Container Registry 45 | uses: docker/login-action@v1 46 | with: 47 | registry: ghcr.io 48 | username: ${{ github.repository_owner }} 49 | password: ${{ secrets.GITHUB_TOKEN }} 50 | - 51 | name: Build pickle 52 | uses: docker/build-push-action@v2 53 | with: 54 | context: . 55 | platforms: linux/amd64,linux/arm64 56 | file: docker/Dockerfile.{{ matrix.backend }} 57 | push: true 58 | tags: ${{ steps.meta.outputs.tags }} 59 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | *.txt 113 | progress.py 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | 134 | config.json 135 | db 136 | 137 | *.mp3 138 | 139 | convert.py 140 | data.json 141 | database/* 142 | -------------------------------------------------------------------------------- /niscoin/configuration.py: -------------------------------------------------------------------------------- 1 | import json 2 | from enum import Enum 3 | 4 | 5 | class PersistenceType(Enum): 6 | PICKLE = "pickle" 7 | POSTGRES = "postgres" 8 | 9 | 10 | class Configuration: 11 | """ 12 | Configuration class 13 | 14 | Args: 15 | raw_config (dict): The configuration dictionary. 16 | 17 | Attributes: 18 | persistence_type (PersistenceType): The persistence type. 19 | host (str): The hostname of the database. 20 | port (int): The port of the database. 21 | username (str): The username of the database. 22 | password (str): The password of the database. 23 | database (str): The database name. 24 | schema (str): The schema name. 25 | """ 26 | 27 | supported_persistence = ["pickle", "postgres"] 28 | 29 | def __init__(self, raw_config): 30 | self.raw_config = raw_config 31 | if type(self.raw_config) is not dict: 32 | raise TypeError("Configuration must be a dictionary.") 33 | 34 | self._parse_config() 35 | 36 | def __repr__(self): 37 | return "".format(self.config) 38 | 39 | def __str__(self): 40 | return json.dumps(self.raw_config, indent=4) 41 | 42 | def _parse_config(self): 43 | raw_config = self.raw_config 44 | 45 | self.bot_token = raw_config.get("bot_token", None) 46 | if "persistence" not in raw_config: 47 | raise KeyError("Persistence type not specified.") 48 | 49 | persistence = raw_config["persistence"].get("backend", "pickle") 50 | 51 | supported_persistences = set(item.value for item in PersistenceType) 52 | if persistence not in supported_persistences: 53 | raise ValueError( 54 | "Persistence type {} is not supported.".format(persistence) 55 | ) 56 | 57 | self.persistence_type = PersistenceType(persistence) 58 | 59 | if self.persistence_type == PersistenceType.POSTGRES: 60 | self._set_postgres_config() 61 | 62 | def _set_postgres_config(self): 63 | db_config = self.raw_config["persistence"] 64 | 65 | self.host = db_config.get("postgres_host", "localhost") 66 | self.port = db_config.get("postgres_port", 5432) 67 | self.username = db_config.get("postgres_username", "postgres") 68 | self.password = db_config.get("postgres_password", "postgres") 69 | self.database = db_config.get("postgres_database", "niscoin") 70 | self.schema = db_config.get("postgres_schema", "public") 71 | 72 | def set_pickle_directory(self, pickle_path): 73 | self.pickle_path = pickle_path 74 | 75 | def get_connection_string(self): 76 | if self.persistence_type == PersistenceType.POSTGRES: 77 | return "postgresql://{}:{}@{}:{}/{}".format( 78 | self.username, self.password, self.host, self.port, self.database 79 | ) 80 | else: 81 | return None 82 | -------------------------------------------------------------------------------- /niscoin/conversation.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from misc import configure_levels 3 | from help import help_strings, get_back_keyboard, get_help_keyboard 4 | from handlers import process_commands 5 | from telegram import ( 6 | Update, 7 | InlineKeyboardMarkup, 8 | ) 9 | from telegram.constants import ParseMode, ChatType 10 | from telegram.error import BadRequest 11 | import logging 12 | 13 | logging.basicConfig( 14 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO 15 | ) 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | back_keyboard = get_back_keyboard() 20 | help_keyboard = get_help_keyboard() 21 | 22 | help_main = """ 23 | Help 24 | 25 | Hello\! I am `niscoin`, a bot that allows you to game\-ify your group chats\. 26 | I support a number of commands, which are listed below\. 27 | If you find any bugs or have any suggestions, please visit my [GitHub repo page](https://github.com/Nischay\-Pro/niscoin) 28 | 29 | All commands can be used with the following \! prefix\. 30 | """ 31 | 32 | 33 | async def error(update: Update, context: Any) -> None: 34 | logger.warning('Update "%s" caused error "%s"', update, context.error) 35 | 36 | 37 | async def start(update: Update, context: Any) -> None: 38 | """Send a message when the command /start is issued.""" 39 | 40 | await configure_levels(update, context) 41 | await update.message.reply_text( 42 | "Type `!start` to begin", parse_mode=ParseMode.MARKDOWN_V2 43 | ) 44 | 45 | 46 | async def help_button(update: Update, context: Any) -> None: 47 | query = update.callback_query 48 | 49 | await query.answer() 50 | 51 | if query.data == "back": 52 | await query.edit_message_text( 53 | text=help_main, 54 | reply_markup=InlineKeyboardMarkup(help_keyboard), 55 | parse_mode=ParseMode.MARKDOWN_V2, 56 | disable_web_page_preview=True, 57 | ) 58 | else: 59 | text = f""" 60 | {help_strings[query.data].keyboard_text} `{help_strings[query.data].command}` 61 | {help_strings[query.data].description} 62 | """ 63 | try: 64 | await query.edit_message_text( 65 | text=text, 66 | reply_markup=InlineKeyboardMarkup(back_keyboard), 67 | parse_mode=ParseMode.MARKDOWN_V2, 68 | ) 69 | except BadRequest: 70 | pass 71 | 72 | 73 | async def help(update: Update, context: Any) -> None: 74 | """Send a message when the command /help is issued.""" 75 | 76 | if update.effective_chat.type != ChatType.PRIVATE: 77 | await update.message.reply_text( 78 | "Help is accessible only from a private chat. DM this bot to get help." 79 | ) 80 | return 81 | 82 | reply_markup = InlineKeyboardMarkup(help_keyboard) 83 | await update.message.reply_text( 84 | text=help_main, 85 | parse_mode=ParseMode.MARKDOWN_V2, 86 | reply_markup=reply_markup, 87 | disable_web_page_preview=True, 88 | ) 89 | 90 | 91 | async def echo(update: Update, context: Any) -> None: 92 | """Echo the user message.""" 93 | 94 | # Check if the user is in a group chat 95 | if update.effective_chat.type == ChatType.PRIVATE: 96 | await update.message.reply_text( 97 | "Niscoin is a group-only bot. Please run it in a group chat." 98 | ) 99 | else: 100 | await process_commands(update, context) 101 | -------------------------------------------------------------------------------- /niscoin/messages.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Messages(Enum): 5 | def __str__(self): 6 | return str(self.value) 7 | 8 | unauthorized_user = "You are not authorized to use this command." 9 | unauthorized_admin = "Only the owner can use this command." 10 | self_not_allowed = "You can't use this command on yourself." 11 | initate_success = "Niscoin has been initiated. Type away!" 12 | initate_fail = "Niscoin has already been initiated." 13 | 14 | coins_disabled = "Coins is disabled for this chat." 15 | rep_disabled = "Reputation is disabled for this chat." 16 | 17 | set_coins_invalid = "Please specify a valid number of coins." 18 | set_coins_noreply = "Please reply to the user you want to set coins for." 19 | set_coins_success = "Coins set successfully." 20 | 21 | set_xp_invalid = "Please specify a valid number of XP." 22 | set_xp_noreply = "Please reply to the user you want to set XP for." 23 | set_xp_success = "XP set successfully." 24 | 25 | set_rep_invalid = "Please specify a valid number of reputation." 26 | set_rep_noreply = "Please reply to the user you want to set reputation for." 27 | set_rep_success = "Reputation set successfully." 28 | 29 | exchange_invalid = "Please specify a valid coin amount to exchange your XP for." 30 | exchange_success = "Successfully exchanged coins." 31 | exchange_cost = "The exchange cost is {cost} XP." 32 | exchange_not_enough = "You don't have enough XP to exchange." 33 | 34 | give_noreply = "Please reply to the user you want to give coins to." 35 | give_invalid = "Please specify a valid amount of coins to give." 36 | give_success = "Successfully gave {amount} coins to {user}." 37 | give_insufficient = "You don't have enough coins to give." 38 | 39 | get_coins = "You have {coins} coins." 40 | get_xp = "You have {xp} XP with {level} levels." 41 | get_reputation = "You have {reputation} reputation." 42 | 43 | translation_disabled = "Text To Speech (TTS) is disabled for this chat." 44 | translation_not_enough_coins = "You don't have enough coins for TTS." 45 | translation_no_message = "Please type the message you want to TTS or reply to the message you want to TTS." 46 | translation_max_length = "The maximum length of a TTS message is 1000 characters." 47 | translation_invalid_slow_mode = "Please specify a valid flag for slow mode." 48 | translation_invalid_language = "Please specify a valid language." 49 | translation_cost = "The translation cost is {cost} coins." 50 | 51 | reputation_positive = "{user_give} ({user_give_rep}) increased the reputation of {user_receive} ({user_receive_rep})." 52 | reputation_negative = "{user_give} ({user_give_rep}) decreased the reputation of {user_receive} ({user_receive_rep})." 53 | reputation_unauthorized = ( 54 | "Only the owner and the administrators can use this command." 55 | ) 56 | 57 | cli_success = "Parameter changed successfully." 58 | cli_invalid = "Invalid cli arguments." 59 | cli_filter_word_exists = "The filter word {word} already exists." 60 | cli_filter_word_does_not_exist = "The filter word {word} does not exist." 61 | cli_filter_word_invalid_characters = ( 62 | "The filter word {word} contains invalid characters." 63 | ) 64 | cli_filter_word_added = "The filter word {word} has been added." 65 | cli_filter_word_removed = "The filter word {word} has been removed." 66 | cli_user_already_ignored = "User {user} is already ignored." 67 | cli_user_does_not_exist = "User with ID {user} does not exist." 68 | cli_user_added = "User {user} has been added to the ignore list." 69 | cli_user_removed = "User {user} has been removed from the ignore list." 70 | cli_user_not_in_ignore_list = "User {user} is not in the ignore list." 71 | cli_ignore_list_empty = "No users are currently ignored." 72 | -------------------------------------------------------------------------------- /niscoin/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import os 4 | import logging 5 | 6 | try: 7 | import ujson as json 8 | except ImportError: 9 | import json 10 | 11 | from configuration import Configuration, PersistenceType 12 | import conversation 13 | from misc import configure_levels 14 | 15 | try: 16 | import psycopg 17 | except ImportError: 18 | pass 19 | import sys 20 | from logging.handlers import TimedRotatingFileHandler 21 | 22 | from telegram.ext import ( 23 | Application, 24 | CommandHandler, 25 | CallbackQueryHandler, 26 | MessageHandler, 27 | PicklePersistence, 28 | filters, 29 | ) 30 | 31 | try: 32 | from telegres import PostgresPersistence 33 | except ImportError: 34 | pass 35 | 36 | cwd = os.getcwd() 37 | log_path = os.path.join(cwd, "logs") 38 | 39 | 40 | def main(): 41 | logger = logging.getLogger(__name__) 42 | parser = argparse.ArgumentParser( 43 | description="Command-line arguments to start niscoin." 44 | ) 45 | parser.add_argument( 46 | "--config", 47 | "-c", 48 | help="Path to config file.", 49 | required=False, 50 | default="config.json", 51 | ) 52 | parser.add_argument( 53 | "--database", 54 | "-d", 55 | help="Path to database directory.", 56 | required=False, 57 | default="data", 58 | ) 59 | 60 | args = parser.parse_args() 61 | 62 | if not os.path.isfile(args.config): 63 | logger.error("[-] Config file not found.") 64 | exit(1) 65 | 66 | try: 67 | config_data = Configuration(json.load(open(args.config))) 68 | except json.decoder.JSONDecodeError as e: 69 | logger.error("[-] Could not read config file.") 70 | logger.error(f"[-] Error: {e}") 71 | exit(1) 72 | 73 | if config_data.persistence_type == PersistenceType.PICKLE: 74 | if not os.path.isdir(args.database): 75 | logger.error("[-] Database directory not found.") 76 | exit(1) 77 | else: 78 | config_data.set_pickle_directory( 79 | os.path.join(os.path.abspath(args.database), "data.db") 80 | ) 81 | logger.info(f"[+] Pickle directory set to {config_data.pickle_path}") 82 | 83 | elif config_data.persistence_type == PersistenceType.POSTGRES: 84 | try: 85 | conn = psycopg.connect(config_data.get_connection_string()) 86 | conn.close() 87 | except psycopg.OperationalError as e: 88 | logger.error("[-] Could not connect to database.") 89 | logger.error("f[-] Error: {e}") 90 | exit(1) 91 | 92 | logger.info("[+] Configuration loaded.") 93 | logger.info("[+] Starting niscoin.") 94 | 95 | if config_data.persistence_type == PersistenceType.PICKLE: 96 | logger.info("[+] Using pickle persistence.") 97 | persistence = PicklePersistence(config_data.pickle_path) 98 | elif config_data.persistence_type == PersistenceType.POSTGRES: 99 | logger.info("[+] Using postgres persistence.") 100 | persistence = PostgresPersistence( 101 | postgres_url=config_data.get_connection_string(), 102 | postgres_schema=config_data.schema, 103 | ) 104 | 105 | niscoin_app = ( 106 | Application.builder() 107 | .token(config_data.bot_token) 108 | .persistence(persistence) 109 | .build() 110 | ) 111 | 112 | niscoin_app.add_handler(CommandHandler("start", conversation.start)) 113 | niscoin_app.add_handler(CommandHandler("help", conversation.help)) 114 | niscoin_app.add_handler( 115 | MessageHandler(filters.TEXT & ~filters.COMMAND, conversation.echo) 116 | ) 117 | niscoin_app.add_handler(CallbackQueryHandler(conversation.help_button)) 118 | niscoin_app.run_polling() 119 | 120 | 121 | if __name__ == "__main__": 122 | logger = logging.getLogger(__name__) 123 | log_file = os.path.join(log_path, "niscoin.log") 124 | if not os.path.isdir(log_path): 125 | os.mkdir(log_path) 126 | handler = TimedRotatingFileHandler(log_file, when="D", interval=1, backupCount=5) 127 | logger.addHandler(handler) 128 | logging.basicConfig( 129 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 130 | level=logger.info, 131 | ) 132 | 133 | main() 134 | -------------------------------------------------------------------------------- /niscoin/chat.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | 4 | CONFIGURATION_VERSION = 1 5 | 6 | 7 | class ChatConfiguration: 8 | def __init__(self): 9 | self.configuration = { 10 | "reputation": { 11 | "enabled": True, 12 | "adminOnly": False, 13 | "ignorelist": [], 14 | "filters": { 15 | "minipositive": { 16 | "base": [".+", "miniplus", "mini+", "dotplus", "dp"], 17 | "custom": [], 18 | "enabled": True, 19 | }, 20 | "positive": { 21 | "base": ["+", "+1", "plus"], 22 | "custom": [], 23 | "enabled": True, 24 | }, 25 | "megapositive": { 26 | "base": ["++", "+2", "megaplus", "mega+"], 27 | "custom": [], 28 | "enabled": True, 29 | }, 30 | "mininegative": { 31 | "base": [".-", "miniminus", "mini-", "dotminus", "dm"], 32 | "custom": [], 33 | "enabled": True, 34 | }, 35 | "negative": { 36 | "base": ["-", "-1", "minus"], 37 | "custom": [], 38 | "enabled": True, 39 | }, 40 | "meganegative": { 41 | "base": ["--", "-2", "megaminus", "mega-"], 42 | "custom": [], 43 | "enabled": True, 44 | }, 45 | }, 46 | }, 47 | "lottery": { 48 | "enabled": True, 49 | "odds": 200, 50 | "multiplier": 10, 51 | }, 52 | "translation": { 53 | "enabled": True, 54 | "cost": 50, 55 | }, 56 | "coins": { 57 | "enabled": True, 58 | "exchangeRate": 10, 59 | }, 60 | "initiated": False, 61 | "configuration_version": CONFIGURATION_VERSION, 62 | } 63 | self.user_data = {} 64 | 65 | def __repr__(self) -> str: 66 | return str(self.configuration) 67 | 68 | def get(self, key): 69 | return self.configuration[key] 70 | 71 | def as_dict(self): 72 | configuration = { 73 | "configuration": self.configuration, 74 | "user_data": self.user_data, 75 | } 76 | return configuration 77 | 78 | 79 | def user_template(user_id: int) -> dict: 80 | return { 81 | "xp": 0, 82 | "coins": 0, 83 | "rep": 0, 84 | "last_message": 0, 85 | "delta_award_time": 0, 86 | } 87 | 88 | 89 | def check_migration(configuration): 90 | if configuration.get("configuration_version") < CONFIGURATION_VERSION: 91 | return True 92 | return False 93 | 94 | 95 | def from_dict(configuration): 96 | new_configuration = ChatConfiguration() 97 | new_configuration.configuration = configuration["configuration"] 98 | new_configuration.user_data = configuration["user_data"] 99 | return new_configuration 100 | 101 | 102 | legacy_carry_forward_keys = [ 103 | "xp", 104 | "rep", 105 | "coins", 106 | "last_message", 107 | "delta_award_time", 108 | ] 109 | 110 | 111 | def migrate_legacy_user_data(old_user_data, new_configuration): 112 | new_configuration.user_data = old_user_data["users"] 113 | temp = copy.deepcopy(new_configuration.user_data) 114 | for user in temp: 115 | for user_item in temp[user].keys(): 116 | if user_item not in legacy_carry_forward_keys: 117 | del new_configuration.user_data[user][user_item] 118 | if "init" in old_user_data.keys(): 119 | new_configuration.configuration["initiated"] = old_user_data["init"] 120 | return new_configuration 121 | 122 | 123 | def migrate_configuration(old_configuration): 124 | old_version = old_configuration.get("configuration_version") 125 | if old_version is None: 126 | return ChatConfiguration 127 | elif old_version == CONFIGURATION_VERSION: 128 | return old_configuration 129 | elif old_version > CONFIGURATION_VERSION: 130 | raise Exception("Configuration version is too new.") 131 | else: 132 | raise Exception("Unknown configuration version") 133 | -------------------------------------------------------------------------------- /niscoin/help.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from telegram import ( 3 | InlineKeyboardButton, 4 | ) 5 | 6 | 7 | def get_help_keyboard() -> list: 8 | return list( 9 | map( 10 | list, 11 | zip( 12 | *[ 13 | iter( 14 | [ 15 | InlineKeyboardButton( 16 | f"{itm.keyboard_text}", callback_data=itm.name 17 | ) 18 | for itm in iter(help_strings.values()) 19 | ] 20 | ) 21 | ] 22 | * 3 23 | ), 24 | ) 25 | ) 26 | 27 | 28 | def get_back_keyboard() -> list: 29 | return [[InlineKeyboardButton("Back", callback_data="back")]] 30 | 31 | 32 | class HelpString: 33 | def __init__(self, name, description, keyboard_text, command): 34 | self.name = name 35 | self.description = textwrap.dedent(description) 36 | self.keyboard_text = keyboard_text 37 | self.command = command 38 | 39 | def __str__(self): 40 | return self.description 41 | 42 | 43 | help_strings = { 44 | "start": HelpString( 45 | "start", 46 | """ 47 | Start the bot\. You can run this command only in the group chat\. 48 | Invoke the command by typing `\!start` in the group chat\. 49 | 50 | *Who can run this command?* 51 | → Group Owner 52 | """, 53 | "Start", 54 | "\!start", 55 | ), 56 | "reset": HelpString( 57 | "reset", 58 | """ 59 | Reset the bot to default settings *\(Wipes bot data\!\)*\. 60 | 61 | *Who can run this command?* 62 | → Group Owner 63 | """, 64 | "Reset", 65 | "\!reset", 66 | ), 67 | "topxp": HelpString( 68 | "topxp", 69 | """ 70 | Display a table containing users with the most XP and level they current are\. 71 | Only the top 10 users are displayed\. 72 | 73 | *Who can run this command?* 74 | → Anyone 75 | """, 76 | "Top XP", 77 | "\!topxp", 78 | ), 79 | "toplvl": HelpString( 80 | "toplvl", 81 | """ 82 | Alias to topxp\. 83 | 84 | *Who can run this command?* 85 | → Anyone 86 | """, 87 | "Top XP", 88 | "\!toplvl", 89 | ), 90 | "topcoins": HelpString( 91 | "topcoins", 92 | """ 93 | Display a table containing users with the highest number of coins\. 94 | Only the top 10 users are displayed\. 95 | 96 | *Who can run this command?* 97 | → Anyone 98 | """, 99 | "Top Coins", 100 | "\!topcoins", 101 | ), 102 | "toprep": HelpString( 103 | "toprep", 104 | """ 105 | Display a table containing users with the highest reputation\. 106 | Only the top 10 users are displayed\. 107 | 108 | *Who can run this command?* 109 | → Anyone 110 | """, 111 | "Top Reputation", 112 | "\!toprep", 113 | ), 114 | "setxp": HelpString( 115 | "setxp", 116 | """ 117 | Sets the XP of a user\. You need to reply to the user you want to set XP for\. 118 | 119 | *Who can run this command?* 120 | → Group Owner 121 | → Admins 122 | """, 123 | "Set XP", 124 | "\!setxp", 125 | ), 126 | "setrep": HelpString( 127 | "setrep", 128 | """ 129 | Sets the reputation of a user\. You need to reply to the user you want to set rep for\. 130 | 131 | *Who can run this command?* 132 | → Group Owner 133 | → Admins 134 | """, 135 | "Set Reputation", 136 | "\!setrep", 137 | ), 138 | "setcoins": HelpString( 139 | "setcoins", 140 | """ 141 | Sets the coins of a user\. You need to reply to the user you want to set coins for\. 142 | 143 | *Who can run this command?* 144 | → Group Owner 145 | → Admins 146 | """, 147 | "Set Coins", 148 | "\!setcoins", 149 | ), 150 | "exchange": HelpString( 151 | "exchange", 152 | """ 153 | Exchange XP for coins\. 154 | 155 | *Example usage*: 156 | `\!exchange 10` 157 | 158 | This will exchange 10 XP for 1 coin\. You can also configure 159 | the exchange rate in the settings using the `\!cli` command\. 160 | 161 | *Who can run this command?* 162 | → Anyone 163 | """, 164 | "Exchange", 165 | "\!exchange", 166 | ), 167 | "getcoins": HelpString( 168 | "getcoins", 169 | """ 170 | Gets the number of coins \(Self\)\. 171 | 172 | *Who can run this command?* 173 | → Anyone 174 | """, 175 | "Get Coins", 176 | "\!getcoins", 177 | ), 178 | "play": HelpString( 179 | "play", 180 | """ 181 | Spend coins to TTS a custom provided message\. 182 | 183 | *Example usage*: 184 | WIP 185 | 186 | This will play the provided message in exchange for coins\. 187 | You can also configure the cost of the message in the settings 188 | using the `\!cli` command\. 189 | 190 | *Who can run this command?* 191 | → Anyone 192 | """, 193 | "Play", 194 | "\!play", 195 | ), 196 | "give": HelpString( 197 | "give", 198 | """ 199 | Give coins to a user\. You need to reply to the user you want to give coins to\. 200 | 201 | *Example usage*: 202 | `\!give 10` 203 | 204 | This will give 10 coins to the user you replied to\. 205 | 206 | *Who can run this command?* 207 | → Anyone 208 | """, 209 | "Give", 210 | "\!give", 211 | ), 212 | "debug": HelpString( 213 | "debug", 214 | """ 215 | Access bot debug info \(Restricted\)\. 216 | 217 | *Who can run this command?* 218 | → Group Owner 219 | """, 220 | "Debug", 221 | "\!debug", 222 | ), 223 | "about": HelpString( 224 | "about", 225 | """ 226 | Display the about information for this bot\. 227 | 228 | *Who can run this command?* 229 | → Anyone 230 | """, 231 | "About", 232 | "\!about", 233 | ), 234 | "getxp": HelpString( 235 | "getxp", 236 | """ 237 | Get the current XP and level of a user \(Self\)\. 238 | 239 | *Example usage*: 240 | `\!getxp` 241 | 242 | *Who can run this command?* 243 | → Anyone 244 | """, 245 | "Get XP", 246 | "\!getxp", 247 | ), 248 | "getlvl": HelpString( 249 | "getlvl", 250 | """ 251 | Alias to getxp\. 252 | 253 | *Who can run this command?* 254 | → Anyone 255 | """, 256 | "Get XP", 257 | "\!getlvl", 258 | ), 259 | "getrep": HelpString( 260 | "getrep", 261 | """ 262 | Get the current reputation of a user \(Self\)\. 263 | 264 | *Example usage*: 265 | `\!getrep` 266 | 267 | *Who can run this command?* 268 | → Anyone 269 | """, 270 | "Get Reputation", 271 | "\!getrep", 272 | ), 273 | "getfilters": HelpString( 274 | "getfilters", 275 | """ 276 | Get the list of reputation filter words\. 277 | 278 | *Who can run this command?* 279 | → Anyone 280 | """, 281 | "Get Filters", 282 | "\!getfilters", 283 | ), 284 | "statistics": HelpString( 285 | "statistics", 286 | """ 287 | Displays the general statistics of Niscoin\. 288 | 289 | *Who can run this command?* 290 | → Anyone 291 | """, 292 | "Statistics", 293 | "\!statistics", 294 | ), 295 | } 296 | -------------------------------------------------------------------------------- /niscoin/handlers.py: -------------------------------------------------------------------------------- 1 | from ctypes import Union 2 | from typing import Any 3 | from telegram import ( 4 | Update, 5 | ) 6 | import chat 7 | import random 8 | import time 9 | import shlex 10 | import commands 11 | import sys 12 | from constants import Reputation 13 | from messages import Messages 14 | from commands import _get_user_type, restricted_commands 15 | from telegram.constants import ParseMode, ChatMemberStatus 16 | from telegram.error import BadRequest 17 | from misc import configure_levels 18 | 19 | 20 | async def process_commands(update: Update, context: Any) -> None: 21 | """Process commands.""" 22 | 23 | await check_configuration(context) 24 | 25 | if _is_bot_initated(update, context): 26 | try: 27 | if update.message.text.startswith("!"): 28 | await parse_command(update, context) 29 | else: 30 | if await handle_reputation(update, context) is None: 31 | await reward_user(update, context) 32 | except AttributeError: 33 | return 34 | elif update.message.text == "!start": 35 | await parse_command(update, context) 36 | 37 | 38 | async def reward_user(update: Update, context: Any) -> None: 39 | if await _is_message_bot(update, context): 40 | return 41 | current_user_id = update.message.from_user.id 42 | current_user = update.message.from_user.full_name 43 | current_chat_id = update.message.chat_id 44 | chat_configuration = context.chat_data["configuration"] 45 | if _check_user_exists(current_user_id, chat_configuration): 46 | user_epoch = _get_user_key(current_user_id, "last_message", chat_configuration) 47 | epoch_time = int(time.time()) 48 | if (epoch_time - user_epoch) >= _get_user_key( 49 | current_user_id, "delta_award_time", chat_configuration 50 | ): 51 | random_xp = random.randint(1, 12) 52 | delta_award = random.randint(1 * 60, 4 * 60) 53 | if chat_configuration["configuration"]["lottery"]["enabled"]: 54 | odds = chat_configuration["configuration"]["lottery"]["odds"] 55 | multiplier = chat_configuration["configuration"]["lottery"][ 56 | "multiplier" 57 | ] 58 | if random.randint(1, odds) == 1: 59 | random_xp *= multiplier 60 | await context.bot.send_message( 61 | text=f"7️⃣7️⃣7️⃣ Lucky message! {current_user} has received {random_xp} XP!", 62 | chat_id=current_chat_id, 63 | ) 64 | _set_user_key(current_user_id, "xp", random_xp, chat_configuration, True) 65 | _set_user_key( 66 | current_user_id, "last_message", epoch_time, chat_configuration 67 | ) 68 | _set_user_key( 69 | current_user_id, "delta_award_time", delta_award, chat_configuration 70 | ) 71 | else: 72 | _create_user_data(current_user_id, chat_configuration) 73 | 74 | 75 | async def handle_reputation(update: Update, context: Any) -> Any: 76 | current_user_id = update.message.from_user.id 77 | chat_configuration = context.chat_data["configuration"] 78 | if not chat_configuration["configuration"]["reputation"]["enabled"]: 79 | return 80 | 81 | if ( 82 | current_user_id 83 | in context.chat_data["configuration"]["configuration"]["reputation"][ 84 | "ignorelist" 85 | ] 86 | ): 87 | return 88 | 89 | if update.message.reply_to_message is None: 90 | return 91 | 92 | if update.message.reply_to_message.from_user.id == update.message.from_user.id: 93 | return 94 | filters_list = context.chat_data["configuration"]["configuration"]["reputation"][ 95 | "filters" 96 | ] 97 | enabled_filters = tuple( 98 | filter(lambda x: filters_list[x]["enabled"], filters_list.keys()) 99 | ) 100 | reputation_triggers = {} 101 | reputation_list = [] 102 | for filter_itm in enabled_filters: 103 | reputation_triggers[filter_itm] = filters_list[filter_itm]["base"] 104 | reputation_triggers[filter_itm].extend(filters_list[filter_itm]["custom"]) 105 | reputation_list.extend(reputation_triggers[filter_itm]) 106 | 107 | reputation_type = None 108 | if update.message.text in reputation_list: 109 | for reputation_itm in reputation_triggers.keys(): 110 | if update.message.text in reputation_triggers[reputation_itm]: 111 | reputation_type = Reputation[reputation_itm.upper()] 112 | break 113 | 114 | if reputation_type is None: 115 | return 116 | 117 | reputation_modifier = reputation_type.value 118 | 119 | reply_user_id = update.message.reply_to_message.from_user.id 120 | chat_id = update.message.chat_id 121 | current_user = update.message.from_user.full_name 122 | reply_user = update.message.reply_to_message.from_user.full_name 123 | 124 | reputation_permissions = chat_configuration["configuration"]["reputation"][ 125 | "adminOnly" 126 | ] 127 | user_type = await _get_user_type(update, context, current_user_id) 128 | if reputation_permissions and user_type not in restricted_commands: 129 | await context.bot.send_message( 130 | text=Messages.reputation_unauthorized.value, 131 | chat_id=chat_id, 132 | reply_to_message_id=update.message.message_id, 133 | ) 134 | return 135 | 136 | if await _is_message_bot(update, context): 137 | return 138 | 139 | _ensure_user_exists(current_user_id, chat_configuration) 140 | _ensure_user_exists(reply_user_id, chat_configuration) 141 | 142 | chat_configuration["user_data"][str(reply_user_id)]["rep"] += reputation_modifier 143 | 144 | if abs(reputation_modifier) == 2: 145 | chat_configuration["user_data"][str(current_user_id)]["rep"] -= 1 146 | 147 | current_user_rep = _get_user_key(current_user_id, "rep", chat_configuration) 148 | reply_user_rep = _get_user_key(reply_user_id, "rep", chat_configuration) 149 | 150 | rep_positive = True if reputation_modifier > 0 else False 151 | if rep_positive: 152 | await context.bot.send_message( 153 | text=Messages.reputation_positive.value.format( 154 | user_give=current_user, 155 | user_give_rep=current_user_rep, 156 | user_receive=reply_user, 157 | user_receive_rep=reply_user_rep, 158 | ), 159 | chat_id=chat_id, 160 | parse_mode=ParseMode.HTML, 161 | ) 162 | else: 163 | await context.bot.send_message( 164 | text=Messages.reputation_negative.value.format( 165 | user_give=current_user, 166 | user_give_rep=current_user_rep, 167 | user_receive=reply_user, 168 | user_receive_rep=reply_user_rep, 169 | ), 170 | chat_id=chat_id, 171 | parse_mode=ParseMode.HTML, 172 | ) 173 | return True 174 | 175 | 176 | def _is_bot_initated(update: Update, context: Any) -> bool: 177 | return context.chat_data["configuration"]["configuration"]["initiated"] 178 | 179 | 180 | def _create_user_data(user_id: int, chat_configuration: dict) -> None: 181 | user_id = str(user_id) 182 | chat_configuration["user_data"][user_id] = chat.user_template(user_id) 183 | 184 | 185 | def _set_user_key( 186 | user_id: int, key: str, value: int, chat_configuration: dict, append: bool = False 187 | ) -> None: 188 | user_id = str(user_id) 189 | if append: 190 | chat_configuration["user_data"][user_id][key] += value 191 | else: 192 | chat_configuration["user_data"][user_id][key] = value 193 | 194 | 195 | def _get_user_key(user_id: int, key: str, chat_configuration: dict) -> int: 196 | user_id = str(user_id) 197 | return chat_configuration["user_data"][user_id][key] 198 | 199 | 200 | def _check_user_exists(user_id: int, chat_configuration: dict) -> bool: 201 | user_id = str(user_id) 202 | if user_id in chat_configuration["user_data"].keys(): 203 | return True 204 | else: 205 | return False 206 | 207 | 208 | def _ensure_user_exists(user_id: int, chat_configuration: dict) -> None: 209 | if not _check_user_exists(user_id, chat_configuration): 210 | _create_user_data(user_id, chat_configuration) 211 | 212 | 213 | async def _is_user_bot( 214 | user_id: int, chat_id: int, update: Update, context: Any 215 | ) -> bool: 216 | try: 217 | user_data = await context.bot.get_chat_member(chat_id, user_id) 218 | return user_data.user.is_bot 219 | except BadRequest: 220 | return True 221 | 222 | 223 | async def _is_message_bot(update: Update, context: Any) -> bool: 224 | user_id = update.message.from_user.id 225 | chat_id = update.message.chat_id 226 | if update.message.reply_to_message is not None: 227 | reply_user_id = update.message.reply_to_message.from_user.id 228 | if await _is_user_bot(reply_user_id, chat_id, update, context): 229 | return True 230 | 231 | return await _is_user_bot(user_id, chat_id, update, context) 232 | 233 | 234 | def _extract_command_argument(message: str) -> str: 235 | message = message.lower().replace("!", "") 236 | try: 237 | message = shlex.split(message) 238 | except ValueError: 239 | return {"command": None, "argument": None} 240 | if len(message) == 1: 241 | return {"command": message[0], "arguments": []} 242 | else: 243 | return {"command": message[0], "arguments": message[1:]} 244 | 245 | 246 | async def parse_command(update: Update, context: Any) -> None: 247 | args = _extract_command_argument(update.message.text) 248 | 249 | if await _is_message_bot(update, context): 250 | return 251 | 252 | if args["command"]: 253 | command = commands.supported_commands.get(args["command"], None) 254 | if command is not None: 255 | if args["command"] != "start": 256 | chat_configuration = context.chat_data["configuration"] 257 | _ensure_user_exists(update.message.from_user.id, chat_configuration) 258 | if update.message.reply_to_message is not None: 259 | _ensure_user_exists( 260 | update.message.reply_to_message.from_user.id, chat_configuration 261 | ) 262 | await command.execute( 263 | update=update, context=context, args=args["arguments"] 264 | ) 265 | else: 266 | await context.bot.send_message( 267 | text=f"Command !{args['command']} not found!", 268 | chat_id=update.message.chat_id, 269 | ) 270 | 271 | 272 | async def check_configuration(context: Any) -> None: 273 | if "configuration" in context.chat_data.keys(): 274 | chat_configuration = chat.from_dict(context.chat_data["configuration"]) 275 | if chat.check_migration(chat_configuration): 276 | context.chat_data["configuration"] = chat.migrate_configuration( 277 | chat_configuration 278 | ).as_dict() 279 | else: 280 | if "init" in context.chat_data.keys(): 281 | new_configuration = chat.migrate_legacy_user_data( 282 | context.chat_data, chat.ChatConfiguration() 283 | ) 284 | del context.chat_data["init"] 285 | del context.chat_data["duels"] 286 | del context.chat_data["users"] 287 | await configure_levels(None, context) 288 | context.chat_data["configuration"] = new_configuration.as_dict() 289 | else: 290 | context.chat_data["configuration"] = chat.ChatConfiguration().as_dict() 291 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /niscoin/commands.py: -------------------------------------------------------------------------------- 1 | from email import message 2 | from lib2to3.pytree import Base 3 | from help import help_strings 4 | from messages import Messages 5 | import sys 6 | import inspect 7 | from misc import ( 8 | get_level_from_xp, 9 | booleanify, 10 | boolean_to_user, 11 | match_only_alphanumeric, 12 | ) 13 | from constants import Award, CliStoreType, Reputation 14 | from telegram.constants import ChatMemberStatus, ParseMode 15 | from telegram.error import BadRequest 16 | import tempfile 17 | from gtts import gTTS, lang 18 | import os 19 | import textwrap 20 | 21 | supported_tts_languages = lang.tts_langs() 22 | 23 | 24 | async def _get_user_name(update, context, user_id, chat_id): 25 | try: 26 | user_data = await context.bot.get_chat_member(chat_id, user_id) 27 | except BadRequest: 28 | return None 29 | user_first_name = user_data.user.first_name 30 | user_last_name = user_data.user.last_name 31 | 32 | if user_last_name is None: 33 | user_last_name = "" 34 | 35 | return { 36 | "first_name": user_first_name, 37 | "last_name": user_last_name, 38 | } 39 | 40 | 41 | async def _get_user_type(update, context, user_id): 42 | user_data = await context.bot.get_chat_member(update.message.chat_id, user_id) 43 | user_type = user_data.status 44 | 45 | return user_type 46 | 47 | 48 | def _type_base_command(value): 49 | try: 50 | value.__command__ 51 | return True 52 | except AttributeError: 53 | return False 54 | 55 | 56 | def _is_type_int(value): 57 | try: 58 | int(value) 59 | return True 60 | except ValueError: 61 | return False 62 | 63 | 64 | def _is_positive(value): 65 | try: 66 | if int(value) > 0: 67 | return True 68 | else: 69 | return False 70 | except ValueError: 71 | return False 72 | 73 | 74 | restricted_commands = (ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER) 75 | 76 | 77 | class BaseCommand: 78 | def __init__(self, name="base"): 79 | self.name = name 80 | 81 | def execute(self, context=None, update=None, args=None): 82 | raise NotImplementedError 83 | 84 | def help(self): 85 | try: 86 | return help_strings[self.name].description 87 | except KeyError: 88 | raise NotImplementedError(f"Help for command {self.name} not found") 89 | 90 | def __str__(self): 91 | return self.name 92 | 93 | def __command__(self): 94 | return True 95 | 96 | 97 | class StartCommand(BaseCommand): 98 | def __init__(self): 99 | super().__init__("start") 100 | 101 | async def execute(self, context=None, update=None, args=None): 102 | chat_id = update.message.chat_id 103 | user_id = update.message.from_user.id 104 | message_id = update.message.message_id 105 | 106 | if not await _get_user_type(update, context, user_id) in ChatMemberStatus.OWNER: 107 | await context.bot.send_message( 108 | text=Messages.unauthorized_admin.value, 109 | chat_id=chat_id, 110 | reply_to_message_id=message_id, 111 | ) 112 | return 113 | 114 | if context.chat_data["configuration"]["configuration"]["initiated"]: 115 | await context.bot.send_message( 116 | text=Messages.initate_fail.value, 117 | chat_id=chat_id, 118 | reply_to_message_id=message_id, 119 | ) 120 | return 121 | else: 122 | context.chat_data["configuration"]["configuration"]["initiated"] = True 123 | await context.bot.send_message( 124 | text=Messages.initate_success.value, 125 | chat_id=chat_id, 126 | reply_to_message_id=message_id, 127 | ) 128 | 129 | 130 | class TopCoinsCommand(BaseCommand): 131 | def __init__(self): 132 | super().__init__("topcoins") 133 | 134 | async def execute(self, update=None, context=None, args=None): 135 | chat_text = "The current Coins table: \n" 136 | chat_id = update.message.chat_id 137 | users = context.chat_data["configuration"]["user_data"] 138 | users_sorted = sorted(users.items(), key=lambda x: x[1]["coins"], reverse=True) 139 | for idx, user in enumerate(users_sorted[0:10]): 140 | try: 141 | user_name = await _get_user_name(update, context, user[0], chat_id) 142 | chat_text += f'{idx + 1}. {user_name["first_name"]} {user_name["last_name"]} ({user[1]["coins"]})\n' 143 | except KeyError: 144 | pass 145 | await context.bot.send_message( 146 | chat_id=chat_id, text=chat_text, parse_mode="HTML" 147 | ) 148 | 149 | 150 | class TopReputationCommand(BaseCommand): 151 | def __init__(self): 152 | super().__init__("toprep") 153 | 154 | async def execute(self, update=None, context=None, args=None): 155 | chat_text = "The current Reputation table: \n" 156 | chat_id = update.message.chat_id 157 | users = context.chat_data["configuration"]["user_data"] 158 | users_sorted = sorted(users.items(), key=lambda x: x[1]["rep"], reverse=True) 159 | for idx, user in enumerate(users_sorted[0:10]): 160 | try: 161 | user_name = await _get_user_name(update, context, user[0], chat_id) 162 | award = "" 163 | if idx < 3: 164 | award = list(Award)[idx].value 165 | chat_text += f'{idx + 1}. {user_name["first_name"]} {user_name["last_name"]} ({user[1]["rep"]}) {award}\n' 166 | except KeyError: 167 | pass 168 | await context.bot.send_message( 169 | chat_id=chat_id, text=chat_text, parse_mode="HTML" 170 | ) 171 | 172 | 173 | class TopXPCommand(BaseCommand): 174 | def __init__(self): 175 | super().__init__("topxp") 176 | 177 | async def execute(self, update=None, context=None, args=None): 178 | chat_text = "The current XP table: \n" 179 | chat_id = update.message.chat_id 180 | users = context.chat_data["configuration"]["user_data"] 181 | users_sorted = sorted(users.items(), key=lambda x: x[1]["xp"], reverse=True) 182 | for idx, user in enumerate(users_sorted[0:10]): 183 | try: 184 | user_name = await _get_user_name(update, context, user[0], chat_id) 185 | user_xp = user[1]["xp"] 186 | levels = context.bot_data["configuration"]["levels"] 187 | user_level = get_level_from_xp(user_xp, levels) 188 | user_level_bound = levels[user_level] 189 | award = "" 190 | if idx < 3: 191 | award = list(Award)[idx].value 192 | chat_text += f'{idx + 1}. {user_name["first_name"]} {user_name["last_name"]} ({user_xp} / {user_level_bound}) - Level {user_level} {award}\n' 193 | except KeyError: 194 | pass 195 | await context.bot.send_message( 196 | chat_id=chat_id, text=chat_text, parse_mode="HTML" 197 | ) 198 | 199 | 200 | class ExchangeCommand(BaseCommand): 201 | def __init__(self): 202 | super().__init__("exchange") 203 | 204 | async def execute(self, context=None, update=None, args=None): 205 | chat_id = update.message.chat_id 206 | user_id = update.message.from_user.id 207 | message_id = update.message.message_id 208 | 209 | current_user_xp = context.chat_data["configuration"]["user_data"][str(user_id)][ 210 | "xp" 211 | ] 212 | current_exchange_rate = context.chat_data["configuration"]["configuration"][ 213 | "coins" 214 | ]["exchangeRate"] 215 | 216 | if not context.chat_data["configuration"]["configuration"]["coins"]["enabled"]: 217 | await context.bot.send_message( 218 | chat_id=chat_id, 219 | text=Messages.coins_disabled.value, 220 | reply_to_message_id=message_id, 221 | ) 222 | return 223 | 224 | if len(args) == 1: 225 | if _is_type_int(args[0]) and _is_positive(args[0]): 226 | coins_to_exchange = int(args[0]) 227 | if coins_to_exchange * current_exchange_rate > current_user_xp: 228 | await context.bot.send_message( 229 | chat_id=chat_id, 230 | text=Messages.exchange_not_enough.value, 231 | reply_to_message_id=message_id, 232 | ) 233 | return 234 | else: 235 | new_user_xp = current_user_xp - ( 236 | coins_to_exchange * current_exchange_rate 237 | ) 238 | context.chat_data["configuration"]["user_data"][str(user_id)][ 239 | "xp" 240 | ] = new_user_xp 241 | context.chat_data["configuration"]["user_data"][str(user_id)][ 242 | "coins" 243 | ] += coins_to_exchange 244 | await context.bot.send_message( 245 | chat_id=chat_id, 246 | text=Messages.exchange_success.value, 247 | reply_to_message_id=message_id, 248 | ) 249 | return 250 | elif args[0] == "rate": 251 | await context.bot.send_message( 252 | chat_id=chat_id, 253 | text=Messages.exchange_cost.value.format( 254 | cost=current_exchange_rate 255 | ), 256 | reply_to_message_id=message_id, 257 | ) 258 | return 259 | else: 260 | await context.bot.send_message( 261 | chat_id=chat_id, 262 | text=Messages.exchange_invalid.value, 263 | reply_to_message_id=message_id, 264 | ) 265 | return 266 | else: 267 | await context.bot.send_message( 268 | chat_id=chat_id, 269 | text=Messages.exchange_invalid.value, 270 | reply_to_message_id=message_id, 271 | ) 272 | return 273 | 274 | 275 | class GiveCoinsCommand(BaseCommand): 276 | def __init__(self): 277 | super().__init__("give") 278 | 279 | async def execute(self, context=None, update=None, args=None): 280 | 281 | chat_id = update.message.chat_id 282 | user_id = update.message.from_user.id 283 | message_id = update.message.message_id 284 | 285 | current_user_coins = context.chat_data["configuration"]["user_data"][ 286 | str(user_id) 287 | ]["coins"] 288 | 289 | if not context.chat_data["configuration"]["configuration"]["coins"]["enabled"]: 290 | await context.bot.send_message( 291 | chat_id=chat_id, 292 | text=Messages.coins_disabled.value, 293 | reply_to_message_id=message_id, 294 | ) 295 | return 296 | 297 | if update.message.reply_to_message is None: 298 | await context.bot.send_message( 299 | chat_id=chat_id, 300 | text=Messages.give_noreply.value, 301 | reply_to_message_id=message_id, 302 | ) 303 | return 304 | 305 | if update.message.reply_to_message.from_user.id == user_id: 306 | await context.bot.send_message( 307 | chat_id=chat_id, 308 | text=Messages.self_not_allowed.value, 309 | reply_to_message_id=message_id, 310 | ) 311 | return 312 | 313 | if len(args) == 0: 314 | await context.bot.send_message( 315 | chat_id=chat_id, 316 | text=Messages.give_invalid.value, 317 | reply_to_message_id=message_id, 318 | ) 319 | return 320 | 321 | elif len(args) == 1: 322 | if _is_type_int(args[0]) and _is_positive(args[0]): 323 | coins_to_give = int(args[0]) 324 | if coins_to_give > current_user_coins: 325 | await context.bot.send_message( 326 | chat_id=chat_id, 327 | text=Messages.give_insufficient.value, 328 | reply_to_message_id=message_id, 329 | ) 330 | return 331 | else: 332 | context.chat_data["configuration"]["user_data"][str(user_id)][ 333 | "coins" 334 | ] -= coins_to_give 335 | context.chat_data["configuration"]["user_data"][ 336 | str(update.message.reply_to_message.from_user.id) 337 | ]["coins"] += coins_to_give 338 | await context.bot.send_message( 339 | chat_id=chat_id, 340 | text=Messages.give_success.value.format( 341 | amount=coins_to_give, 342 | user=update.message.reply_to_message.from_user.first_name, 343 | ), 344 | reply_to_message_id=message_id, 345 | ) 346 | return 347 | 348 | else: 349 | await context.bot.send_message( 350 | chat_id=chat_id, 351 | text=Messages.give_invalid.value, 352 | reply_to_message_id=message_id, 353 | ) 354 | return 355 | 356 | else: 357 | await context.bot.send_message( 358 | chat_id=chat_id, 359 | text=Messages.give_invalid.value, 360 | reply_to_message_id=message_id, 361 | ) 362 | return 363 | 364 | 365 | class GetCoinsCommand(BaseCommand): 366 | def __init__(self): 367 | super().__init__("getcoins") 368 | 369 | async def execute(self, context=None, update=None, args=None): 370 | 371 | chat_id = update.message.chat_id 372 | user_id = update.message.from_user.id 373 | message_id = update.message.message_id 374 | 375 | current_user_coins = context.chat_data["configuration"]["user_data"][ 376 | str(user_id) 377 | ]["coins"] 378 | 379 | if not context.chat_data["configuration"]["configuration"]["coins"]["enabled"]: 380 | await context.bot.send_message( 381 | chat_id=chat_id, 382 | text=Messages.coins_disabled.value, 383 | reply_to_message_id=message_id, 384 | ) 385 | return 386 | 387 | await context.bot.send_message( 388 | chat_id=chat_id, 389 | text=Messages.get_coins.value.format(coins=current_user_coins), 390 | reply_to_message_id=message_id, 391 | ) 392 | return 393 | 394 | 395 | class GetXPCommand(BaseCommand): 396 | def __init__(self): 397 | super().__init__("getxp") 398 | 399 | async def execute(self, context=None, update=None, args=None): 400 | 401 | chat_id = update.message.chat_id 402 | user_id = update.message.from_user.id 403 | message_id = update.message.message_id 404 | 405 | current_user_xp = context.chat_data["configuration"]["user_data"][str(user_id)][ 406 | "xp" 407 | ] 408 | 409 | xp_levels = context.bot_data["configuration"]["levels"] 410 | 411 | await context.bot.send_message( 412 | chat_id=chat_id, 413 | text=Messages.get_xp.value.format( 414 | xp=current_user_xp, level=get_level_from_xp(current_user_xp, xp_levels) 415 | ), 416 | reply_to_message_id=message_id, 417 | ) 418 | return 419 | 420 | 421 | class GetReputationCommand(BaseCommand): 422 | def __init__(self): 423 | super().__init__("getrep") 424 | 425 | async def execute(self, context=None, update=None, args=None): 426 | 427 | chat_id = update.message.chat_id 428 | user_id = update.message.from_user.id 429 | message_id = update.message.message_id 430 | 431 | current_user_reputation = context.chat_data["configuration"]["user_data"][ 432 | str(user_id) 433 | ]["rep"] 434 | 435 | await context.bot.send_message( 436 | chat_id=chat_id, 437 | text=Messages.get_reputation.value.format( 438 | reputation=current_user_reputation 439 | ), 440 | reply_to_message_id=message_id, 441 | ) 442 | return 443 | 444 | 445 | class SetCoinsCommand(BaseCommand): 446 | def __init__(self): 447 | super().__init__("setcoins") 448 | 449 | async def execute(self, context=None, update=None, args=None): 450 | 451 | chat_id = update.message.chat_id 452 | user_id = update.message.from_user.id 453 | message_id = update.message.message_id 454 | 455 | if not context.chat_data["configuration"]["configuration"]["coins"]["enabled"]: 456 | await context.bot.send_message( 457 | chat_id=chat_id, 458 | text=Messages.coins_disabled.value, 459 | reply_to_message_id=message_id, 460 | ) 461 | return 462 | 463 | reply_user_id = update.message.reply_to_message.from_user.id 464 | 465 | if not await _get_user_type(update, context, user_id) in restricted_commands: 466 | await context.bot.send_message( 467 | chat_id=chat_id, 468 | text=Messages.unauthorized_user.value, 469 | reply_to_message_id=message_id, 470 | ) 471 | return 472 | 473 | if update.message.reply_to_message is None: 474 | await context.bot.send_message( 475 | chat_id=chat_id, 476 | text=Messages.set_coins_noreply.value, 477 | reply_to_message_id=message_id, 478 | ) 479 | return 480 | 481 | if len(args) == 1 and _is_type_int(args[0]) and _is_positive(args[0]): 482 | context.chat_data["configuration"]["user_data"][str(reply_user_id)][ 483 | "coins" 484 | ] = int(args[0]) 485 | await context.bot.send_message( 486 | chat_id=chat_id, 487 | text=Messages.set_coins_success.value, 488 | reply_to_message_id=message_id, 489 | ) 490 | return 491 | else: 492 | await context.bot.send_message( 493 | chat_id=chat_id, 494 | text=Messages.set_coins_invalid.value, 495 | reply_to_message_id=message_id, 496 | ) 497 | return 498 | 499 | 500 | class SetXPCommand(BaseCommand): 501 | def __init__(self): 502 | super().__init__("setxp") 503 | 504 | async def execute(self, context=None, update=None, args=None): 505 | 506 | chat_id = update.message.chat_id 507 | user_id = update.message.from_user.id 508 | message_id = update.message.message_id 509 | 510 | if not await _get_user_type(update, context, user_id) in restricted_commands: 511 | await context.bot.send_message( 512 | chat_id=chat_id, 513 | text=Messages.unauthorized_user.value, 514 | reply_to_message_id=message_id, 515 | ) 516 | return 517 | 518 | if update.message.reply_to_message is None: 519 | await context.bot.send_message( 520 | chat_id=chat_id, 521 | text=Messages.set_xp_noreply.value, 522 | reply_to_message_id=message_id, 523 | ) 524 | return 525 | 526 | reply_user_id = update.message.reply_to_message.from_user.id 527 | 528 | if len(args) == 1 and _is_type_int(args[0]) and _is_positive(args[0]): 529 | context.chat_data["configuration"]["user_data"][str(reply_user_id)][ 530 | "xp" 531 | ] = int(args[0]) 532 | await context.bot.send_message( 533 | chat_id=chat_id, 534 | text=Messages.set_xp_success.value, 535 | reply_to_message_id=message_id, 536 | ) 537 | return 538 | else: 539 | await context.bot.send_message( 540 | chat_id=chat_id, 541 | text=Messages.set_xp_invalid.value, 542 | reply_to_message_id=message_id, 543 | ) 544 | return 545 | 546 | 547 | class SetReputationCommand(BaseCommand): 548 | def __init__(self): 549 | super().__init__("setrep") 550 | 551 | async def execute(self, context=None, update=None, args=None): 552 | 553 | chat_id = update.message.chat_id 554 | user_id = update.message.from_user.id 555 | message_id = update.message.message_id 556 | 557 | if not await _get_user_type(update, context, user_id) in restricted_commands: 558 | await context.bot.send_message( 559 | chat_id=chat_id, 560 | text=Messages.unauthorized_user.value, 561 | reply_to_message_id=message_id, 562 | ) 563 | return 564 | 565 | if update.message.reply_to_message is None: 566 | await context.bot.send_message( 567 | chat_id=chat_id, 568 | text=Messages.set_rep_noreply.value, 569 | reply_to_message_id=message_id, 570 | ) 571 | return 572 | 573 | reply_user_id = update.message.reply_to_message.from_user.id 574 | 575 | if len(args) == 1 and _is_type_int(args[0]) and _is_positive(args[0]): 576 | context.chat_data["configuration"]["user_data"][str(reply_user_id)][ 577 | "rep" 578 | ] = int(args[0]) 579 | await context.bot.send_message( 580 | chat_id=chat_id, 581 | text=Messages.set_rep_success.value, 582 | reply_to_message_id=message_id, 583 | ) 584 | return 585 | else: 586 | await context.bot.send_message( 587 | chat_id=chat_id, 588 | text=Messages.set_rep_invalid.value, 589 | reply_to_message_id=message_id, 590 | ) 591 | return 592 | 593 | 594 | class PlayCommand(BaseCommand): 595 | def __init__(self): 596 | super().__init__("play") 597 | 598 | async def parse_tts(self, context=None, update=None, args=None): 599 | if args[0] in ["lang", "language", "l"]: 600 | text = "Supported languages are:\n\n" 601 | for lang in supported_tts_languages.keys(): 602 | text += f"{lang} → {supported_tts_languages[lang]}" + "\n" 603 | await context.bot.send_message( 604 | chat_id=update.message.chat_id, 605 | text=text, 606 | reply_to_message_id=update.message.message_id, 607 | ) 608 | return None 609 | 610 | if args[0] in ["cost"]: 611 | tts_cost = context.chat_data["configuration"]["configuration"][ 612 | "translation" 613 | ]["cost"] 614 | await context.bot.send_message( 615 | chat_id=update.message.chat_id, 616 | text=Messages.translation_cost.value.format(cost=tts_cost), 617 | reply_to_message_id=update.message.message_id, 618 | ) 619 | return None 620 | 621 | if args[0] not in supported_tts_languages.keys(): 622 | await context.bot.send_message( 623 | chat_id=update.message.chat_id, 624 | text=Messages.translation_invalid_language.value, 625 | reply_to_message_id=update.message.message_id, 626 | ) 627 | return None 628 | 629 | try: 630 | slow_mode = booleanify(args[1]) 631 | except ValueError: 632 | await context.bot.send_message( 633 | chat_id=update.message.chat_id, 634 | text=Messages.translation_invalid_slow_mode.value, 635 | reply_to_message_id=update.message.message_id, 636 | ) 637 | return None 638 | 639 | if update.message.reply_to_message is None: 640 | try: 641 | tts_message = args[2] 642 | except IndexError: 643 | await context.bot.send_message( 644 | chat_id=update.message.chat_id, 645 | text=Messages.translation_no_message.value, 646 | reply_to_message_id=update.message.message_id, 647 | ) 648 | return None 649 | else: 650 | tts_message = update.message.reply_to_message.text 651 | 652 | return { 653 | "language": args[0], 654 | "slow_mode": slow_mode, 655 | "message": tts_message, 656 | } 657 | 658 | async def execute(self, context=None, update=None, args=None): 659 | chat_id = update.message.chat_id 660 | user_id = update.message.from_user.id 661 | message_id = update.message.message_id 662 | 663 | if not context.chat_data["configuration"]["configuration"]["translation"][ 664 | "enabled" 665 | ]: 666 | await context.bot.send_message( 667 | chat_id=chat_id, 668 | text=Messages.translation_disabled.value, 669 | reply_to_message_id=message_id, 670 | ) 671 | return 672 | 673 | user_coins = context.chat_data["configuration"]["user_data"][str(user_id)][ 674 | "coins" 675 | ] 676 | tts_cost = context.chat_data["configuration"]["configuration"]["translation"][ 677 | "cost" 678 | ] 679 | 680 | if len(args) == 0: 681 | await context.bot.send_message( 682 | chat_id=chat_id, 683 | text=Messages.translation_no_message.value, 684 | reply_to_message_id=message_id, 685 | ) 686 | return 687 | elif len(args) > 0: 688 | tts_options = await self.parse_tts(context, update, args) 689 | if tts_options is None: 690 | return 691 | 692 | if len(tts_options["message"]) > 1000: 693 | await context.bot.send_message( 694 | chat_id=chat_id, 695 | text=Messages.translation_max_length.value, 696 | reply_to_message_id=message_id, 697 | ) 698 | return 699 | 700 | else: 701 | if user_coins < tts_cost: 702 | await context.bot.send_message( 703 | chat_id=chat_id, 704 | text=Messages.translation_not_enough_coins.value, 705 | reply_to_message_id=message_id, 706 | ) 707 | return 708 | tts_output = gTTS( 709 | tts_options["message"], 710 | lang=tts_options["language"], 711 | slow=tts_options["slow_mode"], 712 | ) 713 | with tempfile.TemporaryDirectory() as tmp_dir: 714 | tmp_file = os.path.join(tmp_dir, "tts.mp3") 715 | tts_output.save(tmp_file) 716 | await context.bot.send_voice( 717 | chat_id=chat_id, 718 | voice=open(tmp_file, "rb"), 719 | reply_to_message_id=message_id, 720 | ) 721 | context.chat_data["configuration"]["user_data"][str(user_id)][ 722 | "coins" 723 | ] -= tts_cost 724 | return 725 | 726 | 727 | class GetFiltersCommand(BaseCommand): 728 | def __init__(self): 729 | super().__init__("getfilters") 730 | 731 | async def execute(self, context=None, update=None, args=None): 732 | chat_id = update.message.chat_id 733 | message_id = update.message.message_id 734 | 735 | filters_list = context.chat_data["configuration"]["configuration"][ 736 | "reputation" 737 | ]["filters"] 738 | enabled_filters = tuple( 739 | filter(lambda x: filters_list[x]["enabled"], filters_list.keys()) 740 | ) 741 | disabled_filters = set(filters_list.keys()) - set(enabled_filters) 742 | 743 | text = "Currently enabled reputation filters: \n" 744 | for idx, filter_name in enumerate(enabled_filters): 745 | filter_triggers = ( 746 | filters_list[filter_name]["base"] + filters_list[filter_name]["custom"] 747 | ) 748 | filter_triggers = ", ".join(filter_triggers) 749 | text += f"{idx + 1}. {filter_name} → {filter_triggers}\n" 750 | 751 | if len(disabled_filters) > 0: 752 | text += "\nCurrently disabled reputation filters: \n" 753 | for idx, filter_name in enumerate(disabled_filters): 754 | text += f"{idx + 1}. {filter_name}\n" 755 | 756 | await context.bot.send_message( 757 | chat_id=chat_id, 758 | text=text, 759 | reply_to_message_id=message_id, 760 | parse_mode=ParseMode.HTML, 761 | ) 762 | return 763 | 764 | 765 | class CliCommand(BaseCommand): 766 | def __init__(self): 767 | super().__init__("cli") 768 | 769 | help_text_main = """ 770 | Usage: !cli [ OBJECT ] [ OPTIONS ] [ ARGUMENTS ] 771 | 772 | Where: 773 | OPTIONS :- { set | get | add | remove | list } 774 | OBJECT :- { coins | reputation | lottery | translation | filters } 775 | 776 | Description: 777 | coins :- Configure the bot's coin system, including the XP to coin exchange rate. 778 | reputation :- Configure the bot's reputation system and the permissions of who 779 | can give and take reputation. 780 | lottery :- Configure the bot's XP lottery system, including the probability and 781 | multiplier. 782 | translation :- Configure the bot's text to speech system and the cost for each 783 | TTS request. 784 | filters :- Configure the bot's reputation filters, including enabling and disabling 785 | individual filters. You can also view the status of all filters and add 786 | or remove new filter triggers.""" 787 | 788 | help_text_coins = """ 789 | Usage: 790 | 791 | !cli coins [ set | get ] [ enabled | disabled ] 792 | !cli coins [ set | get ] [ exchange / rate ] [ amount ]""" 793 | 794 | help_text_lottery = """ 795 | Usage: 796 | 797 | !cli lottery [ set | get ] [ enabled | disabled ] 798 | !cli lottery odds [ set | get ] [ amount ] 799 | !cli lottery multipler [ set | get ] [ amount ]""" 800 | 801 | help_text_translation = """ 802 | Usage: 803 | 804 | !cli [ translation / tts ] [ set | get ] [ enabled | disabled ] 805 | !cli [ translation / tts ] cost [ set | get ] [ amount ]""" 806 | 807 | help_text_reputation = """ 808 | Usage: 809 | 810 | !cli reputation [ set | get ] [ enabled | disabled ] 811 | !cli reputation adminOnly [ set | get ] [ enabled | disabled ] 812 | !cli reputation ignoreList [ add | remove ] ( As a reply to the user to ignore) 813 | !cli reputation ignoreList [ add | remove ] [@user]""" 814 | 815 | help_text_filters = """ 816 | Usage: 817 | 818 | !cli filters [ get ] 819 | !cli filters [ filter_name ] [ set | get ] [ enabled | disabled ] 820 | !cli filters [ filter_name ] custom [ add | remove | list ] [ trigger ]""" 821 | 822 | async def invalid_option(self, context=None, update=None): 823 | chat_id = update.message.chat_id 824 | message_id = update.message.message_id 825 | 826 | await context.bot.send_message( 827 | chat_id=chat_id, 828 | text=Messages.cli_invalid.value, 829 | reply_to_message_id=message_id, 830 | ) 831 | return 832 | 833 | async def coin_handler(self, context=None, update=None, args=None): 834 | if len(args) == 0 or args[0] == "help": 835 | await context.bot.send_message( 836 | chat_id=update.message.chat_id, 837 | text=textwrap.dedent(self.help_text_coins), 838 | reply_to_message_id=update.message.message_id, 839 | parse_mode=ParseMode.HTML, 840 | ) 841 | return 842 | else: 843 | if args[0] == "set" and len(args) > 1: 844 | if args[1] == "enabled": 845 | context.chat_data["configuration"]["configuration"]["coins"][ 846 | "enabled" 847 | ] = True 848 | await context.bot.send_message( 849 | chat_id=update.message.chat_id, 850 | text=Messages.cli_success.value, 851 | reply_to_message_id=update.message.message_id, 852 | ) 853 | return 854 | elif args[1] == "disabled": 855 | context.chat_data["configuration"]["configuration"]["coins"][ 856 | "enabled" 857 | ] = False 858 | await context.bot.send_message( 859 | chat_id=update.message.chat_id, 860 | text=Messages.cli_success.value, 861 | reply_to_message_id=update.message.message_id, 862 | ) 863 | return 864 | else: 865 | await self.invalid_option(context, update) 866 | return 867 | 868 | elif args[0] == "get": 869 | await context.bot.send_message( 870 | chat_id=update.message.chat_id, 871 | text=boolean_to_user( 872 | context.chat_data["configuration"]["configuration"]["coins"][ 873 | "enabled" 874 | ] 875 | ), 876 | reply_to_message_id=update.message.message_id, 877 | ) 878 | return 879 | elif args[0] in ("rate", "exchange"): 880 | await self.int_handler(context, update, args[1:], CliStoreType.EXCHANGE) 881 | return 882 | else: 883 | await self.invalid_option(context, update) 884 | return 885 | 886 | async def lottery_handler(self, context=None, update=None, args=None): 887 | if len(args) == 0 or args[0] == "help": 888 | await context.bot.send_message( 889 | chat_id=update.message.chat_id, 890 | text=textwrap.dedent(self.help_text_lottery), 891 | reply_to_message_id=update.message.message_id, 892 | parse_mode=ParseMode.HTML, 893 | ) 894 | return 895 | else: 896 | if args[0] == "set" and len(args) > 1: 897 | if args[1] == "enabled": 898 | context.chat_data["configuration"]["configuration"]["lottery"][ 899 | "enabled" 900 | ] = True 901 | await context.bot.send_message( 902 | chat_id=update.message.chat_id, 903 | text=Messages.cli_success.value, 904 | reply_to_message_id=update.message.message_id, 905 | ) 906 | return 907 | elif args[1] == "disabled": 908 | context.chat_data["configuration"]["configuration"]["lottery"][ 909 | "enabled" 910 | ] = False 911 | await context.bot.send_message( 912 | chat_id=update.message.chat_id, 913 | text=Messages.cli_success.value, 914 | reply_to_message_id=update.message.message_id, 915 | ) 916 | return 917 | else: 918 | await self.invalid_option(context, update) 919 | return 920 | elif args[0] == "get": 921 | await context.bot.send_message( 922 | chat_id=update.message.chat_id, 923 | text=boolean_to_user( 924 | context.chat_data["configuration"]["configuration"]["lottery"][ 925 | "enabled" 926 | ] 927 | ), 928 | reply_to_message_id=update.message.message_id, 929 | ) 930 | return 931 | elif args[0] == "odds": 932 | await self.int_handler( 933 | context, update, args[1:], store=CliStoreType.ODDS 934 | ) 935 | return 936 | elif args[0] == "multiplier": 937 | await self.int_handler( 938 | context, update, args[1:], store=CliStoreType.MULTIPLIER 939 | ) 940 | return 941 | else: 942 | await self.invalid_option(context, update) 943 | return 944 | 945 | async def reputation_ignore_handler(self, context=None, update=None, args=None): 946 | chat_id = update.message.chat_id 947 | if len(args) == 0: 948 | await self.invalid_option(context, update) 949 | return 950 | elif (len(args) == 2 and args[0] in ("add", "remove")) or ( 951 | len(args) == 1 952 | and args[0] in ("add", "remove") 953 | and update.message.reply_to_message is not None 954 | ): 955 | user_list = context.chat_data["configuration"]["configuration"][ 956 | "reputation" 957 | ]["ignorelist"] 958 | if update.message.reply_to_message is None: 959 | try: 960 | user_id = int(args[1]) 961 | except ValueError: 962 | await self.invalid_option(context, update) 963 | return 964 | else: 965 | user_id = update.message.reply_to_message.from_user.id 966 | if user_id in user_list: 967 | user_name = await _get_user_name(update, context, user_id, chat_id) 968 | if args[0] == "add": 969 | text = Messages.cli_user_already_ignored.value.format( 970 | user=user_name["first_name"] 971 | ) 972 | else: 973 | user_list.remove(user_id) 974 | text = Messages.cli_user_removed.value.format( 975 | user=user_name["first_name"] 976 | ) 977 | 978 | await context.bot.send_message( 979 | chat_id=update.message.chat_id, 980 | text=text, 981 | reply_to_message_id=update.message.message_id, 982 | ) 983 | return 984 | else: 985 | user_name = await _get_user_name(update, context, user_id, chat_id) 986 | if user_name is None: 987 | await context.bot.send_message( 988 | chat_id=update.message.chat_id, 989 | text=Messages.cli_user_does_not_exist.value.format( 990 | user=user_id 991 | ), 992 | reply_to_message_id=update.message.message_id, 993 | ) 994 | return 995 | else: 996 | if args[0] == "add": 997 | user_list.append(user_id) 998 | text = Messages.cli_user_added.value.format( 999 | user=user_name["first_name"] 1000 | ) 1001 | else: 1002 | text = Messages.cli_user_not_in_ignore_list.value.format( 1003 | user=user_name["first_name"] 1004 | ) 1005 | await context.bot.send_message( 1006 | chat_id=update.message.chat_id, 1007 | text=text, 1008 | reply_to_message_id=update.message.message_id, 1009 | ) 1010 | return 1011 | elif args[0] == "list": 1012 | user_list = context.chat_data["configuration"]["configuration"][ 1013 | "reputation" 1014 | ]["ignorelist"] 1015 | if len(user_list) == 0: 1016 | text = Messages.cli_ignore_list_empty.value 1017 | else: 1018 | text = "Users in ignore list:\n" 1019 | for user_id in user_list.copy(): 1020 | user_name = await _get_user_name(update, context, user_id, chat_id) 1021 | try: 1022 | text += f'→ {user_name["first_name"]} {user_name["last_name"]}\n' 1023 | except (KeyError, TypeError): 1024 | user_list.remove(user_id) 1025 | await context.bot.send_message( 1026 | chat_id=update.message.chat_id, 1027 | text=text, 1028 | reply_to_message_id=update.message.message_id, 1029 | parse_mode=ParseMode.HTML, 1030 | ) 1031 | return 1032 | else: 1033 | await self.invalid_option(context, update) 1034 | return 1035 | 1036 | async def reputation_handler(self, context=None, update=None, args=None): 1037 | if len(args) == 0 or args[0] == "help": 1038 | await context.bot.send_message( 1039 | chat_id=update.message.chat_id, 1040 | text=textwrap.dedent(self.help_text_reputation), 1041 | reply_to_message_id=update.message.message_id, 1042 | parse_mode=ParseMode.HTML, 1043 | ) 1044 | return 1045 | else: 1046 | if args[0] == "set" and len(args) > 1: 1047 | if args[1] == "enabled": 1048 | context.chat_data["configuration"]["configuration"]["reputation"][ 1049 | "enabled" 1050 | ] = True 1051 | await context.bot.send_message( 1052 | chat_id=update.message.chat_id, 1053 | text=Messages.cli_success.value, 1054 | reply_to_message_id=update.message.message_id, 1055 | ) 1056 | return 1057 | elif args[1] == "disabled": 1058 | context.chat_data["configuration"]["configuration"]["reputation"][ 1059 | "enabled" 1060 | ] = False 1061 | await context.bot.send_message( 1062 | chat_id=update.message.chat_id, 1063 | text=Messages.cli_success.value, 1064 | reply_to_message_id=update.message.message_id, 1065 | ) 1066 | return 1067 | else: 1068 | await self.invalid_option(context, update) 1069 | return 1070 | elif args[0] == "get": 1071 | await context.bot.send_message( 1072 | chat_id=update.message.chat_id, 1073 | text=boolean_to_user( 1074 | context.chat_data["configuration"]["configuration"][ 1075 | "reputation" 1076 | ]["enabled"] 1077 | ), 1078 | reply_to_message_id=update.message.message_id, 1079 | ) 1080 | return 1081 | elif args[0] == "adminonly": 1082 | if args[1] == "get": 1083 | await context.bot.send_message( 1084 | chat_id=update.message.chat_id, 1085 | text=boolean_to_user( 1086 | context.chat_data["configuration"]["configuration"][ 1087 | "reputation" 1088 | ]["adminOnly"] 1089 | ), 1090 | reply_to_message_id=update.message.message_id, 1091 | ) 1092 | return 1093 | elif args[1] == "set": 1094 | if args[2] == "enabled": 1095 | context.chat_data["configuration"]["configuration"][ 1096 | "reputation" 1097 | ]["adminOnly"] = True 1098 | await context.bot.send_message( 1099 | chat_id=update.message.chat_id, 1100 | text=Messages.cli_success.value, 1101 | reply_to_message_id=update.message.message_id, 1102 | ) 1103 | return 1104 | elif args[2] == "disabled": 1105 | context.chat_data["configuration"]["configuration"][ 1106 | "reputation" 1107 | ]["adminOnly"] = False 1108 | await context.bot.send_message( 1109 | chat_id=update.message.chat_id, 1110 | text=Messages.cli_success.value, 1111 | reply_to_message_id=update.message.message_id, 1112 | ) 1113 | return 1114 | else: 1115 | await self.invalid_option(context, update) 1116 | return 1117 | elif args[0] == "ignorelist": 1118 | await self.reputation_ignore_handler(context, update, args[1:]) 1119 | return 1120 | else: 1121 | await self.invalid_option(context, update) 1122 | return 1123 | 1124 | async def translation_handler(self, context=None, update=None, args=None): 1125 | if len(args) == 0 or args[0] == "help": 1126 | await context.bot.send_message( 1127 | chat_id=update.message.chat_id, 1128 | text=textwrap.dedent(self.help_text_translation), 1129 | reply_to_message_id=update.message.message_id, 1130 | parse_mode=ParseMode.HTML, 1131 | ) 1132 | return 1133 | else: 1134 | if args[0] == "set" and len(args) > 1: 1135 | if args[1] == "enabled": 1136 | context.chat_data["configuration"]["configuration"]["translation"][ 1137 | "enabled" 1138 | ] = True 1139 | await context.bot.send_message( 1140 | chat_id=update.message.chat_id, 1141 | text=Messages.cli_success.value, 1142 | reply_to_message_id=update.message.message_id, 1143 | ) 1144 | return 1145 | elif args[1] == "disabled": 1146 | context.chat_data["configuration"]["configuration"]["translation"][ 1147 | "enabled" 1148 | ] = False 1149 | await context.bot.send_message( 1150 | chat_id=update.message.chat_id, 1151 | text=Messages.cli_success.value, 1152 | reply_to_message_id=update.message.message_id, 1153 | ) 1154 | return 1155 | else: 1156 | await self.invalid_option(context, update) 1157 | return 1158 | elif args[0] == "get": 1159 | await context.bot.send_message( 1160 | chat_id=update.message.chat_id, 1161 | text=boolean_to_user( 1162 | context.chat_data["configuration"]["configuration"][ 1163 | "translation" 1164 | ]["enabled"] 1165 | ), 1166 | reply_to_message_id=update.message.message_id, 1167 | ) 1168 | return 1169 | elif args[0] in "cost": 1170 | await self.int_handler( 1171 | context, update, args[1:], CliStoreType.TRANSLATION 1172 | ) 1173 | return 1174 | else: 1175 | await self.invalid_option(context, update) 1176 | return 1177 | 1178 | async def reputation_filter_handler( 1179 | self, context=None, update=None, args=None, reputation_filter=None 1180 | ): 1181 | filter_dict = context.chat_data["configuration"]["configuration"]["reputation"][ 1182 | "filters" 1183 | ] 1184 | reputation_list = [] 1185 | for filter_itm in filter_dict: 1186 | reputation_list.extend(filter_dict[filter_itm]["base"]) 1187 | reputation_list.extend(filter_dict[filter_itm]["custom"]) 1188 | if args[0] == "list": 1189 | rep_filters_base = context.chat_data["configuration"]["configuration"][ 1190 | "reputation" 1191 | ]["filters"][reputation_filter.name.lower()]["base"] 1192 | rep_filters_custom = context.chat_data["configuration"]["configuration"][ 1193 | "reputation" 1194 | ]["filters"][reputation_filter.name.lower()]["custom"] 1195 | text = f"Currently active trigger words for {reputation_filter.name.lower()}:\n" 1196 | text += "Base triggers:\n" 1197 | for word in rep_filters_base: 1198 | text += f"→ {word}\n" 1199 | text += "Custom triggers:\n" 1200 | if len(rep_filters_custom) == 0: 1201 | text += "None configured\n" 1202 | else: 1203 | for word in rep_filters_custom: 1204 | text += f"→ {word}\n" 1205 | await context.bot.send_message( 1206 | chat_id=update.message.chat_id, 1207 | text=text, 1208 | reply_to_message_id=update.message.message_id, 1209 | parse_mode=ParseMode.HTML, 1210 | ) 1211 | return 1212 | elif len(args) == 2 and args[0] == "add": 1213 | custom_word = args[1] 1214 | if custom_word in reputation_list: 1215 | await context.bot.send_message( 1216 | chat_id=update.message.chat_id, 1217 | text=Messages.cli_filter_word_exists.value.format(word=custom_word), 1218 | reply_to_message_id=update.message.message_id, 1219 | ) 1220 | return 1221 | else: 1222 | if not match_only_alphanumeric(custom_word): 1223 | await context.bot.send_message( 1224 | chat_id=update.message.chat_id, 1225 | text=Messages.cli_filter_word_invalid_characters.value.format( 1226 | word=custom_word 1227 | ), 1228 | reply_to_message_id=update.message.message_id, 1229 | ) 1230 | return 1231 | context.chat_data["configuration"]["configuration"]["reputation"][ 1232 | "filters" 1233 | ][reputation_filter.name.lower()]["custom"].append(custom_word) 1234 | await context.bot.send_message( 1235 | chat_id=update.message.chat_id, 1236 | text=Messages.cli_filter_word_added.value.format(word=custom_word), 1237 | reply_to_message_id=update.message.message_id, 1238 | ) 1239 | return 1240 | elif len(args) == 2 and args[0] == "remove": 1241 | custom_word = args[1] 1242 | if custom_word in reputation_list: 1243 | context.chat_data["configuration"]["configuration"]["reputation"][ 1244 | "filters" 1245 | ][reputation_filter.name.lower()]["custom"].remove(custom_word) 1246 | await context.bot.send_message( 1247 | chat_id=update.message.chat_id, 1248 | text=Messages.cli_filter_word_removed.value.format( 1249 | word=custom_word 1250 | ), 1251 | reply_to_message_id=update.message.message_id, 1252 | ) 1253 | return 1254 | else: 1255 | await context.bot.send_message( 1256 | chat_id=update.message.chat_id, 1257 | text=Messages.cli_filter_word_does_not_exist.value.format( 1258 | word=custom_word 1259 | ), 1260 | reply_to_message_id=update.message.message_id, 1261 | ) 1262 | return 1263 | else: 1264 | await self.invalid_option(context, update) 1265 | return 1266 | return 1267 | 1268 | async def filters_handler(self, context=None, update=None, args=None, store=None): 1269 | if len(args) == 0 or args[0] == "help": 1270 | await context.bot.send_message( 1271 | chat_id=update.message.chat_id, 1272 | text=textwrap.dedent(self.help_text_filters), 1273 | reply_to_message_id=update.message.message_id, 1274 | parse_mode=ParseMode.HTML, 1275 | ) 1276 | return 1277 | elif len(args) == 1 and args[0] == "get": 1278 | filters = context.chat_data["configuration"]["configuration"]["reputation"][ 1279 | "filters" 1280 | ].keys() 1281 | text = "Reputation Filters available:\n" 1282 | for idx, filter in enumerate(filters): 1283 | filter_status = boolean_to_user( 1284 | context.chat_data["configuration"]["configuration"]["reputation"][ 1285 | "filters" 1286 | ][filter]["enabled"] 1287 | ) 1288 | filter_value = Reputation[filter.upper()].value 1289 | text += f"{idx + 1}. {filter.title()} ({filter_value}) → {filter_status}\n" 1290 | await context.bot.send_message( 1291 | chat_id=update.message.chat_id, 1292 | text=text, 1293 | reply_to_message_id=update.message.message_id, 1294 | parse_mode=ParseMode.HTML, 1295 | ) 1296 | return 1297 | elif len(args) == 2 and args[1] == "get": 1298 | try: 1299 | filter = Reputation[args[0].upper()] 1300 | except KeyError: 1301 | await self.invalid_option(context, update) 1302 | return 1303 | await context.bot.send_message( 1304 | chat_id=update.message.chat_id, 1305 | text=boolean_to_user( 1306 | context.chat_data["configuration"]["configuration"]["reputation"][ 1307 | "filters" 1308 | ][filter.name.lower()]["enabled"] 1309 | ), 1310 | reply_to_message_id=update.message.message_id, 1311 | ) 1312 | return 1313 | elif len(args) == 3 and args[1] == "set": 1314 | try: 1315 | filter = Reputation[args[0].upper()] 1316 | except KeyError: 1317 | await self.invalid_option(context, update) 1318 | return 1319 | if args[2] == "enabled": 1320 | context.chat_data["configuration"]["configuration"]["reputation"][ 1321 | "filters" 1322 | ][filter.name.lower()]["enabled"] = True 1323 | await context.bot.send_message( 1324 | chat_id=update.message.chat_id, 1325 | text=Messages.cli_success.value, 1326 | reply_to_message_id=update.message.message_id, 1327 | ) 1328 | return 1329 | elif args[2] == "disabled": 1330 | context.chat_data["configuration"]["configuration"]["reputation"][ 1331 | "filters" 1332 | ][filter.name.lower()]["enabled"] = False 1333 | await context.bot.send_message( 1334 | chat_id=update.message.chat_id, 1335 | text=Messages.cli_success.value, 1336 | reply_to_message_id=update.message.message_id, 1337 | ) 1338 | return 1339 | else: 1340 | await self.invalid_option(context, update) 1341 | return 1342 | elif len(args) > 2 and args[1] == "custom": 1343 | try: 1344 | filter = Reputation[args[0].upper()] 1345 | except KeyError: 1346 | await self.invalid_option(context, update) 1347 | return 1348 | await self.reputation_filter_handler(context, update, args[2:], filter) 1349 | return 1350 | else: 1351 | await self.invalid_option(context, update) 1352 | return 1353 | 1354 | async def int_handler(self, context=None, update=None, args=None, store=None): 1355 | if store == CliStoreType.EXCHANGE: 1356 | store_path = context.chat_data["configuration"]["configuration"]["coins"][ 1357 | "exchangeRate" 1358 | ] 1359 | elif store == CliStoreType.ODDS: 1360 | store_path = context.chat_data["configuration"]["configuration"]["lottery"][ 1361 | "odds" 1362 | ] 1363 | elif store == CliStoreType.MULTIPLIER: 1364 | store_path = context.chat_data["configuration"]["configuration"]["lottery"][ 1365 | "multiplier" 1366 | ] 1367 | elif store == CliStoreType.TRANSLATION: 1368 | store_path = context.chat_data["configuration"]["configuration"][ 1369 | "translation" 1370 | ]["cost"] 1371 | if args[0] == "get": 1372 | await context.bot.send_message( 1373 | chat_id=update.message.chat_id, 1374 | text=f"{store_path}", 1375 | reply_to_message_id=update.message.message_id, 1376 | ) 1377 | return 1378 | elif args[0] == "set": 1379 | if _is_type_int(args[1]) and _is_positive(args[1]): 1380 | if store == CliStoreType.EXCHANGE: 1381 | context.chat_data["configuration"]["configuration"]["coins"][ 1382 | "exchangeRate" 1383 | ] = int(args[1]) 1384 | elif store == CliStoreType.ODDS: 1385 | context.chat_data["configuration"]["configuration"]["lottery"][ 1386 | "odds" 1387 | ] = int(args[1]) 1388 | elif store == CliStoreType.MULTIPLIER: 1389 | context.chat_data["configuration"]["configuration"]["lottery"][ 1390 | "multiplier" 1391 | ] = int(args[1]) 1392 | elif store == CliStoreType.TRANSLATION: 1393 | context.chat_data["configuration"]["configuration"]["translation"][ 1394 | "cost" 1395 | ] = int(args[1]) 1396 | await context.bot.send_message( 1397 | chat_id=update.message.chat_id, 1398 | text=Messages.cli_success.value, 1399 | reply_to_message_id=update.message.message_id, 1400 | ) 1401 | return 1402 | else: 1403 | await self.invalid_option(context, update) 1404 | return 1405 | else: 1406 | await self.invalid_option(context, update) 1407 | return 1408 | 1409 | async def execute(self, context=None, update=None, args=None): 1410 | user_id = update.message.from_user.id 1411 | if not await _get_user_type(update, context, user_id) in ChatMemberStatus.OWNER: 1412 | await context.bot.send_message( 1413 | chat_id=update.message.chat_id, 1414 | text=Messages.unauthorized_admin.value, 1415 | reply_to_message_id=update.message.message_id, 1416 | ) 1417 | return 1418 | if len(args) == 0 or args[0] == "help": 1419 | await context.bot.send_message( 1420 | chat_id=update.message.chat_id, 1421 | text=textwrap.dedent(self.help_text_main), 1422 | reply_to_message_id=update.message.message_id, 1423 | parse_mode=ParseMode.HTML, 1424 | ) 1425 | return 1426 | elif len(args) > 0: 1427 | if args[0] == "coins": 1428 | await self.coin_handler(context, update, args[1:]) 1429 | return 1430 | elif args[0] == "lottery": 1431 | await self.lottery_handler(context, update, args[1:]) 1432 | return 1433 | elif args[0] in ("translation", "tts"): 1434 | await self.translation_handler(context, update, args[1:]) 1435 | return 1436 | elif args[0] == "filters": 1437 | await self.filters_handler(context, update, args[1:]) 1438 | return 1439 | elif args[0] == "reputation": 1440 | await self.reputation_handler(context, update, args[1:]) 1441 | return 1442 | else: 1443 | await context.bot.send_message( 1444 | chat_id=update.message.chat_id, 1445 | text=textwrap.dedent(self.help_text_main), 1446 | reply_to_message_id=update.message.message_id, 1447 | parse_mode=ParseMode.HTML, 1448 | ) 1449 | return 1450 | 1451 | 1452 | supported_commands = { 1453 | itm[1]().__str__(): itm[1]() 1454 | for itm in inspect.getmembers(sys.modules[__name__], predicate=_type_base_command) 1455 | } 1456 | 1457 | del supported_commands["base"] 1458 | --------------------------------------------------------------------------------