├── .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 |
--------------------------------------------------------------------------------