├── .dependabot └── config.yml ├── .github ├── dependabot.yml └── workflows │ ├── greetings.yml │ └── stale.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.md ├── config.sample ├── docker └── dev │ ├── Dockerfile │ ├── Pipfile │ ├── Pipfile.lock │ └── docker-compose.yml ├── requirements.txt ├── runtime.txt └── tg_bot ├── __init__.py ├── __main__.py ├── _version.py ├── config.py ├── deactivated-modules ├── modules ├── __init__.py ├── admin.py ├── afk.py ├── antiflood.py ├── backups.py ├── bans.py ├── blacklist.py ├── cust_filters.py ├── disable.py ├── global_bans.py ├── helper_funcs │ ├── __init__.py │ ├── chat_status.py │ ├── extraction.py │ ├── filters.py │ ├── handlers.py │ ├── misc.py │ ├── msg_types.py │ └── string_handling.py ├── locks.py ├── log_channel.py ├── misc.py ├── msg_deleting.py ├── muting.py ├── notes.py ├── reporting.py ├── rss.py ├── rules.py ├── sed.py ├── spam.py ├── sql │ ├── __init__.py │ ├── afk_sql.py │ ├── antiflood_sql.py │ ├── blacklist_sql.py │ ├── cust_filters_sql.py │ ├── disable_sql.py │ ├── global_bans_sql.py │ ├── locks_sql.py │ ├── log_channel_sql.py │ ├── notes_sql.py │ ├── reporting_sql.py │ ├── rss_sql.py │ ├── rules_sql.py │ ├── userinfo_sql.py │ ├── users_sql.py │ ├── warns_sql.py │ └── welcome_sql.py ├── translation.py ├── userinfo.py ├── users.py ├── warns.py └── welcome.py └── sample_config.py /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | # Bare-minium dependabot configuration 2 | version: 1 3 | update_configs: 4 | - package_manager: "python" 5 | directory: "/" 6 | update_schedule: "live" 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Grazie per aver aperto un issue! Stiamo lavorando per rendere il bot sempre migliore:)'' first issue' 13 | pr-message: 'Grazie per la tua richiesta, assicurati di aver letto CONTRIBUTIONS.md prima di continuare:)'' first pr' 14 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'Questo issue è inattivo da molto tempo. Per favore, controlla le nuove versioni del bot: se il problema persiste commenta qui per evitare la chiusura del thread.' 17 | stale-pr-message: 'Questa pull-request è inattiva da molto tempo. Per favore, controlla le nuove versioni del bot e se pensi che la PR sia ancora valida commenta qui.' 18 | stale-issue-label: 'old-issue' 19 | stale-pr-label: 'old-pr' 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tg_bot/config.py 2 | *.pyc 3 | .idea/ 4 | .project 5 | .pydevproject 6 | .directory 7 | .vscode 8 | *.env* 9 | *.DS_Store* 10 | 11 | # Created by https://www.gitignore.io/api/python 12 | # Edit at https://www.gitignore.io/?templates=python 13 | 14 | ### Python ### 15 | # Byte-compiled / optimized / DLL files 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Distribution / packaging 24 | .Python 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | pip-wheel-metadata/ 38 | share/python-wheels/ 39 | *.egg-info/ 40 | .installed.cfg 41 | *.egg 42 | MANIFEST 43 | 44 | # PyInstaller 45 | # Usually these files are written by a python script from a template 46 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 47 | *.manifest 48 | *.spec 49 | 50 | # Installer logs 51 | pip-log.txt 52 | pip-delete-this-directory.txt 53 | 54 | # Unit test / coverage reports 55 | htmlcov/ 56 | .tox/ 57 | .nox/ 58 | .coverage 59 | .coverage.* 60 | .cache 61 | nosetests.xml 62 | coverage.xml 63 | *.cover 64 | .hypothesis/ 65 | .pytest_cache/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | .python-version 99 | 100 | # pipenv 101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 104 | # install all needed dependencies. 105 | #Pipfile.lock 106 | 107 | # celery beat schedule file 108 | celerybeat-schedule 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | 140 | # End of https://www.gitignore.io/api/python 141 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | default_language_version: 7 | python: python3.6 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are very welcome! Here are some guidelines on how the project is designed. 4 | 5 | ### CodeStyle 6 | 7 | - Adhere to PEP8 as much as possible. 8 | 9 | - Line lengths should be under 120 characters, use list comprehensions over map/filter, don't leave trailing whitespace. 10 | 11 | - More complex pieces of code should be commented for future reference. 12 | 13 | ### Structure 14 | 15 | There are a few self-imposed rules on the project structure, to keep the project as tidy as possible. 16 | - All modules should go into the `modules/` directory. 17 | - Any database accesses should be done in `modules/sql/` - no instances of SESSION should be imported anywhere else. 18 | - Make sure your database sessions are properly scoped! Always close them properly. 19 | - When creating a new module, there should be as few changes to other files as possible required to incorporate it. 20 | Removing the module file should result in a bot which is still in perfect working condition. 21 | - If a module is dependent on multiple other files, which might not be loaded, then create a list of at module 22 | load time, in `__main__`, by looking at attributes. This is how migration, /help, /stats, /info, and many other things 23 | are based off of. It allows the bot to work fine with the LOAD and NO_LOAD configurations. 24 | - Keep in mind that some things might clash; eg a regex handler could clash with a command handler - in this case, you 25 | should put them in different dispatcher groups. 26 | 27 | Might seem complicated, but it'll make sense when you get into it. Feel free to ask me for a hand/advice! 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | COPY . . 4 | 5 | RUN pip3 install -r requirements.txt 6 | 7 | CMD [ "python3", "-m" , "tg_bot"] 8 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | black = "*" 8 | 9 | [packages] 10 | future = "==0.17.1" 11 | emoji = "==0.5.3" 12 | requests = "==2.22.0" 13 | python-telegram-bot = "==11.1.0" 14 | psycopg2-binary = "==2.8.3" 15 | feedparser = "==6.0.10" 16 | SQLAlchemy = "==1.3.6" 17 | pre-commit = "==1.18.0" 18 | asn1crypto = "==0.24.0" 19 | certifi = "==2019.6.16" 20 | cffi = "==1.12.3" 21 | cfgv = "==2.0.1" 22 | chardet = "==3.0.4" 23 | cryptography = "==2.7" 24 | identify = "==1.4.5" 25 | idna = "==2.8" 26 | importlib-metadata = "==0.19" 27 | nodeenv = "==1.3.3" 28 | pycparser = "==2.19" 29 | six = "==1.12.0" 30 | toml = "==0.10.0" 31 | urllib3 = "==1.25.3" 32 | virtualenv = "==16.7.2" 33 | zipp = "==0.5.2" 34 | "aspy.yaml" = "==1.3.0" 35 | importlib_resources = "==1.0.2" 36 | PyYAML = "==5.1.2" 37 | 38 | [requires] 39 | 40 | 41 | [pipenv] 42 | allow_prereleases = true 43 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python3 -m tg_bot 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PythonItaliaTGbot 2 | 3 | Bot principale per il gruppo Telegram di [PythonItalia](https://t.me/python_ita). 4 | 5 | ## Che cos'è? 6 | 7 | Questo bot è un fork della versione base di tgBot (ex Marie). Lo sviluppo orizzontale del bot ha permesso di aggiungere funzionalità e risolvere bug presenti nel codice sorgente originale. 8 | 9 | ### Il deploy 10 | 11 | Il deploy del bot può essere effettuato su Heroku (settando le variabili di ambiente) che su una VPS dedicata (preferibilmente con kernel linux > 2.6.13). 12 | 13 | ### Configurazione del db 14 | 15 | Il primo step necessario è la configurazione del database postgres. 16 | 17 | #### Installazione e configurazione di postgres 18 | - Installa postgres: 19 | 20 | ``` 21 | sudo apt-get update && sudo apt-get install postgresql 22 | ``` 23 | 24 | - Cambia l'utente postgres: 25 | 26 | ``` 27 | sudo su - postgres 28 | ``` 29 | 30 | - Crea un nuovo database utente (cambia USER con il nome dell'utente): 31 | 32 | ``` 33 | createuser -P -s -e USER 34 | ``` 35 | Ti verrà chiesto di inserire una password. 36 | 37 | - Crea una nuova tabella nel db: 38 | 39 | ``` 40 | createdb -O USER YDB_NAME 41 | ``` 42 | - In fine 43 | 44 | ``` 45 | psql DB_NAME -h YOUR_HOST USER 46 | ``` 47 | A questo punto sarai in grado di connetterti al db via terminal. Di default YOUR_HOST dovrebbe essere 0.0.0.0:5432. 48 | Il database-uri sarà quindi: 49 | ``` 50 | postgres://username:pw@hostname:port/db_name 51 | ``` 52 | 53 | 54 | ## Configurazione 55 | 56 | Esistono due modi per configurare il bot: modificando il file config.py oppure impostando delle variabili d'ambiente. 57 | 58 | Il metodo migliore è l'uso del file config.py perchè è più semplice rivedere tutte le impostazioni in un singolo file. 59 | Il metodo predefinito per creare il file config.py è estendere la classe di sample_config. 60 | 61 | Un esempio di config.py potrebbe essere: 62 | 63 | ``` 64 | from tg_bot.sample_config import Config 65 | 66 | 67 | class Development(Config): 68 | OWNER_ID = 00000000 # my telegram ID 69 | OWNER_USERNAME = "########" # my telegram username 70 | API_KEY = "your bot api key" # my api key, as provided by the botfather 71 | SQLALCHEMY_DATABASE_URI = 'postgresql://username:password@localhost:5432/database' # sample db credentials 72 | MESSAGE_DUMP = '00000000' # some group chat that your bot is a member of 73 | USE_MESSAGE_DUMP = True 74 | SUDO_USERS = [0000000, 000000] # List of id's for users which have sudo access to the bot. 75 | LOAD = [] 76 | NO_LOAD = ['translation'] 77 | ``` 78 | 79 | Nel caso in cui tu voglia deployare il bot su heroku dovrai impostare le ENV. Sono supportate le seguenti variabili: 80 | 81 | 82 | 83 | ENV: Setting this to ANYTHING will enable env variables 84 | 85 | TOKEN: Token del bot, come stringa. 86 | 87 | OWNER_ID: Numero intero che identifica il proprietario del bot (id di Telegram) 88 | 89 | OWNER_USERNAME: Il tuo username 90 | 91 | DATABASE_URL: URI del db 92 | 93 | MESSAGE_DUMP: opzionale: chat in cui sono salvate le risposte del bot dove non possono essere cancellate 94 | 95 | LOAD: Lista separata da spazi di moduli che vuoi abilitare 96 | 97 | NO_LOAD: Lista separata da spazi di moduli che NON vuoi abilitare 98 | 99 | WEBHOOK: Impostarlo a ANYTHING abiliterà i webhooks nei messaggi env 100 | 101 | URL: URL del webhook (richiesto solo se abilitata la modalità webhook) 102 | 103 | SUDO_USERS: Lista separata da spazi di ids di amministratori del bot 104 | 105 | SUPPORT_USERS: Lista separata da spazi di ids di utenti-supporter (possono gban/ungban, e basta) 106 | 107 | WHITELIST_USERS: Lista separata da spazi di ids di utenti che non possono essere bannati 108 | 109 | DONATION_LINK: Opzionale: Link per le donazioni 110 | 111 | CERT_PATH: Path del certificato webhooks 112 | 113 | PORT: Porta usata per connettersi al tuo servizio webhooks 114 | 115 | DEL_CMDS: Se cancellare i comandi dagli utenti che non hanno i diritti per usare quel comando 116 | 117 | STRICT_GBAN: Imponi gban su nuovi gruppi e vecchi gruppi. Quando un utente gbanned parla, sarà bannato 118 | 119 | WORKERS: Numero di threads da usare. 8 è raccomandato (e numero di default). Nota che aumentare questo numero non porterà necessariamente dei benefici alla velocità del bot. 120 | 121 | BAN_STICKER: Sticker da usare quando viene bannato un utente. 122 | 123 | ALLOW_EXCL: Se consentire l'utilizzo di punti esclamativi ! per i comandi e /. 124 | 125 | 126 | 127 | ### Dependency 128 | 129 | Installa le dependency con questo comando: 130 | 131 | ``` 132 | pip3 install -r requirements.txt 133 | ``` 134 | 135 | ## Moduli 136 | 137 | #### Imposta l'ordine di caricamento dei moduli 138 | 139 | L'ordine di caricamento in memoria dei moduli può essere opportunamente modificato tramite l'uso di LOAD e NO_LOAD. 140 | 141 | Nota: NO_LOAD è prioritario rispetto a LOAD 142 | 143 | ## Avviare il bot con docker 144 | 145 | #### Requisiti 146 | - docker 147 | - docker-compose 148 | 149 | #### Avvio 150 | - Crea un file .env usando docker/dev/config.sample come template e salvandola in docker/dev/ 151 | - Assicurati di essere nella root del progetto e inserisci il seguente comando: 152 | ``` 153 | docker-compose -f docker/dev/docker-compose.yml up -d 154 | ``` 155 | 156 | ## Costruito con 157 | 158 | * [tgbot](https://github.com/PaulSonOfLars/tgbot) - Bot modulare scritto in Python3 159 | * [Trevis CI](https://travis-ci.com) - Deploy in production 160 | * [Docker](https://www.docker.com/) - Usato per sviluppare il bot in ambiente dev 161 | 162 | ## Come contribuire 163 | 164 | Per favore leggi [CONTRIBUTING.md](https://gist.github.com/PurpleBooth/b24679402957c63ec426) per avere dettagli sulle regole 165 | per contribuire e come effettuare una pull-request. 166 | 167 | ## Versioning 168 | 169 | Noi usiamo [SemVer](http://semver.org/) per il versioning, sincronizzato con i tag in production di GH. 170 | 171 | ## Autori 172 | 173 | Controlla la lista di [contributors](https://github.com/Kavuti/python-italy-telegram-bot/graphs/contributors) che hanno reso questo progetto grande. 174 | 175 | ## License 176 | 177 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 178 | 179 | -------------------------------------------------------------------------------- /config.sample: -------------------------------------------------------------------------------- 1 | ENV=boolean 2 | TOKEN=your-token 3 | OWNER_ID=your-owner-id 4 | OWNER_USERNAME=your-owner-username 5 | DATABASE_URL=postgresql://your-user:your-pwd@db:5432/tgbot 6 | SUDO_USERS=your-sudo-users-id 7 | POSTGRES_PASSWORD=your-pg-password 8 | POSTGRES_DB=tgbot 9 | POSTGRES_USER=your-pg-user 10 | -------------------------------------------------------------------------------- /docker/dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-stretch 2 | 3 | ENV PYTHONDONTWRITEBYTECODE 1 4 | ENV PYTHONUNBUFFERED 1 5 | 6 | WORKDIR /tgbot 7 | 8 | RUN pip install pipenv 9 | COPY requirements.txt Pipfile Pipfile.lock /tgbot/ 10 | RUN pipenv install -r requirements.txt 11 | 12 | COPY . /tgbot/ 13 | -------------------------------------------------------------------------------- /docker/dev/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3.6" 12 | -------------------------------------------------------------------------------- /docker/dev/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "415dfdcb118dd9bdfef17671cb7dcd78dbd69b6ae7d4f39e8b44e71d60ca72e7" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": {} 20 | } 21 | -------------------------------------------------------------------------------- /docker/dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | db: 5 | image: postgres:11.4 6 | env_file: config.env 7 | volumes: 8 | - db-data:/var/lib/postgresql/data 9 | tgbot: 10 | restart: on-failure 11 | depends_on: 12 | - db 13 | build: 14 | context: ../../ 15 | dockerfile: ./docker/dev/Dockerfile 16 | ports: 17 | - "5003:5003" 18 | env_file: config.env 19 | volumes: 20 | - ../../:/tgbot 21 | command: pipenv run python -m tg_bot 22 | 23 | volumes: 24 | db-data: 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | asn1crypto==0.24.0 3 | aspy.yaml==1.3.0 4 | certifi==2019.6.16 5 | cffi==1.12.3 6 | cfgv==2.0.1 7 | chardet==3.0.4 8 | cryptography==2.7 9 | emoji==0.5.3 10 | feedparser==5.2.1 11 | future==0.17.1 12 | identify==1.4.5 13 | idna==2.8 14 | importlib-metadata==0.19 15 | importlib-resources==1.0.2 ; python_version < '3.7' 16 | nodeenv==1.3.3 17 | pre-commit==1.18.0 18 | psycopg2-binary==2.8.6 19 | pycparser==2.19 20 | python-telegram-bot==11.1.0 21 | pyyaml==5.1.2 22 | requests==2.22.0 23 | six==1.12.0 24 | sqlalchemy==1.3.6 25 | toml==0.10.0 26 | urllib3==1.25.3 27 | virtualenv==16.7.2 28 | zipp==0.5.2 29 | tldextract==3.1.0 30 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.4 2 | -------------------------------------------------------------------------------- /tg_bot/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | import telegram.ext as tg 6 | 7 | from tg_bot._version import __version__ 8 | 9 | # enable logging 10 | logging.basicConfig( 11 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO 12 | ) 13 | 14 | LOGGER = logging.getLogger(__name__) 15 | 16 | # if version < 3.6, stop bot. 17 | if sys.version_info[0] < 3 or sys.version_info[1] < 6: 18 | LOGGER.error( 19 | "You MUST have a python version of at least 3.6! Multiple features depend on this. Bot quitting." 20 | ) 21 | quit(1) 22 | 23 | ENV = bool(os.environ.get("ENV", False)) 24 | 25 | if ENV: 26 | TOKEN = os.environ.get("TOKEN", None) 27 | try: 28 | OWNER_ID = int(os.environ.get("OWNER_ID", None)) 29 | except ValueError: 30 | raise Exception("Your OWNER_ID env variable is not a valid integer.") 31 | 32 | MESSAGE_DUMP = os.environ.get("MESSAGE_DUMP", None) 33 | OWNER_USERNAME = os.environ.get("OWNER_USERNAME", None) 34 | 35 | try: 36 | SUDO_USERS = set(int(x) for x in os.environ.get("SUDO_USERS", "").split()) 37 | except ValueError: 38 | raise Exception("Your sudo users list does not contain valid integers.") 39 | 40 | try: 41 | SUPPORT_USERS = set(int(x) for x in os.environ.get("SUPPORT_USERS", "").split()) 42 | except ValueError: 43 | raise Exception("Your support users list does not contain valid integers.") 44 | 45 | try: 46 | WHITELIST_USERS = set( 47 | int(x) for x in os.environ.get("WHITELIST_USERS", "").split() 48 | ) 49 | except ValueError: 50 | raise Exception("Your whitelisted users list does not contain valid integers.") 51 | 52 | WEBHOOK = bool(os.environ.get("WEBHOOK", False)) 53 | URL = os.environ.get("URL", "") # Does not contain token 54 | PORT = int(os.environ.get("PORT", 5000)) 55 | CERT_PATH = os.environ.get("CERT_PATH") 56 | 57 | DB_URI = os.environ.get("DATABASE_URL") 58 | DONATION_LINK = os.environ.get("DONATION_LINK") 59 | LOAD = ( 60 | os.environ.get("LOAD", "").split(",") 61 | if "," in os.environ.get("LOAD", "") 62 | else os.environ.get("LOAD", "").split() 63 | ) 64 | NO_LOAD = os.environ.get("NO_LOAD", "translation").split(",") 65 | DEL_CMDS = bool(os.environ.get("DEL_CMDS", False)) 66 | STRICT_GBAN = bool(os.environ.get("STRICT_GBAN", False)) 67 | WORKERS = int(os.environ.get("WORKERS", 8)) 68 | BAN_STICKER = os.environ.get("BAN_STICKER", "CAADAgADOwADPPEcAXkko5EB3YGYAg") 69 | ALLOW_EXCL = os.environ.get("ALLOW_EXCL", False) 70 | DEFAULT_CHAT_ID = os.environ.get("DEFAULT_CHAT_ID", None) 71 | VERSION = __version__ 72 | 73 | else: 74 | from tg_bot.config import Development as Config 75 | 76 | TOKEN = Config.API_KEY 77 | try: 78 | OWNER_ID = int(Config.OWNER_ID) 79 | except ValueError: 80 | raise Exception("Your OWNER_ID variable is not a valid integer.") 81 | 82 | MESSAGE_DUMP = Config.MESSAGE_DUMP 83 | OWNER_USERNAME = Config.OWNER_USERNAME 84 | 85 | try: 86 | SUDO_USERS = set(int(x) for x in Config.SUDO_USERS or []) 87 | except ValueError: 88 | raise Exception("Your sudo users list does not contain valid integers.") 89 | 90 | try: 91 | SUPPORT_USERS = set(int(x) for x in Config.SUPPORT_USERS or []) 92 | except ValueError: 93 | raise Exception("Your support users list does not contain valid integers.") 94 | 95 | try: 96 | WHITELIST_USERS = set(int(x) for x in Config.WHITELIST_USERS or []) 97 | except ValueError: 98 | raise Exception("Your whitelisted users list does not contain valid integers.") 99 | 100 | WEBHOOK = Config.WEBHOOK 101 | URL = Config.URL 102 | PORT = Config.PORT 103 | CERT_PATH = Config.CERT_PATH 104 | 105 | DB_URI = Config.SQLALCHEMY_DATABASE_URI 106 | DONATION_LINK = Config.DONATION_LINK 107 | LOAD = Config.LOAD 108 | NO_LOAD = Config.NO_LOAD 109 | DEL_CMDS = Config.DEL_CMDS 110 | STRICT_GBAN = Config.STRICT_GBAN 111 | WORKERS = Config.WORKERS 112 | BAN_STICKER = Config.BAN_STICKER 113 | ALLOW_EXCL = Config.ALLOW_EXCL 114 | DEFAULT_CHAT_ID = Config.DEFAULT_CHAT_ID 115 | VERSION = __version__ 116 | 117 | SUDO_USERS.add(OWNER_ID) 118 | 119 | updater = tg.Updater(TOKEN, workers=WORKERS) 120 | 121 | dispatcher = updater.dispatcher 122 | 123 | SUDO_USERS = list(SUDO_USERS) 124 | WHITELIST_USERS = list(WHITELIST_USERS) 125 | SUPPORT_USERS = list(SUPPORT_USERS) 126 | 127 | # Load at end to ensure all prev variables have been set 128 | from tg_bot.modules.helper_funcs.handlers import ( 129 | CustomCommandHandler, 130 | CustomRegexHandler, 131 | ) 132 | 133 | # make sure the regex handler can take extra kwargs 134 | tg.RegexHandler = CustomRegexHandler 135 | 136 | if ALLOW_EXCL: 137 | tg.CommandHandler = CustomCommandHandler 138 | -------------------------------------------------------------------------------- /tg_bot/_version.py: -------------------------------------------------------------------------------- 1 | # PEP440 - STANDARDIZED VERSIONING SYSTEM 2 | __version__ = "$Revision: 1ce20de $" 3 | -------------------------------------------------------------------------------- /tg_bot/config.py: -------------------------------------------------------------------------------- 1 | ENV=True 2 | -------------------------------------------------------------------------------- /tg_bot/deactivated-modules: -------------------------------------------------------------------------------- 1 | NO_LOAD = ['translation','rss', 'sed', 'afk','notes','filters', 'spam'] 2 | -------------------------------------------------------------------------------- /tg_bot/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from tg_bot import LOAD, NO_LOAD, LOGGER 2 | 3 | 4 | def __list_all_modules(): 5 | from os.path import dirname, basename, isfile 6 | import glob 7 | 8 | # This generates a list of modules in this folder for the * in __main__ to work. 9 | mod_paths = glob.glob(dirname(__file__) + "/*.py") 10 | all_modules = [ 11 | basename(f)[:-3] 12 | for f in mod_paths 13 | if isfile(f) and f.endswith(".py") and not f.endswith("__init__.py") 14 | ] 15 | 16 | if LOAD or NO_LOAD: 17 | to_load = LOAD 18 | if to_load: 19 | if not all( 20 | any(mod == module_name for module_name in all_modules) 21 | for mod in to_load 22 | ): 23 | LOGGER.error("Invalid loadorder names. Quitting.") 24 | quit(1) 25 | 26 | else: 27 | to_load = all_modules 28 | 29 | if NO_LOAD: 30 | LOGGER.info("Not loading: {}".format(NO_LOAD)) 31 | return [item for item in to_load if item not in NO_LOAD] 32 | 33 | return to_load 34 | 35 | return all_modules 36 | 37 | 38 | ALL_MODULES = sorted(__list_all_modules()) 39 | LOGGER.info("Modules to load: %s", str(ALL_MODULES)) 40 | __all__ = ALL_MODULES + ["ALL_MODULES"] 41 | -------------------------------------------------------------------------------- /tg_bot/modules/admin.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Chat, Update, Bot, User 5 | from telegram import ParseMode 6 | from telegram.error import BadRequest 7 | from telegram.ext import CommandHandler, Filters 8 | from telegram.ext.dispatcher import run_async 9 | from telegram.utils.helpers import escape_markdown, mention_html 10 | 11 | from tg_bot import dispatcher 12 | from tg_bot.modules.disable import DisableAbleCommandHandler 13 | from tg_bot.modules.helper_funcs.chat_status import ( 14 | bot_admin, 15 | can_promote, 16 | user_admin, 17 | can_pin, 18 | ) 19 | from tg_bot.modules.helper_funcs.extraction import extract_user 20 | from tg_bot.modules.log_channel import loggable 21 | 22 | 23 | @run_async 24 | @bot_admin 25 | @can_promote 26 | @user_admin 27 | @loggable 28 | def promote(bot: Bot, update: Update, args: List[str]) -> str: 29 | chat_id = update.effective_chat.id 30 | message = update.effective_message # type: Optional[Message] 31 | chat = update.effective_chat # type: Optional[Chat] 32 | user = update.effective_user # type: Optional[User] 33 | 34 | user_id = extract_user(message, args) 35 | if not user_id: 36 | message.reply_text("Non ti stai riferendo ad un utente.") 37 | return "" 38 | 39 | user_member = chat.get_member(user_id) 40 | if user_member.status == "administrator" or user_member.status == "creator": 41 | message.reply_text("Come faccio a promuovere qualcuno che è già admin?") 42 | return "" 43 | 44 | if user_id == bot.id: 45 | message.reply_text("Non posso auto-promuovermi.. devi contattare un admin.") 46 | return "" 47 | 48 | # set same perms as bot - bot can't assign higher perms than itself! 49 | bot_member = chat.get_member(bot.id) 50 | 51 | bot.promoteChatMember( 52 | chat_id, 53 | user_id, 54 | can_change_info=bot_member.can_change_info, 55 | can_post_messages=bot_member.can_post_messages, 56 | can_edit_messages=bot_member.can_edit_messages, 57 | can_delete_messages=bot_member.can_delete_messages, 58 | # can_invite_users=bot_member.can_invite_users, 59 | can_restrict_members=bot_member.can_restrict_members, 60 | can_pin_messages=bot_member.can_pin_messages, 61 | can_promote_members=bot_member.can_promote_members, 62 | ) 63 | 64 | message.reply_text("Utente promosso.") 65 | return ( 66 | "{}:" 67 | "\n#PROMOZIONE" 68 | "\nAdmin: {}" 69 | "\nUtente: {}".format( 70 | html.escape(chat.title), 71 | mention_html(user.id, user.first_name), 72 | mention_html(user_member.user.id, user_member.user.first_name), 73 | ) 74 | ) 75 | 76 | 77 | @run_async 78 | @bot_admin 79 | @can_promote 80 | @user_admin 81 | @loggable 82 | def demote(bot: Bot, update: Update, args: List[str]) -> str: 83 | chat = update.effective_chat # type: Optional[Chat] 84 | message = update.effective_message # type: Optional[Message] 85 | user = update.effective_user # type: Optional[User] 86 | 87 | user_id = extract_user(message, args) 88 | if not user_id: 89 | message.reply_text("Non ti stai riferendo ad un utente.") 90 | return "" 91 | 92 | user_member = chat.get_member(user_id) 93 | if user_member.status == "creator": 94 | message.reply_text( 95 | "Questa persona ha CREATO la chat.. come pensi che io possa degradarlo?" 96 | ) 97 | return "" 98 | 99 | if not user_member.status == "administrator": 100 | message.reply_text("Non posso degradare qualcuno che non era admin!") 101 | return "" 102 | 103 | if user_id == bot.id: 104 | message.reply_text("Non posso auto-degradarmi.") 105 | return "" 106 | 107 | try: 108 | bot.promoteChatMember( 109 | int(chat.id), 110 | int(user_id), 111 | can_change_info=False, 112 | can_post_messages=False, 113 | can_edit_messages=False, 114 | can_delete_messages=False, 115 | can_invite_users=False, 116 | can_restrict_members=False, 117 | can_pin_messages=False, 118 | can_promote_members=False, 119 | ) 120 | message.reply_text("Successfully demoted!") 121 | return ( 122 | "{}:" 123 | "\n#DEGRADATO" 124 | "\nAdmin: {}" 125 | "\nUtente: {}".format( 126 | html.escape(chat.title), 127 | mention_html(user.id, user.first_name), 128 | mention_html(user_member.user.id, user_member.user.first_name), 129 | ) 130 | ) 131 | 132 | except BadRequest: 133 | message.reply_text( 134 | "Non posso degradare. Forse non sono admin, o le impostazioni della privacy del gruppo " 135 | "non me lo permettono, quindi non posso fare niente!" 136 | ) 137 | return "" 138 | 139 | 140 | @run_async 141 | @bot_admin 142 | @can_pin 143 | @user_admin 144 | @loggable 145 | def pin(bot: Bot, update: Update, args: List[str]) -> str: 146 | user = update.effective_user # type: Optional[User] 147 | chat = update.effective_chat # type: Optional[Chat] 148 | 149 | is_group = chat.type != "private" and chat.type != "channel" 150 | 151 | prev_message = update.effective_message.reply_to_message 152 | 153 | is_silent = True 154 | if len(args) >= 1: 155 | is_silent = not ( 156 | args[0].lower() == "notify" 157 | or args[0].lower() == "loud" 158 | or args[0].lower() == "violent" 159 | ) 160 | 161 | if prev_message and is_group: 162 | try: 163 | bot.pinChatMessage( 164 | chat.id, prev_message.message_id, disable_notification=is_silent 165 | ) 166 | except BadRequest as excp: 167 | if excp.message == "Chat_not_modified": 168 | pass 169 | else: 170 | raise 171 | return ( 172 | "{}:" 173 | "\n#PINNATO" 174 | "\nAdmin: {}".format( 175 | html.escape(chat.title), mention_html(user.id, user.first_name) 176 | ) 177 | ) 178 | 179 | return "" 180 | 181 | 182 | @run_async 183 | @bot_admin 184 | @can_pin 185 | @user_admin 186 | @loggable 187 | def unpin(bot: Bot, update: Update) -> str: 188 | chat = update.effective_chat 189 | user = update.effective_user # type: Optional[User] 190 | 191 | try: 192 | bot.unpinChatMessage(chat.id) 193 | except BadRequest as excp: 194 | if excp.message == "Chat_not_modified": 195 | pass 196 | else: 197 | raise 198 | 199 | return ( 200 | "{}:" 201 | "\n#UN-PINNATO" 202 | "\nAdmin: {}".format( 203 | html.escape(chat.title), mention_html(user.id, user.first_name) 204 | ) 205 | ) 206 | 207 | 208 | @run_async 209 | @bot_admin 210 | @user_admin 211 | def invite(bot: Bot, update: Update): 212 | chat = update.effective_chat # type: Optional[Chat] 213 | if chat.username: 214 | update.effective_message.reply_text(chat.username) 215 | elif chat.type == chat.SUPERGROUP or chat.type == chat.CHANNEL: 216 | bot_member = chat.get_member(bot.id) 217 | if bot_member.can_invite_users: 218 | invitelink = bot.exportChatInviteLink(chat.id) 219 | update.effective_message.reply_text(invitelink) 220 | else: 221 | update.effective_message.reply_text("Non ho accesso al link di invito!") 222 | else: 223 | update.effective_message.reply_text( 224 | "Posso fornire un link di invito solo per i supergruppi o per i canali.. mi dispiace!" 225 | ) 226 | 227 | 228 | @run_async 229 | def adminlist(bot: Bot, update: Update): 230 | administrators = update.effective_chat.get_administrators() 231 | text = "Admins in *{}*:".format(update.effective_chat.title or "questa chat") 232 | for admin in administrators: 233 | user = admin.user 234 | name = "[{}](tg://user?id={})".format( 235 | user.first_name + (user.last_name or ""), user.id 236 | ) 237 | if user.username: 238 | name = escape_markdown("@" + user.username) 239 | text += "\n - {}".format(name) 240 | 241 | update.effective_message.reply_text(text, parse_mode=ParseMode.MARKDOWN) 242 | 243 | 244 | def __chat_settings__(chat_id, user_id): 245 | return "Tu sei *admin*: `{}`".format( 246 | dispatcher.bot.get_chat_member(chat_id, user_id).status 247 | in ("administrator", "creator") 248 | ) 249 | 250 | 251 | __help__ = """ 252 | - /adminlist: lista di admin nella chat 253 | 254 | *Admin only:* 255 | - /pin: pin silenzioso di un messaggio - aggiungi 'loud' o 'notify' per inviare una notifica ai partecipanti. 256 | - /unpin: unpin del messaggio corrente 257 | - /invitelink: ricevi il link di invito 258 | - /promote: promuovi un utente rispondendo con questo comando 259 | - /demote: degrada un utente rispondendo con questo comando 260 | """ 261 | 262 | __mod_name__ = "Admin" 263 | 264 | PIN_HANDLER = CommandHandler("pin", pin, pass_args=True, filters=Filters.group) 265 | UNPIN_HANDLER = CommandHandler("unpin", unpin, filters=Filters.group) 266 | 267 | INVITE_HANDLER = CommandHandler("invitelink", invite, filters=Filters.group) 268 | 269 | PROMOTE_HANDLER = CommandHandler( 270 | "promote", promote, pass_args=True, filters=Filters.group 271 | ) 272 | DEMOTE_HANDLER = CommandHandler("demote", demote, pass_args=True, filters=Filters.group) 273 | 274 | ADMINLIST_HANDLER = DisableAbleCommandHandler( 275 | "adminlist", adminlist, filters=Filters.group 276 | ) 277 | 278 | dispatcher.add_handler(PIN_HANDLER) 279 | dispatcher.add_handler(UNPIN_HANDLER) 280 | dispatcher.add_handler(INVITE_HANDLER) 281 | dispatcher.add_handler(PROMOTE_HANDLER) 282 | dispatcher.add_handler(DEMOTE_HANDLER) 283 | dispatcher.add_handler(ADMINLIST_HANDLER) 284 | -------------------------------------------------------------------------------- /tg_bot/modules/afk.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from telegram import Message, Update, Bot, User 4 | from telegram import MessageEntity 5 | from telegram.ext import Filters, MessageHandler, run_async 6 | 7 | from tg_bot import dispatcher 8 | from tg_bot.modules.disable import DisableAbleCommandHandler, DisableAbleRegexHandler 9 | from tg_bot.modules.sql import afk_sql as sql 10 | from tg_bot.modules.users import get_user_id 11 | 12 | AFK_GROUP = 7 13 | AFK_REPLY_GROUP = 8 14 | 15 | 16 | @run_async 17 | def afk(bot: Bot, update: Update): 18 | args = update.effective_message.text.split(None, 1) 19 | if len(args) >= 2: 20 | reason = args[1] 21 | else: 22 | reason = "" 23 | 24 | sql.set_afk(update.effective_user.id, reason) 25 | update.effective_message.reply_text( 26 | "{} is now AFK!".format(update.effective_user.first_name) 27 | ) 28 | 29 | 30 | @run_async 31 | def no_longer_afk(bot: Bot, update: Update): 32 | user = update.effective_user # type: Optional[User] 33 | 34 | if not user: # ignore channels 35 | return 36 | 37 | res = sql.rm_afk(user.id) 38 | if res: 39 | update.effective_message.reply_text( 40 | "{} is no longer AFK!".format(update.effective_user.first_name) 41 | ) 42 | 43 | 44 | @run_async 45 | def reply_afk(bot: Bot, update: Update): 46 | message = update.effective_message # type: Optional[Message] 47 | entities = message.parse_entities( 48 | [MessageEntity.TEXT_MENTION, MessageEntity.MENTION] 49 | ) 50 | if message.entities and entities: 51 | for ent in entities: 52 | if ent.type == MessageEntity.TEXT_MENTION: 53 | user_id = ent.user.id 54 | fst_name = ent.user.first_name 55 | 56 | elif ent.type == MessageEntity.MENTION: 57 | user_id = get_user_id( 58 | message.text[ent.offset: ent.offset + ent.length] 59 | ) 60 | if not user_id: 61 | # Should never happen, since for a user to become AFK they must have spoken. Maybe changed username? 62 | return 63 | chat = bot.get_chat(user_id) 64 | fst_name = chat.first_name 65 | 66 | else: 67 | return 68 | 69 | if sql.is_afk(user_id): 70 | valid, reason = sql.check_afk_status(user_id) 71 | if valid: 72 | if not reason: 73 | res = "{} is AFK!".format(fst_name) 74 | else: 75 | res = "{} is AFK! says its because of:\n{}".format( 76 | fst_name, reason 77 | ) 78 | message.reply_text(res) 79 | 80 | 81 | def __gdpr__(user_id): 82 | sql.rm_afk(user_id) 83 | 84 | 85 | __help__ = """ 86 | - /afk : mark yourself as AFK. 87 | - brb : same as the afk command - but not a command. 88 | 89 | When marked as AFK, any mentions will be replied to with a message to say you're not available! 90 | """ 91 | 92 | __mod_name__ = "AFK" 93 | 94 | AFK_HANDLER = DisableAbleCommandHandler("afk", afk) 95 | AFK_REGEX_HANDLER = DisableAbleRegexHandler("(?i)brb", afk, friendly="afk") 96 | NO_AFK_HANDLER = MessageHandler(Filters.all & Filters.group, no_longer_afk) 97 | AFK_REPLY_HANDLER = MessageHandler( 98 | Filters.entity(MessageEntity.MENTION) | Filters.entity(MessageEntity.TEXT_MENTION), 99 | reply_afk, 100 | ) 101 | 102 | dispatcher.add_handler(AFK_HANDLER, AFK_GROUP) 103 | dispatcher.add_handler(AFK_REGEX_HANDLER, AFK_GROUP) 104 | dispatcher.add_handler(NO_AFK_HANDLER, AFK_GROUP) 105 | dispatcher.add_handler(AFK_REPLY_HANDLER, AFK_REPLY_GROUP) 106 | -------------------------------------------------------------------------------- /tg_bot/modules/antiflood.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Chat, Update, Bot, User 5 | from telegram.error import BadRequest 6 | from telegram.ext import Filters, MessageHandler, CommandHandler, run_async 7 | from telegram.utils.helpers import mention_html 8 | 9 | from tg_bot import dispatcher 10 | from tg_bot.modules.helper_funcs.chat_status import ( 11 | is_user_admin, 12 | user_admin, 13 | can_restrict, 14 | ) 15 | from tg_bot.modules.log_channel import loggable 16 | from tg_bot.modules.sql import antiflood_sql as sql 17 | 18 | FLOOD_GROUP = 3 19 | 20 | 21 | @run_async 22 | @loggable 23 | def check_flood(bot: Bot, update: Update) -> str: 24 | user = update.effective_user # type: Optional[User] 25 | chat = update.effective_chat # type: Optional[Chat] 26 | msg = update.effective_message # type: Optional[Message] 27 | 28 | if not user: # ignore channels 29 | return "" 30 | 31 | # ignore admins 32 | if is_user_admin(chat, user.id): 33 | sql.update_flood(chat.id, None) 34 | return "" 35 | 36 | should_ban = sql.update_flood(chat.id, user.id) 37 | if not should_ban: 38 | return "" 39 | 40 | try: 41 | chat.kick_member(user.id) 42 | msg.reply_text( 43 | "Non ti vergogni? Spammare in questo modo in un gruppo di gente civile? " 44 | "Sei una delusione. Addio." 45 | ) 46 | 47 | return ( 48 | "{}:" 49 | "\n#BAN" 50 | "\nUser: {}" 51 | "\nFlood delo gruppo.".format( 52 | html.escape(chat.title), mention_html(user.id, user.first_name) 53 | ) 54 | ) 55 | 56 | except BadRequest: 57 | msg.reply_text( 58 | "Non posso kickkare persone qui. Dammi i permessi così potrò disabilitare l'antiflood." 59 | ) 60 | sql.set_flood(chat.id, 0) 61 | return ( 62 | "{}:" 63 | "\n#INFO" 64 | "\nNon posso kickkare, quindi ho automaticamente disabilitare l'antiflood.".format( 65 | chat.title 66 | ) 67 | ) 68 | 69 | 70 | def check_msg_lenght_flood(): 71 | # analysis of the number of characters in a message 72 | pass 73 | 74 | 75 | @run_async 76 | @user_admin 77 | @can_restrict 78 | @loggable 79 | def set_flood(bot: Bot, update: Update, args: List[str]) -> str: 80 | chat = update.effective_chat # type: Optional[Chat] 81 | user = update.effective_user # type: Optional[User] 82 | message = update.effective_message # type: Optional[Message] 83 | 84 | if len(args) >= 1: 85 | val = args[0].lower() 86 | if val == "off" or val == "no" or val == "0": 87 | sql.set_flood(chat.id, 0) 88 | message.reply_text("L'antiflood è stato disabilitato con successo.") 89 | 90 | elif val.isdigit(): 91 | amount = int(val) 92 | if amount <= 0: 93 | sql.set_flood(chat.id, 0) 94 | message.reply_text("L'antiflood è stato disabilitato.") 95 | return ( 96 | "{}:" 97 | "\n#SETFLOOD" 98 | "\nAdmin: {}" 99 | "\nDisabilitazione antiflood.".format( 100 | html.escape(chat.title), mention_html(user.id, user.first_name) 101 | ) 102 | ) 103 | 104 | elif amount < 3: 105 | message.reply_text( 106 | "L'antiflood deve essere impostato a 0 (disabilitato), o un numero maggiore di 3!" 107 | ) 108 | return "" 109 | 110 | else: 111 | sql.set_flood(chat.id, amount) 112 | message.reply_text( 113 | "Modulo antiflood aggiornato e impostato a {}".format(amount) 114 | ) 115 | return ( 116 | "{}:" 117 | "\n#SETFLOOD" 118 | "\nAdmin: {}" 119 | "\nAntiflood impostato a {}.".format( 120 | html.escape(chat.title), 121 | mention_html(user.id, user.first_name), 122 | amount, 123 | ) 124 | ) 125 | 126 | else: 127 | message.reply_text( 128 | "Argomento non riconosciuto - usare un numero, 'off', oppure 'on'." 129 | ) 130 | 131 | return "" 132 | 133 | 134 | @run_async 135 | def flood(bot: Bot, update: Update): 136 | chat = update.effective_chat # type: Optional[Chat] 137 | 138 | limit = sql.get_flood_limit(chat.id) 139 | if limit == 0: 140 | update.effective_message.reply_text( 141 | "Sto monitorando la chat alla ricerca di flooding!" 142 | ) 143 | else: 144 | update.effective_message.reply_text( 145 | "Bannerò gli utenti che invieranno più di {} messaggi consecutivi.".format( 146 | limit 147 | ) 148 | ) 149 | 150 | 151 | def __migrate__(old_chat_id, new_chat_id): 152 | sql.migrate_chat(old_chat_id, new_chat_id) 153 | 154 | 155 | def __chat_settings__(chat_id, user_id): 156 | limit = sql.get_flood_limit(chat_id) 157 | if limit == 0: 158 | return "*Non* sto controllando la chat dai flood." 159 | else: 160 | return "Antiflood impostato a `{}` messaggi.".format(limit) 161 | 162 | 163 | __help__ = """ 164 | - /flood: Restituisce le attuali impostazioni di flood 165 | 166 | *Admin only:* 167 | - /setflood : Abilita o disabilita l'antiflood 168 | - /setmsglen : Abilita o disabilita l'antiflood sulla lunghezza del messaggio 169 | """ 170 | 171 | __mod_name__ = "AntiFlood" 172 | 173 | FLOOD_BAN_HANDLER = MessageHandler( 174 | Filters.all & ~Filters.status_update & Filters.group, check_flood 175 | ) 176 | SET_FLOOD_HANDLER = CommandHandler( 177 | "setflood", set_flood, pass_args=True, filters=Filters.group 178 | ) 179 | FLOOD_HANDLER = CommandHandler("flood", flood, filters=Filters.group) 180 | 181 | dispatcher.add_handler(FLOOD_BAN_HANDLER, FLOOD_GROUP) 182 | dispatcher.add_handler(SET_FLOOD_HANDLER) 183 | dispatcher.add_handler(FLOOD_HANDLER) 184 | -------------------------------------------------------------------------------- /tg_bot/modules/backups.py: -------------------------------------------------------------------------------- 1 | import json 2 | from io import BytesIO 3 | from typing import Optional 4 | 5 | from telegram import Message, Chat, Update, Bot 6 | from telegram.error import BadRequest 7 | from telegram.ext import CommandHandler, run_async 8 | 9 | from tg_bot import dispatcher, LOGGER 10 | from tg_bot.__main__ import DATA_IMPORT 11 | from tg_bot.modules.helper_funcs.chat_status import user_admin 12 | 13 | 14 | @run_async 15 | @user_admin 16 | def import_data(bot: Bot, update): 17 | msg = update.effective_message # type: Optional[Message] 18 | chat = update.effective_chat # type: Optional[Chat] 19 | # TODO: allow uploading doc with command, not just as reply 20 | # only work with a doc 21 | if msg.reply_to_message and msg.reply_to_message.document: 22 | try: 23 | file_info = bot.get_file(msg.reply_to_message.document.file_id) 24 | except BadRequest: 25 | msg.reply_text( 26 | "Prova a scaricare e ricaricare il file manualmente. " 27 | "Momentaneamente questa richiesta non può essere completata." 28 | ) 29 | return 30 | 31 | with BytesIO() as file: 32 | file_info.download(out=file) 33 | file.seek(0) 34 | data = json.load(file) 35 | 36 | # only import one group 37 | if len(data) > 1 and str(chat.id) not in data: 38 | msg.reply_text( 39 | "Ci sono più chat nello stesso file ma nessuno ha l'id di questa chat " 40 | "- come posso capire cosa importare qui?" 41 | ) 42 | return 43 | 44 | # Select data source 45 | if str(chat.id) in data: 46 | data = data[str(chat.id)]["hashes"] 47 | else: 48 | data = data[list(data.keys())[0]]["hashes"] 49 | 50 | try: 51 | for mod in DATA_IMPORT: 52 | mod.__import_data__(str(chat.id), data) 53 | except Exception: 54 | msg.reply_text( 55 | "Un eccezione è avvenuto mentre provavo a ripristinare i messaggi. Il processo potrebbe non essere completo. Se" 56 | "hai problemi con il file ti consiglio di inviarlo al gruppo admin e in modo da debuggarlo. " 57 | "I report su github sono molto apprezzati! Grazie! :)" 58 | ) 59 | LOGGER.exception( 60 | "Import for chatid %s with name %s failed.", 61 | str(chat.id), 62 | str(chat.title), 63 | ) 64 | return 65 | 66 | # TODO: some of that link logic 67 | # NOTE: consider default permissions stuff? 68 | msg.reply_text("Backup ricaricato! Ben tornati!") 69 | 70 | 71 | @run_async 72 | @user_admin 73 | def export_data(bot: Bot, update: Update): 74 | msg = update.effective_message # type: Optional[Message] 75 | msg.reply_text("") 76 | 77 | 78 | __mod_name__ = "Backups" 79 | 80 | __help__ = """ 81 | *Admin only:* 82 | - /import: Rispondi con un file di backup per ricaricare tutte le chat! Attenzione \ 83 | i file e le foto non posso essere importati a causa delle restrizioni di Telegram. 84 | - /export: !!! Questo comando non è ancora disponibile. L'exoport deve essere effettuato manualmente dal db! 85 | """ 86 | IMPORT_HANDLER = CommandHandler("import", import_data) 87 | EXPORT_HANDLER = CommandHandler("export", export_data) 88 | 89 | dispatcher.add_handler(IMPORT_HANDLER) 90 | # dispatcher.add_handler(EXPORT_HANDLER) 91 | -------------------------------------------------------------------------------- /tg_bot/modules/blacklist.py: -------------------------------------------------------------------------------- 1 | import html 2 | import re 3 | from typing import Optional, List 4 | 5 | from telegram import Message, Chat, Update, Bot, ParseMode 6 | from telegram.error import BadRequest 7 | from telegram.ext import CommandHandler, MessageHandler, Filters, run_async 8 | 9 | import tg_bot.modules.sql.blacklist_sql as sql 10 | from tg_bot import dispatcher, LOGGER 11 | from tg_bot.modules.disable import DisableAbleCommandHandler 12 | from tg_bot.modules.helper_funcs.chat_status import user_admin, user_not_admin 13 | from tg_bot.modules.helper_funcs.extraction import extract_text 14 | from tg_bot.modules.helper_funcs.misc import split_message 15 | 16 | BLACKLIST_GROUP = 11 17 | 18 | BASE_BLACKLIST_STRING = "Parole nella blacklist:\n" 19 | 20 | 21 | @run_async 22 | def blacklist(bot: Bot, update: Update, args: List[str]): 23 | msg = update.effective_message # type: Optional[Message] 24 | chat = update.effective_chat # type: Optional[Chat] 25 | 26 | all_blacklisted = sql.get_chat_blacklist(chat.id) 27 | 28 | filter_list = BASE_BLACKLIST_STRING 29 | 30 | if len(args) > 0 and args[0].lower() == "copy": 31 | for trigger in all_blacklisted: 32 | filter_list += "{}\n".format(html.escape(trigger)) 33 | else: 34 | for trigger in all_blacklisted: 35 | filter_list += " - {}\n".format(html.escape(trigger)) 36 | 37 | split_text = split_message(filter_list) 38 | for text in split_text: 39 | if text == BASE_BLACKLIST_STRING: 40 | msg.reply_text("Non sono presenti trigger nella blacklist!") 41 | return 42 | msg.reply_text(text, parse_mode=ParseMode.HTML) 43 | 44 | 45 | @run_async 46 | @user_admin 47 | def add_blacklist(bot: Bot, update: Update): 48 | msg = update.effective_message # type: Optional[Message] 49 | chat = update.effective_chat # type: Optional[Chat] 50 | words = msg.text.split(None, 1) 51 | if len(words) > 1: 52 | text = words[1] 53 | to_blacklist = list( 54 | set(trigger.strip() for trigger in text.split("\n") if trigger.strip()) 55 | ) 56 | for trigger in to_blacklist: 57 | sql.add_to_blacklist(chat.id, trigger.lower()) 58 | 59 | if len(to_blacklist) == 1: 60 | msg.reply_text( 61 | "Aggiunto {} alla blacklist!".format( 62 | html.escape(to_blacklist[0]) 63 | ), 64 | parse_mode=ParseMode.HTML, 65 | ) 66 | 67 | else: 68 | msg.reply_text( 69 | "Aggiunti {} triggers alla blacklist.".format( 70 | len(to_blacklist) 71 | ), 72 | parse_mode=ParseMode.HTML, 73 | ) 74 | 75 | else: 76 | msg.reply_text("Dimmi quale parola vuoi aggiungere alla blacklist.") 77 | 78 | 79 | @run_async 80 | @user_admin 81 | def unblacklist(bot: Bot, update: Update): 82 | msg = update.effective_message # type: Optional[Message] 83 | chat = update.effective_chat # type: Optional[Chat] 84 | words = msg.text.split(None, 1) 85 | if len(words) > 1: 86 | text = words[1] 87 | to_unblacklist = list( 88 | set(trigger.strip() for trigger in text.split("\n") if trigger.strip()) 89 | ) 90 | successful = 0 91 | for trigger in to_unblacklist: 92 | success = sql.rm_from_blacklist(chat.id, trigger.lower()) 93 | if success: 94 | successful += 1 95 | 96 | if len(to_unblacklist) == 1: 97 | if successful: 98 | msg.reply_text( 99 | "Rimosso {} dalla blacklist!".format( 100 | html.escape(to_unblacklist[0]) 101 | ), 102 | parse_mode=ParseMode.HTML, 103 | ) 104 | else: 105 | msg.reply_text("Questo non è un blacklisted trigger...!") 106 | 107 | elif successful == len(to_unblacklist): 108 | msg.reply_text( 109 | "Rimosso il trigger {} dalla blacklist.".format( 110 | successful 111 | ), 112 | parse_mode=ParseMode.HTML, 113 | ) 114 | 115 | elif not successful: 116 | msg.reply_text( 117 | "Nessuno di questi trigger esiste, quindi non sono stati rimossi.".format( 118 | successful, len(to_unblacklist) - successful 119 | ), 120 | parse_mode=ParseMode.HTML, 121 | ) 122 | 123 | else: 124 | msg.reply_text( 125 | "Rimosso il trigger {} dalla blacklist. {} Non esistono, " 126 | "quindi non sono stati rimossi.".format( 127 | successful, len(to_unblacklist) - successful 128 | ), 129 | parse_mode=ParseMode.HTML, 130 | ) 131 | else: 132 | msg.reply_text("Dimmi quale parola vuoi rimuovere dalla blacklist.") 133 | 134 | 135 | @run_async 136 | @user_not_admin 137 | def del_blacklist(bot: Bot, update: Update): 138 | chat = update.effective_chat # type: Optional[Chat] 139 | message = update.effective_message # type: Optional[Message] 140 | to_match = extract_text(message) 141 | if not to_match: 142 | return 143 | 144 | chat_filters = sql.get_chat_blacklist(chat.id) 145 | for trigger in chat_filters: 146 | pattern = r"( |^|[^\w])" + re.escape(trigger) + r"( |$|[^\w])" 147 | if re.search(pattern, to_match, flags=re.IGNORECASE): 148 | try: 149 | message.delete() 150 | except BadRequest as excp: 151 | if excp.message == "Message to delete not found": 152 | pass 153 | else: 154 | LOGGER.exception("Error while deleting blacklist message.") 155 | break 156 | 157 | 158 | def __migrate__(old_chat_id, new_chat_id): 159 | sql.migrate_chat(old_chat_id, new_chat_id) 160 | 161 | 162 | def __chat_settings__(chat_id, user_id): 163 | blacklisted = sql.num_blacklist_chat_filters(chat_id) 164 | return "Ci sono {} parole nella blacklist.".format(blacklisted) 165 | 166 | 167 | def __stats__(): 168 | return "{} blacklist triggers, in {} chats.".format( 169 | sql.num_blacklist_filters(), sql.num_blacklist_filter_chats() 170 | ) 171 | 172 | 173 | __mod_name__ = "Word Blacklists" 174 | 175 | __help__ = """ 176 | Blacklist è un modulo per fermare certi trigger/parole dall'essere detti o mandati in un gruppo. Ogni volta che il trigger viene menzionato \ 177 | il messaggio verrà immediatamente cancellato. La cosa migliore da fare è combinare questi triggers con il sistema di warn. 178 | 179 | *NOTE:* blacklists non funziona nel gruppo admin. 180 | 181 | - /blacklist: Controlla le parole attualmente vietate. 182 | 183 | *Admin only:* 184 | - /addblacklist : Aggiungi una parola alla blacklist. Ogni linea viene considerata come una parola nuova. 185 | - /unblacklist : Rimuovi un trigger dalla blacklist. Si applica la stessa logica delle linee del comando addblacklist. 186 | - /rmblacklist : Comando equivalente di quello sopra. 187 | """ 188 | 189 | BLACKLIST_HANDLER = DisableAbleCommandHandler( 190 | "blacklist", blacklist, filters=Filters.group, pass_args=True, admin_ok=True 191 | ) 192 | ADD_BLACKLIST_HANDLER = CommandHandler( 193 | "addblacklist", add_blacklist, filters=Filters.group 194 | ) 195 | UNBLACKLIST_HANDLER = CommandHandler( 196 | ["unblacklist", "rmblacklist"], unblacklist, filters=Filters.group 197 | ) 198 | BLACKLIST_DEL_HANDLER = MessageHandler( 199 | (Filters.text | Filters.command | Filters.sticker | Filters.photo) & Filters.group, 200 | del_blacklist, 201 | edited_updates=True, 202 | ) 203 | 204 | dispatcher.add_handler(BLACKLIST_HANDLER) 205 | dispatcher.add_handler(ADD_BLACKLIST_HANDLER) 206 | dispatcher.add_handler(UNBLACKLIST_HANDLER) 207 | dispatcher.add_handler(BLACKLIST_DEL_HANDLER, group=BLACKLIST_GROUP) 208 | -------------------------------------------------------------------------------- /tg_bot/modules/disable.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List, Optional 2 | 3 | from future.utils import string_types 4 | from telegram import ParseMode, Update, Bot, Chat, User 5 | from telegram.ext import CommandHandler, RegexHandler, Filters 6 | from telegram.utils.helpers import escape_markdown 7 | 8 | from tg_bot import dispatcher 9 | from tg_bot.modules.helper_funcs.handlers import CMD_STARTERS 10 | from tg_bot.modules.helper_funcs.misc import is_module_loaded 11 | 12 | FILENAME = __name__.rsplit(".", 1)[-1] 13 | 14 | # If module is due to be loaded, then setup all the magical handlers 15 | if is_module_loaded(FILENAME): 16 | from tg_bot.modules.helper_funcs.chat_status import user_admin, is_user_admin 17 | from telegram.ext.dispatcher import run_async 18 | 19 | from tg_bot.modules.sql import disable_sql as sql 20 | 21 | DISABLE_CMDS = [] 22 | DISABLE_OTHER = [] 23 | ADMIN_CMDS = [] 24 | 25 | 26 | class DisableAbleCommandHandler(CommandHandler): 27 | def __init__(self, command, callback, admin_ok=False, **kwargs): 28 | super().__init__(command, callback, **kwargs) 29 | self.admin_ok = admin_ok 30 | if isinstance(command, string_types): 31 | DISABLE_CMDS.append(command) 32 | if admin_ok: 33 | ADMIN_CMDS.append(command) 34 | else: 35 | DISABLE_CMDS.extend(command) 36 | if admin_ok: 37 | ADMIN_CMDS.extend(command) 38 | 39 | def check_update(self, update): 40 | chat = update.effective_chat # type: Optional[Chat] 41 | user = update.effective_user # type: Optional[User] 42 | if super().check_update(update): 43 | # Should be safe since check_update passed. 44 | command = update.effective_message.text_html.split(None, 1)[0][ 45 | 1: 46 | ].split("@")[0] 47 | 48 | # disabled, admincmd, user admin 49 | if sql.is_command_disabled(chat.id, command): 50 | return command in ADMIN_CMDS and is_user_admin(chat, user.id) 51 | 52 | # not disabled 53 | else: 54 | return True 55 | 56 | return False 57 | 58 | 59 | class DisableAbleRegexHandler(RegexHandler): 60 | def __init__(self, pattern, callback, friendly="", **kwargs): 61 | super().__init__(pattern, callback, **kwargs) 62 | DISABLE_OTHER.append(friendly or pattern) 63 | self.friendly = friendly or pattern 64 | 65 | def check_update(self, update): 66 | chat = update.effective_chat 67 | return super().check_update(update) and not sql.is_command_disabled( 68 | chat.id, self.friendly 69 | ) 70 | 71 | 72 | @run_async 73 | @user_admin 74 | def disable(bot: Bot, update: Update, args: List[str]): 75 | chat = update.effective_chat # type: Optional[Chat] 76 | if len(args) >= 1: 77 | disable_cmd = args[0] 78 | if disable_cmd.startswith(CMD_STARTERS): 79 | disable_cmd = disable_cmd[1:] 80 | 81 | if disable_cmd in set(DISABLE_CMDS + DISABLE_OTHER): 82 | sql.disable_command(chat.id, disable_cmd) 83 | update.effective_message.reply_text( 84 | "Disabilito il comando `{}`".format(disable_cmd), 85 | parse_mode=ParseMode.MARKDOWN, 86 | ) 87 | else: 88 | update.effective_message.reply_text( 89 | "Questo comando non può essere disabilitato" 90 | ) 91 | 92 | else: 93 | update.effective_message.reply_text("Cosa dovrei disabilitare?") 94 | 95 | 96 | @run_async 97 | @user_admin 98 | def enable(bot: Bot, update: Update, args: List[str]): 99 | chat = update.effective_chat # type: Optional[Chat] 100 | if len(args) >= 1: 101 | enable_cmd = args[0] 102 | if enable_cmd.startswith(CMD_STARTERS): 103 | enable_cmd = enable_cmd[1:] 104 | 105 | if sql.enable_command(chat.id, enable_cmd): 106 | update.effective_message.reply_text( 107 | "Abilito l'uso del comando `{}`".format(enable_cmd), 108 | parse_mode=ParseMode.MARKDOWN, 109 | ) 110 | else: 111 | update.effective_message.reply_text( 112 | "Sei sicuro che questo comando fosse disabilitato?" 113 | ) 114 | 115 | else: 116 | update.effective_message.reply_text("Cosa dovrei abilitare?") 117 | 118 | 119 | @run_async 120 | @user_admin 121 | def list_cmds(bot: Bot, update: Update): 122 | if DISABLE_CMDS + DISABLE_OTHER: 123 | result = "" 124 | for cmd in set(DISABLE_CMDS + DISABLE_OTHER): 125 | result += " - `{}`\n".format(escape_markdown(cmd)) 126 | update.effective_message.reply_text( 127 | "I seguenti comando sono toggleable:\n{}".format(result), 128 | parse_mode=ParseMode.MARKDOWN, 129 | ) 130 | else: 131 | update.effective_message.reply_text("Tutti i comandi sono abilitati.") 132 | 133 | 134 | # do not async 135 | def build_curr_disabled(chat_id: Union[str, int]) -> str: 136 | disabled = sql.get_all_disabled(chat_id) 137 | if not disabled: 138 | return "Nessun comando è disabilitato!" 139 | 140 | result = "" 141 | for cmd in disabled: 142 | result += " - `{}`\n".format(escape_markdown(cmd)) 143 | return "I seguenti comando sono attualmente ristretti:\n{}".format(result) 144 | 145 | 146 | @run_async 147 | def commands(bot: Bot, update: Update): 148 | chat = update.effective_chat 149 | update.effective_message.reply_text( 150 | build_curr_disabled(chat.id), parse_mode=ParseMode.MARKDOWN 151 | ) 152 | 153 | 154 | def __stats__(): 155 | return "{} comandi disabilitati, in {} chats.".format( 156 | sql.num_disabled(), sql.num_chats() 157 | ) 158 | 159 | 160 | def __migrate__(old_chat_id, new_chat_id): 161 | sql.migrate_chat(old_chat_id, new_chat_id) 162 | 163 | 164 | def __chat_settings__(chat_id, user_id): 165 | return build_curr_disabled(chat_id) 166 | 167 | 168 | __mod_name__ = "Command disabling" 169 | 170 | __help__ = """ 171 | - /cmds: controlla lo stato dei comandi disabilitati 172 | 173 | *Admin only:* 174 | - /enable : abilita il comando 175 | - /disable : disabilita il comando 176 | - /listcmds: lista di tutti i comandi toggleable 177 | """ 178 | 179 | DISABLE_HANDLER = CommandHandler( 180 | "disable", disable, pass_args=True, filters=Filters.group 181 | ) 182 | ENABLE_HANDLER = CommandHandler( 183 | "enable", enable, pass_args=True, filters=Filters.group 184 | ) 185 | COMMANDS_HANDLER = CommandHandler( 186 | ["cmds", "disabled"], commands, filters=Filters.group 187 | ) 188 | TOGGLE_HANDLER = CommandHandler("listcmds", list_cmds, filters=Filters.group) 189 | 190 | dispatcher.add_handler(DISABLE_HANDLER) 191 | dispatcher.add_handler(ENABLE_HANDLER) 192 | dispatcher.add_handler(COMMANDS_HANDLER) 193 | dispatcher.add_handler(TOGGLE_HANDLER) 194 | 195 | else: 196 | DisableAbleCommandHandler = CommandHandler 197 | DisableAbleRegexHandler = RegexHandler 198 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonitalia/python-italy-telegram-bot/d69c2ead7fbcdd555265cb684353310c84164b80/tg_bot/modules/helper_funcs/__init__.py -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/chat_status.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Optional 3 | 4 | from telegram import User, Chat, ChatMember, Update, Bot 5 | 6 | from tg_bot import DEL_CMDS, SUDO_USERS, WHITELIST_USERS 7 | 8 | 9 | def can_delete(chat: Chat, bot_id: int) -> bool: 10 | return chat.get_member(bot_id).can_delete_messages 11 | 12 | 13 | def is_user_ban_protected(chat: Chat, user_id: int, member: ChatMember = None) -> bool: 14 | if ( 15 | chat.type == "private" 16 | or user_id in SUDO_USERS 17 | or user_id in WHITELIST_USERS 18 | or chat.all_members_are_administrators 19 | ): 20 | return True 21 | 22 | if not member: 23 | member = chat.get_member(user_id) 24 | return member.status in ("administrator", "creator") 25 | 26 | 27 | def is_user_admin(chat: Chat, user_id: int, member: ChatMember = None) -> bool: 28 | if ( 29 | chat.type == "private" 30 | or user_id in SUDO_USERS 31 | or chat.all_members_are_administrators 32 | ): 33 | return True 34 | 35 | if not member: 36 | member = chat.get_member(user_id) 37 | return member.status in ("administrator", "creator") 38 | 39 | 40 | def is_bot_admin(chat: Chat, bot_id: int, bot_member: ChatMember = None) -> bool: 41 | if chat.type == "private" or chat.all_members_are_administrators: 42 | return True 43 | 44 | if not bot_member: 45 | bot_member = chat.get_member(bot_id) 46 | return bot_member.status in ("administrator", "creator") 47 | 48 | 49 | def is_user_in_chat(chat: Chat, user_id: int) -> bool: 50 | member = chat.get_member(user_id) 51 | return member.status not in ("left", "kicked") 52 | 53 | 54 | def bot_can_delete(func): 55 | @wraps(func) 56 | def delete_rights(bot: Bot, update: Update, *args, **kwargs): 57 | if can_delete(update.effective_chat, bot.id): 58 | return func(bot, update, *args, **kwargs) 59 | else: 60 | update.effective_message.reply_text( 61 | "Non posso cancellare i messaggi qui! " 62 | "Assicurati di avermi fatto admin." 63 | ) 64 | 65 | return delete_rights 66 | 67 | 68 | def can_pin(func): 69 | @wraps(func) 70 | def pin_rights(bot: Bot, update: Update, *args, **kwargs): 71 | if update.effective_chat.get_member(bot.id).can_pin_messages: 72 | return func(bot, update, *args, **kwargs) 73 | else: 74 | update.effective_message.reply_text( 75 | "NOn posso pinnare i messaggi! " "Assicurati di avermi fatto admin." 76 | ) 77 | 78 | return pin_rights 79 | 80 | 81 | def can_promote(func): 82 | @wraps(func) 83 | def promote_rights(bot: Bot, update: Update, *args, **kwargs): 84 | if update.effective_chat.get_member(bot.id).can_promote_members: 85 | return func(bot, update, *args, **kwargs) 86 | else: 87 | update.effective_message.reply_text( 88 | "Non posso promuovere le persone qui! " 89 | "Assicurati di avermi fatto admin." 90 | ) 91 | 92 | return promote_rights 93 | 94 | 95 | def can_restrict(func): 96 | @wraps(func) 97 | def promote_rights(bot: Bot, update: Update, *args, **kwargs): 98 | if update.effective_chat.get_member(bot.id).can_restrict_members: 99 | return func(bot, update, *args, **kwargs) 100 | else: 101 | update.effective_message.reply_text( 102 | "Non posso eseguire queste operazioni qui! " 103 | "Assicurati di avermi fatto admin." 104 | ) 105 | 106 | return promote_rights 107 | 108 | 109 | def bot_admin(func): 110 | @wraps(func) 111 | def is_admin(bot: Bot, update: Update, *args, **kwargs): 112 | if is_bot_admin(update.effective_chat, bot.id): 113 | return func(bot, update, *args, **kwargs) 114 | else: 115 | update.effective_message.reply_text("Non sono admin!") 116 | 117 | return is_admin 118 | 119 | 120 | def user_admin(func): 121 | @wraps(func) 122 | def is_admin(bot: Bot, update: Update, *args, **kwargs): 123 | user = update.effective_user # type: Optional[User] 124 | if user and is_user_admin(update.effective_chat, user.id): 125 | return func(bot, update, *args, **kwargs) 126 | 127 | elif not user: 128 | pass 129 | 130 | elif DEL_CMDS and " " not in update.effective_message.text: 131 | update.effective_message.delete() 132 | 133 | else: 134 | update.effective_message.reply_text( 135 | "Non sei admin e non puoi eseguire questo comando :)" 136 | ) 137 | 138 | return is_admin 139 | 140 | 141 | def user_admin_no_reply(func): 142 | @wraps(func) 143 | def is_admin(bot: Bot, update: Update, *args, **kwargs): 144 | user = update.effective_user # type: Optional[User] 145 | if user and is_user_admin(update.effective_chat, user.id): 146 | return func(bot, update, *args, **kwargs) 147 | 148 | elif not user: 149 | pass 150 | 151 | elif DEL_CMDS and " " not in update.effective_message.text: 152 | update.effective_message.delete() 153 | 154 | return is_admin 155 | 156 | 157 | def user_not_admin(func): 158 | @wraps(func) 159 | def is_not_admin(bot: Bot, update: Update, *args, **kwargs): 160 | user = update.effective_user # type: Optional[User] 161 | if user and not is_user_admin(update.effective_chat, user.id): 162 | return func(bot, update, *args, **kwargs) 163 | 164 | return is_not_admin 165 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/extraction.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from telegram import Message, MessageEntity 4 | from telegram.error import BadRequest 5 | 6 | from tg_bot import LOGGER 7 | from tg_bot.modules.users import get_user_id 8 | 9 | 10 | def id_from_reply(message): 11 | prev_message = message.reply_to_message 12 | if not prev_message: 13 | return None, None 14 | user_id = prev_message.from_user.id 15 | res = message.text.split(None, 1) 16 | if len(res) < 2: 17 | return user_id, "" 18 | return user_id, res[1] 19 | 20 | 21 | def extract_user(message: Message, args: List[str]) -> Optional[int]: 22 | return extract_user_and_text(message, args)[0] 23 | 24 | 25 | def extract_user_and_text( 26 | message: Message, args: List[str] 27 | ) -> (Optional[int], Optional[str]): 28 | prev_message = message.reply_to_message 29 | split_text = message.text.split(None, 1) 30 | 31 | if len(split_text) < 2: 32 | return id_from_reply(message) # only option possible 33 | 34 | text_to_parse = split_text[1] 35 | 36 | text = "" 37 | 38 | entities = list(message.parse_entities([MessageEntity.TEXT_MENTION])) 39 | if len(entities) > 0: 40 | ent = entities[0] 41 | else: 42 | ent = None 43 | 44 | # if entity offset matches (command end/text start) then all good 45 | if entities and ent and ent.offset == len(message.text) - len(text_to_parse): 46 | ent = entities[0] 47 | user_id = ent.user.id 48 | text = message.text[ent.offset + ent.length:] 49 | 50 | elif len(args) >= 1 and args[0][0] == "@": 51 | user = args[0] 52 | user_id = get_user_id(user) 53 | if not user_id: 54 | message.reply_text( 55 | "Non ho quell'utente nel mio db. Sarai in grado di interagire con lui se " 56 | "rispondi al messaggio di quella persona o mi inoltri uno dei messaggi di quell'utente." 57 | ) 58 | return None, None 59 | 60 | else: 61 | user_id = user_id 62 | res = message.text.split(None, 2) 63 | if len(res) >= 3: 64 | text = res[2] 65 | 66 | elif len(args) >= 1 and args[0].isdigit(): 67 | user_id = int(args[0]) 68 | res = message.text.split(None, 2) 69 | if len(res) >= 3: 70 | text = res[2] 71 | 72 | elif prev_message: 73 | user_id, text = id_from_reply(message) 74 | 75 | else: 76 | return None, None 77 | 78 | try: 79 | message.bot.get_chat(user_id) 80 | except BadRequest as excp: 81 | if excp.message in ("User_id_invalid", "Chat not found"): 82 | message.reply_text( 83 | "Non ho mai interagito con questo utente prima - per favore inoltrami un suo messaggio " 84 | "per permettermi di prendere il controllo! (come una bambola voodoo, ho bisogno di un piccolo pezzo di questo utente " 85 | "per eseguire alcuni comandi..)" 86 | ) 87 | else: 88 | LOGGER.exception("Exception %s on user %s", excp.message, user_id) 89 | 90 | return None, None 91 | 92 | return user_id, text 93 | 94 | 95 | def extract_text(message) -> str: 96 | return ( 97 | message.text 98 | or message.caption 99 | or (message.sticker.emoji if message.sticker else None) 100 | ) 101 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/filters.py: -------------------------------------------------------------------------------- 1 | from telegram import Message 2 | from telegram.ext import BaseFilter 3 | 4 | from tg_bot import SUPPORT_USERS, SUDO_USERS 5 | 6 | 7 | class CustomFilters(object): 8 | class _Supporters(BaseFilter): 9 | def filter(self, message: Message): 10 | return bool(message.from_user and message.from_user.id in SUPPORT_USERS) 11 | 12 | support_filter = _Supporters() 13 | 14 | class _Sudoers(BaseFilter): 15 | def filter(self, message: Message): 16 | return bool(message.from_user and message.from_user.id in SUDO_USERS) 17 | 18 | sudo_filter = _Sudoers() 19 | 20 | class _MimeType(BaseFilter): 21 | def __init__(self, mimetype): 22 | self.mime_type = mimetype 23 | self.name = "CustomFilters.mime_type({})".format(self.mime_type) 24 | 25 | def filter(self, message: Message): 26 | return bool( 27 | message.document and message.document.mime_type == self.mime_type 28 | ) 29 | 30 | mime_type = _MimeType 31 | 32 | class _HasText(BaseFilter): 33 | def filter(self, message: Message): 34 | return bool( 35 | message.text 36 | or message.sticker 37 | or message.photo 38 | or message.document 39 | or message.video 40 | ) 41 | 42 | has_text = _HasText() 43 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/handlers.py: -------------------------------------------------------------------------------- 1 | import telegram.ext as tg 2 | from telegram import Update 3 | 4 | CMD_STARTERS = ("/", "!") 5 | 6 | 7 | class CustomCommandHandler(tg.CommandHandler): 8 | def __init__(self, command, callback, **kwargs): 9 | if "admin_ok" in kwargs: 10 | del kwargs["admin_ok"] 11 | super().__init__(command, callback, **kwargs) 12 | 13 | def check_update(self, update): 14 | if isinstance(update, Update) and ( 15 | update.message or update.edited_message and self.allow_edited 16 | ): 17 | message = update.message or update.edited_message 18 | 19 | if message.text and len(message.text) > 1: 20 | fst_word = message.text_html.split(None, 1)[0] 21 | if len(fst_word) > 1 and any( 22 | fst_word.startswith(start) for start in CMD_STARTERS 23 | ): 24 | command = fst_word[1:].split("@") 25 | command.append( 26 | message.bot.username 27 | ) # in case the command was sent without a username 28 | if self.filters is None: 29 | res = True 30 | elif isinstance(self.filters, list): 31 | res = any(func(message) for func in self.filters) 32 | else: 33 | res = self.filters(message) 34 | 35 | return res and ( 36 | command[0].lower() in self.command 37 | and command[1].lower() == message.bot.username.lower() 38 | ) 39 | 40 | return False 41 | 42 | 43 | class CustomRegexHandler(tg.RegexHandler): 44 | def __init__(self, pattern, callback, friendly="", **kwargs): 45 | super().__init__(pattern, callback, **kwargs) 46 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/misc.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | from typing import List, Dict 3 | 4 | from telegram import MAX_MESSAGE_LENGTH, InlineKeyboardButton, Bot, ParseMode 5 | from telegram.error import TelegramError 6 | 7 | from tg_bot import LOAD, NO_LOAD 8 | 9 | 10 | class EqInlineKeyboardButton(InlineKeyboardButton): 11 | def __eq__(self, other): 12 | return self.text == other.text 13 | 14 | def __lt__(self, other): 15 | return self.text < other.text 16 | 17 | def __gt__(self, other): 18 | return self.text > other.text 19 | 20 | 21 | def split_message(msg: str) -> List[str]: 22 | if len(msg) < MAX_MESSAGE_LENGTH: 23 | return [msg] 24 | 25 | else: 26 | lines = msg.splitlines(True) 27 | small_msg = "" 28 | result = [] 29 | for line in lines: 30 | if len(small_msg) + len(line) < MAX_MESSAGE_LENGTH: 31 | small_msg += line 32 | else: 33 | result.append(small_msg) 34 | small_msg = line 35 | else: 36 | # Else statement at the end of the for loop, so append the leftover string. 37 | result.append(small_msg) 38 | 39 | return result 40 | 41 | 42 | def paginate_modules(page_n: int, module_dict: Dict, prefix, chat=None) -> List: 43 | if not chat: 44 | modules = sorted( 45 | [ 46 | EqInlineKeyboardButton( 47 | x.__mod_name__, 48 | callback_data="{}_module({})".format( 49 | prefix, x.__mod_name__.lower() 50 | ), 51 | ) 52 | for x in module_dict.values() 53 | ] 54 | ) 55 | else: 56 | modules = sorted( 57 | [ 58 | EqInlineKeyboardButton( 59 | x.__mod_name__, 60 | callback_data="{}_module({},{})".format( 61 | prefix, chat, x.__mod_name__.lower() 62 | ), 63 | ) 64 | for x in module_dict.values() 65 | ] 66 | ) 67 | 68 | pairs = list(zip(modules[::2], modules[1::2])) 69 | 70 | if len(modules) % 2 == 1: 71 | pairs.append((modules[-1],)) 72 | 73 | max_num_pages = ceil(len(pairs) / 7) 74 | modulo_page = page_n % max_num_pages 75 | 76 | # can only have a certain amount of buttons side by side 77 | if len(pairs) > 7: 78 | pairs = pairs[modulo_page * 7: 7 * (modulo_page + 1)] + [ 79 | ( 80 | EqInlineKeyboardButton( 81 | "<", callback_data="{}_prev({})".format(prefix, modulo_page) 82 | ), 83 | EqInlineKeyboardButton( 84 | ">", callback_data="{}_next({})".format(prefix, modulo_page) 85 | ), 86 | ) 87 | ] 88 | 89 | return pairs 90 | 91 | 92 | def send_to_list( 93 | bot: Bot, send_to: list, message: str, markdown=False, html=False 94 | ) -> None: 95 | if html and markdown: 96 | raise Exception("Can only send with either markdown or HTML!") 97 | for user_id in set(send_to): 98 | try: 99 | if markdown: 100 | bot.send_message(user_id, message, parse_mode=ParseMode.MARKDOWN) 101 | elif html: 102 | bot.send_message(user_id, message, parse_mode=ParseMode.HTML) 103 | else: 104 | bot.send_message(user_id, message) 105 | except TelegramError: 106 | pass # ignore users who fail 107 | 108 | 109 | def build_keyboard(buttons): 110 | keyb = [] 111 | for btn in buttons: 112 | if btn.same_line and keyb: 113 | keyb[-1].append(InlineKeyboardButton(btn.name, url=btn.url)) 114 | else: 115 | keyb.append([InlineKeyboardButton(btn.name, url=btn.url)]) 116 | 117 | return keyb 118 | 119 | 120 | def revert_buttons(buttons): 121 | res = "" 122 | for btn in buttons: 123 | if btn.same_line: 124 | res += "\n[{}](buttonurl://{}:same)".format(btn.name, btn.url) 125 | else: 126 | res += "\n[{}](buttonurl://{})".format(btn.name, btn.url) 127 | 128 | return res 129 | 130 | 131 | def is_module_loaded(name): 132 | return (not LOAD or name in LOAD) and name not in NO_LOAD 133 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/msg_types.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, unique 2 | 3 | from telegram import Message 4 | 5 | from tg_bot.modules.helper_funcs.string_handling import button_markdown_parser 6 | 7 | 8 | @unique 9 | class Types(IntEnum): 10 | TEXT = 0 11 | BUTTON_TEXT = 1 12 | STICKER = 2 13 | DOCUMENT = 3 14 | PHOTO = 4 15 | AUDIO = 5 16 | VOICE = 6 17 | VIDEO = 7 18 | 19 | 20 | def get_note_type(msg: Message): 21 | data_type = None 22 | content = None 23 | text = "" 24 | raw_text = msg.text or msg.caption 25 | args = raw_text.split(None, 2) # use python's maxsplit to separate cmd and args 26 | note_name = args[1] 27 | 28 | buttons = [] 29 | # determine what the contents of the filter are - text, image, sticker, etc 30 | if len(args) >= 3: 31 | offset = len(args[2]) - len( 32 | raw_text 33 | ) # set correct offset relative to command + notename 34 | text, buttons = button_markdown_parser( 35 | args[2], 36 | entities=msg.parse_entities() or msg.parse_caption_entities(), 37 | offset=offset, 38 | ) 39 | if buttons: 40 | data_type = Types.BUTTON_TEXT 41 | else: 42 | data_type = Types.TEXT 43 | 44 | elif msg.reply_to_message: 45 | entities = msg.reply_to_message.parse_entities() 46 | msgtext = msg.reply_to_message.text or msg.reply_to_message.caption 47 | if len(args) >= 2 and msg.reply_to_message.text: # not caption, text 48 | text, buttons = button_markdown_parser(msgtext, entities=entities) 49 | if buttons: 50 | data_type = Types.BUTTON_TEXT 51 | else: 52 | data_type = Types.TEXT 53 | 54 | elif msg.reply_to_message.sticker: 55 | content = msg.reply_to_message.sticker.file_id 56 | data_type = Types.STICKER 57 | 58 | elif msg.reply_to_message.document: 59 | content = msg.reply_to_message.document.file_id 60 | text, buttons = button_markdown_parser(msgtext, entities=entities) 61 | data_type = Types.DOCUMENT 62 | 63 | elif msg.reply_to_message.photo: 64 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 65 | text, buttons = button_markdown_parser(msgtext, entities=entities) 66 | data_type = Types.PHOTO 67 | 68 | elif msg.reply_to_message.audio: 69 | content = msg.reply_to_message.audio.file_id 70 | text, buttons = button_markdown_parser(msgtext, entities=entities) 71 | data_type = Types.AUDIO 72 | 73 | elif msg.reply_to_message.voice: 74 | content = msg.reply_to_message.voice.file_id 75 | text, buttons = button_markdown_parser(msgtext, entities=entities) 76 | data_type = Types.VOICE 77 | 78 | elif msg.reply_to_message.video: 79 | content = msg.reply_to_message.video.file_id 80 | text, buttons = button_markdown_parser(msgtext, entities=entities) 81 | data_type = Types.VIDEO 82 | 83 | return note_name, text, data_type, content, buttons 84 | 85 | 86 | # note: add own args? 87 | def get_welcome_type(msg: Message): 88 | data_type = None 89 | content = None 90 | text = "" 91 | 92 | args = msg.text.split(None, 1) # use python's maxsplit to separate cmd and args 93 | 94 | buttons = [] 95 | # determine what the contents of the filter are - text, image, sticker, etc 96 | if len(args) >= 2: 97 | offset = len(args[1]) - len( 98 | msg.text 99 | ) # set correct offset relative to command + notename 100 | text, buttons = button_markdown_parser( 101 | args[1], entities=msg.parse_entities(), offset=offset 102 | ) 103 | if buttons: 104 | data_type = Types.BUTTON_TEXT 105 | else: 106 | data_type = Types.TEXT 107 | 108 | elif msg.reply_to_message and msg.reply_to_message.sticker: 109 | content = msg.reply_to_message.sticker.file_id 110 | text = msg.reply_to_message.text 111 | data_type = Types.STICKER 112 | 113 | elif msg.reply_to_message and msg.reply_to_message.document: 114 | content = msg.reply_to_message.document.file_id 115 | text = msg.reply_to_message.text 116 | data_type = Types.DOCUMENT 117 | 118 | elif msg.reply_to_message and msg.reply_to_message.photo: 119 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 120 | text = msg.reply_to_message.text 121 | data_type = Types.PHOTO 122 | 123 | elif msg.reply_to_message and msg.reply_to_message.audio: 124 | content = msg.reply_to_message.audio.file_id 125 | text = msg.reply_to_message.text 126 | data_type = Types.AUDIO 127 | 128 | elif msg.reply_to_message and msg.reply_to_message.voice: 129 | content = msg.reply_to_message.voice.file_id 130 | text = msg.reply_to_message.text 131 | data_type = Types.VOICE 132 | 133 | elif msg.reply_to_message and msg.reply_to_message.video: 134 | content = msg.reply_to_message.video.file_id 135 | text = msg.reply_to_message.text 136 | data_type = Types.VIDEO 137 | 138 | return text, data_type, content, buttons 139 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/string_handling.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | from typing import Dict, List 4 | 5 | import emoji 6 | from telegram import MessageEntity 7 | from telegram.utils.helpers import escape_markdown 8 | 9 | # NOTE: the url \ escape may cause double escapes 10 | # match * (bold) (don't escape if in url) 11 | # match _ (italics) (don't escape if in url) 12 | # match ` (code) 13 | # match []() (markdown link) 14 | # else, escape *, _, `, and [ 15 | MATCH_MD = re.compile( 16 | r"\*(.*?)\*|" 17 | r"_(.*?)_|" 18 | r"`(.*?)`|" 19 | r"(?[*_`\[])" 21 | ) 22 | 23 | # regex to find []() links -> hyperlinks/buttons 24 | LINK_REGEX = re.compile(r"(? str: 29 | """ 30 | Escape all invalid markdown 31 | 32 | :param to_parse: text to escape 33 | :return: valid markdown string 34 | """ 35 | offset = 0 # offset to be used as adding a \ character causes the string to shift 36 | for match in MATCH_MD.finditer(to_parse): 37 | if match.group("esc"): 38 | ent_start = match.start() 39 | to_parse = ( 40 | to_parse[: ent_start + offset] + "\\" + to_parse[ent_start + offset:] 41 | ) 42 | offset += 1 43 | return to_parse 44 | 45 | 46 | # This is a fun one. 47 | def _calc_emoji_offset(to_calc) -> int: 48 | # Get all emoji in text. 49 | emoticons = emoji.get_emoji_regexp().finditer(to_calc) 50 | # Check the utf16 length of the emoji to determine the offset it caused. 51 | # Normal, 1 character emoji don't affect; hence sub 1. 52 | # special, eg with two emoji characters (eg face, and skin col) will have length 2, so by subbing one we 53 | # know we'll get one extra offset, 54 | return sum(len(e.group(0).encode("utf-16-le")) // 2 - 1 for e in emoticons) 55 | 56 | 57 | def markdown_parser( 58 | txt: str, entities: Dict[MessageEntity, str] = None, offset: int = 0 59 | ) -> str: 60 | """ 61 | Parse a string, escaping all invalid markdown entities. 62 | 63 | Escapes URL's so as to avoid URL mangling. 64 | Re-adds any telegram code entities obtained from the entities object. 65 | 66 | :param txt: text to parse 67 | :param entities: dict of message entities in text 68 | :param offset: message offset - command and notename length 69 | :return: valid markdown string 70 | """ 71 | if not entities: 72 | entities = {} 73 | if not txt: 74 | return "" 75 | 76 | prev = 0 77 | res = "" 78 | # Loop over all message entities, and: 79 | # reinsert code 80 | # escape free-standing urls 81 | for ent, ent_text in entities.items(): 82 | if ent.offset < -offset: 83 | continue 84 | 85 | start = ent.offset + offset # start of entity 86 | end = ent.offset + offset + ent.length - 1 # end of entity 87 | 88 | # we only care about code, url, text links 89 | if ent.type in ("code", "url", "text_link"): 90 | # count emoji to switch counter 91 | count = _calc_emoji_offset(txt[:start]) 92 | start -= count 93 | end -= count 94 | 95 | # URL handling -> do not escape if in [](), escape otherwise. 96 | if ent.type == "url": 97 | if any( 98 | match.start(1) <= start and end <= match.end(1) 99 | for match in LINK_REGEX.finditer(txt) 100 | ): 101 | continue 102 | # else, check the escapes between the prev and last and forcefully escape the url to avoid mangling 103 | else: 104 | # TODO: investigate possible offset bug when lots of emoji are present 105 | res += _selective_escape(txt[prev:start] or "") + escape_markdown( 106 | ent_text 107 | ) 108 | 109 | # code handling 110 | elif ent.type == "code": 111 | res += _selective_escape(txt[prev:start]) + "`" + ent_text + "`" 112 | 113 | # handle markdown/html links 114 | elif ent.type == "text_link": 115 | res += _selective_escape(txt[prev:start]) + "[{}]({})".format( 116 | ent_text, ent.url 117 | ) 118 | 119 | end += 1 120 | 121 | # anything else 122 | else: 123 | continue 124 | 125 | prev = end 126 | 127 | res += _selective_escape(txt[prev:]) # add the rest of the text 128 | return res 129 | 130 | 131 | def button_markdown_parser( 132 | txt: str, entities: Dict[MessageEntity, str] = None, offset: int = 0 133 | ) -> (str, List): 134 | markdown_note = markdown_parser(txt, entities, offset) 135 | prev = 0 136 | note_data = "" 137 | buttons = [] 138 | for match in BTN_URL_REGEX.finditer(markdown_note): 139 | # Check if btnurl is escaped 140 | n_escapes = 0 141 | to_check = match.start(1) - 1 142 | while to_check > 0 and markdown_note[to_check] == "\\": 143 | n_escapes += 1 144 | to_check -= 1 145 | 146 | # if even, not escaped -> create button 147 | if n_escapes % 2 == 0: 148 | # create a thruple with button label, url, and newline status 149 | buttons.append((match.group(2), match.group(3), bool(match.group(4)))) 150 | note_data += markdown_note[prev: match.start(1)] 151 | prev = match.end(1) 152 | # if odd, escaped -> move along 153 | else: 154 | note_data += markdown_note[prev:to_check] 155 | prev = match.start(1) - 1 156 | else: 157 | note_data += markdown_note[prev:] 158 | 159 | return note_data, buttons 160 | 161 | 162 | def escape_invalid_curly_brackets(text: str, valids: List[str]) -> str: 163 | new_text = "" 164 | idx = 0 165 | while idx < len(text): 166 | if text[idx] == "{": 167 | if idx + 1 < len(text) and text[idx + 1] == "{": 168 | idx += 2 169 | new_text += "{{{{" 170 | continue 171 | else: 172 | success = False 173 | for v in valids: 174 | if text[idx:].startswith("{" + v + "}"): 175 | success = True 176 | break 177 | if success: 178 | new_text += text[idx: idx + len(v) + 2] 179 | idx += len(v) + 2 180 | continue 181 | else: 182 | new_text += "{{" 183 | 184 | elif text[idx] == "}": 185 | if idx + 1 < len(text) and text[idx + 1] == "}": 186 | idx += 2 187 | new_text += "}}}}" 188 | continue 189 | else: 190 | new_text += "}}" 191 | 192 | else: 193 | new_text += text[idx] 194 | idx += 1 195 | 196 | return new_text 197 | 198 | 199 | SMART_OPEN = "“" 200 | SMART_CLOSE = "”" 201 | START_CHAR = ("'", '"', SMART_OPEN) 202 | 203 | 204 | def split_quotes(text: str) -> List: 205 | if any(text.startswith(char) for char in START_CHAR): 206 | counter = 1 # ignore first char -> is some kind of quote 207 | while counter < len(text): 208 | if text[counter] == "\\": 209 | counter += 1 210 | elif text[counter] == text[0] or ( 211 | text[0] == SMART_OPEN and text[counter] == SMART_CLOSE 212 | ): 213 | break 214 | counter += 1 215 | else: 216 | return text.split(None, 1) 217 | 218 | # 1 to avoid starting quote, and counter is exclusive so avoids ending 219 | key = remove_escapes(text[1:counter].strip()) 220 | # index will be in range, or `else` would have been executed and returned 221 | rest = text[counter + 1:].strip() 222 | if not key: 223 | key = text[0] + text[0] 224 | return list(filter(None, [key, rest])) 225 | else: 226 | return text.split(None, 1) 227 | 228 | 229 | def remove_escapes(text: str) -> str: 230 | counter = 0 231 | res = "" 232 | is_escaped = False 233 | while counter < len(text): 234 | if is_escaped: 235 | res += text[counter] 236 | is_escaped = False 237 | elif text[counter] == "\\": 238 | is_escaped = True 239 | else: 240 | res += text[counter] 241 | counter += 1 242 | return res 243 | 244 | 245 | def escape_chars(text: str, to_escape: List[str]) -> str: 246 | to_escape.append("\\") 247 | new_text = "" 248 | for x in text: 249 | if x in to_escape: 250 | new_text += "\\" 251 | new_text += x 252 | return new_text 253 | 254 | 255 | def extract_time(message, time_val): 256 | if any(time_val.endswith(unit) for unit in ("m", "h", "d")): 257 | unit = time_val[-1] 258 | time_num = time_val[:-1] # type: str 259 | if not time_num.isdigit(): 260 | message.reply_text("Invalid time amount specified.") 261 | return "" 262 | 263 | if unit == "m": 264 | bantime = int(time.time() + int(time_num) * 60) 265 | elif unit == "h": 266 | bantime = int(time.time() + int(time_num) * 60 * 60) 267 | elif unit == "d": 268 | bantime = int(time.time() + int(time_num) * 24 * 60 * 60) 269 | else: 270 | # how even...? 271 | return "" 272 | return bantime 273 | else: 274 | message.reply_text( 275 | "Stringa invalida. Mi aspetto questo ordine: m,h oppure d. Ho ricevuto: {}".format( 276 | time_val[-1] 277 | ) 278 | ) 279 | return "" 280 | -------------------------------------------------------------------------------- /tg_bot/modules/msg_deleting.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Chat, Update, Bot, User 5 | from telegram.error import BadRequest 6 | from telegram.ext import CommandHandler, Filters 7 | from telegram.ext.dispatcher import run_async 8 | from telegram.utils.helpers import mention_html 9 | 10 | from tg_bot import dispatcher, LOGGER 11 | from tg_bot.modules.helper_funcs.chat_status import user_admin, can_delete 12 | from tg_bot.modules.log_channel import loggable 13 | 14 | 15 | @run_async 16 | @user_admin 17 | @loggable 18 | def purge(bot: Bot, update: Update, args: List[str]) -> str: 19 | msg = update.effective_message # type: Optional[Message] 20 | if msg.reply_to_message: 21 | user = update.effective_user # type: Optional[User] 22 | chat = update.effective_chat # type: Optional[Chat] 23 | if can_delete(chat, bot.id): 24 | message_id = msg.reply_to_message.message_id 25 | delete_to = msg.message_id - 1 26 | if args and args[0].isdigit(): 27 | new_del = message_id + int(args[0]) 28 | # No point deleting messages which haven't been written yet. 29 | if new_del < delete_to: 30 | delete_to = new_del 31 | 32 | for m_id in range( 33 | delete_to, message_id - 1, -1 34 | ): # Reverse iteration over message ids 35 | try: 36 | bot.deleteMessage(chat.id, m_id) 37 | except BadRequest as err: 38 | if err.message == "Message can't be deleted": 39 | bot.send_message( 40 | chat.id, 41 | "Non posso cancellare tutti i messaggi. I messaggi potrebbero essere troppo vecchi, oppure " 42 | "non sono amministratore, o questo non è un supergruppo.", 43 | ) 44 | 45 | elif err.message != "Message to delete not found": 46 | LOGGER.exception("Errore durante l'eliminazione dei messaggi.") 47 | 48 | try: 49 | msg.delete() 50 | except BadRequest as err: 51 | if err.message == "Message can't be deleted": 52 | bot.send_message( 53 | chat.id, 54 | "Non posso cancellare tutti i messaggi. I messaggi potrebbero essere troppo vecchi, potrei " 55 | "non essere amministratore, o questo potrebbe non essere un supergruppo.", 56 | ) 57 | 58 | elif err.message != "Message to delete not found": 59 | LOGGER.exception("Errore durante l'eliminazione dei messaggi.") 60 | 61 | bot.send_message(chat.id, "Purge completo.") 62 | return ( 63 | "{}:" 64 | "\n#PURGE" 65 | "\nAdmin: {}" 66 | "\nPurged {} messages.".format( 67 | html.escape(chat.title), 68 | mention_html(user.id, user.first_name), 69 | delete_to - message_id, 70 | ) 71 | ) 72 | 73 | else: 74 | msg.reply_text( 75 | "Rispondi a un messaggio per selezionare da dove iniziare l'eliminazione dei messaggi." 76 | ) 77 | 78 | return "" 79 | 80 | 81 | @run_async 82 | @user_admin 83 | @loggable 84 | def del_message(bot: Bot, update: Update) -> str: 85 | if update.effective_message.reply_to_message: 86 | user = update.effective_user # type: Optional[User] 87 | chat = update.effective_chat # type: Optional[Chat] 88 | if can_delete(chat, bot.id): 89 | update.effective_message.reply_to_message.delete() 90 | update.effective_message.delete() 91 | return ( 92 | "{}:" 93 | "\n#DEL" 94 | "\nAdmin: {}" 95 | "\nMessage deleted.".format( 96 | html.escape(chat.title), mention_html(user.id, user.first_name) 97 | ) 98 | ) 99 | else: 100 | update.effective_message.reply_text("Cosa vuoi cancellare?") 101 | 102 | return "" 103 | 104 | 105 | __help__ = """ 106 | *Admin only:* 107 | - /del: cancella il messaggio a cui hai risposto 108 | - /purge: cancella tutti i messaggi tra questo e il messaggio risposto. 109 | - /purge : cancella il messaggio risposto e i messaggi X che lo seguono. 110 | """ 111 | 112 | __mod_name__ = "Purges" 113 | 114 | DELETE_HANDLER = CommandHandler("del", del_message, filters=Filters.group) 115 | PURGE_HANDLER = CommandHandler("purge", purge, filters=Filters.group, pass_args=True) 116 | 117 | dispatcher.add_handler(DELETE_HANDLER) 118 | dispatcher.add_handler(PURGE_HANDLER) 119 | -------------------------------------------------------------------------------- /tg_bot/modules/muting.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Chat, Update, Bot, User 5 | from telegram.error import BadRequest 6 | from telegram.ext import CommandHandler, Filters 7 | from telegram.ext.dispatcher import run_async 8 | from telegram.utils.helpers import mention_html 9 | 10 | from tg_bot import dispatcher, LOGGER 11 | from tg_bot.modules.helper_funcs.chat_status import ( 12 | bot_admin, 13 | user_admin, 14 | is_user_admin, 15 | can_restrict, 16 | ) 17 | from tg_bot.modules.helper_funcs.extraction import extract_user, extract_user_and_text 18 | from tg_bot.modules.helper_funcs.string_handling import extract_time 19 | from tg_bot.modules.log_channel import loggable 20 | 21 | 22 | @run_async 23 | @bot_admin 24 | @user_admin 25 | @loggable 26 | def mute(bot: Bot, update: Update, args: List[str]) -> str: 27 | chat = update.effective_chat # type: Optional[Chat] 28 | user = update.effective_user # type: Optional[User] 29 | message = update.effective_message # type: Optional[Message] 30 | 31 | user_id = extract_user(message, args) 32 | if not user_id: 33 | message.reply_text( 34 | "Dovrai darmi un nome utente per mutarlo o rispondere a qualcuno che deve essere mutato." 35 | ) 36 | return "" 37 | 38 | if user_id == bot.id: 39 | message.reply_text("Non mi muterò!") 40 | return "" 41 | 42 | member = chat.get_member(int(user_id)) 43 | 44 | if member: 45 | if is_user_admin(chat, user_id, member=member): 46 | message.reply_text( 47 | "Ho paura di non poter impedire a un amministratore di parlare!" 48 | ) 49 | 50 | elif member.can_send_messages is None or member.can_send_messages: 51 | bot.restrict_chat_member(chat.id, user_id, can_send_messages=False) 52 | message.reply_text("Membro silenziato.") 53 | return ( 54 | "{}:" 55 | "\n#MUTE" 56 | "\nAdmin: {}" 57 | "\nUser: {}".format( 58 | html.escape(chat.title), 59 | mention_html(user.id, user.first_name), 60 | mention_html(member.user.id, member.user.first_name), 61 | ) 62 | ) 63 | 64 | else: 65 | message.reply_text("Questo utente è già in muto!") 66 | else: 67 | message.reply_text("Questo utente non esiste in questa chat!") 68 | 69 | return "" 70 | 71 | 72 | @run_async 73 | @bot_admin 74 | @user_admin 75 | @loggable 76 | def unmute(bot: Bot, update: Update, args: List[str]) -> str: 77 | chat = update.effective_chat # type: Optional[Chat] 78 | user = update.effective_user # type: Optional[User] 79 | message = update.effective_message # type: Optional[Message] 80 | 81 | user_id = extract_user(message, args) 82 | if not user_id: 83 | message.reply_text( 84 | "Dovrai o darmi un nome utente per mutarlo o rispondere a qualcuno che deve essere mutato." 85 | ) 86 | return "" 87 | 88 | member = chat.get_member(int(user_id)) 89 | 90 | if member: 91 | if is_user_admin(chat, user_id, member=member): 92 | message.reply_text("Questo è un admin, cosa ti aspetti che faccia?") 93 | return "" 94 | 95 | elif member.status != "kicked" and member.status != "left": 96 | if ( 97 | member.can_send_messages 98 | and member.can_send_media_messages 99 | and member.can_send_other_messages 100 | and member.can_add_web_page_previews 101 | ): 102 | message.reply_text("Questo utente può parlare.") 103 | return "" 104 | else: 105 | bot.restrict_chat_member( 106 | chat.id, 107 | int(user_id), 108 | can_send_messages=True, 109 | can_send_media_messages=True, 110 | can_send_other_messages=True, 111 | can_add_web_page_previews=True, 112 | ) 113 | message.reply_text("Muto disattivato!") 114 | return ( 115 | "{}:" 116 | "\n#UNMUTE" 117 | "\nAdmin: {}" 118 | "\nUser: {}".format( 119 | html.escape(chat.title), 120 | mention_html(user.id, user.first_name), 121 | mention_html(member.user.id, member.user.first_name), 122 | ) 123 | ) 124 | else: 125 | message.reply_text( 126 | "Questo utente non è nella chat, unmutarlo non gli permetterà di parlare." 127 | ) 128 | 129 | return "" 130 | 131 | 132 | @run_async 133 | @bot_admin 134 | @can_restrict 135 | @user_admin 136 | @loggable 137 | def temp_mute(bot: Bot, update: Update, args: List[str]) -> str: 138 | chat = update.effective_chat # type: Optional[Chat] 139 | user = update.effective_user # type: Optional[User] 140 | message = update.effective_message # type: Optional[Message] 141 | 142 | user_id, reason = extract_user_and_text(message, args) 143 | 144 | if not user_id: 145 | message.reply_text("Non ti stai riferendo ad un utente") 146 | return "" 147 | 148 | try: 149 | member = chat.get_member(user_id) 150 | except BadRequest as excp: 151 | if excp.message == "User not found": 152 | message.reply_text("Non riesco a trovare questo utente") 153 | return "" 154 | else: 155 | raise 156 | 157 | if is_user_admin(chat, user_id, member): 158 | message.reply_text("Mi piacerebbe tanto poter mutare gli admin a volte...") 159 | return "" 160 | 161 | if user_id == bot.id: 162 | message.reply_text("Non mi muterò da solo.. Sei pazzo?") 163 | return "" 164 | 165 | if not reason: 166 | message.reply_text("Non hai specificato per quanto tempo mutare questo utente!") 167 | return "" 168 | 169 | split_reason = reason.split(None, 1) 170 | 171 | time_val = split_reason[0].lower() 172 | if len(split_reason) > 1: 173 | reason = split_reason[1] 174 | else: 175 | reason = "" 176 | 177 | mutetime = extract_time(message, time_val) 178 | 179 | if not mutetime: 180 | return "" 181 | 182 | log = ( 183 | "{}:" 184 | "\n#TEMP MUTED" 185 | "\nAdmin: {}" 186 | "\nUser: {}" 187 | "\nTime: {}".format( 188 | html.escape(chat.title), 189 | mention_html(user.id, user.first_name), 190 | mention_html(member.user.id, member.user.first_name), 191 | time_val, 192 | ) 193 | ) 194 | if reason: 195 | log += "\nMotivo: {}".format(reason) 196 | 197 | try: 198 | if member.can_send_messages is None or member.can_send_messages: 199 | bot.restrict_chat_member( 200 | chat.id, user_id, until_date=mutetime, can_send_messages=False 201 | ) 202 | message.reply_text("Mutato per {}!".format(time_val)) 203 | return log 204 | else: 205 | message.reply_text("Questo utente è già mutato.") 206 | 207 | except BadRequest as excp: 208 | if excp.message == "Reply message not found": 209 | # Do not reply 210 | message.reply_text("Mutato per {}!".format(time_val), quote=False) 211 | return log 212 | else: 213 | LOGGER.warning(update) 214 | LOGGER.exception( 215 | "ERROR muting user %s in chat %s (%s) due to %s", 216 | user_id, 217 | chat.title, 218 | chat.id, 219 | excp.message, 220 | ) 221 | message.reply_text("Diamine, non posso mutare questo utente.") 222 | 223 | return "" 224 | 225 | 226 | __help__ = """ 227 | *Admin only:* 228 | - /mute : silenzia un utente. Può anche essere usato come risposta, disattivando la risposta all'utente. 229 | - /tmute x (m / h / d): silenzia un utente per x tempo. (tramite handle o risposta). m = minuti, h = ore, d = giorni. 230 | - /unmute : riattiva un utente. Può anche essere usato come risposta, disattivando la risposta all'utente. 231 | """ 232 | 233 | __mod_name__ = "Muting" 234 | 235 | MUTE_HANDLER = CommandHandler("mute", mute, pass_args=True, filters=Filters.group) 236 | UNMUTE_HANDLER = CommandHandler("unmute", unmute, pass_args=True, filters=Filters.group) 237 | TEMPMUTE_HANDLER = CommandHandler( 238 | ["tmute", "tempmute"], temp_mute, pass_args=True, filters=Filters.group 239 | ) 240 | 241 | dispatcher.add_handler(MUTE_HANDLER) 242 | dispatcher.add_handler(UNMUTE_HANDLER) 243 | dispatcher.add_handler(TEMPMUTE_HANDLER) 244 | -------------------------------------------------------------------------------- /tg_bot/modules/reporting.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Chat, Update, Bot, User, ParseMode 5 | from telegram.error import BadRequest, Unauthorized 6 | from telegram.ext import CommandHandler, RegexHandler, run_async, Filters 7 | from telegram.utils.helpers import mention_html 8 | 9 | from tg_bot import dispatcher, LOGGER 10 | from tg_bot.modules.helper_funcs.chat_status import user_not_admin, user_admin 11 | from tg_bot.modules.log_channel import loggable 12 | from tg_bot.modules.sql import reporting_sql as sql 13 | 14 | REPORT_GROUP = 5 15 | 16 | 17 | @run_async 18 | @user_admin 19 | def report_setting(bot: Bot, update: Update, args: List[str]): 20 | chat = update.effective_chat # type: Optional[Chat] 21 | msg = update.effective_message # type: Optional[Message] 22 | 23 | if chat.type == chat.PRIVATE: 24 | if len(args) >= 1: 25 | if args[0] in ("yes", "on"): 26 | sql.set_user_setting(chat.id, True) 27 | msg.reply_text( 28 | "Reporting acceso! Verrai notificati ogni volta che qualcuno riporta qualcosa." 29 | ) 30 | 31 | elif args[0] in ("no", "off"): 32 | sql.set_user_setting(chat.id, False) 33 | msg.reply_text("Reporting spento. Non verrai notificato.") 34 | else: 35 | msg.reply_text( 36 | "Le tue impostazioni attuali per il modulo Reporting sono: `{}`".format( 37 | sql.user_should_report(chat.id) 38 | ), 39 | parse_mode=ParseMode.MARKDOWN, 40 | ) 41 | 42 | else: 43 | if len(args) >= 1: 44 | if args[0] in ("yes", "on"): 45 | sql.set_chat_setting(chat.id, True) 46 | msg.reply_text( 47 | "Reporting acceso! Tutti gli admin che hanno abilitato il reporting verranno notificati ogni voltra che un utente usa il comando /reporting " 48 | "o @admin" 49 | ) 50 | 51 | elif args[0] in ("no", "off"): 52 | sql.set_chat_setting(chat.id, False) 53 | msg.reply_text("Reporting spento. Nessun admin verrà notificato.") 54 | else: 55 | msg.reply_text( 56 | "La chat è attualmente impostata: `{}`".format( 57 | sql.chat_should_report(chat.id) 58 | ), 59 | parse_mode=ParseMode.MARKDOWN, 60 | ) 61 | 62 | 63 | @run_async 64 | @user_not_admin 65 | @loggable 66 | def report(bot: Bot, update: Update) -> str: 67 | message = update.effective_message # type: Optional[Message] 68 | chat = update.effective_chat # type: Optional[Chat] 69 | user = update.effective_user # type: Optional[User] 70 | 71 | if chat and message.reply_to_message and sql.chat_should_report(chat.id): 72 | reported_user = message.reply_to_message.from_user # type: Optional[User] 73 | chat_name = chat.title or chat.first or chat.username 74 | admin_list = chat.get_administrators() 75 | 76 | if chat.username and chat.type == Chat.SUPERGROUP: 77 | msg = ( 78 | "{}:" 79 | "\nReported user: {} ({})" 80 | "\nReported by: {} ({})".format( 81 | html.escape(chat.title), 82 | mention_html(reported_user.id, reported_user.first_name), 83 | reported_user.id, 84 | mention_html(user.id, user.first_name), 85 | user.id, 86 | ) 87 | ) 88 | link = ( 89 | "\nLink: " 90 | 'qui'.format( 91 | chat.username, message.message_id 92 | ) 93 | ) 94 | 95 | should_forward = False 96 | 97 | else: 98 | msg = '{} sta chiamando gli admin in "{}"!'.format( 99 | mention_html(user.id, user.first_name), html.escape(chat_name) 100 | ) 101 | link = "" 102 | should_forward = True 103 | 104 | for admin in admin_list: 105 | if admin.user.is_bot: # can't message bots 106 | continue 107 | 108 | if sql.user_should_report(admin.user.id): 109 | try: 110 | bot.send_message( 111 | admin.user.id, msg + link, parse_mode=ParseMode.HTML 112 | ) 113 | 114 | if should_forward: 115 | message.reply_to_message.forward(admin.user.id) 116 | 117 | if ( 118 | len(message.text.split()) > 1 119 | ): # If user is giving a reason, send his message too 120 | message.forward(admin.user.id) 121 | 122 | except Unauthorized: 123 | pass 124 | except BadRequest as excp: # TODO: cleanup exceptions 125 | LOGGER.exception("Exception while reporting user") 126 | return msg 127 | 128 | return "" 129 | 130 | 131 | def __migrate__(old_chat_id, new_chat_id): 132 | sql.migrate_chat(old_chat_id, new_chat_id) 133 | 134 | 135 | def __chat_settings__(chat_id, user_id): 136 | return "Questa chat è configurata per inviare report utente agli amministratori, tramite /report e @admin: `{}`".format( 137 | sql.chat_should_report(chat_id) 138 | ) 139 | 140 | 141 | def __user_settings__(user_id): 142 | return "Riceverei i report per queste chat: `{}`.\nAttiva/disattiva i reports in PM.".format( 143 | sql.user_should_report(user_id) 144 | ) 145 | 146 | 147 | __mod_name__ = "Reporting" 148 | 149 | __help__ = """ 150 | - /report : rispondi a un messaggio per segnalarlo agli amministratori. 151 | - @admin: rispondi a un messaggio per segnalarlo agli amministratori. 152 | NOTA: nessuno di questi verrà attivato se utilizzato dagli amministratori 153 | 154 | *Admin only:* 155 | - /reports : modifica l'impostazione del report o visualizza lo stato corrente. 156 | - Se fatto in pm, alterna il tuo stato. 157 | - Se in chat, attiva lo stato della chat. 158 | """ 159 | 160 | REPORT_HANDLER = CommandHandler("report", report, filters=Filters.group) 161 | SETTING_HANDLER = CommandHandler("reports", report_setting, pass_args=True) 162 | ADMIN_REPORT_HANDLER = RegexHandler("(?i)@admin(s)?", report) 163 | 164 | dispatcher.add_handler(REPORT_HANDLER, REPORT_GROUP) 165 | dispatcher.add_handler(ADMIN_REPORT_HANDLER, REPORT_GROUP) 166 | dispatcher.add_handler(SETTING_HANDLER) 167 | -------------------------------------------------------------------------------- /tg_bot/modules/rules.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from telegram import Message, Update, Bot, User 4 | from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton 5 | from telegram.error import BadRequest 6 | from telegram.ext import CommandHandler, run_async, Filters 7 | from telegram.utils.helpers import escape_markdown 8 | 9 | import tg_bot.modules.sql.rules_sql as sql 10 | from tg_bot import dispatcher 11 | from tg_bot.modules.helper_funcs.chat_status import user_admin 12 | from tg_bot.modules.helper_funcs.string_handling import markdown_parser 13 | 14 | 15 | @run_async 16 | def get_rules(bot: Bot, update: Update): 17 | chat_id = update.effective_chat.id 18 | send_rules(update, chat_id) 19 | 20 | 21 | # Do not async - not from a handler 22 | def send_rules(update, chat_id, from_pm=False): 23 | bot = dispatcher.bot 24 | user = update.effective_user # type: Optional[User] 25 | try: 26 | chat = bot.get_chat(chat_id) 27 | except BadRequest as excp: 28 | if excp.message == "Chat not found" and from_pm: 29 | bot.send_message( 30 | user.id, 31 | "Lo shortcut delle regole per questa chat non è stato impostato correttamente. Usa @admin " 32 | "per contattare gli admin.", 33 | ) 34 | return 35 | else: 36 | raise 37 | 38 | rules = sql.get_rules(chat_id) 39 | text = "Ecco il regolamento di *{}*:\n\n{}".format( 40 | escape_markdown(chat.title), rules 41 | ) 42 | 43 | if from_pm and rules: 44 | bot.send_message( 45 | user.id, 46 | text, 47 | parse_mode=ParseMode.MARKDOWN, 48 | preview=False, 49 | disable_web_page_preview=True, 50 | reply_markup=InlineKeyboardMarkup( 51 | [ 52 | [ 53 | InlineKeyboardButton( 54 | text="Leggi il CoC", 55 | url="https://telegra.ph/CoC-di-PythonItalia-07-09", 56 | ) 57 | ] 58 | ] 59 | ), 60 | ) 61 | elif from_pm: 62 | bot.send_message( 63 | user.id, 64 | "Gli admin non hanno ancora impostato le regole del gruppo. " 65 | "Per favore attieniti alle regole di rispetto reciproco che vigono in ogni chat online.", 66 | ) 67 | elif rules: 68 | update.effective_message.reply_text( 69 | "Contattami per avere la lista delle regole.", 70 | reply_markup=InlineKeyboardMarkup( 71 | [ 72 | [ 73 | InlineKeyboardButton( 74 | text="Rules", 75 | url="t.me/{}?start={}".format(bot.username, chat_id), 76 | ) 77 | ] 78 | ] 79 | ), 80 | ) 81 | else: 82 | update.effective_message.reply_text( 83 | "Il gruppo non ha ancora nessuna regola impostata. " 84 | "Per favore attieniti alle regole di rispetto reciproco che vigono in ogni chat online." 85 | ) 86 | 87 | 88 | @run_async 89 | @user_admin 90 | def set_rules(bot: Bot, update: Update): 91 | chat_id = update.effective_chat.id 92 | msg = update.effective_message # type: Optional[Message] 93 | raw_text = msg.text 94 | args = raw_text.split(None, 1) # use python's maxsplit to separate cmd and args 95 | if len(args) == 2: 96 | txt = args[1] 97 | offset = len(txt) - len(raw_text) # set correct offset relative to command 98 | markdown_rules = markdown_parser( 99 | txt, entities=msg.parse_entities(), offset=offset 100 | ) 101 | 102 | sql.set_rules(chat_id, markdown_rules) 103 | update.effective_message.reply_text("Regola aggiunta con successo.") 104 | 105 | 106 | @run_async 107 | @user_admin 108 | def clear_rules(bot: Bot, update: Update): 109 | chat_id = update.effective_chat.id 110 | sql.set_rules(chat_id, "") 111 | update.effective_message.reply_text("Regole cancellate con successo!") 112 | 113 | 114 | def __stats__(): 115 | return "{} ha regole impostate.".format(sql.num_chats()) 116 | 117 | 118 | def __import_data__(chat_id, data): 119 | # set chat rules 120 | rules = data.get("info", {}).get("rules", "") 121 | sql.set_rules(chat_id, rules) 122 | 123 | 124 | def __migrate__(old_chat_id, new_chat_id): 125 | sql.migrate_chat(old_chat_id, new_chat_id) 126 | 127 | 128 | def __chat_settings__(chat_id, user_id): 129 | return "Questa chat ha le seguenti regole: `{}`".format( 130 | bool(sql.get_rules(chat_id)) 131 | ) 132 | 133 | 134 | __help__ = """ 135 | - /rules: ottieni le regole per questa chat. 136 | 137 | *Admin only:* 138 | - /setrules : imposta una regola per la chat. 139 | - /clearrules: cancella le regole della chat. 140 | """ 141 | 142 | __mod_name__ = "Rules" 143 | 144 | GET_RULES_HANDLER = CommandHandler("rules", get_rules, filters=Filters.group) 145 | SET_RULES_HANDLER = CommandHandler("setrules", set_rules, filters=Filters.group) 146 | RESET_RULES_HANDLER = CommandHandler("clearrules", clear_rules, filters=Filters.group) 147 | 148 | dispatcher.add_handler(GET_RULES_HANDLER) 149 | dispatcher.add_handler(SET_RULES_HANDLER) 150 | dispatcher.add_handler(RESET_RULES_HANDLER) 151 | -------------------------------------------------------------------------------- /tg_bot/modules/sed.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sre_constants 3 | 4 | import telegram 5 | from telegram import Update, Bot 6 | from telegram.ext import run_async 7 | 8 | from tg_bot import dispatcher, LOGGER 9 | from tg_bot.modules.disable import DisableAbleRegexHandler 10 | 11 | DELIMITERS = ("/", ":", "|", "_") 12 | 13 | 14 | def separate_sed(sed_string): 15 | if ( 16 | len(sed_string) >= 3 17 | and sed_string[1] in DELIMITERS 18 | and sed_string.count(sed_string[1]) >= 2 19 | ): 20 | delim = sed_string[1] 21 | start = counter = 2 22 | while counter < len(sed_string): 23 | if sed_string[counter] == "\\": 24 | counter += 1 25 | 26 | elif sed_string[counter] == delim: 27 | replace = sed_string[start:counter] 28 | counter += 1 29 | start = counter 30 | break 31 | 32 | counter += 1 33 | 34 | else: 35 | return None 36 | 37 | while counter < len(sed_string): 38 | if ( 39 | sed_string[counter] == "\\" 40 | and counter + 1 < len(sed_string) 41 | and sed_string[counter + 1] == delim 42 | ): 43 | sed_string = sed_string[:counter] + sed_string[counter + 1:] 44 | 45 | elif sed_string[counter] == delim: 46 | replace_with = sed_string[start:counter] 47 | counter += 1 48 | break 49 | 50 | counter += 1 51 | else: 52 | return replace, sed_string[start:], "" 53 | 54 | flags = "" 55 | if counter < len(sed_string): 56 | flags = sed_string[counter:] 57 | return replace, replace_with, flags.lower() 58 | 59 | 60 | @run_async 61 | def sed(bot: Bot, update: Update): 62 | sed_result = separate_sed(update.effective_message.text) 63 | if sed_result and update.effective_message.reply_to_message: 64 | if update.effective_message.reply_to_message.text: 65 | to_fix = update.effective_message.reply_to_message.text 66 | elif update.effective_message.reply_to_message.caption: 67 | to_fix = update.effective_message.reply_to_message.caption 68 | else: 69 | return 70 | 71 | repl, repl_with, flags = sed_result 72 | 73 | if not repl: 74 | update.effective_message.reply_to_message.reply_text( 75 | "Stai cercando di sostituire... " "il nulla con qualcosa?" 76 | ) 77 | return 78 | 79 | try: 80 | check = re.match(repl, to_fix, flags=re.IGNORECASE) 81 | 82 | if check and check.group(0).lower() == to_fix.lower(): 83 | update.effective_message.reply_to_message.reply_text( 84 | "Hey ragazzi, {} sta cercando di farmi " 85 | "dire cose che non voglio " 86 | "dire!".format(update.effective_user.first_name) 87 | ) 88 | return 89 | 90 | if "i" in flags and "g" in flags: 91 | text = re.sub(repl, repl_with, to_fix, flags=re.I).strip() 92 | elif "i" in flags: 93 | text = re.sub(repl, repl_with, to_fix, count=1, flags=re.I).strip() 94 | elif "g" in flags: 95 | text = re.sub(repl, repl_with, to_fix).strip() 96 | else: 97 | text = re.sub(repl, repl_with, to_fix, count=1).strip() 98 | except sre_constants.error: 99 | LOGGER.warning(update.effective_message.text) 100 | LOGGER.exception("SRE constant error") 101 | update.effective_message.reply_text("Sed invalido.") 102 | return 103 | 104 | # empty string errors -_- 105 | if len(text) >= telegram.MAX_MESSAGE_LENGTH: 106 | update.effective_message.reply_text( 107 | "Il risultato del comando sed era troppo lungo per \ 108 | telegram!" 109 | ) 110 | elif text: 111 | update.effective_message.reply_to_message.reply_text(text) 112 | 113 | 114 | __help__ = """ 115 | - s//(/): Reply to a message with this to perform a sed operation on that message, replacing all \ 116 | occurrences of 'text1' with 'text2'. Flags are optional, and currently include 'i' for ignore case, 'g' for global, \ 117 | or nothing. Delimiters include `/`, `_`, `|`, and `:`. Text grouping is supported. The resulting message cannot be \ 118 | larger than {}. 119 | 120 | *Reminder:* Sed uses some special characters to make matching easier, such as these: `+*.?\\` 121 | If you want to use these characters, make sure you escape them! 122 | eg: \\?. 123 | """.format( 124 | telegram.MAX_MESSAGE_LENGTH 125 | ) 126 | 127 | __mod_name__ = "Sed/Regex" 128 | 129 | SED_HANDLER = DisableAbleRegexHandler( 130 | r"s([{}]).*?\1.*".format("".join(DELIMITERS)), sed, friendly="sed" 131 | ) 132 | 133 | dispatcher.add_handler(SED_HANDLER) 134 | -------------------------------------------------------------------------------- /tg_bot/modules/spam.py: -------------------------------------------------------------------------------- 1 | from telegram import Update, Bot 2 | from telegram.ext import MessageHandler, Filters, CommandHandler 3 | from telegram.ext.dispatcher import run_async 4 | 5 | from tg_bot import dispatcher, LOGGER 6 | from tg_bot.modules.helper_funcs.chat_status import bot_can_delete 7 | 8 | FORBIDDEN_ENTITY_TYPES = ["url", "text_link", "email", "phone_number"] 9 | 10 | 11 | @run_async 12 | @bot_can_delete 13 | def spam_filter(bot: Bot, update: Update): 14 | """ 15 | Filter messages with spam into message's text 16 | """ 17 | msg = update.effective_message.text 18 | message_entities = update.effective_message.parse_entities() 19 | message_caption_entities = update.effective_message.parse_caption_entities() 20 | 21 | found = False 22 | for descriptor, entity in message_entities.items(): 23 | LOGGER.debug(f"Found message entity: {descriptor['type']} {entity}") 24 | if descriptor["type"] in FORBIDDEN_ENTITY_TYPES: 25 | found = True 26 | 27 | if found: 28 | spam_action(update) 29 | 30 | 31 | def spam_action(update: Update): 32 | user = update.effective_user.username 33 | update.effective_message.reply_text(f"Non spammare! @{user}", quote=False) 34 | update.effective_message.delete() 35 | 36 | 37 | def white_spam_add(bot: Bot, update: Update): 38 | if update.effective_message.reply_to_message: 39 | pass 40 | else: 41 | update.effective_message.reply_text("Cosa vuoi aggiungere in whitelist?") 42 | 43 | 44 | SPAM_HANDLER = MessageHandler(Filters.all & Filters.group, spam_filter) 45 | WHITE_SPAM_HANDLER = CommandHandler("whitespam", white_spam_add) 46 | dispatcher.add_handler(SPAM_HANDLER) 47 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/__init__.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker, scoped_session 4 | 5 | from tg_bot import DB_URI 6 | 7 | 8 | def start() -> scoped_session: 9 | engine = create_engine(DB_URI, client_encoding="utf8", pool_pre_ping=True, pool_recycle=3600) 10 | BASE.metadata.bind = engine 11 | BASE.metadata.create_all(engine) 12 | return scoped_session(sessionmaker(bind=engine, autoflush=False)) 13 | 14 | 15 | BASE = declarative_base() 16 | SESSION = start() 17 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/afk_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Boolean, Integer 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | 8 | class AFK(BASE): 9 | __tablename__ = "afk_users" 10 | 11 | user_id = Column(Integer, primary_key=True) 12 | is_afk = Column(Boolean) 13 | reason = Column(UnicodeText) 14 | 15 | def __init__(self, user_id, reason="", is_afk=True): 16 | self.user_id = user_id 17 | self.reason = reason 18 | self.is_afk = is_afk 19 | 20 | def __repr__(self): 21 | return "afk_status for {}".format(self.user_id) 22 | 23 | 24 | AFK.__table__.create(checkfirst=True) 25 | INSERTION_LOCK = threading.RLock() 26 | 27 | AFK_USERS = {} 28 | 29 | 30 | def is_afk(user_id): 31 | return user_id in AFK_USERS 32 | 33 | 34 | def check_afk_status(user_id): 35 | if user_id in AFK_USERS: 36 | return True, AFK_USERS[user_id] 37 | return False, "" 38 | 39 | 40 | def set_afk(user_id, reason=""): 41 | with INSERTION_LOCK: 42 | curr = SESSION.query(AFK).get(user_id) 43 | if not curr: 44 | curr = AFK(user_id, reason, True) 45 | else: 46 | curr.is_afk = True 47 | curr.reason = reason 48 | 49 | AFK_USERS[user_id] = reason 50 | 51 | SESSION.add(curr) 52 | SESSION.commit() 53 | 54 | 55 | def rm_afk(user_id): 56 | with INSERTION_LOCK: 57 | curr = SESSION.query(AFK).get(user_id) 58 | if curr: 59 | if user_id in AFK_USERS: # sanity check 60 | del AFK_USERS[user_id] 61 | 62 | SESSION.delete(curr) 63 | SESSION.commit() 64 | return True 65 | 66 | SESSION.close() 67 | return False 68 | 69 | 70 | def __load_afk_users(): 71 | global AFK_USERS 72 | try: 73 | all_afk = SESSION.query(AFK).all() 74 | AFK_USERS = {user.user_id: user.reason for user in all_afk if user.is_afk} 75 | finally: 76 | SESSION.close() 77 | 78 | 79 | __load_afk_users() 80 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/antiflood_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, String 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | DEF_COUNT = 0 8 | DEF_LIMIT = 0 9 | DEF_OBJ = (None, DEF_COUNT, DEF_LIMIT) 10 | 11 | 12 | class FloodControl(BASE): 13 | __tablename__ = "antiflood" 14 | chat_id = Column(String(14), primary_key=True) 15 | user_id = Column(Integer) 16 | count = Column(Integer, default=DEF_COUNT) 17 | limit = Column(Integer, default=DEF_LIMIT) 18 | 19 | def __init__(self, chat_id): 20 | self.chat_id = str(chat_id) # ensure string 21 | 22 | def __repr__(self): 23 | return "" % self.chat_id 24 | 25 | 26 | FloodControl.__table__.create(checkfirst=True) 27 | 28 | INSERTION_LOCK = threading.RLock() 29 | 30 | CHAT_FLOOD = {} 31 | 32 | 33 | def set_flood(chat_id, amount): 34 | with INSERTION_LOCK: 35 | flood = SESSION.query(FloodControl).get(str(chat_id)) 36 | if not flood: 37 | flood = FloodControl(str(chat_id)) 38 | 39 | flood.user_id = None 40 | flood.limit = amount 41 | 42 | CHAT_FLOOD[str(chat_id)] = (None, DEF_COUNT, amount) 43 | 44 | SESSION.add(flood) 45 | SESSION.commit() 46 | 47 | 48 | def update_flood(chat_id: str, user_id) -> bool: 49 | if str(chat_id) in CHAT_FLOOD: 50 | curr_user_id, count, limit = CHAT_FLOOD.get(str(chat_id), DEF_OBJ) 51 | 52 | if limit == 0: # no antiflood 53 | return False 54 | 55 | if user_id != curr_user_id or user_id is None: # other user 56 | CHAT_FLOOD[str(chat_id)] = (user_id, DEF_COUNT + 1, limit) 57 | return False 58 | 59 | count += 1 60 | if count > limit: # too many msgs, kick 61 | CHAT_FLOOD[str(chat_id)] = (None, DEF_COUNT, limit) 62 | return True 63 | 64 | # default -> update 65 | CHAT_FLOOD[str(chat_id)] = (user_id, count, limit) 66 | return False 67 | 68 | 69 | def get_flood_limit(chat_id): 70 | return CHAT_FLOOD.get(str(chat_id), DEF_OBJ)[2] 71 | 72 | 73 | def migrate_chat(old_chat_id, new_chat_id): 74 | with INSERTION_LOCK: 75 | flood = SESSION.query(FloodControl).get(str(old_chat_id)) 76 | if flood: 77 | CHAT_FLOOD[str(new_chat_id)] = CHAT_FLOOD.get(str(old_chat_id), DEF_OBJ) 78 | flood.chat_id = str(new_chat_id) 79 | SESSION.commit() 80 | 81 | SESSION.close() 82 | 83 | 84 | def __load_flood_settings(): 85 | global CHAT_FLOOD 86 | try: 87 | all_chats = SESSION.query(FloodControl).all() 88 | CHAT_FLOOD = {chat.chat_id: (None, DEF_COUNT, chat.limit) for chat in all_chats} 89 | finally: 90 | SESSION.close() 91 | 92 | 93 | __load_flood_settings() 94 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/blacklist_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import func, distinct, Column, String, UnicodeText 4 | 5 | from tg_bot.modules.sql import SESSION, BASE 6 | 7 | 8 | class BlackListFilters(BASE): 9 | __tablename__ = "blacklist" 10 | chat_id = Column(String(14), primary_key=True) 11 | trigger = Column(UnicodeText, primary_key=True, nullable=False) 12 | 13 | def __init__(self, chat_id, trigger): 14 | self.chat_id = str(chat_id) # ensure string 15 | self.trigger = trigger 16 | 17 | def __repr__(self): 18 | return "" % (self.trigger, self.chat_id) 19 | 20 | def __eq__(self, other): 21 | return bool( 22 | isinstance(other, BlackListFilters) 23 | and self.chat_id == other.chat_id 24 | and self.trigger == other.trigger 25 | ) 26 | 27 | 28 | BlackListFilters.__table__.create(checkfirst=True) 29 | 30 | BLACKLIST_FILTER_INSERTION_LOCK = threading.RLock() 31 | 32 | CHAT_BLACKLISTS = {} 33 | 34 | 35 | def add_to_blacklist(chat_id, trigger): 36 | with BLACKLIST_FILTER_INSERTION_LOCK: 37 | blacklist_filt = BlackListFilters(str(chat_id), trigger) 38 | 39 | SESSION.merge(blacklist_filt) # merge to avoid duplicate key issues 40 | SESSION.commit() 41 | CHAT_BLACKLISTS.setdefault(str(chat_id), set()).add(trigger) 42 | 43 | 44 | def rm_from_blacklist(chat_id, trigger): 45 | with BLACKLIST_FILTER_INSERTION_LOCK: 46 | blacklist_filt = SESSION.query(BlackListFilters).get((str(chat_id), trigger)) 47 | if blacklist_filt: 48 | if trigger in CHAT_BLACKLISTS.get(str(chat_id), set()): # sanity check 49 | CHAT_BLACKLISTS.get(str(chat_id), set()).remove(trigger) 50 | 51 | SESSION.delete(blacklist_filt) 52 | SESSION.commit() 53 | return True 54 | 55 | SESSION.close() 56 | return False 57 | 58 | 59 | def get_chat_blacklist(chat_id): 60 | return CHAT_BLACKLISTS.get(str(chat_id), set()) 61 | 62 | 63 | def num_blacklist_filters(): 64 | try: 65 | return SESSION.query(BlackListFilters).count() 66 | finally: 67 | SESSION.close() 68 | 69 | 70 | def num_blacklist_chat_filters(chat_id): 71 | try: 72 | return ( 73 | SESSION.query(BlackListFilters.chat_id) 74 | .filter(BlackListFilters.chat_id == str(chat_id)) 75 | .count() 76 | ) 77 | finally: 78 | SESSION.close() 79 | 80 | 81 | def num_blacklist_filter_chats(): 82 | try: 83 | return SESSION.query(func.count(distinct(BlackListFilters.chat_id))).scalar() 84 | finally: 85 | SESSION.close() 86 | 87 | 88 | def __load_chat_blacklists(): 89 | global CHAT_BLACKLISTS 90 | try: 91 | chats = SESSION.query(BlackListFilters.chat_id).distinct().all() 92 | for (chat_id,) in chats: # remove tuple by ( ,) 93 | CHAT_BLACKLISTS[chat_id] = [] 94 | 95 | all_filters = SESSION.query(BlackListFilters).all() 96 | for x in all_filters: 97 | CHAT_BLACKLISTS[x.chat_id] += [x.trigger] 98 | 99 | CHAT_BLACKLISTS = {x: set(y) for x, y in CHAT_BLACKLISTS.items()} 100 | 101 | finally: 102 | SESSION.close() 103 | 104 | 105 | def migrate_chat(old_chat_id, new_chat_id): 106 | with BLACKLIST_FILTER_INSERTION_LOCK: 107 | chat_filters = ( 108 | SESSION.query(BlackListFilters) 109 | .filter(BlackListFilters.chat_id == str(old_chat_id)) 110 | .all() 111 | ) 112 | for filt in chat_filters: 113 | filt.chat_id = str(new_chat_id) 114 | SESSION.commit() 115 | 116 | 117 | __load_chat_blacklists() 118 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/cust_filters_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, Boolean, Integer, distinct, func 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | 8 | class CustomFilters(BASE): 9 | __tablename__ = "cust_filters" 10 | chat_id = Column(String(14), primary_key=True) 11 | keyword = Column(UnicodeText, primary_key=True, nullable=False) 12 | reply = Column(UnicodeText, nullable=False) 13 | is_sticker = Column(Boolean, nullable=False, default=False) 14 | is_document = Column(Boolean, nullable=False, default=False) 15 | is_image = Column(Boolean, nullable=False, default=False) 16 | is_audio = Column(Boolean, nullable=False, default=False) 17 | is_voice = Column(Boolean, nullable=False, default=False) 18 | is_video = Column(Boolean, nullable=False, default=False) 19 | 20 | has_buttons = Column(Boolean, nullable=False, default=False) 21 | # NOTE: Here for legacy purposes, to ensure older filters don't mess up. 22 | has_markdown = Column(Boolean, nullable=False, default=False) 23 | 24 | def __init__( 25 | self, 26 | chat_id, 27 | keyword, 28 | reply, 29 | is_sticker=False, 30 | is_document=False, 31 | is_image=False, 32 | is_audio=False, 33 | is_voice=False, 34 | is_video=False, 35 | has_buttons=False, 36 | ): 37 | self.chat_id = str(chat_id) # ensure string 38 | self.keyword = keyword 39 | self.reply = reply 40 | self.is_sticker = is_sticker 41 | self.is_document = is_document 42 | self.is_image = is_image 43 | self.is_audio = is_audio 44 | self.is_voice = is_voice 45 | self.is_video = is_video 46 | self.has_buttons = has_buttons 47 | self.has_markdown = True 48 | 49 | def __repr__(self): 50 | return "" % self.chat_id 51 | 52 | def __eq__(self, other): 53 | return bool( 54 | isinstance(other, CustomFilters) 55 | and self.chat_id == other.chat_id 56 | and self.keyword == other.keyword 57 | ) 58 | 59 | 60 | class Buttons(BASE): 61 | __tablename__ = "cust_filter_urls" 62 | id = Column(Integer, primary_key=True, autoincrement=True) 63 | chat_id = Column(String(14), primary_key=True) 64 | keyword = Column(UnicodeText, primary_key=True) 65 | name = Column(UnicodeText, nullable=False) 66 | url = Column(UnicodeText, nullable=False) 67 | same_line = Column(Boolean, default=False) 68 | 69 | def __init__(self, chat_id, keyword, name, url, same_line=False): 70 | self.chat_id = str(chat_id) 71 | self.keyword = keyword 72 | self.name = name 73 | self.url = url 74 | self.same_line = same_line 75 | 76 | 77 | CustomFilters.__table__.create(checkfirst=True) 78 | Buttons.__table__.create(checkfirst=True) 79 | 80 | CUST_FILT_LOCK = threading.RLock() 81 | BUTTON_LOCK = threading.RLock() 82 | CHAT_FILTERS = {} 83 | 84 | 85 | def get_all_filters(): 86 | try: 87 | return SESSION.query(CustomFilters).all() 88 | finally: 89 | SESSION.close() 90 | 91 | 92 | def add_filter( 93 | chat_id, 94 | keyword, 95 | reply, 96 | is_sticker=False, 97 | is_document=False, 98 | is_image=False, 99 | is_audio=False, 100 | is_voice=False, 101 | is_video=False, 102 | buttons=None, 103 | ): 104 | global CHAT_FILTERS 105 | 106 | if buttons is None: 107 | buttons = [] 108 | 109 | with CUST_FILT_LOCK: 110 | prev = SESSION.query(CustomFilters).get((str(chat_id), keyword)) 111 | if prev: 112 | with BUTTON_LOCK: 113 | prev_buttons = ( 114 | SESSION.query(Buttons) 115 | .filter(Buttons.chat_id == str(chat_id), Buttons.keyword == keyword) 116 | .all() 117 | ) 118 | for btn in prev_buttons: 119 | SESSION.delete(btn) 120 | SESSION.delete(prev) 121 | 122 | filt = CustomFilters( 123 | str(chat_id), 124 | keyword, 125 | reply, 126 | is_sticker, 127 | is_document, 128 | is_image, 129 | is_audio, 130 | is_voice, 131 | is_video, 132 | bool(buttons), 133 | ) 134 | 135 | if keyword not in CHAT_FILTERS.get(str(chat_id), []): 136 | CHAT_FILTERS[str(chat_id)] = sorted( 137 | CHAT_FILTERS.get(str(chat_id), []) + [keyword], 138 | key=lambda x: (-len(x), x), 139 | ) 140 | 141 | SESSION.add(filt) 142 | SESSION.commit() 143 | 144 | for b_name, url, same_line in buttons: 145 | add_note_button_to_db(chat_id, keyword, b_name, url, same_line) 146 | 147 | 148 | def remove_filter(chat_id, keyword): 149 | global CHAT_FILTERS 150 | with CUST_FILT_LOCK: 151 | filt = SESSION.query(CustomFilters).get((str(chat_id), keyword)) 152 | if filt: 153 | if keyword in CHAT_FILTERS.get(str(chat_id), []): # Sanity check 154 | CHAT_FILTERS.get(str(chat_id), []).remove(keyword) 155 | 156 | with BUTTON_LOCK: 157 | prev_buttons = ( 158 | SESSION.query(Buttons) 159 | .filter(Buttons.chat_id == str(chat_id), Buttons.keyword == keyword) 160 | .all() 161 | ) 162 | for btn in prev_buttons: 163 | SESSION.delete(btn) 164 | 165 | SESSION.delete(filt) 166 | SESSION.commit() 167 | return True 168 | 169 | SESSION.close() 170 | return False 171 | 172 | 173 | def get_chat_triggers(chat_id): 174 | return CHAT_FILTERS.get(str(chat_id), set()) 175 | 176 | 177 | def get_chat_filters(chat_id): 178 | try: 179 | return ( 180 | SESSION.query(CustomFilters) 181 | .filter(CustomFilters.chat_id == str(chat_id)) 182 | .order_by(func.length(CustomFilters.keyword).desc()) 183 | .order_by(CustomFilters.keyword.asc()) 184 | .all() 185 | ) 186 | finally: 187 | SESSION.close() 188 | 189 | 190 | def get_filter(chat_id, keyword): 191 | try: 192 | return SESSION.query(CustomFilters).get((str(chat_id), keyword)) 193 | finally: 194 | SESSION.close() 195 | 196 | 197 | def add_note_button_to_db(chat_id, keyword, b_name, url, same_line): 198 | with BUTTON_LOCK: 199 | button = Buttons(chat_id, keyword, b_name, url, same_line) 200 | SESSION.add(button) 201 | SESSION.commit() 202 | 203 | 204 | def get_buttons(chat_id, keyword): 205 | try: 206 | return ( 207 | SESSION.query(Buttons) 208 | .filter(Buttons.chat_id == str(chat_id), Buttons.keyword == keyword) 209 | .order_by(Buttons.id) 210 | .all() 211 | ) 212 | finally: 213 | SESSION.close() 214 | 215 | 216 | def num_filters(): 217 | try: 218 | return SESSION.query(CustomFilters).count() 219 | finally: 220 | SESSION.close() 221 | 222 | 223 | def num_chats(): 224 | try: 225 | return SESSION.query(func.count(distinct(CustomFilters.chat_id))).scalar() 226 | finally: 227 | SESSION.close() 228 | 229 | 230 | def __load_chat_filters(): 231 | global CHAT_FILTERS 232 | try: 233 | chats = SESSION.query(CustomFilters.chat_id).distinct().all() 234 | for (chat_id,) in chats: # remove tuple by ( ,) 235 | CHAT_FILTERS[chat_id] = [] 236 | 237 | all_filters = SESSION.query(CustomFilters).all() 238 | for x in all_filters: 239 | CHAT_FILTERS[x.chat_id] += [x.keyword] 240 | 241 | CHAT_FILTERS = { 242 | x: sorted(set(y), key=lambda i: (-len(i), i)) 243 | for x, y in CHAT_FILTERS.items() 244 | } 245 | 246 | finally: 247 | SESSION.close() 248 | 249 | 250 | def migrate_chat(old_chat_id, new_chat_id): 251 | with CUST_FILT_LOCK: 252 | chat_filters = ( 253 | SESSION.query(CustomFilters) 254 | .filter(CustomFilters.chat_id == str(old_chat_id)) 255 | .all() 256 | ) 257 | for filt in chat_filters: 258 | filt.chat_id = str(new_chat_id) 259 | SESSION.commit() 260 | CHAT_FILTERS[str(new_chat_id)] = CHAT_FILTERS[str(old_chat_id)] 261 | del CHAT_FILTERS[str(old_chat_id)] 262 | 263 | with BUTTON_LOCK: 264 | chat_buttons = ( 265 | SESSION.query(Buttons).filter(Buttons.chat_id == str(old_chat_id)).all() 266 | ) 267 | for btn in chat_buttons: 268 | btn.chat_id = str(new_chat_id) 269 | SESSION.commit() 270 | 271 | 272 | __load_chat_filters() 273 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/disable_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, func, distinct 4 | 5 | from tg_bot.modules.sql import SESSION, BASE 6 | 7 | 8 | class Disable(BASE): 9 | __tablename__ = "disabled_commands" 10 | chat_id = Column(String(14), primary_key=True) 11 | command = Column(UnicodeText, primary_key=True) 12 | 13 | def __init__(self, chat_id, command): 14 | self.chat_id = chat_id 15 | self.command = command 16 | 17 | def __repr__(self): 18 | return "Disabled cmd {} in {}".format(self.command, self.chat_id) 19 | 20 | 21 | Disable.__table__.create(checkfirst=True) 22 | DISABLE_INSERTION_LOCK = threading.RLock() 23 | 24 | DISABLED = {} 25 | 26 | 27 | def disable_command(chat_id, disable): 28 | with DISABLE_INSERTION_LOCK: 29 | disabled = SESSION.query(Disable).get((str(chat_id), disable)) 30 | 31 | if not disabled: 32 | DISABLED.setdefault(str(chat_id), set()).add(disable) 33 | 34 | disabled = Disable(str(chat_id), disable) 35 | SESSION.add(disabled) 36 | SESSION.commit() 37 | return True 38 | 39 | SESSION.close() 40 | return False 41 | 42 | 43 | def enable_command(chat_id, enable): 44 | with DISABLE_INSERTION_LOCK: 45 | disabled = SESSION.query(Disable).get((str(chat_id), enable)) 46 | 47 | if disabled: 48 | if enable in DISABLED.get(str(chat_id)): # sanity check 49 | DISABLED.setdefault(str(chat_id), set()).remove(enable) 50 | 51 | SESSION.delete(disabled) 52 | SESSION.commit() 53 | return True 54 | 55 | SESSION.close() 56 | return False 57 | 58 | 59 | def is_command_disabled(chat_id, cmd): 60 | return cmd in DISABLED.get(str(chat_id), set()) 61 | 62 | 63 | def get_all_disabled(chat_id): 64 | return DISABLED.get(str(chat_id), set()) 65 | 66 | 67 | def num_chats(): 68 | try: 69 | return SESSION.query(func.count(distinct(Disable.chat_id))).scalar() 70 | finally: 71 | SESSION.close() 72 | 73 | 74 | def num_disabled(): 75 | try: 76 | return SESSION.query(Disable).count() 77 | finally: 78 | SESSION.close() 79 | 80 | 81 | def migrate_chat(old_chat_id, new_chat_id): 82 | with DISABLE_INSERTION_LOCK: 83 | chats = SESSION.query(Disable).filter(Disable.chat_id == str(old_chat_id)).all() 84 | for chat in chats: 85 | chat.chat_id = str(new_chat_id) 86 | SESSION.add(chat) 87 | 88 | if str(old_chat_id) in DISABLED: 89 | DISABLED[str(new_chat_id)] = DISABLED.get(str(old_chat_id), set()) 90 | 91 | SESSION.commit() 92 | 93 | 94 | def __load_disabled_commands(): 95 | global DISABLED 96 | try: 97 | all_chats = SESSION.query(Disable).all() 98 | for chat in all_chats: 99 | DISABLED.setdefault(chat.chat_id, set()).add(chat.command) 100 | 101 | finally: 102 | SESSION.close() 103 | 104 | 105 | __load_disabled_commands() 106 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/global_bans_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Integer, String, Boolean 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | 8 | class GloballyBannedUsers(BASE): 9 | __tablename__ = "gbans" 10 | user_id = Column(Integer, primary_key=True) 11 | name = Column(UnicodeText, nullable=False) 12 | reason = Column(UnicodeText) 13 | 14 | def __init__(self, user_id, name, reason=None): 15 | self.user_id = user_id 16 | self.name = name 17 | self.reason = reason 18 | 19 | def __repr__(self): 20 | return "".format(self.name, self.user_id) 21 | 22 | def to_dict(self): 23 | return {"user_id": self.user_id, "name": self.name, "reason": self.reason} 24 | 25 | 26 | class GbanSettings(BASE): 27 | __tablename__ = "gban_settings" 28 | chat_id = Column(String(14), primary_key=True) 29 | setting = Column(Boolean, default=True, nullable=False) 30 | 31 | def __init__(self, chat_id, enabled): 32 | self.chat_id = str(chat_id) 33 | self.setting = enabled 34 | 35 | def __repr__(self): 36 | return "".format(self.chat_id, self.setting) 37 | 38 | 39 | GloballyBannedUsers.__table__.create(checkfirst=True) 40 | GbanSettings.__table__.create(checkfirst=True) 41 | 42 | GBANNED_USERS_LOCK = threading.RLock() 43 | GBAN_SETTING_LOCK = threading.RLock() 44 | GBANNED_LIST = set() 45 | GBANSTAT_LIST = set() 46 | 47 | 48 | def gban_user(user_id, name, reason=None): 49 | with GBANNED_USERS_LOCK: 50 | user = SESSION.query(GloballyBannedUsers).get(user_id) 51 | if not user: 52 | user = GloballyBannedUsers(user_id, name, reason) 53 | else: 54 | user.name = name 55 | user.reason = reason 56 | 57 | SESSION.merge(user) 58 | SESSION.commit() 59 | __load_gbanned_userid_list() 60 | 61 | 62 | def update_gban_reason(user_id, name, reason=None): 63 | with GBANNED_USERS_LOCK: 64 | user = SESSION.query(GloballyBannedUsers).get(user_id) 65 | if not user: 66 | return None 67 | old_reason = user.reason 68 | user.name = name 69 | user.reason = reason 70 | 71 | SESSION.merge(user) 72 | SESSION.commit() 73 | return old_reason 74 | 75 | 76 | def ungban_user(user_id): 77 | with GBANNED_USERS_LOCK: 78 | user = SESSION.query(GloballyBannedUsers).get(user_id) 79 | if user: 80 | SESSION.delete(user) 81 | 82 | SESSION.commit() 83 | __load_gbanned_userid_list() 84 | 85 | 86 | def is_user_gbanned(user_id): 87 | return user_id in GBANNED_LIST 88 | 89 | 90 | def get_gbanned_user(user_id): 91 | try: 92 | return SESSION.query(GloballyBannedUsers).get(user_id) 93 | finally: 94 | SESSION.close() 95 | 96 | 97 | def get_gban_list(): 98 | try: 99 | return [x.to_dict() for x in SESSION.query(GloballyBannedUsers).all()] 100 | finally: 101 | SESSION.close() 102 | 103 | 104 | def enable_gbans(chat_id): 105 | with GBAN_SETTING_LOCK: 106 | chat = SESSION.query(GbanSettings).get(str(chat_id)) 107 | if not chat: 108 | chat = GbanSettings(chat_id, True) 109 | 110 | chat.setting = True 111 | SESSION.add(chat) 112 | SESSION.commit() 113 | if str(chat_id) in GBANSTAT_LIST: 114 | GBANSTAT_LIST.remove(str(chat_id)) 115 | 116 | 117 | def disable_gbans(chat_id): 118 | with GBAN_SETTING_LOCK: 119 | chat = SESSION.query(GbanSettings).get(str(chat_id)) 120 | if not chat: 121 | chat = GbanSettings(chat_id, False) 122 | 123 | chat.setting = False 124 | SESSION.add(chat) 125 | SESSION.commit() 126 | GBANSTAT_LIST.add(str(chat_id)) 127 | 128 | 129 | def does_chat_gban(chat_id): 130 | return str(chat_id) not in GBANSTAT_LIST 131 | 132 | 133 | def num_gbanned_users(): 134 | return len(GBANNED_LIST) 135 | 136 | 137 | def __load_gbanned_userid_list(): 138 | global GBANNED_LIST 139 | try: 140 | GBANNED_LIST = {x.user_id for x in SESSION.query(GloballyBannedUsers).all()} 141 | finally: 142 | SESSION.close() 143 | 144 | 145 | def __load_gban_stat_list(): 146 | global GBANSTAT_LIST 147 | try: 148 | GBANSTAT_LIST = { 149 | x.chat_id for x in SESSION.query(GbanSettings).all() if not x.setting 150 | } 151 | finally: 152 | SESSION.close() 153 | 154 | 155 | def migrate_chat(old_chat_id, new_chat_id): 156 | with GBAN_SETTING_LOCK: 157 | chat = SESSION.query(GbanSettings).get(str(old_chat_id)) 158 | if chat: 159 | chat.chat_id = new_chat_id 160 | SESSION.add(chat) 161 | 162 | SESSION.commit() 163 | 164 | 165 | # Create in memory userid to avoid disk access 166 | __load_gbanned_userid_list() 167 | __load_gban_stat_list() 168 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/locks_sql.py: -------------------------------------------------------------------------------- 1 | # New chat added -> setup permissions 2 | import threading 3 | 4 | from sqlalchemy import Column, String, Boolean 5 | 6 | from tg_bot.modules.sql import SESSION, BASE 7 | 8 | 9 | class Permissions(BASE): 10 | __tablename__ = "permissions" 11 | chat_id = Column(String(14), primary_key=True) 12 | # Booleans are for "is this locked", _NOT_ "is this allowed" 13 | audio = Column(Boolean, default=False) 14 | voice = Column(Boolean, default=False) 15 | contact = Column(Boolean, default=False) 16 | video = Column(Boolean, default=False) 17 | videonote = Column(Boolean, default=False) 18 | document = Column(Boolean, default=False) 19 | photo = Column(Boolean, default=False) 20 | sticker = Column(Boolean, default=False) 21 | gif = Column(Boolean, default=False) 22 | url = Column(Boolean, default=False) 23 | bots = Column(Boolean, default=False) 24 | forward = Column(Boolean, default=False) 25 | game = Column(Boolean, default=False) 26 | location = Column(Boolean, default=False) 27 | 28 | def __init__(self, chat_id): 29 | self.chat_id = str(chat_id) # ensure string 30 | self.audio = False 31 | self.voice = False 32 | self.contact = False 33 | self.video = False 34 | self.videonote = False 35 | self.document = False 36 | self.photo = False 37 | self.sticker = False 38 | self.gif = False 39 | self.url = False 40 | self.bots = False 41 | self.forward = False 42 | self.game = False 43 | self.location = False 44 | 45 | def __repr__(self): 46 | return "" % self.chat_id 47 | 48 | 49 | class Restrictions(BASE): 50 | __tablename__ = "restrictions" 51 | chat_id = Column(String(14), primary_key=True) 52 | # Booleans are for "is this restricted", _NOT_ "is this allowed" 53 | messages = Column(Boolean, default=False) 54 | media = Column(Boolean, default=False) 55 | other = Column(Boolean, default=False) 56 | preview = Column(Boolean, default=False) 57 | 58 | def __init__(self, chat_id): 59 | self.chat_id = str(chat_id) # ensure string 60 | self.messages = False 61 | self.media = False 62 | self.other = False 63 | self.preview = False 64 | 65 | def __repr__(self): 66 | return "" % self.chat_id 67 | 68 | 69 | Permissions.__table__.create(checkfirst=True) 70 | Restrictions.__table__.create(checkfirst=True) 71 | 72 | PERM_LOCK = threading.RLock() 73 | RESTR_LOCK = threading.RLock() 74 | 75 | 76 | def init_permissions(chat_id, reset=False): 77 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 78 | if reset: 79 | SESSION.delete(curr_perm) 80 | SESSION.flush() 81 | perm = Permissions(str(chat_id)) 82 | SESSION.add(perm) 83 | SESSION.commit() 84 | return perm 85 | 86 | 87 | def init_restrictions(chat_id, reset=False): 88 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 89 | if reset: 90 | SESSION.delete(curr_restr) 91 | SESSION.flush() 92 | restr = Restrictions(str(chat_id)) 93 | SESSION.add(restr) 94 | SESSION.commit() 95 | return restr 96 | 97 | 98 | def update_lock(chat_id, lock_type, locked): 99 | with PERM_LOCK: 100 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 101 | if not curr_perm: 102 | curr_perm = init_permissions(chat_id) 103 | 104 | if lock_type == "audio": 105 | curr_perm.audio = locked 106 | elif lock_type == "voice": 107 | curr_perm.voice = locked 108 | elif lock_type == "contact": 109 | curr_perm.contact = locked 110 | elif lock_type == "video": 111 | curr_perm.video = locked 112 | elif lock_type == "videonote": 113 | curr_perm.videonote = locked 114 | elif lock_type == "document": 115 | curr_perm.document = locked 116 | elif lock_type == "photo": 117 | curr_perm.photo = locked 118 | elif lock_type == "sticker": 119 | curr_perm.sticker = locked 120 | elif lock_type == "gif": 121 | curr_perm.gif = locked 122 | elif lock_type == "url": 123 | curr_perm.url = locked 124 | elif lock_type == "bots": 125 | curr_perm.bots = locked 126 | elif lock_type == "forward": 127 | curr_perm.forward = locked 128 | elif lock_type == "game": 129 | curr_perm.game = locked 130 | elif lock_type == "location": 131 | curr_perm.location = locked 132 | 133 | SESSION.add(curr_perm) 134 | SESSION.commit() 135 | 136 | 137 | def update_restriction(chat_id, restr_type, locked): 138 | with RESTR_LOCK: 139 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 140 | if not curr_restr: 141 | curr_restr = init_restrictions(chat_id) 142 | 143 | if restr_type == "messages": 144 | curr_restr.messages = locked 145 | elif restr_type == "media": 146 | curr_restr.media = locked 147 | elif restr_type == "other": 148 | curr_restr.other = locked 149 | elif restr_type == "previews": 150 | curr_restr.preview = locked 151 | elif restr_type == "all": 152 | curr_restr.messages = locked 153 | curr_restr.media = locked 154 | curr_restr.other = locked 155 | curr_restr.preview = locked 156 | SESSION.add(curr_restr) 157 | SESSION.commit() 158 | 159 | 160 | def is_locked(chat_id, lock_type): 161 | with PERM_LOCK: 162 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 163 | SESSION.close() 164 | 165 | if not curr_perm: 166 | return False 167 | 168 | elif lock_type == "sticker": 169 | return curr_perm.sticker 170 | elif lock_type == "photo": 171 | return curr_perm.photo 172 | elif lock_type == "audio": 173 | return curr_perm.audio 174 | elif lock_type == "voice": 175 | return curr_perm.voice 176 | elif lock_type == "contact": 177 | return curr_perm.contact 178 | elif lock_type == "video": 179 | return curr_perm.video 180 | elif lock_type == "videonote": 181 | return curr_perm.videonote 182 | elif lock_type == "document": 183 | return curr_perm.document 184 | elif lock_type == "gif": 185 | return curr_perm.gif 186 | elif lock_type == "url": 187 | return curr_perm.url 188 | elif lock_type == "bots": 189 | return curr_perm.bots 190 | elif lock_type == "forward": 191 | return curr_perm.forward 192 | elif lock_type == "game": 193 | return curr_perm.game 194 | elif lock_type == "location": 195 | return curr_perm.location 196 | 197 | 198 | def is_restr_locked(chat_id, lock_type): 199 | with PERM_LOCK: 200 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 201 | SESSION.close() 202 | 203 | if not curr_restr: 204 | return False 205 | 206 | if lock_type == "messages": 207 | return curr_restr.messages 208 | elif lock_type == "media": 209 | return curr_restr.media 210 | elif lock_type == "other": 211 | return curr_restr.other 212 | elif lock_type == "previews": 213 | return curr_restr.preview 214 | elif lock_type == "all": 215 | return ( 216 | curr_restr.messages 217 | and curr_restr.media 218 | and curr_restr.other 219 | and curr_restr.preview 220 | ) 221 | 222 | 223 | def get_locks(chat_id): 224 | try: 225 | return SESSION.query(Permissions).get(str(chat_id)) 226 | finally: 227 | SESSION.close() 228 | 229 | 230 | def get_restr(chat_id): 231 | try: 232 | return SESSION.query(Restrictions).get(str(chat_id)) 233 | finally: 234 | SESSION.close() 235 | 236 | 237 | def migrate_chat(old_chat_id, new_chat_id): 238 | with PERM_LOCK: 239 | perms = SESSION.query(Permissions).get(str(old_chat_id)) 240 | if perms: 241 | perms.chat_id = str(new_chat_id) 242 | SESSION.commit() 243 | 244 | with RESTR_LOCK: 245 | rest = SESSION.query(Restrictions).get(str(old_chat_id)) 246 | if rest: 247 | rest.chat_id = str(new_chat_id) 248 | SESSION.commit() 249 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/log_channel_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, func, distinct 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | 8 | class GroupLogs(BASE): 9 | __tablename__ = "log_channels" 10 | chat_id = Column(String(14), primary_key=True) 11 | log_channel = Column(String(14), nullable=False) 12 | 13 | def __init__(self, chat_id, log_channel): 14 | self.chat_id = str(chat_id) 15 | self.log_channel = str(log_channel) 16 | 17 | 18 | GroupLogs.__table__.create(checkfirst=True) 19 | 20 | LOGS_INSERTION_LOCK = threading.RLock() 21 | 22 | CHANNELS = {} 23 | 24 | 25 | def set_chat_log_channel(chat_id, log_channel): 26 | with LOGS_INSERTION_LOCK: 27 | res = SESSION.query(GroupLogs).get(str(chat_id)) 28 | if res: 29 | res.log_channel = log_channel 30 | else: 31 | res = GroupLogs(chat_id, log_channel) 32 | SESSION.add(res) 33 | 34 | CHANNELS[str(chat_id)] = log_channel 35 | SESSION.commit() 36 | 37 | 38 | def get_chat_log_channel(chat_id): 39 | return CHANNELS.get(str(chat_id)) 40 | 41 | 42 | def stop_chat_logging(chat_id): 43 | with LOGS_INSERTION_LOCK: 44 | res = SESSION.query(GroupLogs).get(str(chat_id)) 45 | if res: 46 | if str(chat_id) in CHANNELS: 47 | del CHANNELS[str(chat_id)] 48 | 49 | log_channel = res.log_channel 50 | SESSION.delete(res) 51 | SESSION.commit() 52 | return log_channel 53 | 54 | 55 | def num_logchannels(): 56 | try: 57 | return SESSION.query(func.count(distinct(GroupLogs.chat_id))).scalar() 58 | finally: 59 | SESSION.close() 60 | 61 | 62 | def migrate_chat(old_chat_id, new_chat_id): 63 | with LOGS_INSERTION_LOCK: 64 | chat = SESSION.query(GroupLogs).get(str(old_chat_id)) 65 | if chat: 66 | chat.chat_id = str(new_chat_id) 67 | SESSION.add(chat) 68 | if str(old_chat_id) in CHANNELS: 69 | CHANNELS[str(new_chat_id)] = CHANNELS.get(str(old_chat_id)) 70 | 71 | SESSION.commit() 72 | 73 | 74 | def __load_log_channels(): 75 | global CHANNELS 76 | try: 77 | all_chats = SESSION.query(GroupLogs).all() 78 | CHANNELS = {chat.chat_id: chat.log_channel for chat in all_chats} 79 | finally: 80 | SESSION.close() 81 | 82 | 83 | __load_log_channels() 84 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/notes_sql.py: -------------------------------------------------------------------------------- 1 | # Note: chat_id's are stored as strings because the int is too large to be stored in a PSQL database. 2 | import threading 3 | 4 | from sqlalchemy import Column, String, Boolean, UnicodeText, Integer, func, distinct 5 | 6 | from tg_bot.modules.helper_funcs.msg_types import Types 7 | from tg_bot.modules.sql import SESSION, BASE 8 | 9 | 10 | class Notes(BASE): 11 | __tablename__ = "notes" 12 | chat_id = Column(String(14), primary_key=True) 13 | name = Column(UnicodeText, primary_key=True) 14 | value = Column(UnicodeText, nullable=False) 15 | file = Column(UnicodeText) 16 | is_reply = Column(Boolean, default=False) 17 | has_buttons = Column(Boolean, default=False) 18 | msgtype = Column(Integer, default=Types.BUTTON_TEXT.value) 19 | 20 | def __init__(self, chat_id, name, value, msgtype, file=None): 21 | self.chat_id = str(chat_id) # ensure string 22 | self.name = name 23 | self.value = value 24 | self.msgtype = msgtype 25 | self.file = file 26 | 27 | def __repr__(self): 28 | return "" % self.name 29 | 30 | 31 | class Buttons(BASE): 32 | __tablename__ = "note_urls" 33 | id = Column(Integer, primary_key=True, autoincrement=True) 34 | chat_id = Column(String(14), primary_key=True) 35 | note_name = Column(UnicodeText, primary_key=True) 36 | name = Column(UnicodeText, nullable=False) 37 | url = Column(UnicodeText, nullable=False) 38 | same_line = Column(Boolean, default=False) 39 | 40 | def __init__(self, chat_id, note_name, name, url, same_line=False): 41 | self.chat_id = str(chat_id) 42 | self.note_name = note_name 43 | self.name = name 44 | self.url = url 45 | self.same_line = same_line 46 | 47 | 48 | Notes.__table__.create(checkfirst=True) 49 | Buttons.__table__.create(checkfirst=True) 50 | 51 | NOTES_INSERTION_LOCK = threading.RLock() 52 | BUTTONS_INSERTION_LOCK = threading.RLock() 53 | 54 | 55 | def add_note_to_db(chat_id, note_name, note_data, msgtype, buttons=None, file=None): 56 | if not buttons: 57 | buttons = [] 58 | 59 | with NOTES_INSERTION_LOCK: 60 | prev = SESSION.query(Notes).get((str(chat_id), note_name)) 61 | if prev: 62 | with BUTTONS_INSERTION_LOCK: 63 | prev_buttons = ( 64 | SESSION.query(Buttons) 65 | .filter( 66 | Buttons.chat_id == str(chat_id), Buttons.note_name == note_name 67 | ) 68 | .all() 69 | ) 70 | for btn in prev_buttons: 71 | SESSION.delete(btn) 72 | SESSION.delete(prev) 73 | note = Notes( 74 | str(chat_id), note_name, note_data or "", msgtype=msgtype.value, file=file 75 | ) 76 | SESSION.add(note) 77 | SESSION.commit() 78 | 79 | for b_name, url, same_line in buttons: 80 | add_note_button_to_db(chat_id, note_name, b_name, url, same_line) 81 | 82 | 83 | def get_note(chat_id, note_name): 84 | try: 85 | return SESSION.query(Notes).get((str(chat_id), note_name)) 86 | finally: 87 | SESSION.close() 88 | 89 | 90 | def rm_note(chat_id, note_name): 91 | with NOTES_INSERTION_LOCK: 92 | note = SESSION.query(Notes).get((str(chat_id), note_name)) 93 | if note: 94 | with BUTTONS_INSERTION_LOCK: 95 | buttons = ( 96 | SESSION.query(Buttons) 97 | .filter( 98 | Buttons.chat_id == str(chat_id), Buttons.note_name == note_name 99 | ) 100 | .all() 101 | ) 102 | for btn in buttons: 103 | SESSION.delete(btn) 104 | 105 | SESSION.delete(note) 106 | SESSION.commit() 107 | return True 108 | 109 | else: 110 | SESSION.close() 111 | return False 112 | 113 | 114 | def get_all_chat_notes(chat_id): 115 | try: 116 | return ( 117 | SESSION.query(Notes) 118 | .filter(Notes.chat_id == str(chat_id)) 119 | .order_by(Notes.name.asc()) 120 | .all() 121 | ) 122 | finally: 123 | SESSION.close() 124 | 125 | 126 | def add_note_button_to_db(chat_id, note_name, b_name, url, same_line): 127 | with BUTTONS_INSERTION_LOCK: 128 | button = Buttons(chat_id, note_name, b_name, url, same_line) 129 | SESSION.add(button) 130 | SESSION.commit() 131 | 132 | 133 | def get_buttons(chat_id, note_name): 134 | try: 135 | return ( 136 | SESSION.query(Buttons) 137 | .filter(Buttons.chat_id == str(chat_id), Buttons.note_name == note_name) 138 | .order_by(Buttons.id) 139 | .all() 140 | ) 141 | finally: 142 | SESSION.close() 143 | 144 | 145 | def num_notes(): 146 | try: 147 | return SESSION.query(Notes).count() 148 | finally: 149 | SESSION.close() 150 | 151 | 152 | def num_chats(): 153 | try: 154 | return SESSION.query(func.count(distinct(Notes.chat_id))).scalar() 155 | finally: 156 | SESSION.close() 157 | 158 | 159 | def migrate_chat(old_chat_id, new_chat_id): 160 | with NOTES_INSERTION_LOCK: 161 | chat_notes = ( 162 | SESSION.query(Notes).filter(Notes.chat_id == str(old_chat_id)).all() 163 | ) 164 | for note in chat_notes: 165 | note.chat_id = str(new_chat_id) 166 | 167 | with BUTTONS_INSERTION_LOCK: 168 | chat_buttons = ( 169 | SESSION.query(Buttons).filter(Buttons.chat_id == str(old_chat_id)).all() 170 | ) 171 | for btn in chat_buttons: 172 | btn.chat_id = str(new_chat_id) 173 | 174 | SESSION.commit() 175 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/reporting_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Union 3 | 4 | from sqlalchemy import Column, Integer, String, Boolean 5 | 6 | from tg_bot.modules.sql import SESSION, BASE 7 | 8 | 9 | class ReportingUserSettings(BASE): 10 | __tablename__ = "user_report_settings" 11 | user_id = Column(Integer, primary_key=True) 12 | should_report = Column(Boolean, default=True) 13 | 14 | def __init__(self, user_id): 15 | self.user_id = user_id 16 | 17 | def __repr__(self): 18 | return "".format(self.user_id) 19 | 20 | 21 | class ReportingChatSettings(BASE): 22 | __tablename__ = "chat_report_settings" 23 | chat_id = Column(String(14), primary_key=True) 24 | should_report = Column(Boolean, default=True) 25 | 26 | def __init__(self, chat_id): 27 | self.chat_id = str(chat_id) 28 | 29 | def __repr__(self): 30 | return "".format(self.chat_id) 31 | 32 | 33 | ReportingUserSettings.__table__.create(checkfirst=True) 34 | ReportingChatSettings.__table__.create(checkfirst=True) 35 | 36 | CHAT_LOCK = threading.RLock() 37 | USER_LOCK = threading.RLock() 38 | 39 | 40 | def chat_should_report(chat_id: Union[str, int]) -> bool: 41 | try: 42 | chat_setting = SESSION.query(ReportingChatSettings).get(str(chat_id)) 43 | if chat_setting: 44 | return chat_setting.should_report 45 | return False 46 | finally: 47 | SESSION.close() 48 | 49 | 50 | def user_should_report(user_id: int) -> bool: 51 | try: 52 | user_setting = SESSION.query(ReportingUserSettings).get(user_id) 53 | if user_setting: 54 | return user_setting.should_report 55 | return True 56 | finally: 57 | SESSION.close() 58 | 59 | 60 | def set_chat_setting(chat_id: Union[int, str], setting: bool): 61 | with CHAT_LOCK: 62 | chat_setting = SESSION.query(ReportingChatSettings).get(str(chat_id)) 63 | if not chat_setting: 64 | chat_setting = ReportingChatSettings(chat_id) 65 | 66 | chat_setting.should_report = setting 67 | SESSION.add(chat_setting) 68 | SESSION.commit() 69 | 70 | 71 | def set_user_setting(user_id: int, setting: bool): 72 | with USER_LOCK: 73 | user_setting = SESSION.query(ReportingUserSettings).get(user_id) 74 | if not user_setting: 75 | user_setting = ReportingUserSettings(user_id) 76 | 77 | user_setting.should_report = setting 78 | SESSION.add(user_setting) 79 | SESSION.commit() 80 | 81 | 82 | def migrate_chat(old_chat_id, new_chat_id): 83 | with CHAT_LOCK: 84 | chat_notes = ( 85 | SESSION.query(ReportingChatSettings) 86 | .filter(ReportingChatSettings.chat_id == str(old_chat_id)) 87 | .all() 88 | ) 89 | for note in chat_notes: 90 | note.chat_id = str(new_chat_id) 91 | SESSION.commit() 92 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/rss_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Integer 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | 8 | class RSS(BASE): 9 | __tablename__ = "rss_feed" 10 | id = Column(Integer, primary_key=True) 11 | chat_id = Column(UnicodeText, nullable=False) 12 | feed_link = Column(UnicodeText) 13 | old_entry_link = Column(UnicodeText) 14 | 15 | def __init__(self, chat_id, feed_link, old_entry_link): 16 | self.chat_id = chat_id 17 | self.feed_link = feed_link 18 | self.old_entry_link = old_entry_link 19 | 20 | def __repr__(self): 21 | return "".format( 22 | self.chat_id, self.feed_link, self.old_entry_link 23 | ) 24 | 25 | 26 | RSS.__table__.create(checkfirst=True) 27 | INSERTION_LOCK = threading.RLock() 28 | 29 | 30 | def check_url_availability(tg_chat_id, tg_feed_link): 31 | try: 32 | return ( 33 | SESSION.query(RSS) 34 | .filter(RSS.feed_link == tg_feed_link, RSS.chat_id == tg_chat_id) 35 | .all() 36 | ) 37 | finally: 38 | SESSION.close() 39 | 40 | 41 | def add_url(tg_chat_id, tg_feed_link, tg_old_entry_link): 42 | with INSERTION_LOCK: 43 | action = RSS(tg_chat_id, tg_feed_link, tg_old_entry_link) 44 | 45 | SESSION.add(action) 46 | SESSION.commit() 47 | 48 | 49 | def remove_url(tg_chat_id, tg_feed_link): 50 | with INSERTION_LOCK: 51 | # this loops to delete any possible duplicates for the same TG User ID, TG Chat ID and link 52 | for row in check_url_availability(tg_chat_id, tg_feed_link): 53 | # add the action to the DB query 54 | SESSION.delete(row) 55 | 56 | SESSION.commit() 57 | 58 | 59 | def get_urls(tg_chat_id): 60 | try: 61 | return SESSION.query(RSS).filter(RSS.chat_id == tg_chat_id).all() 62 | finally: 63 | SESSION.close() 64 | 65 | 66 | def get_all(): 67 | try: 68 | return SESSION.query(RSS).all() 69 | finally: 70 | SESSION.close() 71 | 72 | 73 | def update_url(row_id, new_entry_links): 74 | with INSERTION_LOCK: 75 | row = SESSION.query(RSS).get(row_id) 76 | 77 | # set the new old_entry_link with the latest update from the RSS Feed 78 | row.old_entry_link = new_entry_links[0] 79 | 80 | # commit the changes to the DB 81 | SESSION.commit() 82 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/rules_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, func, distinct 4 | 5 | from tg_bot.modules.sql import SESSION, BASE 6 | 7 | 8 | class Rules(BASE): 9 | __tablename__ = "rules" 10 | chat_id = Column(String(14), primary_key=True) 11 | rules = Column(UnicodeText, default="") 12 | 13 | def __init__(self, chat_id): 14 | self.chat_id = chat_id 15 | 16 | def __repr__(self): 17 | return "".format(self.chat_id, self.rules) 18 | 19 | 20 | Rules.__table__.create(checkfirst=True) 21 | 22 | INSERTION_LOCK = threading.RLock() 23 | 24 | 25 | def set_rules(chat_id, rules_text): 26 | with INSERTION_LOCK: 27 | rules = SESSION.query(Rules).get(str(chat_id)) 28 | if not rules: 29 | rules = Rules(str(chat_id)) 30 | rules.rules = rules_text 31 | 32 | SESSION.add(rules) 33 | SESSION.commit() 34 | 35 | 36 | def get_rules(chat_id): 37 | rules = SESSION.query(Rules).get(str(chat_id)) 38 | ret = "" 39 | if rules: 40 | ret = rules.rules 41 | 42 | SESSION.close() 43 | return ret 44 | 45 | 46 | def num_chats(): 47 | try: 48 | return SESSION.query(func.count(distinct(Rules.chat_id))).scalar() 49 | finally: 50 | SESSION.close() 51 | 52 | 53 | def migrate_chat(old_chat_id, new_chat_id): 54 | with INSERTION_LOCK: 55 | chat = SESSION.query(Rules).get(str(old_chat_id)) 56 | if chat: 57 | chat.chat_id = str(new_chat_id) 58 | SESSION.commit() 59 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/userinfo_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, UnicodeText 4 | 5 | from tg_bot.modules.sql import SESSION, BASE 6 | 7 | 8 | class UserInfo(BASE): 9 | __tablename__ = "userinfo" 10 | user_id = Column(Integer, primary_key=True) 11 | info = Column(UnicodeText) 12 | 13 | def __init__(self, user_id, info): 14 | self.user_id = user_id 15 | self.info = info 16 | 17 | def __repr__(self): 18 | return "" % self.user_id 19 | 20 | 21 | class UserBio(BASE): 22 | __tablename__ = "userbio" 23 | user_id = Column(Integer, primary_key=True) 24 | bio = Column(UnicodeText) 25 | 26 | def __init__(self, user_id, bio): 27 | self.user_id = user_id 28 | self.bio = bio 29 | 30 | def __repr__(self): 31 | return "" % self.user_id 32 | 33 | 34 | UserInfo.__table__.create(checkfirst=True) 35 | UserBio.__table__.create(checkfirst=True) 36 | 37 | INSERTION_LOCK = threading.RLock() 38 | 39 | 40 | def get_user_me_info(user_id): 41 | userinfo = SESSION.query(UserInfo).get(user_id) 42 | SESSION.close() 43 | if userinfo: 44 | return userinfo.info 45 | return None 46 | 47 | 48 | def set_user_me_info(user_id, info): 49 | with INSERTION_LOCK: 50 | userinfo = SESSION.query(UserInfo).get(user_id) 51 | if userinfo: 52 | userinfo.info = info 53 | else: 54 | userinfo = UserInfo(user_id, info) 55 | SESSION.add(userinfo) 56 | SESSION.commit() 57 | 58 | 59 | def get_user_bio(user_id): 60 | userbio = SESSION.query(UserBio).get(user_id) 61 | SESSION.close() 62 | if userbio: 63 | return userbio.bio 64 | return None 65 | 66 | 67 | def set_user_bio(user_id, bio): 68 | with INSERTION_LOCK: 69 | userbio = SESSION.query(UserBio).get(user_id) 70 | if userbio: 71 | userbio.bio = bio 72 | else: 73 | userbio = UserBio(user_id, bio) 74 | 75 | SESSION.add(userbio) 76 | SESSION.commit() 77 | 78 | 79 | def clear_user_info(user_id): 80 | with INSERTION_LOCK: 81 | curr = SESSION.query(UserInfo).get(user_id) 82 | if curr: 83 | SESSION.delete(curr) 84 | SESSION.commit() 85 | return True 86 | 87 | SESSION.close() 88 | return False 89 | 90 | 91 | def clear_user_bio(user_id): 92 | with INSERTION_LOCK: 93 | curr = SESSION.query(UserBio).get(user_id) 94 | if curr: 95 | SESSION.delete(curr) 96 | SESSION.commit() 97 | return True 98 | 99 | SESSION.close() 100 | return False 101 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/users_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import ( 4 | Column, 5 | BigInteger, 6 | UnicodeText, 7 | String, 8 | ForeignKey, 9 | UniqueConstraint, 10 | func, 11 | ) 12 | 13 | from tg_bot import dispatcher 14 | from tg_bot.modules.sql import BASE, SESSION 15 | 16 | 17 | class Users(BASE): 18 | __tablename__ = "users" 19 | user_id = Column(BigInteger, primary_key=True) 20 | username = Column(UnicodeText) 21 | 22 | def __init__(self, user_id, username=None): 23 | self.user_id = user_id 24 | self.username = username 25 | 26 | def __repr__(self): 27 | return "".format(self.username, self.user_id) 28 | 29 | 30 | class RemovedUser(BASE): 31 | __tablename__ = "removed_user" 32 | user_id = Column(BigInteger, primary_key=True) 33 | chat_id = Column(String(14), primary_key=True) 34 | username = Column(UnicodeText) 35 | 36 | def __init__(self, user_id, chat_id, username): 37 | self.user_id = user_id 38 | self.chat_id = str(chat_id) 39 | self.username = str(username) 40 | 41 | def __repr__(self): 42 | return f"" 43 | 44 | 45 | class Chats(BASE): 46 | __tablename__ = "chats" 47 | chat_id = Column(String(14), primary_key=True) 48 | chat_name = Column(UnicodeText, nullable=False) 49 | 50 | def __init__(self, chat_id, chat_name): 51 | self.chat_id = str(chat_id) 52 | self.chat_name = chat_name 53 | 54 | def __repr__(self): 55 | return "".format(self.chat_name, self.chat_id) 56 | 57 | 58 | class ChatMembers(BASE): 59 | __tablename__ = "chat_members" 60 | priv_chat_id = Column(BigInteger, primary_key=True) 61 | # NOTE: Use dual primary key instead of private primary key? 62 | chat = Column( 63 | String(14), 64 | ForeignKey("chats.chat_id", onupdate="CASCADE", ondelete="CASCADE"), 65 | nullable=False, 66 | ) 67 | user = Column( 68 | BigInteger, 69 | ForeignKey("users.user_id", onupdate="CASCADE", ondelete="CASCADE"), 70 | nullable=False, 71 | ) 72 | __table_args__ = (UniqueConstraint("chat", "user", name="_chat_members_uc"),) 73 | 74 | def __init__(self, chat, user): 75 | self.chat = chat 76 | self.user = user 77 | 78 | def __repr__(self): 79 | return "".format( 80 | self.user.username, 81 | self.user.user_id, 82 | self.chat.chat_name, 83 | self.chat.chat_id, 84 | ) 85 | 86 | 87 | Users.__table__.create(checkfirst=True) 88 | Chats.__table__.create(checkfirst=True) 89 | ChatMembers.__table__.create(checkfirst=True) 90 | RemovedUser.__table__.create(checkfirst=True) 91 | 92 | INSERTION_LOCK = threading.RLock() 93 | 94 | 95 | def ensure_bot_in_db(): 96 | with INSERTION_LOCK: 97 | bot = Users(dispatcher.bot.id, dispatcher.bot.username) 98 | SESSION.merge(bot) 99 | SESSION.commit() 100 | 101 | 102 | def update_user(user_id, username, chat_id=None, chat_name=None): 103 | with INSERTION_LOCK: 104 | user = SESSION.query(Users).get(user_id) 105 | if not user: 106 | user = Users(user_id, username) 107 | SESSION.add(user) 108 | SESSION.flush() 109 | else: 110 | user.username = username 111 | 112 | if not chat_id or not chat_name: 113 | SESSION.commit() 114 | return 115 | 116 | chat = SESSION.query(Chats).get(str(chat_id)) 117 | if not chat: 118 | chat = Chats(str(chat_id), chat_name) 119 | SESSION.add(chat) 120 | SESSION.flush() 121 | 122 | else: 123 | chat.chat_name = chat_name 124 | 125 | member = ( 126 | SESSION.query(ChatMembers) 127 | .filter(ChatMembers.chat == chat.chat_id, ChatMembers.user == user.user_id) 128 | .first() 129 | ) 130 | if not member: 131 | chat_member = ChatMembers(chat.chat_id, user.user_id) 132 | SESSION.add(chat_member) 133 | 134 | SESSION.commit() 135 | 136 | 137 | def get_userid_by_name(username): 138 | try: 139 | return ( 140 | SESSION.query(Users) 141 | .filter(func.lower(Users.username) == username.lower()) 142 | .all() 143 | ) 144 | finally: 145 | SESSION.close() 146 | 147 | 148 | def get_name_by_userid(user_id): 149 | try: 150 | return SESSION.query(Users).get(Users.user_id == int(user_id)).first() 151 | finally: 152 | SESSION.close() 153 | 154 | 155 | def get_chat_members(chat_id): 156 | try: 157 | return SESSION.query(ChatMembers).filter(ChatMembers.chat == str(chat_id)).all() 158 | finally: 159 | SESSION.close() 160 | 161 | 162 | def get_all_chats(): 163 | try: 164 | return SESSION.query(Chats).all() 165 | finally: 166 | SESSION.close() 167 | 168 | 169 | def get_user_num_chats(user_id): 170 | try: 171 | return ( 172 | SESSION.query(ChatMembers).filter(ChatMembers.user == int(user_id)).count() 173 | ) 174 | finally: 175 | SESSION.close() 176 | 177 | 178 | def num_chats(): 179 | try: 180 | return SESSION.query(Chats).count() 181 | finally: 182 | SESSION.close() 183 | 184 | 185 | def num_users(): 186 | try: 187 | return SESSION.query(Users).count() 188 | finally: 189 | SESSION.close() 190 | 191 | 192 | def migrate_chat(old_chat_id, new_chat_id): 193 | with INSERTION_LOCK: 194 | chat = SESSION.query(Chats).get(str(old_chat_id)) 195 | if chat: 196 | chat.chat_id = str(new_chat_id) 197 | SESSION.add(chat) 198 | 199 | SESSION.flush() 200 | 201 | chat_members = ( 202 | SESSION.query(ChatMembers) 203 | .filter(ChatMembers.chat == str(old_chat_id)) 204 | .all() 205 | ) 206 | for member in chat_members: 207 | member.chat = str(new_chat_id) 208 | SESSION.add(member) 209 | 210 | SESSION.commit() 211 | 212 | 213 | ensure_bot_in_db() 214 | 215 | 216 | def remove_user(user_id, chat_id, username): 217 | with INSERTION_LOCK: 218 | user = SESSION.query(RemovedUser).get((user_id, str(chat_id))) 219 | if not user: 220 | user = RemovedUser(user_id, chat_id, username) 221 | SESSION.add(user) 222 | SESSION.flush() 223 | SESSION.commit() 224 | 225 | 226 | def del_user(user_id): 227 | with INSERTION_LOCK: 228 | curr = SESSION.query(Users).get(user_id) 229 | if curr: 230 | SESSION.delete(curr) 231 | SESSION.commit() 232 | return True 233 | 234 | ChatMembers.query.filter(ChatMembers.user == user_id).delete() 235 | SESSION.commit() 236 | SESSION.close() 237 | return False 238 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/warns_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Integer, Column, String, UnicodeText, func, distinct, Boolean 4 | from sqlalchemy.dialects import postgresql 5 | 6 | from tg_bot.modules.sql import SESSION, BASE 7 | 8 | 9 | class Warns(BASE): 10 | __tablename__ = "warns" 11 | 12 | user_id = Column(Integer, primary_key=True) 13 | chat_id = Column(String(14), primary_key=True) 14 | num_warns = Column(Integer, default=0) 15 | reasons = Column(postgresql.ARRAY(UnicodeText)) 16 | 17 | def __init__(self, user_id, chat_id): 18 | self.user_id = user_id 19 | self.chat_id = str(chat_id) 20 | self.num_warns = 0 21 | self.reasons = [] 22 | 23 | def __repr__(self): 24 | return "<{} warns for {} in {} for reasons {}>".format( 25 | self.num_warns, self.user_id, self.chat_id, self.reasons 26 | ) 27 | 28 | 29 | class WarnFilters(BASE): 30 | __tablename__ = "warn_filters" 31 | chat_id = Column(String(14), primary_key=True) 32 | keyword = Column(UnicodeText, primary_key=True, nullable=False) 33 | reply = Column(UnicodeText, nullable=False) 34 | 35 | def __init__(self, chat_id, keyword, reply): 36 | self.chat_id = str(chat_id) # ensure string 37 | self.keyword = keyword 38 | self.reply = reply 39 | 40 | def __repr__(self): 41 | return "" % self.chat_id 42 | 43 | def __eq__(self, other): 44 | return bool( 45 | isinstance(other, WarnFilters) 46 | and self.chat_id == other.chat_id 47 | and self.keyword == other.keyword 48 | ) 49 | 50 | 51 | class WarnSettings(BASE): 52 | __tablename__ = "warn_settings" 53 | chat_id = Column(String(14), primary_key=True) 54 | warn_limit = Column(Integer, default=3) 55 | soft_warn = Column(Boolean, default=False) 56 | 57 | def __init__(self, chat_id, warn_limit=3, soft_warn=False): 58 | self.chat_id = str(chat_id) 59 | self.warn_limit = warn_limit 60 | self.soft_warn = soft_warn 61 | 62 | def __repr__(self): 63 | return "<{} has {} possible warns.>".format(self.chat_id, self.warn_limit) 64 | 65 | 66 | Warns.__table__.create(checkfirst=True) 67 | WarnFilters.__table__.create(checkfirst=True) 68 | WarnSettings.__table__.create(checkfirst=True) 69 | 70 | WARN_INSERTION_LOCK = threading.RLock() 71 | WARN_FILTER_INSERTION_LOCK = threading.RLock() 72 | WARN_SETTINGS_LOCK = threading.RLock() 73 | 74 | WARN_FILTERS = {} 75 | 76 | 77 | def warn_user(user_id, chat_id, reason=None): 78 | with WARN_INSERTION_LOCK: 79 | warned_user = SESSION.query(Warns).get((user_id, str(chat_id))) 80 | if not warned_user: 81 | warned_user = Warns(user_id, str(chat_id)) 82 | 83 | warned_user.num_warns += 1 84 | if reason: 85 | warned_user.reasons = warned_user.reasons + [ 86 | reason 87 | ] # TODO:: double check this wizardry 88 | 89 | reasons = warned_user.reasons 90 | num = warned_user.num_warns 91 | 92 | SESSION.add(warned_user) 93 | SESSION.commit() 94 | 95 | return num, reasons 96 | 97 | 98 | def remove_warn(user_id, chat_id): 99 | with WARN_INSERTION_LOCK: 100 | removed = False 101 | warned_user = SESSION.query(Warns).get((user_id, str(chat_id))) 102 | 103 | if warned_user and warned_user.num_warns > 0: 104 | warned_user.num_warns -= 1 105 | 106 | SESSION.add(warned_user) 107 | SESSION.commit() 108 | removed = True 109 | 110 | SESSION.close() 111 | return removed 112 | 113 | 114 | def reset_warns(user_id, chat_id): 115 | with WARN_INSERTION_LOCK: 116 | warned_user = SESSION.query(Warns).get((user_id, str(chat_id))) 117 | if warned_user: 118 | warned_user.num_warns = 0 119 | warned_user.reasons = [] 120 | 121 | SESSION.add(warned_user) 122 | SESSION.commit() 123 | SESSION.close() 124 | 125 | 126 | def get_warns(user_id, chat_id): 127 | try: 128 | user = SESSION.query(Warns).get((user_id, str(chat_id))) 129 | if not user: 130 | return None 131 | reasons = user.reasons 132 | num = user.num_warns 133 | return num, reasons 134 | finally: 135 | SESSION.close() 136 | 137 | 138 | def add_warn_filter(chat_id, keyword, reply): 139 | with WARN_FILTER_INSERTION_LOCK: 140 | warn_filt = WarnFilters(str(chat_id), keyword, reply) 141 | 142 | if keyword not in WARN_FILTERS.get(str(chat_id), []): 143 | WARN_FILTERS[str(chat_id)] = sorted( 144 | WARN_FILTERS.get(str(chat_id), []) + [keyword], 145 | key=lambda x: (-len(x), x), 146 | ) 147 | 148 | SESSION.merge(warn_filt) # merge to avoid duplicate key issues 149 | SESSION.commit() 150 | 151 | 152 | def remove_warn_filter(chat_id, keyword): 153 | with WARN_FILTER_INSERTION_LOCK: 154 | warn_filt = SESSION.query(WarnFilters).get((str(chat_id), keyword)) 155 | if warn_filt: 156 | if keyword in WARN_FILTERS.get(str(chat_id), []): # sanity check 157 | WARN_FILTERS.get(str(chat_id), []).remove(keyword) 158 | 159 | SESSION.delete(warn_filt) 160 | SESSION.commit() 161 | return True 162 | SESSION.close() 163 | return False 164 | 165 | 166 | def get_chat_warn_triggers(chat_id): 167 | return WARN_FILTERS.get(str(chat_id), set()) 168 | 169 | 170 | def get_chat_warn_filters(chat_id): 171 | try: 172 | return ( 173 | SESSION.query(WarnFilters).filter(WarnFilters.chat_id == str(chat_id)).all() 174 | ) 175 | finally: 176 | SESSION.close() 177 | 178 | 179 | def get_warn_filter(chat_id, keyword): 180 | try: 181 | return SESSION.query(WarnFilters).get((str(chat_id), keyword)) 182 | finally: 183 | SESSION.close() 184 | 185 | 186 | def set_warn_limit(chat_id, warn_limit): 187 | with WARN_SETTINGS_LOCK: 188 | curr_setting = SESSION.query(WarnSettings).get(str(chat_id)) 189 | if not curr_setting: 190 | curr_setting = WarnSettings(chat_id, warn_limit=warn_limit) 191 | 192 | curr_setting.warn_limit = warn_limit 193 | 194 | SESSION.add(curr_setting) 195 | SESSION.commit() 196 | 197 | 198 | def set_warn_strength(chat_id, soft_warn): 199 | with WARN_SETTINGS_LOCK: 200 | curr_setting = SESSION.query(WarnSettings).get(str(chat_id)) 201 | if not curr_setting: 202 | curr_setting = WarnSettings(chat_id, soft_warn=soft_warn) 203 | 204 | curr_setting.soft_warn = soft_warn 205 | 206 | SESSION.add(curr_setting) 207 | SESSION.commit() 208 | 209 | 210 | def get_warn_setting(chat_id): 211 | try: 212 | setting = SESSION.query(WarnSettings).get(str(chat_id)) 213 | if setting: 214 | return setting.warn_limit, setting.soft_warn 215 | else: 216 | return 3, False 217 | 218 | finally: 219 | SESSION.close() 220 | 221 | 222 | def num_warns(): 223 | try: 224 | return SESSION.query(func.sum(Warns.num_warns)).scalar() or 0 225 | finally: 226 | SESSION.close() 227 | 228 | 229 | def num_warn_chats(): 230 | try: 231 | return SESSION.query(func.count(distinct(Warns.chat_id))).scalar() 232 | finally: 233 | SESSION.close() 234 | 235 | 236 | def num_warn_filters(): 237 | try: 238 | return SESSION.query(WarnFilters).count() 239 | finally: 240 | SESSION.close() 241 | 242 | 243 | def num_warn_chat_filters(chat_id): 244 | try: 245 | return ( 246 | SESSION.query(WarnFilters.chat_id) 247 | .filter(WarnFilters.chat_id == str(chat_id)) 248 | .count() 249 | ) 250 | finally: 251 | SESSION.close() 252 | 253 | 254 | def num_warn_filter_chats(): 255 | try: 256 | return SESSION.query(func.count(distinct(WarnFilters.chat_id))).scalar() 257 | finally: 258 | SESSION.close() 259 | 260 | 261 | def __load_chat_warn_filters(): 262 | global WARN_FILTERS 263 | try: 264 | chats = SESSION.query(WarnFilters.chat_id).distinct().all() 265 | for (chat_id,) in chats: # remove tuple by ( ,) 266 | WARN_FILTERS[chat_id] = [] 267 | 268 | all_filters = SESSION.query(WarnFilters).all() 269 | for x in all_filters: 270 | WARN_FILTERS[x.chat_id] += [x.keyword] 271 | 272 | WARN_FILTERS = { 273 | x: sorted(set(y), key=lambda i: (-len(i), i)) 274 | for x, y in WARN_FILTERS.items() 275 | } 276 | 277 | finally: 278 | SESSION.close() 279 | 280 | 281 | def migrate_chat(old_chat_id, new_chat_id): 282 | with WARN_INSERTION_LOCK: 283 | chat_notes = ( 284 | SESSION.query(Warns).filter(Warns.chat_id == str(old_chat_id)).all() 285 | ) 286 | for note in chat_notes: 287 | note.chat_id = str(new_chat_id) 288 | SESSION.commit() 289 | 290 | with WARN_FILTER_INSERTION_LOCK: 291 | chat_filters = ( 292 | SESSION.query(WarnFilters) 293 | .filter(WarnFilters.chat_id == str(old_chat_id)) 294 | .all() 295 | ) 296 | for filt in chat_filters: 297 | filt.chat_id = str(new_chat_id) 298 | SESSION.commit() 299 | WARN_FILTERS[str(new_chat_id)] = WARN_FILTERS[str(old_chat_id)] 300 | del WARN_FILTERS[str(old_chat_id)] 301 | 302 | with WARN_SETTINGS_LOCK: 303 | chat_settings = ( 304 | SESSION.query(WarnSettings) 305 | .filter(WarnSettings.chat_id == str(old_chat_id)) 306 | .all() 307 | ) 308 | for setting in chat_settings: 309 | setting.chat_id = str(new_chat_id) 310 | SESSION.commit() 311 | 312 | 313 | __load_chat_warn_filters() 314 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/welcome_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, Boolean, UnicodeText, Integer, BigInteger 4 | 5 | from tg_bot.modules.helper_funcs.msg_types import Types 6 | from tg_bot.modules.sql import SESSION, BASE 7 | 8 | DEFAULT_WELCOME = "Hey {first}, how are you?" 9 | DEFAULT_GOODBYE = "Nice knowing ya!" 10 | 11 | 12 | class Welcome(BASE): 13 | __tablename__ = "welcome_pref" 14 | chat_id = Column(String(14), primary_key=True) 15 | should_welcome = Column(Boolean, default=True) 16 | should_goodbye = Column(Boolean, default=True) 17 | 18 | custom_welcome = Column(UnicodeText, default=DEFAULT_WELCOME) 19 | welcome_type = Column(Integer, default=Types.TEXT.value) 20 | 21 | custom_leave = Column(UnicodeText, default=DEFAULT_GOODBYE) 22 | leave_type = Column(Integer, default=Types.TEXT.value) 23 | 24 | clean_welcome = Column(BigInteger) 25 | 26 | def __init__(self, chat_id, should_welcome=True, should_goodbye=True): 27 | self.chat_id = chat_id 28 | self.should_welcome = should_welcome 29 | self.should_goodbye = should_goodbye 30 | 31 | def __repr__(self): 32 | return "".format( 33 | self.chat_id, self.should_welcome 34 | ) 35 | 36 | 37 | class WelcomeButtons(BASE): 38 | __tablename__ = "welcome_urls" 39 | id = Column(Integer, primary_key=True, autoincrement=True) 40 | chat_id = Column(String(14), primary_key=True) 41 | name = Column(UnicodeText, nullable=False) 42 | url = Column(UnicodeText, nullable=False) 43 | same_line = Column(Boolean, default=False) 44 | 45 | def __init__(self, chat_id, name, url, same_line=False): 46 | self.chat_id = str(chat_id) 47 | self.name = name 48 | self.url = url 49 | self.same_line = same_line 50 | 51 | 52 | class GoodbyeButtons(BASE): 53 | __tablename__ = "leave_urls" 54 | id = Column(Integer, primary_key=True, autoincrement=True) 55 | chat_id = Column(String(14), primary_key=True) 56 | name = Column(UnicodeText, nullable=False) 57 | url = Column(UnicodeText, nullable=False) 58 | same_line = Column(Boolean, default=False) 59 | 60 | def __init__(self, chat_id, name, url, same_line=False): 61 | self.chat_id = str(chat_id) 62 | self.name = name 63 | self.url = url 64 | self.same_line = same_line 65 | 66 | 67 | Welcome.__table__.create(checkfirst=True) 68 | WelcomeButtons.__table__.create(checkfirst=True) 69 | GoodbyeButtons.__table__.create(checkfirst=True) 70 | 71 | INSERTION_LOCK = threading.RLock() 72 | WELC_BTN_LOCK = threading.RLock() 73 | LEAVE_BTN_LOCK = threading.RLock() 74 | 75 | 76 | def get_welc_pref(chat_id): 77 | welc = SESSION.query(Welcome).get(str(chat_id)) 78 | SESSION.close() 79 | if welc: 80 | return welc.should_welcome, welc.custom_welcome, welc.welcome_type 81 | else: 82 | # Welcome by default. 83 | return True, DEFAULT_WELCOME, Types.TEXT 84 | 85 | 86 | def get_gdbye_pref(chat_id): 87 | welc = SESSION.query(Welcome).get(str(chat_id)) 88 | SESSION.close() 89 | if welc: 90 | return welc.should_goodbye, welc.custom_leave, welc.leave_type 91 | else: 92 | # Welcome by default. 93 | return True, DEFAULT_GOODBYE, Types.TEXT 94 | 95 | 96 | def set_clean_welcome(chat_id, clean_welcome): 97 | with INSERTION_LOCK: 98 | curr = SESSION.query(Welcome).get(str(chat_id)) 99 | if not curr: 100 | curr = Welcome(str(chat_id)) 101 | 102 | curr.clean_welcome = int(clean_welcome) 103 | 104 | SESSION.add(curr) 105 | SESSION.commit() 106 | 107 | 108 | def get_clean_pref(chat_id): 109 | welc = SESSION.query(Welcome).get(str(chat_id)) 110 | SESSION.close() 111 | 112 | if welc: 113 | return welc.clean_welcome 114 | 115 | return False 116 | 117 | 118 | def set_welc_preference(chat_id, should_welcome): 119 | with INSERTION_LOCK: 120 | curr = SESSION.query(Welcome).get(str(chat_id)) 121 | if not curr: 122 | curr = Welcome(str(chat_id), should_welcome=should_welcome) 123 | else: 124 | curr.should_welcome = should_welcome 125 | 126 | SESSION.add(curr) 127 | SESSION.commit() 128 | 129 | 130 | def set_gdbye_preference(chat_id, should_goodbye): 131 | with INSERTION_LOCK: 132 | curr = SESSION.query(Welcome).get(str(chat_id)) 133 | if not curr: 134 | curr = Welcome(str(chat_id), should_goodbye=should_goodbye) 135 | else: 136 | curr.should_goodbye = should_goodbye 137 | 138 | SESSION.add(curr) 139 | SESSION.commit() 140 | 141 | 142 | def set_custom_welcome(chat_id, custom_welcome, welcome_type, buttons=None): 143 | if buttons is None: 144 | buttons = [] 145 | 146 | with INSERTION_LOCK: 147 | welcome_settings = SESSION.query(Welcome).get(str(chat_id)) 148 | if not welcome_settings: 149 | welcome_settings = Welcome(str(chat_id), True) 150 | 151 | if custom_welcome: 152 | welcome_settings.custom_welcome = custom_welcome 153 | welcome_settings.welcome_type = welcome_type.value 154 | 155 | else: 156 | welcome_settings.custom_welcome = DEFAULT_GOODBYE 157 | welcome_settings.welcome_type = Types.TEXT.value 158 | 159 | SESSION.add(welcome_settings) 160 | 161 | with WELC_BTN_LOCK: 162 | prev_buttons = ( 163 | SESSION.query(WelcomeButtons) 164 | .filter(WelcomeButtons.chat_id == str(chat_id)) 165 | .all() 166 | ) 167 | for btn in prev_buttons: 168 | SESSION.delete(btn) 169 | 170 | for b_name, url, same_line in buttons: 171 | button = WelcomeButtons(chat_id, b_name, url, same_line) 172 | SESSION.add(button) 173 | 174 | SESSION.commit() 175 | 176 | 177 | def get_custom_welcome(chat_id): 178 | welcome_settings = SESSION.query(Welcome).get(str(chat_id)) 179 | ret = DEFAULT_WELCOME 180 | if welcome_settings and welcome_settings.custom_welcome: 181 | ret = welcome_settings.custom_welcome 182 | 183 | SESSION.close() 184 | return ret 185 | 186 | 187 | def set_custom_gdbye(chat_id, custom_goodbye, goodbye_type, buttons=None): 188 | if buttons is None: 189 | buttons = [] 190 | 191 | with INSERTION_LOCK: 192 | welcome_settings = SESSION.query(Welcome).get(str(chat_id)) 193 | if not welcome_settings: 194 | welcome_settings = Welcome(str(chat_id), True) 195 | 196 | if custom_goodbye: 197 | welcome_settings.custom_leave = custom_goodbye 198 | welcome_settings.leave_type = goodbye_type.value 199 | 200 | else: 201 | welcome_settings.custom_leave = DEFAULT_GOODBYE 202 | welcome_settings.leave_type = Types.TEXT.value 203 | 204 | SESSION.add(welcome_settings) 205 | 206 | with LEAVE_BTN_LOCK: 207 | prev_buttons = ( 208 | SESSION.query(GoodbyeButtons) 209 | .filter(GoodbyeButtons.chat_id == str(chat_id)) 210 | .all() 211 | ) 212 | for btn in prev_buttons: 213 | SESSION.delete(btn) 214 | 215 | for b_name, url, same_line in buttons: 216 | button = GoodbyeButtons(chat_id, b_name, url, same_line) 217 | SESSION.add(button) 218 | 219 | SESSION.commit() 220 | 221 | 222 | def get_custom_gdbye(chat_id): 223 | welcome_settings = SESSION.query(Welcome).get(str(chat_id)) 224 | ret = DEFAULT_GOODBYE 225 | if welcome_settings and welcome_settings.custom_leave: 226 | ret = welcome_settings.custom_leave 227 | 228 | SESSION.close() 229 | return ret 230 | 231 | 232 | def get_welc_buttons(chat_id): 233 | try: 234 | return ( 235 | SESSION.query(WelcomeButtons) 236 | .filter(WelcomeButtons.chat_id == str(chat_id)) 237 | .order_by(WelcomeButtons.id) 238 | .all() 239 | ) 240 | finally: 241 | SESSION.close() 242 | 243 | 244 | def get_gdbye_buttons(chat_id): 245 | try: 246 | return ( 247 | SESSION.query(GoodbyeButtons) 248 | .filter(GoodbyeButtons.chat_id == str(chat_id)) 249 | .order_by(GoodbyeButtons.id) 250 | .all() 251 | ) 252 | finally: 253 | SESSION.close() 254 | 255 | 256 | def migrate_chat(old_chat_id, new_chat_id): 257 | with INSERTION_LOCK: 258 | chat = SESSION.query(Welcome).get(str(old_chat_id)) 259 | if chat: 260 | chat.chat_id = str(new_chat_id) 261 | 262 | with WELC_BTN_LOCK: 263 | chat_buttons = ( 264 | SESSION.query(WelcomeButtons) 265 | .filter(WelcomeButtons.chat_id == str(old_chat_id)) 266 | .all() 267 | ) 268 | for btn in chat_buttons: 269 | btn.chat_id = str(new_chat_id) 270 | 271 | with LEAVE_BTN_LOCK: 272 | chat_buttons = ( 273 | SESSION.query(GoodbyeButtons) 274 | .filter(GoodbyeButtons.chat_id == str(old_chat_id)) 275 | .all() 276 | ) 277 | for btn in chat_buttons: 278 | btn.chat_id = str(new_chat_id) 279 | 280 | SESSION.commit() 281 | -------------------------------------------------------------------------------- /tg_bot/modules/translation.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pprint import pprint 3 | 4 | import requests 5 | from telegram import Update, Bot 6 | from telegram.ext import CommandHandler 7 | 8 | from tg_bot import dispatcher 9 | 10 | # Open API key 11 | API_KEY = "6ae0c3a0-afdc-4532-a810-82ded0054236" 12 | URL = "http://services.gingersoftware.com/Ginger/correct/json/GingerTheText" 13 | 14 | 15 | def translate(bot: Bot, update: Update): 16 | if update.effective_message.reply_to_message: 17 | msg = update.effective_message.reply_to_message 18 | 19 | params = dict(lang="US", clientVersion="2.0", apiKey=API_KEY, text=msg.text) 20 | 21 | res = requests.get(URL, params=params) 22 | # print(res) 23 | # print(res.text) 24 | pprint(json.loads(res.text)) 25 | changes = json.loads(res.text).get("LightGingerTheTextResult") 26 | curr_string = "" 27 | 28 | prev_end = 0 29 | 30 | for change in changes: 31 | start = change.get("From") 32 | end = change.get("To") + 1 33 | suggestions = change.get("Suggestions") 34 | if suggestions: 35 | sugg_str = suggestions[0].get("Text") # should look at this list more 36 | curr_string += msg.text[prev_end:start] + sugg_str 37 | 38 | prev_end = end 39 | 40 | curr_string += msg.text[prev_end:] 41 | print(curr_string) 42 | update.effective_message.reply_text(curr_string) 43 | 44 | 45 | __help__ = """ 46 | - /t: while replying to a message, will reply with a grammar corrected version 47 | """ 48 | 49 | __mod_name__ = "Translator" 50 | 51 | TRANSLATE_HANDLER = CommandHandler("t", translate) 52 | 53 | dispatcher.add_handler(TRANSLATE_HANDLER) 54 | -------------------------------------------------------------------------------- /tg_bot/modules/userinfo.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Update, Bot, User 5 | from telegram import ParseMode, MAX_MESSAGE_LENGTH 6 | from telegram.ext.dispatcher import run_async 7 | from telegram.utils.helpers import escape_markdown 8 | 9 | import tg_bot.modules.sql.userinfo_sql as sql 10 | from tg_bot import dispatcher, SUDO_USERS 11 | from tg_bot.modules.disable import DisableAbleCommandHandler 12 | from tg_bot.modules.helper_funcs.extraction import extract_user 13 | 14 | 15 | @run_async 16 | def about_me(bot: Bot, update: Update, args: List[str]): 17 | message = update.effective_message # type: Optional[Message] 18 | user_id = extract_user(message, args) 19 | 20 | if user_id: 21 | user = bot.get_chat(user_id) 22 | else: 23 | user = message.from_user 24 | 25 | info = sql.get_user_me_info(user.id) 26 | 27 | if info: 28 | update.effective_message.reply_text( 29 | "*{}*:\n{}".format(user.first_name, escape_markdown(info)), 30 | parse_mode=ParseMode.MARKDOWN, 31 | ) 32 | elif message.reply_to_message: 33 | username = message.reply_to_message.from_user.first_name 34 | update.effective_message.reply_text( 35 | username + " non ha ancora impostato un messaggio bio!" 36 | ) 37 | else: 38 | update.effective_message.reply_text("Non hai ancora impostato una bio.") 39 | 40 | 41 | @run_async 42 | def set_about_me(bot: Bot, update: Update): 43 | message = update.effective_message # type: Optional[Message] 44 | user_id = message.from_user.id 45 | text = message.text 46 | info = text.split( 47 | None, 1 48 | ) # use python's maxsplit to only remove the cmd, hence keeping newlines. 49 | if len(info) == 2: 50 | if len(info[1]) < MAX_MESSAGE_LENGTH // 4: 51 | sql.set_user_me_info(user_id, info[1]) 52 | message.reply_text("Informazioni utente aggiornate!") 53 | else: 54 | message.reply_text( 55 | "La tua bio deve avere meno di {} caratteri! Tu hai {}.".format( 56 | MAX_MESSAGE_LENGTH // 4, len(info[1]) 57 | ) 58 | ) 59 | 60 | 61 | @run_async 62 | def about_bio(bot: Bot, update: Update, args: List[str]): 63 | message = update.effective_message # type: Optional[Message] 64 | 65 | user_id = extract_user(message, args) 66 | if user_id: 67 | user = bot.get_chat(user_id) 68 | else: 69 | user = message.from_user 70 | 71 | info = sql.get_user_bio(user.id) 72 | 73 | if info: 74 | update.effective_message.reply_text( 75 | "*{}*:\n{}".format(user.first_name, escape_markdown(info)), 76 | parse_mode=ParseMode.MARKDOWN, 77 | ) 78 | elif message.reply_to_message: 79 | username = user.first_name 80 | update.effective_message.reply_text( 81 | "{} non ha una bio impostata!".format(username) 82 | ) 83 | else: 84 | update.effective_message.reply_text("Non hai ancora impostato una bio!") 85 | 86 | 87 | @run_async 88 | def set_about_bio(bot: Bot, update: Update): 89 | message = update.effective_message # type: Optional[Message] 90 | sender = update.effective_user # type: Optional[User] 91 | if message.reply_to_message: 92 | repl_message = message.reply_to_message 93 | user_id = repl_message.from_user.id 94 | if user_id == message.from_user.id: 95 | message.reply_text( 96 | "Mi dispiace, non puoi impostare la biiografia qui!" 97 | ) 98 | return 99 | elif user_id == bot.id and sender.id not in SUDO_USERS: 100 | message.reply_text( 101 | "Ehm ... sì, mi fido solo degli amministratori per impostare la mia biografia." 102 | ) 103 | return 104 | 105 | text = message.text 106 | bio = text.split( 107 | None, 1 108 | ) # use python's maxsplit to only remove the cmd, hence keeping newlines. 109 | if len(bio) == 2: 110 | if len(bio[1]) < MAX_MESSAGE_LENGTH // 4: 111 | sql.set_user_bio(user_id, bio[1]) 112 | message.reply_text( 113 | "Ho aggiornato la bio di {}!".format( 114 | repl_message.from_user.first_name 115 | ) 116 | ) 117 | else: 118 | message.reply_text( 119 | "La bio deve avere meno di {} caratteri! Tu hai provato ad impostare {}.".format( 120 | MAX_MESSAGE_LENGTH // 4, len(bio[1]) 121 | ) 122 | ) 123 | else: 124 | message.reply_text( 125 | "Rispondi al messaggio di qualcuno per impostare la sua bio!" 126 | ) 127 | 128 | 129 | def __user_info__(user_id): 130 | bio = html.escape(sql.get_user_bio(user_id) or "") 131 | me = html.escape(sql.get_user_me_info(user_id) or "") 132 | if bio and me: 133 | return "Sull'utente:\n{me}\nCosa dicono gli altri:\n{bio}".format( 134 | me=me, bio=bio 135 | ) 136 | elif bio: 137 | return "Cosa dicono gli altri:\n{bio}\n".format(me=me, bio=bio) 138 | elif me: 139 | return "Sull'utente:\n{me}" "".format(me=me, bio=bio) 140 | else: 141 | return "" 142 | 143 | 144 | def __gdpr__(user_id): 145 | sql.clear_user_info(user_id) 146 | sql.clear_user_bio(user_id) 147 | 148 | 149 | __help__ = """ 150 | - /setbio : mentre rispondi, salverà la biografia di un altro utente 151 | - /bio: otterrà la bio del tuo o di un altro utente. Questo non può essere impostato da solo. 152 | - /setme : imposterà le tue informazioni 153 | - /me: otterrai le tue info o di un altro utente 154 | """ 155 | 156 | __mod_name__ = "Bios and Abouts" 157 | 158 | SET_BIO_HANDLER = DisableAbleCommandHandler("setbio", set_about_bio) 159 | GET_BIO_HANDLER = DisableAbleCommandHandler("bio", about_bio, pass_args=True) 160 | 161 | SET_ABOUT_HANDLER = DisableAbleCommandHandler("setme", set_about_me) 162 | GET_ABOUT_HANDLER = DisableAbleCommandHandler("me", about_me, pass_args=True) 163 | 164 | dispatcher.add_handler(SET_BIO_HANDLER) 165 | dispatcher.add_handler(GET_BIO_HANDLER) 166 | dispatcher.add_handler(SET_ABOUT_HANDLER) 167 | dispatcher.add_handler(GET_ABOUT_HANDLER) 168 | -------------------------------------------------------------------------------- /tg_bot/modules/users.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from time import sleep 3 | from typing import Optional 4 | 5 | from telegram import TelegramError, Chat, Message 6 | from telegram import Update, Bot 7 | from telegram.error import BadRequest 8 | from telegram.ext import MessageHandler, Filters, CommandHandler 9 | from telegram.ext.dispatcher import run_async 10 | 11 | import tg_bot.modules.sql.users_sql as sql 12 | from tg_bot import dispatcher, OWNER_ID, LOGGER 13 | from tg_bot.modules.helper_funcs.filters import CustomFilters 14 | 15 | USERS_GROUP = 4 16 | 17 | 18 | def get_user_id(username): 19 | # ensure valid userid 20 | if len(username) <= 5: 21 | return None 22 | 23 | if username.startswith("@"): 24 | username = username[1:] 25 | 26 | users = sql.get_userid_by_name(username) 27 | 28 | if not users: 29 | return None 30 | 31 | elif len(users) == 1: 32 | return users[0].user_id 33 | 34 | else: 35 | for user_obj in users: 36 | try: 37 | userdat = dispatcher.bot.get_chat(user_obj.user_id) 38 | if userdat.username == username: 39 | return userdat.id 40 | 41 | except BadRequest as excp: 42 | if excp.message == "Chat not found": 43 | pass 44 | else: 45 | LOGGER.exception("Error extracting user ID") 46 | 47 | return None 48 | 49 | 50 | @run_async 51 | def broadcast(bot: Bot, update: Update): 52 | to_send = update.effective_message.text.split(None, 1) 53 | if len(to_send) >= 2: 54 | chats = sql.get_all_chats() or [] 55 | failed = 0 56 | for chat in chats: 57 | try: 58 | bot.sendMessage(int(chat.chat_id), to_send[1]) 59 | sleep(0.1) 60 | except TelegramError: 61 | failed += 1 62 | LOGGER.warning( 63 | "Impossibile inviare messaggio broadcast a %s gruppi/o. Nome dei gruppi/o: %s", 64 | str(chat.chat_id), 65 | str(chat.chat_name), 66 | ) 67 | 68 | update.effective_message.reply_text( 69 | "Broadcast completato. {} gruppi non hanno ricevuto il messaggio, probabilmente " 70 | "perchè sono stato kickato.".format(failed) 71 | ) 72 | 73 | 74 | @run_async 75 | def log_user(bot: Bot, update: Update): 76 | chat = update.effective_chat # type: Optional[Chat] 77 | msg = update.effective_message # type: Optional[Message] 78 | 79 | sql.update_user(msg.from_user.id, msg.from_user.username, chat.id, chat.title) 80 | 81 | if msg.reply_to_message: 82 | sql.update_user( 83 | msg.reply_to_message.from_user.id, 84 | msg.reply_to_message.from_user.username, 85 | chat.id, 86 | chat.title, 87 | ) 88 | 89 | if msg.forward_from: 90 | sql.update_user(msg.forward_from.id, msg.forward_from.username) 91 | 92 | 93 | @run_async 94 | def chats(bot: Bot, update: Update): 95 | all_chats = sql.get_all_chats() or [] 96 | chatfile = "List of chats.\n" 97 | for chat in all_chats: 98 | chatfile += "{} - ({})\n".format(chat.chat_name, chat.chat_id) 99 | 100 | with BytesIO(str.encode(chatfile)) as output: 101 | output.name = "chatlist.txt" 102 | update.effective_message.reply_document( 103 | document=output, 104 | filename="chatlist.txt", 105 | caption="Lista di chat nel mio db.", 106 | ) 107 | 108 | 109 | def __user_info__(user_id): 110 | if user_id == dispatcher.bot.id: 111 | return """L'ho visto in.. wow. In tutte le chat! Ah.. sono io!.""" 112 | num_chats = sql.get_user_num_chats(user_id) 113 | return """L'ho visto in {} chats in totale.""".format(num_chats) 114 | 115 | 116 | def __stats__(): 117 | return "{} utenti, in {} chats".format(sql.num_users(), sql.num_chats()) 118 | 119 | 120 | def __gdpr__(user_id): 121 | sql.del_user(user_id) 122 | 123 | 124 | def __migrate__(old_chat_id, new_chat_id): 125 | sql.migrate_chat(old_chat_id, new_chat_id) 126 | 127 | 128 | __help__ = "" # no help string 129 | 130 | __mod_name__ = "Users" 131 | 132 | BROADCAST_HANDLER = CommandHandler( 133 | "broadcast", broadcast, filters=Filters.user(OWNER_ID) 134 | ) 135 | USER_HANDLER = MessageHandler(Filters.all & Filters.group, log_user) 136 | CHATLIST_HANDLER = CommandHandler("chatlist", chats, filters=CustomFilters.sudo_filter) 137 | 138 | dispatcher.add_handler(USER_HANDLER, USERS_GROUP) 139 | dispatcher.add_handler(BROADCAST_HANDLER) 140 | dispatcher.add_handler(CHATLIST_HANDLER) 141 | -------------------------------------------------------------------------------- /tg_bot/sample_config.py: -------------------------------------------------------------------------------- 1 | if not __name__.endswith("sample_config"): 2 | import sys 3 | 4 | print( 5 | "The README is there to be read. Extend this sample config to a config file, don't just rename and change " 6 | "values here. Doing that WILL backfire on you.\nBot quitting.", 7 | file=sys.stderr, 8 | ) 9 | quit(1) 10 | 11 | 12 | # bot / botbot 13 | 14 | # Create a new config.py file in same dir and import, then extend this class. 15 | class Config(object): 16 | LOGGER = True 17 | 18 | # REQUIRED 19 | API_KEY = "API_KEY_HERE" 20 | OWNER_ID = ( 21 | "YOUR ID HERE" 22 | ) # If you dont know, run the bot and do /id in your private chat with it 23 | OWNER_USERNAME = "YOUR USERNAME HERE" 24 | 25 | # RECOMMENDED 26 | SQLALCHEMY_DATABASE_URI = ( 27 | "sqldbtype://username:pw@hostname:port/db_name" 28 | ) # needed for any database modules 29 | MESSAGE_DUMP = None # needed to make sure 'save from' messages persist 30 | LOAD = [] 31 | # sed has been disabled after the discovery that certain long-running sed commands maxed out cpu usage 32 | # and killed the bot. Be careful re-enabling it! 33 | NO_LOAD = ["translation", "rss", "sed"] 34 | WEBHOOK = False 35 | URL = None 36 | DEFAULT_CHAT_ID = None 37 | 38 | # OPTIONAL 39 | SUDO_USERS = ( 40 | [] 41 | ) # List of id's (not usernames) for users which have sudo access to the bot. 42 | SUPPORT_USERS = ( 43 | [] 44 | ) # List of id's (not usernames) for users which are allowed to gban, but can also be banned. 45 | WHITELIST_USERS = ( 46 | [] 47 | ) # List of id's (not usernames) for users which WONT be banned/kicked by the bot. 48 | DONATION_LINK = None # EG, paypal 49 | CERT_PATH = None 50 | PORT = 5000 51 | DEL_CMDS = False # Whether or not you should delete "blue text must click" commands 52 | STRICT_GBAN = False 53 | WORKERS = ( 54 | 8 55 | ) # Number of subthreads to use. This is the recommended amount - see for yourself what works best! 56 | BAN_STICKER = "CAADAgADXQIAAtzyqweiDpkj4KP25wI" # banhammer electus sticker 57 | ALLOW_EXCL = False # Allow ! commands as well as / 58 | 59 | 60 | class Production(Config): 61 | LOGGER = False 62 | 63 | 64 | class Development(Config): 65 | LOGGER = True 66 | --------------------------------------------------------------------------------