Initial commit

This commit is contained in:
Profitroll 2023-08-10 13:05:40 +02:00
commit 9c9cd96a94
Signed by: profitroll
GPG Key ID: FA35CAB49DACD3B2
21 changed files with 997 additions and 0 deletions

164
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
}

21
LICENSE Normal file
View 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.

1
README.md Normal file
View File

@ -0,0 +1 @@
# EmojiCaptchaBot

11
classes/captcha.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
from apscheduler.schedulers.asyncio import AsyncIOScheduler
scheduler = AsyncIOScheduler()

51
modules/utils.py Normal file
View 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
View 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

View 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)
)

View 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"))

View 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"]),
)

View 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
View 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