Initial commit
This commit is contained in:
commit
9c9cd96a94
164
.gitignore
vendored
Normal file
164
.gitignore
vendored
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# ---> Python
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Custom
|
||||||
|
config.json
|
||||||
|
*.session
|
||||||
|
*.session-journal
|
||||||
|
|
||||||
|
venv
|
||||||
|
venv_linux
|
||||||
|
venv_windows
|
||||||
|
|
||||||
|
.vscode
|
6
.renovaterc
Normal file
6
.renovaterc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
]
|
||||||
|
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Profitroll
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
11
classes/captcha.py
Normal file
11
classes/captcha.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Captcha:
|
||||||
|
__slots__ = ("image", "emojis_all", "emojis_correct")
|
||||||
|
image: BytesIO
|
||||||
|
emojis_all: List[str]
|
||||||
|
emojis_correct: List[str]
|
30
classes/pyroclient.py
Normal file
30
classes/pyroclient.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from libbot.pyrogram.classes import PyroClient
|
||||||
|
from pyrogram.types import User
|
||||||
|
|
||||||
|
from classes.pyrouser import PyroUser
|
||||||
|
from modules.database import col_users
|
||||||
|
|
||||||
|
|
||||||
|
class PyroClient(PyroClient):
|
||||||
|
async def find_user(self, user: Union[int, User], group: int) -> PyroUser:
|
||||||
|
"""Find User by it's ID or User object
|
||||||
|
|
||||||
|
### Args:
|
||||||
|
* user (`Union[int, User]`): ID or User object to extract ID from
|
||||||
|
* group (`int`): ID of the group
|
||||||
|
|
||||||
|
### Returns:
|
||||||
|
* `PyroUser`: PyroUser object
|
||||||
|
"""
|
||||||
|
db_record = col_users.find_one(
|
||||||
|
{"id": user.id if isinstance(user, User) else user, "group": group}
|
||||||
|
)
|
||||||
|
|
||||||
|
if db_record is None:
|
||||||
|
raise KeyError(
|
||||||
|
f"User with ID {user.id if isinstance(user, User) else user} was not found in the database"
|
||||||
|
)
|
||||||
|
|
||||||
|
return PyroUser(**db_record)
|
104
classes/pyrouser.py
Normal file
104
classes/pyrouser.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bson import ObjectId
|
||||||
|
|
||||||
|
from modules.database import col_users
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PyroUser:
|
||||||
|
"""Dataclass of DB entry of a user"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"_id",
|
||||||
|
"id",
|
||||||
|
"group",
|
||||||
|
"failed",
|
||||||
|
"emojis",
|
||||||
|
"selected",
|
||||||
|
"score",
|
||||||
|
"mistakes",
|
||||||
|
)
|
||||||
|
|
||||||
|
_id: ObjectId
|
||||||
|
id: int
|
||||||
|
group: int
|
||||||
|
failed: int
|
||||||
|
emojis: List[str]
|
||||||
|
selected: List[str]
|
||||||
|
score: int
|
||||||
|
mistakes: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_if_not_exists(
|
||||||
|
cls,
|
||||||
|
id: int,
|
||||||
|
group: int,
|
||||||
|
failed: bool = False,
|
||||||
|
emojis: List[str] = [],
|
||||||
|
selected: List[str] = [],
|
||||||
|
score: int = 0,
|
||||||
|
mistakes: int = 0,
|
||||||
|
):
|
||||||
|
db_entry = col_users.find_one({"id": id, "group": group})
|
||||||
|
if db_entry is None:
|
||||||
|
inserted = col_users.insert_one(
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"group": group,
|
||||||
|
"failed": failed,
|
||||||
|
"emojis": emojis,
|
||||||
|
"selected": selected,
|
||||||
|
"score": score,
|
||||||
|
"mistakes": mistakes,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
db_entry = {
|
||||||
|
"_id": inserted.inserted_id,
|
||||||
|
"id": id,
|
||||||
|
"group": group,
|
||||||
|
"failed": failed,
|
||||||
|
"emojis": emojis,
|
||||||
|
"selected": selected,
|
||||||
|
"score": 0,
|
||||||
|
"mistakes": mistakes,
|
||||||
|
}
|
||||||
|
return cls(**db_entry)
|
||||||
|
|
||||||
|
def set_failed(self, failed: bool = True) -> None:
|
||||||
|
logger.debug("%s's failure state has been set to %s", self.id, failed)
|
||||||
|
col_users.update_one({"_id": self._id}, {"$set": {"failed": failed}})
|
||||||
|
|
||||||
|
def set_emojis(self, emojis: List[str]) -> None:
|
||||||
|
logger.debug("%s's emojis have been set to %s", self.id, emojis)
|
||||||
|
col_users.update_one({"_id": self._id}, {"$set": {"emojis": emojis}})
|
||||||
|
|
||||||
|
def set_score(self, score: int = 0) -> None:
|
||||||
|
logger.debug("%s's score has been set to %s", self.id, score)
|
||||||
|
col_users.update_one({"_id": self._id}, {"$set": {"score": score}})
|
||||||
|
|
||||||
|
def set_mistakes(self, mistakes: int = 0) -> None:
|
||||||
|
logger.debug("%s's mistakes count has been set to %s", self.id, mistakes)
|
||||||
|
col_users.update_one({"_id": self._id}, {"$set": {"mistakes": mistakes}})
|
||||||
|
|
||||||
|
def update_score(self, points: int = 1) -> None:
|
||||||
|
logger.debug("%s point(s) have been added to %s score", points, self.id)
|
||||||
|
col_users.update_one(
|
||||||
|
{"_id": self._id}, {"$set": {"score": self.score + points}}
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_mistakes(self, points: int = 1) -> None:
|
||||||
|
logger.debug("%s point(s) have been added to %s mistakes", points, self.id)
|
||||||
|
col_users.update_one(
|
||||||
|
{"_id": self._id}, {"$set": {"mistakes": self.mistakes + points}}
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_selected(self, entry: str) -> None:
|
||||||
|
logger.debug("Emoji %s has been added to %s's selection list", entry, self.id)
|
||||||
|
col_users.update_one(
|
||||||
|
{"_id": self._id}, {"$set": {"selected": self.selected + [entry]}}
|
||||||
|
)
|
109
config_example.json
Normal file
109
config_example.json
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"locale": "en",
|
||||||
|
"bot": {
|
||||||
|
"owner": 0,
|
||||||
|
"api_id": 0,
|
||||||
|
"api_hash": "",
|
||||||
|
"bot_token": "",
|
||||||
|
"workers": 1,
|
||||||
|
"max_concurrent_transmissions": 1,
|
||||||
|
"scoped_commands": true
|
||||||
|
},
|
||||||
|
"reports": {
|
||||||
|
"chat_id": "owner"
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"user": null,
|
||||||
|
"password": null,
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 27017,
|
||||||
|
"name": "captchabot"
|
||||||
|
},
|
||||||
|
"timeouts": {
|
||||||
|
"join": 2,
|
||||||
|
"verify": 3
|
||||||
|
},
|
||||||
|
"whitelist": {
|
||||||
|
"enabled": false,
|
||||||
|
"groups": []
|
||||||
|
},
|
||||||
|
"disabled_plugins": [],
|
||||||
|
"emojis": [
|
||||||
|
"🐻",
|
||||||
|
"🐔",
|
||||||
|
"☁️",
|
||||||
|
"🔮",
|
||||||
|
"🌀",
|
||||||
|
"🌚",
|
||||||
|
"💎",
|
||||||
|
"🐶",
|
||||||
|
"🍩",
|
||||||
|
"🌏",
|
||||||
|
"🐸",
|
||||||
|
"🌕",
|
||||||
|
"🐵",
|
||||||
|
"🌙",
|
||||||
|
"🐧",
|
||||||
|
"🍎",
|
||||||
|
"😀",
|
||||||
|
"🐍",
|
||||||
|
"❄️",
|
||||||
|
"🐚",
|
||||||
|
"🐢",
|
||||||
|
"🌝",
|
||||||
|
"🍺",
|
||||||
|
"🍔",
|
||||||
|
"🍒",
|
||||||
|
"🍫",
|
||||||
|
"💣",
|
||||||
|
"🍟",
|
||||||
|
"🍑",
|
||||||
|
"🍷",
|
||||||
|
"🍧",
|
||||||
|
"🍕",
|
||||||
|
"🍵",
|
||||||
|
"🐋",
|
||||||
|
"🐱",
|
||||||
|
"💄",
|
||||||
|
"👠",
|
||||||
|
"💰",
|
||||||
|
"💸",
|
||||||
|
"🎹",
|
||||||
|
"📦",
|
||||||
|
"📍",
|
||||||
|
"🐊",
|
||||||
|
"🦕",
|
||||||
|
"💋",
|
||||||
|
"🦎",
|
||||||
|
"🦈",
|
||||||
|
"🦷",
|
||||||
|
"🦖",
|
||||||
|
"🐠",
|
||||||
|
"🐟",
|
||||||
|
"💀",
|
||||||
|
"🎃",
|
||||||
|
"👮",
|
||||||
|
"⛑️",
|
||||||
|
"🪢",
|
||||||
|
"🧶",
|
||||||
|
"🧵",
|
||||||
|
"🪡",
|
||||||
|
"🥼",
|
||||||
|
"🥻",
|
||||||
|
"🎩",
|
||||||
|
"👑",
|
||||||
|
"🎒",
|
||||||
|
"🙊",
|
||||||
|
"🐗",
|
||||||
|
"🦋",
|
||||||
|
"🦐",
|
||||||
|
"🐀",
|
||||||
|
"🎻",
|
||||||
|
"🦔",
|
||||||
|
"🦦",
|
||||||
|
"🦫",
|
||||||
|
"🦡",
|
||||||
|
"🐇"
|
||||||
|
],
|
||||||
|
"commands": {}
|
||||||
|
}
|
19
locale/en.json
Normal file
19
locale/en.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"messages": {
|
||||||
|
"verify": "Please, use the buttons below to complete the captcha. Tap on the buttons with emojis you see on this image.",
|
||||||
|
"welcome_verify": "Welcome, {mention}! In order to chat here, we need to verify you're a human. Please, press the button below to start the verification.",
|
||||||
|
"welcome": "Welcome to the chat, {mention}!"
|
||||||
|
},
|
||||||
|
"callbacks": {
|
||||||
|
"captcha_failed": "You have failed the captcha.",
|
||||||
|
"captcha_failed_force": "You have forced {user_id} to fail the captcha.",
|
||||||
|
"captcha_mistake": "Invalid answer. Remaining attempts: {remaining}.",
|
||||||
|
"captcha_passed": "You have passed the captcha. Welcome!",
|
||||||
|
"nothing": "This actions has already been finished.",
|
||||||
|
"wrong_user": "This message is not for you."
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"ban": "Ban (for admins)",
|
||||||
|
"verify": "Verify"
|
||||||
|
}
|
||||||
|
}
|
19
locale/uk.json
Normal file
19
locale/uk.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"messages": {
|
||||||
|
"verify": "Будь ласка, використовуйте кнопки нижче, щоб заповнити капчу. Натисніть на кнопки зі смайликами, які ви бачите на цьому зображенні.",
|
||||||
|
"welcome_verify": "Ласкаво просимо, {mention}! Для того, щоб спілкуватися в чаті, ми повинні переконатися, що ви людина. Будь ласка, натисніть кнопку нижче, щоб почати перевірку.",
|
||||||
|
"welcome": "Ласкаво просимо до чату, {mention}!"
|
||||||
|
},
|
||||||
|
"callbacks": {
|
||||||
|
"captcha_failed": "Ви провалили капчу.",
|
||||||
|
"captcha_failed_force": "Ви змусили {user_id} не пройти капчу.",
|
||||||
|
"captcha_mistake": "Неправильна відповідь. Спроби, що залишилися: {remaining}.",
|
||||||
|
"captcha_passed": "Ви пройшли капчу. Ласкаво просимо!",
|
||||||
|
"nothing": "Цю дію вже завершено.",
|
||||||
|
"wrong_user": "Це повідомлення не для вас."
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"ban": "Заблокувати (для адмінів)",
|
||||||
|
"verify": "Верифікуватись"
|
||||||
|
}
|
||||||
|
}
|
36
main.py
Normal file
36
main.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
from os import getpid
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules.scheduler import scheduler
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(name)s.%(funcName)s | %(levelname)s | %(message)s",
|
||||||
|
datefmt="[%X]",
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
with contextlib.suppress(ImportError):
|
||||||
|
import uvloop
|
||||||
|
|
||||||
|
uvloop.install()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
client = PyroClient(scheduler=scheduler)
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.run()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.warning("Forcefully shutting down with PID %s...", getpid())
|
||||||
|
finally:
|
||||||
|
if client.scheduler is not None:
|
||||||
|
client.scheduler.shutdown()
|
||||||
|
exit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
33
modules/database.py
Normal file
33
modules/database.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""Module that provides all database columns"""
|
||||||
|
|
||||||
|
from pymongo import MongoClient
|
||||||
|
from ujson import loads
|
||||||
|
|
||||||
|
with open("config.json", "r", encoding="utf-8") as f:
|
||||||
|
db_config = loads(f.read())["database"]
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
if db_config["user"] is not None and db_config["password"] is not None:
|
||||||
|
con_string = "mongodb://{0}:{1}@{2}:{3}/{4}".format(
|
||||||
|
db_config["user"],
|
||||||
|
db_config["password"],
|
||||||
|
db_config["host"],
|
||||||
|
db_config["port"],
|
||||||
|
db_config["name"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
con_string = "mongodb://{0}:{1}/{2}".format(
|
||||||
|
db_config["host"], db_config["port"], db_config["name"]
|
||||||
|
)
|
||||||
|
|
||||||
|
db_client = MongoClient(con_string)
|
||||||
|
db = db_client.get_database(name=db_config["name"])
|
||||||
|
|
||||||
|
collections = db.list_collection_names()
|
||||||
|
|
||||||
|
for collection in ["users", "schedule"]:
|
||||||
|
if collection not in collections:
|
||||||
|
db.create_collection(collection)
|
||||||
|
|
||||||
|
col_users = db.get_collection("users")
|
||||||
|
col_schedule = db.get_collection("schedule")
|
31
modules/kicker.py
Normal file
31
modules/kicker.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from pyrogram.types import Message
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
|
||||||
|
|
||||||
|
async def kick_unstarted(
|
||||||
|
app: PyroClient, user_id: int, group_id: int, message_id: int
|
||||||
|
) -> None:
|
||||||
|
user = await app.find_user(user_id, group_id)
|
||||||
|
|
||||||
|
if user.score == 0 and user.failed == 0:
|
||||||
|
banned = await app.ban_chat_member(group_id, user_id)
|
||||||
|
|
||||||
|
if isinstance(banned, Message):
|
||||||
|
await banned.delete()
|
||||||
|
|
||||||
|
await app.delete_messages(group_id, message_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def kick_unverified(
|
||||||
|
app: PyroClient, user_id: int, group_id: int, message_id: int
|
||||||
|
) -> None:
|
||||||
|
user = await app.find_user(user_id, group_id)
|
||||||
|
|
||||||
|
if user.score < 6 or user.failed:
|
||||||
|
banned = await app.ban_chat_member(group_id, user_id)
|
||||||
|
|
||||||
|
if isinstance(banned, Message):
|
||||||
|
await banned.delete()
|
||||||
|
|
||||||
|
await app.delete_messages(group_id, message_id)
|
3
modules/scheduler.py
Normal file
3
modules/scheduler.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
|
||||||
|
scheduler = AsyncIOScheduler()
|
51
modules/utils.py
Normal file
51
modules/utils.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
from random import randint, sample
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from huepaper import generate
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from classes.captcha import Captcha
|
||||||
|
|
||||||
|
|
||||||
|
def get_captcha_image(emojis: List[str]) -> Captcha:
|
||||||
|
emojis_all = sample(emojis, 12)
|
||||||
|
emojis_correct = sample(emojis_all, 6)
|
||||||
|
|
||||||
|
output = BytesIO()
|
||||||
|
|
||||||
|
image_options = [
|
||||||
|
(randint(400, 490), randint(90, 280), randint(105, 125)),
|
||||||
|
(randint(60, 180), randint(180, 240), randint(75, 95)),
|
||||||
|
(randint(320, 440), randint(170, 210), randint(35, 45)),
|
||||||
|
(randint(150, 240), randint(240, 320), randint(80, 105)),
|
||||||
|
(randint(350, 450), randint(280, 380), randint(40, 60)),
|
||||||
|
(randint(180, 350), randint(100, 300), randint(45, 65)),
|
||||||
|
]
|
||||||
|
|
||||||
|
base_img = generate(
|
||||||
|
width=500,
|
||||||
|
height=500,
|
||||||
|
hue_max=1.0,
|
||||||
|
lum_min=0.3,
|
||||||
|
lum_max=0.6,
|
||||||
|
sat_min=0.8,
|
||||||
|
sat_max=1.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
for options, emoji in zip(image_options, emojis_correct):
|
||||||
|
width, height, angle = options
|
||||||
|
base_img.paste(
|
||||||
|
Image.open(Path(f"assets/emojis/{emoji}.png"))
|
||||||
|
.resize((120, 120))
|
||||||
|
.rotate(angle),
|
||||||
|
((base_img.width - width), (base_img.height - height)),
|
||||||
|
Image.open(Path(f"assets/emojis/{emoji}.png"))
|
||||||
|
.resize((120, 120))
|
||||||
|
.rotate(angle),
|
||||||
|
)
|
||||||
|
|
||||||
|
base_img.save(output, format="jpeg")
|
||||||
|
|
||||||
|
return Captcha(output, emojis_all, emojis_correct)
|
43
plugins/callbacks/ban.py
Normal file
43
plugins/callbacks/ban.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.enums.chat_member_status import ChatMemberStatus
|
||||||
|
from pyrogram.types import CallbackQuery, Message
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@PyroClient.on_callback_query(filters.regex(r"ban;[\s\S]*"))
|
||||||
|
async def callback_ban(app: PyroClient, callback: CallbackQuery):
|
||||||
|
if (
|
||||||
|
await app.get_chat_member(callback.message.chat.id, callback.from_user.id)
|
||||||
|
).status not in [ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER]:
|
||||||
|
await callback.answer(app._("wrong_user", "callbacks"), show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
user = await app.find_user(
|
||||||
|
int(str(callback.data).split(";")[1]), callback.message.chat.id
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"User %s has been marked as failed the captcha by %s",
|
||||||
|
user.id,
|
||||||
|
callback.from_user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
user.set_mistakes(3)
|
||||||
|
user.set_failed(True)
|
||||||
|
await callback.answer(
|
||||||
|
app._("captcha_failed_force", "callbacks").format(user_id=user.id),
|
||||||
|
show_alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
banned = await app.ban_chat_member(callback.message.chat.id, user.id)
|
||||||
|
|
||||||
|
if isinstance(banned, Message):
|
||||||
|
await banned.delete()
|
||||||
|
|
||||||
|
await callback.message.delete()
|
||||||
|
return
|
115
plugins/callbacks/emoji_button.py
Normal file
115
plugins/callbacks/emoji_button.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.types import (
|
||||||
|
CallbackQuery,
|
||||||
|
ChatPermissions,
|
||||||
|
InlineKeyboardButton,
|
||||||
|
InlineKeyboardMarkup,
|
||||||
|
Message,
|
||||||
|
)
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@PyroClient.on_callback_query(filters.regex(r"emoji;[\s\S]*"))
|
||||||
|
async def callback_emoji_button(app: PyroClient, callback: CallbackQuery):
|
||||||
|
user_id = int(str(callback.data).split(";")[1])
|
||||||
|
emoji = str(callback.data).split(";")[2]
|
||||||
|
|
||||||
|
if callback.from_user.id != user_id:
|
||||||
|
await callback.answer(app._("wrong_user", "callbacks"), show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
user = await app.find_user(callback.from_user, callback.message.chat.id)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"User %s has pressed the %s emoji '%s'",
|
||||||
|
user.id,
|
||||||
|
"correct" if emoji in user.emojis else "wrong",
|
||||||
|
emoji,
|
||||||
|
)
|
||||||
|
|
||||||
|
if emoji in user.selected:
|
||||||
|
await callback.answer()
|
||||||
|
return
|
||||||
|
|
||||||
|
user.update_selected(emoji)
|
||||||
|
|
||||||
|
if emoji in user.emojis:
|
||||||
|
user.update_score(1)
|
||||||
|
|
||||||
|
if user.score >= 5:
|
||||||
|
logger.info("User %s has passed the captcha", user.id)
|
||||||
|
|
||||||
|
await callback.message.delete()
|
||||||
|
await callback.answer(app._("captcha_passed", "callbacks"), show_alert=True)
|
||||||
|
await app.send_message(
|
||||||
|
callback.message.chat.id,
|
||||||
|
app._(
|
||||||
|
"welcome",
|
||||||
|
"messages",
|
||||||
|
).format(mention=callback.from_user.mention),
|
||||||
|
)
|
||||||
|
await app.restrict_chat_member(
|
||||||
|
chat_id=callback.message.chat.id,
|
||||||
|
user_id=callback.from_user.id,
|
||||||
|
permissions=ChatPermissions(can_send_messages=True),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"User %s has chosen correctly and needs to select %s more emoji(s)",
|
||||||
|
user.id,
|
||||||
|
5 - user.score,
|
||||||
|
)
|
||||||
|
|
||||||
|
emoji_placeholder = "✅"
|
||||||
|
else:
|
||||||
|
if user.mistakes >= 2:
|
||||||
|
logger.info("User %s has failed the captcha", user.id)
|
||||||
|
|
||||||
|
user.set_failed(True)
|
||||||
|
await callback.answer(app._("captcha_failed", "callbacks"), show_alert=True)
|
||||||
|
|
||||||
|
banned = await app.ban_chat_member(
|
||||||
|
callback.message.chat.id, callback.from_user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(banned, Message):
|
||||||
|
await banned.delete()
|
||||||
|
|
||||||
|
await callback.message.delete()
|
||||||
|
return
|
||||||
|
|
||||||
|
user.update_mistakes(1)
|
||||||
|
logger.info(
|
||||||
|
"User %s has made a mistake and has %s attempt(s) left",
|
||||||
|
user.id,
|
||||||
|
2 - user.mistakes,
|
||||||
|
)
|
||||||
|
|
||||||
|
await callback.answer(
|
||||||
|
app._("captcha_mistake", "callbacks").format(remaining=2 - user.mistakes),
|
||||||
|
show_alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
emoji_placeholder = "🛑"
|
||||||
|
|
||||||
|
button_replace = (0, 0)
|
||||||
|
|
||||||
|
for row_index, row in enumerate(callback.message.reply_markup.inline_keyboard):
|
||||||
|
for button_index, button in enumerate(row):
|
||||||
|
if button.text == emoji:
|
||||||
|
button_replace = (row_index, button_index)
|
||||||
|
|
||||||
|
new_keyboard = callback.message.reply_markup.inline_keyboard
|
||||||
|
new_keyboard[button_replace[0]][button_replace[1]] = InlineKeyboardButton(
|
||||||
|
emoji_placeholder, "nothing"
|
||||||
|
)
|
||||||
|
|
||||||
|
await callback.edit_message_reply_markup(
|
||||||
|
reply_markup=InlineKeyboardMarkup(new_keyboard)
|
||||||
|
)
|
9
plugins/callbacks/nothing.py
Normal file
9
plugins/callbacks/nothing.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.types import CallbackQuery
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
|
||||||
|
|
||||||
|
@PyroClient.on_callback_query(filters.regex(r"nothing"))
|
||||||
|
async def callback_nothing(app: PyroClient, callback: CallbackQuery):
|
||||||
|
await callback.answer(app._("nothing", "callbacks"))
|
71
plugins/callbacks/verify.py
Normal file
71
plugins/callbacks/verify.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from apscheduler.jobstores.base import JobLookupError
|
||||||
|
from pykeyboard import InlineButton, InlineKeyboard
|
||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.types import CallbackQuery
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules.database import col_schedule
|
||||||
|
from modules.kicker import kick_unverified
|
||||||
|
from modules.utils import get_captcha_image
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@PyroClient.on_callback_query(filters.regex(r"verify;[\s\S]*"))
|
||||||
|
async def callback_verify(app: PyroClient, callback: CallbackQuery):
|
||||||
|
user_id = int(str(callback.data).split(";")[1])
|
||||||
|
|
||||||
|
if callback.from_user.id != user_id:
|
||||||
|
await callback.answer(app._("wrong_user", "callbacks"), show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
user = await app.find_user(callback.from_user, callback.message.chat.id)
|
||||||
|
captcha = get_captcha_image(app.config["emojis"])
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Captcha for %s has been generated. All: %s, Correct: %s",
|
||||||
|
user.id,
|
||||||
|
captcha.emojis_all,
|
||||||
|
captcha.emojis_correct,
|
||||||
|
)
|
||||||
|
|
||||||
|
scheduled_job = col_schedule.find_one_and_delete(
|
||||||
|
{"user": user_id, "group": callback.message.chat.id}
|
||||||
|
)
|
||||||
|
|
||||||
|
if scheduled_job is not None and app.scheduler is not None:
|
||||||
|
with contextlib.suppress(JobLookupError):
|
||||||
|
app.scheduler.remove_job(scheduled_job["job_id"])
|
||||||
|
|
||||||
|
user.set_emojis(captcha.emojis_correct)
|
||||||
|
|
||||||
|
buttons = [
|
||||||
|
InlineButton(emoji, f"emoji;{user.id};{emoji}") for emoji in captcha.emojis_all
|
||||||
|
]
|
||||||
|
|
||||||
|
keyboard = InlineKeyboard(3)
|
||||||
|
keyboard.add(*buttons)
|
||||||
|
|
||||||
|
await callback.message.delete()
|
||||||
|
|
||||||
|
captcha_message = await app.send_photo(
|
||||||
|
callback.message.chat.id,
|
||||||
|
captcha.image,
|
||||||
|
caption=app._("verify", "messages"),
|
||||||
|
reply_markup=keyboard,
|
||||||
|
)
|
||||||
|
|
||||||
|
del captcha
|
||||||
|
|
||||||
|
if app.scheduler is not None:
|
||||||
|
app.scheduler.add_job(
|
||||||
|
kick_unverified,
|
||||||
|
"date",
|
||||||
|
[app, user.id, callback.message.chat.id, captcha_message.id],
|
||||||
|
run_date=datetime.now()
|
||||||
|
+ timedelta(seconds=app.config["timeouts"]["verify"]),
|
||||||
|
)
|
108
plugins/handlers/user_join.py
Normal file
108
plugins/handlers/user_join.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import asyncio
|
||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.types import (
|
||||||
|
ChatPermissions,
|
||||||
|
InlineKeyboardButton,
|
||||||
|
InlineKeyboardMarkup,
|
||||||
|
Message,
|
||||||
|
)
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
from classes.pyrouser import PyroUser
|
||||||
|
from modules.database import col_schedule
|
||||||
|
from modules.kicker import kick_unstarted
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@PyroClient.on_message(filters.new_chat_members & ~filters.me)
|
||||||
|
async def handler_user_join(app: PyroClient, message: Message):
|
||||||
|
if (
|
||||||
|
app.config["whitelist"]["enabled"]
|
||||||
|
and message.chat.id not in app.config["whitelist"]["groups"]
|
||||||
|
):
|
||||||
|
logger.info(
|
||||||
|
"User %s has joined the group %s, but it's not whitelisted, ignoring.",
|
||||||
|
message.from_user.id,
|
||||||
|
message.chat.id,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"User %s has joined the group %s", message.from_user.id, message.chat.id
|
||||||
|
)
|
||||||
|
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
# If user has already failed the test and joined once more
|
||||||
|
with contextlib.suppress(KeyError):
|
||||||
|
user = await app.find_user(message.from_user, message.chat.id)
|
||||||
|
if user.failed is True:
|
||||||
|
logger.info(
|
||||||
|
"User %s has previously failed the captcha, kicking and banning him",
|
||||||
|
user.id,
|
||||||
|
)
|
||||||
|
banned = await app.ban_chat_member(message.chat.id, user.id)
|
||||||
|
|
||||||
|
if isinstance(banned, Message):
|
||||||
|
await banned.delete()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
await app.restrict_chat_member(
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
user_id=message.from_user.id,
|
||||||
|
permissions=ChatPermissions(can_send_messages=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
user = PyroUser.create_if_not_exists(message.from_user.id, message.chat.id)
|
||||||
|
|
||||||
|
if user.mistakes > 0 or user.score > 0:
|
||||||
|
user.set_score(0)
|
||||||
|
user.set_mistakes(0)
|
||||||
|
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
verification_request = await app.send_message(
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
text=app._(
|
||||||
|
"welcome",
|
||||||
|
"messages",
|
||||||
|
).format(mention=message.from_user.mention),
|
||||||
|
reply_markup=InlineKeyboardMarkup(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
app._(
|
||||||
|
"verify",
|
||||||
|
"buttons",
|
||||||
|
),
|
||||||
|
callback_data=f"verify;{message.from_user.id}",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
app._(
|
||||||
|
"ban",
|
||||||
|
"buttons",
|
||||||
|
),
|
||||||
|
callback_data=f"ban;{message.from_user.id}",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if app.scheduler is not None:
|
||||||
|
job = app.scheduler.add_job(
|
||||||
|
kick_unstarted,
|
||||||
|
"date",
|
||||||
|
[app, user.id, verification_request.chat.id, verification_request.id],
|
||||||
|
run_date=datetime.now() + timedelta(seconds=app.config["timeouts"]["join"]),
|
||||||
|
)
|
||||||
|
col_schedule.insert_one(
|
||||||
|
{"user": message.from_user.id, "group": message.chat.id, "job_id": job.id}
|
||||||
|
)
|
13
requirements.txt
Normal file
13
requirements.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
aiofiles~=23.1.0
|
||||||
|
apscheduler~=3.10.1
|
||||||
|
black~=23.7.0
|
||||||
|
Pillow~=10.0.0
|
||||||
|
pykeyboard==0.1.5
|
||||||
|
pymongo==4.4.1
|
||||||
|
pyrogram==2.0.106
|
||||||
|
tgcrypto==1.2.5
|
||||||
|
ujson==5.8.0
|
||||||
|
uvloop==0.17.0
|
||||||
|
--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple
|
||||||
|
huepaper==0.0.3
|
||||||
|
libbot[speed,pyrogram]==2.0.0
|
Reference in New Issue
Block a user