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