Merge branch 'dev'

This commit is contained in:
Profitroll 2022-12-16 15:14:07 +01:00
commit 09e5048d7f
34 changed files with 612 additions and 300 deletions

3
.gitignore vendored
View File

@ -163,4 +163,5 @@ TASK.md
inline_bot.py inline_bot.py
data/applications.json data/applications.json
!data/cache/avatars/.gitkeep !data/cache/avatars/.gitkeep
data/cache/avatars/* data/cache/avatars/*
.vscode

View File

@ -1,9 +1,11 @@
from os import path, sep from os import makedirs, path, sep
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, JSONResponse from fastapi.responses import FileResponse, JSONResponse
from starlette.status import HTTP_404_NOT_FOUND from starlette.status import HTTP_404_NOT_FOUND
from modules.utils import configGet from modules.utils import configGet
makedirs(f'{configGet("cache", "locations")}{sep}avatars', exist_ok=True)
app = FastAPI(title="HoloUA Avatars API", docs_url=None, redoc_url=None, version="1.0") app = FastAPI(title="HoloUA Avatars API", docs_url=None, redoc_url=None, version="1.0")
@app.get("/check", response_class=JSONResponse, include_in_schema=False) @app.get("/check", response_class=JSONResponse, include_in_schema=False)

2
app.py
View File

@ -4,7 +4,7 @@ from pyrogram.client import Client
app = Client("holochecker", bot_token=configGet("bot_token", "bot"), api_id=configGet("api_id", "bot"), api_hash=configGet("api_hash", "bot")) app = Client("holochecker", bot_token=configGet("bot_token", "bot"), api_id=configGet("api_id", "bot"), api_hash=configGet("api_hash", "bot"))
async def isAnAdmin(admin_id): async def isAnAdmin(admin_id):
if admin_id == configGet("owner") or admin_id in configGet("admins"): if (admin_id == configGet("owner")) or (admin_id in configGet("admins")):
return True return True
async for member in app.get_chat_members(configGet("admin_group")): async for member in app.get_chat_members(configGet("admin_group")):
if member.user.id == admin_id: if member.user.id == admin_id:

0
cache/.gitkeep vendored
View File

View File

105
classes/holo_user.py Normal file
View File

@ -0,0 +1,105 @@
from app import app, isAnAdmin
from typing import Any, List, Union
from pyrogram.types import User, ChatMember, ChatPrivileges, Chat, Message
from pyrogram.client import Client
from pyrogram.errors import bad_request_400
from modules.database import col_users, col_context, col_warnings, col_applications, col_sponsorships, col_messages
from modules.logging import logWrite
from modules.utils import configGet, locale, should_quote
class UserNotFoundError(Exception):
"""HoloUser could not find user with such an ID in database"""
def __init__(self, user, user_id):
self.user = user
self.user_id = user_id
super().__init__(f"User of type {type(self.user)} with id {self.user_id} was not found")
class UserInvalidError(Exception):
"""Provided to HoloUser object is not supported"""
def __init__(self, user):
self.user = user
super().__init__(f"Could not find HoloUser by using {type(self.user)} as an input type")
class HoloUser():
def __init__(self, user: Union[List[User], User, ChatMember, int, str]) -> None:
# Determine input object class and extract id
if isinstance(user, list) and len(user) != 0:
self.id = user[0].id
elif isinstance(user, User):
self.id = user.id
elif isinstance(user, ChatMember):
self.id = user.user.id
elif isinstance(user, int):
self.id = user
elif isinstance(user, str):
try:
self.id = (app.get_users(user)).id # this line requires testing though
except bad_request_400.UsernameNotOccupied:
raise UserInvalidError(user)
except bad_request_400.PeerIdInvalid:
raise UserInvalidError(user)
else:
raise UserInvalidError(user)
# Find user record in DB
holo_user = col_users.find_one({"user": self.id})
if holo_user is None:
raise UserNotFoundError(user=user, user_id=self.id)
self.db_id = holo_user["_id"]
self.link = holo_user["link"]
self.label = holo_user["label"]
self.name = holo_user["tg_name"]
self.phone = holo_user["tg_phone"]
self.locale = holo_user["tg_locale"]
self.username = holo_user["tg_username"]
def set(self, key: str, value: Any) -> None:
"""Set attribute data and save it into database
### Args:
* `key` (`str`): Attribute to be changed
* `value` (`Any`): Value to set
"""
if not hasattr(self, key):
raise AttributeError()
setattr(self, key, value)
col_users.update_one(filter={"_id": self.db_id}, update={ "$set": { key: value } }, upsert=True)
logWrite(f"Set attribute {key} of user {self.id} to {value}")
async def message(self, origin: Message, text: Union[str, None] = None, photo: Union[str, None] = None, video: Union[str, None] = None, file: Union[str, None] = None):
new_message = await app.send_message(self.id, text+locale("message_reply_notice", "message"))
await origin.reply_text(locale("message_sent", "message"), quote=should_quote(origin))
logWrite(f"Admin {origin.from_user.id} sent message '{' '.join(origin.command[2:])}' to {self.id}")
col_messages.insert_one({"origin": {"chat": origin.chat.id, "id": origin.id}, "destination": {"chat": new_message.chat.id, "id": new_message.id}})
async def set_label(self, chat: Chat, label: str):
"""Set label in destination group
### Args:
* app (`Client`): Pyrogram client
* label (`str`): Label you want to set
"""
self.label = label
self.set("label", label)
await app.promote_chat_member(configGet("destination_group"), self.id)
if (not await isAnAdmin(self.id)) and (chat.id == configGet("admin_group")):
await app.set_administrator_title(configGet("destination_group"), self.id, label)
async def reset_label(self, chat: Chat):
"""Reset label in destination group
### Args:
* app (`Client`): Pyrogram client
"""
self.label = ""
self.set("label", "")
await app.set_administrator_title(configGet("destination_group"), self.id, "")
if (not await isAnAdmin(self.id)) and (chat.id == configGet("admin_group")):
await app.promote_chat_member(configGet("destination_group"), self.id, privileges=ChatPrivileges(
can_manage_chat=False
))

View File

@ -4,8 +4,6 @@
"owner": 0, "owner": 0,
"bot_id": 0, "bot_id": 0,
"age_allowed": 0, "age_allowed": 0,
"birthdays_notify": true,
"birthdays_time": "09:00",
"api": "http://example.com", "api": "http://example.com",
"inline_preview_count": 7, "inline_preview_count": 7,
"admin_group": 0, "admin_group": 0,
@ -17,12 +15,28 @@
"api_hash": "", "api_hash": "",
"bot_token": "" "bot_token": ""
}, },
"database": {
"user": null,
"password": null,
"host": "127.0.0.1",
"port": 27017,
"name": "holochecker"
},
"logging": { "logging": {
"size": 512, "size": 512,
"location": "logs" "location": "logs"
}, },
"scheduler": {
"birthdays": {
"time": 9,
"enabled": true
},
"sponsorships": {
"time": 9,
"enabled": true
}
},
"locations": { "locations": {
"data": "data",
"cache": "cache", "cache": "cache",
"locale": "locale" "locale": "locale"
}, },
@ -33,14 +47,16 @@
}, },
"commands_admin": { "commands_admin": {
"reboot": "Restart the bot", "reboot": "Restart the bot",
"message": "Send a message", "message": "Send a message",
"label": "Set user's nickname",
"warnings": "Check user's warnings", "warnings": "Check user's warnings",
"application": "Check user's application", "application": "Check user's application",
"applications": "Retrieve all applications as a JSON" "applications": "Retrieve all applications as a JSON"
}, },
"commands_group_admin": { "commands_group_admin": {
"reboot": "Restart the bot", "reboot": "Restart the bot",
"message": "Send a message", "message": "Send a message",
"label": "Set user's nickname",
"warnings": "Check user's warnings", "warnings": "Check user's warnings",
"application": "Check user's application", "application": "Check user's application",
"applications": "Retrieve all applications as a JSON" "applications": "Retrieve all applications as a JSON"

View File

View File

@ -1,9 +0,0 @@
{
"filling": false,
"applied": false,
"approved": false,
"stage": 0,
"paid": null,
"expires": null,
"nickname": null
}

View File

View File

@ -1,25 +0,0 @@
{
"stage": 0,
"reapply": false,
"link": null,
"sent": false,
"confirmed": false,
"approved": false,
"refused": false,
"telegram_id": null,
"telegram_name": null,
"telegram_phone": null,
"telegram_locale": null,
"application": {
"1": null,
"2": null,
"3": null,
"4": null,
"5": null,
"6": null,
"7": null,
"8": null,
"9": null,
"10": null
}
}

View File

86
holochecker.py Normal file
View File

@ -0,0 +1,86 @@
from os import getpid, makedirs
from modules.utils import *
from modules.inline import *
from app import app
from modules.commands_register import commands_register
from pyrogram import idle
pid = getpid()
makedirs(f'{configGet("cache", "locations")}{sep}avatars', exist_ok=True)
# Importing
from modules.commands.application import *
from modules.commands.applications import *
from modules.commands.label import *
from modules.commands.message import *
from modules.commands.reapply import *
from modules.commands.reboot import *
from modules.commands.rules import *
from modules.commands.sponsorship import *
from modules.commands.start import *
from modules.commands.warn import *
from modules.commands.warnings import *
from modules.callbacks.nothing import *
from modules.callbacks.reapply import *
from modules.callbacks.rules import *
from modules.callbacks.sub import *
from modules.callbacks.sus import *
from modules.handlers.confirmation import *
from modules.handlers.contact import *
from modules.handlers.group_join import *
from modules.handlers.welcome import *
from modules.handlers.everything import *
from modules.scheduled import *
if __name__ == "__main__":
logWrite(f"Starting up with pid {pid}")
# Yes, it should be in some kind of async main() function but I don't give a shit.
# I did compare performance, almost no difference and it's much more useful this way. Change my mind.
app.start()
# if configGet("birthdays_notify"):
# every().day.at(configGet("birthdays_time")).do(check_birthdays, app)
# # Background tasks checker
# def background_task():
# try:
# while True:
# try:
# run_pending()
# #print('Checked')
# time.sleep(1)
# except:
# pass
# except KeyboardInterrupt:
# print('\nShutting down')
# killProc(pid)
# t = Thread(target=background_task)
# t.start()
try:
app.send_message(configGet("owner"), f"Starting up with pid `{pid}`")
except bad_request_400.PeerIdInvalid:
logWrite(f"Could not send startup message to bot owner. Perhaps user has not started the bot yet.")
commands_register(app)
scheduler.start()
idle()
try:
app.send_message(configGet("owner"), f"Shutting with pid `{pid}`")
except bad_request_400.PeerIdInvalid:
logWrite(f"Could not send shutdown message to bot owner. Perhaps user has not started the bot yet.")
app.stop()
killProc(pid)

View File

95
main.py
View File

@ -1,95 +0,0 @@
from threading import Thread
from time import time
from os import getpid, path
from modules.birthdays import check_birthdays
from modules.utils import *
from modules.inline import *
from schedule import run_pending, every
from app import app
from modules.commands_register import commands_register
from pyrogram import idle
pid = getpid()
for entry in [f"{configGet('data', 'locations')}{sep}applications.json", f"{configGet('data', 'locations')}{sep}warnings.json"]:
mode = 'r' if path.exists(entry) else 'w'
with open(entry, mode) as f:
try:
f.write("{}")
except:
pass
for entry in [f"{configGet('data', 'locations')}{sep}messages.json"]:
mode = 'r' if path.exists(entry) else 'w'
with open(entry, mode) as f:
try:
f.write("[]")
except:
pass
# Importing
from modules.commands.application import *
from modules.commands.applications import *
from modules.commands.message import *
from modules.commands.reapply import *
from modules.commands.reboot import *
from modules.commands.rules import *
from modules.commands.sponsorship import *
from modules.commands.start import *
from modules.commands.warn import *
from modules.commands.warnings import *
from modules.callbacks.nothing import *
from modules.callbacks.reapply import *
from modules.callbacks.rules import *
from modules.callbacks.sub import *
from modules.callbacks.sus import *
from modules.handlers.confirmation import *
from modules.handlers.contact import *
from modules.handlers.group_join import *
from modules.handlers.welcome import *
from modules.handlers.everything import *
if __name__ == "__main__":
logWrite(f"Starting up with pid {pid}")
# Yes, it should be in some kind of async main() function but I don't give a shit.
# I did compare performance, almost no difference and it's much more useful this way. Change my mind.
app.start()
if configGet("birthdays_notify"):
every().day.at(configGet("birthdays_time")).do(check_birthdays, app)
# Background tasks checker
def background_task():
try:
while True:
try:
run_pending()
#print('Checked')
time.sleep(1)
except:
pass
except KeyboardInterrupt:
print('\nShutting down')
killProc(pid)
t = Thread(target=background_task)
t.start()
app.send_message(configGet("owner"), f"Starting up with pid `{pid}`")
commands_register(app)
idle()
app.send_message(configGet("owner"), f"Shutting with pid `{pid}`")
app.stop()
killProc(pid)

View File

@ -1,18 +0,0 @@
from datetime import datetime
from os import fsdecode, listdir, sep
from modules.utils import configGet, jsonLoad, locale
from dateutil.relativedelta import relativedelta
def check_birthdays(app):
for user_file in listdir(f"{configGet('data', 'locations')}{sep}users{sep}"):
filename = fsdecode(f"{configGet('data', 'locations')}{sep}users{sep}{user_file}")
if filename.endswith(".json"):
user = jsonLoad(filename)
if isinstance(user["application"]["2"], str):
try:
if ".".join([((user["application"]["2"]).split("."))[0], ((user["application"]["2"]).split("."))[1]]) == datetime.now().strftime("%d.%m"):
tg_user = app.get_users(int(user_file.replace(".json", "")))
app.send_message( configGet("admin_group"), locale("birthday", "message").format(str(tg_user.first_name), str(tg_user.username), str(relativedelta(datetime.now(), datetime.strptime(user["application"]["2"], '%d.%m.%Y')).years)) )
except AttributeError:
continue

View File

@ -1,8 +1,10 @@
from os import sep from os import sep, makedirs, remove
from uuid import uuid1
from app import app, isAnAdmin from app import app, isAnAdmin
from pyrogram import filters from pyrogram import filters
from pyrogram.enums.chat_action import ChatAction from pyrogram.enums.chat_action import ChatAction
from modules.utils import configGet, should_quote from modules.utils import configGet, should_quote, jsonSave
from modules.database import col_applications
# Applications command ========================================================================================================= # Applications command =========================================================================================================
@app.on_message(~ filters.scheduled & filters.command(["applications"], prefixes=["/"])) @app.on_message(~ filters.scheduled & filters.command(["applications"], prefixes=["/"]))
@ -10,5 +12,12 @@ async def cmd_applications(app, msg):
if (await isAnAdmin(msg.from_user.id)) or (msg.chat.id == configGet("admin_group")): if (await isAnAdmin(msg.from_user.id)) or (msg.chat.id == configGet("admin_group")):
await app.send_chat_action(msg.chat.id, ChatAction.UPLOAD_DOCUMENT) await app.send_chat_action(msg.chat.id, ChatAction.UPLOAD_DOCUMENT)
await msg.reply_document(document=f"{configGet('data', 'locations')}{sep}applications.json", quote=should_quote(msg)) filename = uuid1()
output = []
for entry in col_applications.find():
output.append(entry)
makedirs("tmp", exist_ok=True)
jsonSave(output, f"tmp{sep}{filename}.json")
await msg.reply_document(document=f"tmp{sep}{filename}.json", file_name="applications", quote=should_quote(msg))
remove(f"tmp{sep}{filename}.json")
# ============================================================================================================================== # ==============================================================================================================================

33
modules/commands/label.py Normal file
View File

@ -0,0 +1,33 @@
from app import app, isAnAdmin
from pyrogram import filters
from pyrogram.types import ChatPrivileges
from modules.utils import should_quote, find_user, configGet
from classes.holo_user import HoloUser
@app.on_message(~ filters.scheduled & filters.private & filters.command(["label"], prefixes=["/"]))
async def cmd_label(app, msg):
if msg.chat.id == configGet("admin_group") or await isAnAdmin(msg.from_user.id):
if len(msg.command) < 3:
await msg.reply_text("Invalid syntax:\n`/label USER LABEL`")
return
target = await find_user(app, msg.command[1])
if target is not None:
target = HoloUser(target)
label = " ".join(msg.command[2:])
if label.lower() == "reset":
await target.reset_label(msg.chat)
await msg.reply_text(f"Resetting **{target.id}**'s label...", quote=should_quote(msg))
else:
await target.set_label(msg.chat, label)
await msg.reply_text(f"Setting **{target.id}**'s label to **{label}**...", quote=should_quote(msg))
else:
await msg.reply_text(f"User not found")

View File

@ -2,7 +2,9 @@ from os import sep
from app import app, isAnAdmin from app import app, isAnAdmin
from pyrogram import filters from pyrogram import filters
from pyrogram.errors import bad_request_400 from pyrogram.errors import bad_request_400
from classes.holo_user import HoloUser
from modules.utils import jsonLoad, jsonSave, logWrite, locale, configGet, should_quote from modules.utils import jsonLoad, jsonSave, logWrite, locale, configGet, should_quote
from modules.database import col_messages
# Message command ============================================================================================================== # Message command ==============================================================================================================
@app.on_message(~ filters.scheduled & filters.command(["message"], prefixes=["/"])) @app.on_message(~ filters.scheduled & filters.command(["message"], prefixes=["/"]))
@ -11,36 +13,25 @@ async def cmd_message(app, msg):
if msg.chat.id == configGet("admin_group") or await isAnAdmin(msg.from_user.id): if msg.chat.id == configGet("admin_group") or await isAnAdmin(msg.from_user.id):
try: try:
try: try:
destination = await app.get_users(int(msg.command[1])) destination = HoloUser(int(msg.command[1]))
if destination == [] or destination == None:
raise TypeError
except TypeError:
try:
destination = await app.get_users(msg.command[1])
except bad_request_400.UsernameNotOccupied:
await msg.reply_text(locale("message_no_user", "message"), quote=should_quote(msg))
logWrite(f"Admin {msg.from_user.id} tried to send message '{' '.join(msg.command[2:])}' to '{msg.command[1]}' but 'UsernameNotOccupied'")
return
except ValueError: except ValueError:
try: destination = HoloUser(msg.command[1])
destination = await app.get_users(msg.command[1])
except bad_request_400.UsernameNotOccupied:
await msg.reply_text(locale("message_no_user", "message"), quote=should_quote(msg))
logWrite(f"Admin {msg.from_user.id} tried to send message '{' '.join(msg.command[2:])}' to '{msg.command[1]}' but 'UsernameNotOccupied'")
return
void = msg.command[2] void = msg.command[2]
message = " ".join(msg.command[2:]) message = " ".join(msg.command[2:])
try:
new_message = await app.send_message(destination.id, message+locale("message_reply_notice", "message")) await destination.message(msg, msg.command[2:])
await msg.reply_text(locale("message_sent", "message"), quote=should_quote(msg))
logWrite(f"Admin {msg.from_user.id} sent message '{' '.join(msg.command[2:])}' to {destination.id}") # try:
messages = jsonLoad(f"{configGet('data', 'locations')}{sep}messages.json") # new_message = await app.send_message(destination.id, message+locale("message_reply_notice", "message"))
messages.append({"origin": {"chat": msg.chat.id, "id": msg.id}, "destination": {"chat": new_message.chat.id, "id": new_message.id}}) # await msg.reply_text(locale("message_sent", "message"), quote=should_quote(msg))
jsonSave(messages, f"{configGet('data', 'locations')}{sep}messages.json") # logWrite(f"Admin {msg.from_user.id} sent message '{' '.join(msg.command[2:])}' to {destination.id}")
except bad_request_400.PeerIdInvalid: # col_messages.insert_one({"origin": {"chat": msg.chat.id, "id": msg.id}, "destination": {"chat": new_message.chat.id, "id": new_message.id}})
await msg.reply_text(locale("message_no_user", "message"), quote=should_quote(msg)) # except bad_request_400.PeerIdInvalid:
logWrite(f"Admin {msg.from_user.id} tried to send message '{' '.join(msg.command[2:])}' to {destination.id} but 'PeerIdInvalid'") # await msg.reply_text(locale("message_no_user", "message"), quote=should_quote(msg))
# logWrite(f"Admin {msg.from_user.id} tried to send message '{' '.join(msg.command[2:])}' to {destination.id} but 'PeerIdInvalid'")
except IndexError: except IndexError:
await msg.reply_text(locale("message_invalid_syntax", "message"), quote=should_quote(msg)) await msg.reply_text(locale("message_invalid_syntax", "message"), quote=should_quote(msg))
logWrite(f"Admin {msg.from_user.id} tried to send message but 'IndexError'") logWrite(f"Admin {msg.from_user.id} tried to send message but 'IndexError'")

View File

@ -1,7 +1,9 @@
from app import app, isAnAdmin from app import app, isAnAdmin
from os import getpid from os import getpid
from sys import exit
from pyrogram import filters from pyrogram import filters
from modules.utils import configGet, logWrite, killProc, should_quote from modules.utils import configGet, logWrite, should_quote
from modules.scheduled import scheduler
pid = getpid() pid = getpid()
@ -12,5 +14,6 @@ async def cmd_kill(app, msg):
if msg.chat.id == configGet("admin_group") or await isAnAdmin(msg.from_user.id): if msg.chat.id == configGet("admin_group") or await isAnAdmin(msg.from_user.id):
logWrite(f"Shutting down bot with pid {pid}") logWrite(f"Shutting down bot with pid {pid}")
await msg.reply_text(f"Вимкнення бота з підом `{pid}`", quote=should_quote(msg)) await msg.reply_text(f"Вимкнення бота з підом `{pid}`", quote=should_quote(msg))
killProc(pid) scheduler.shutdown()
exit()
# ============================================================================================================================== # ==============================================================================================================================

View File

@ -2,24 +2,27 @@ from app import app
from os import sep from os import sep
from pyrogram import filters from pyrogram import filters
from pyrogram.types import ReplyKeyboardMarkup from pyrogram.types import ReplyKeyboardMarkup
from modules.utils import jsonLoad, jsonSave, configGet, configSet, locale, logWrite from modules.utils import locale, logWrite
from modules.database import col_users
# Start command ================================================================================================================ # Start command ================================================================================================================
@app.on_message(~ filters.scheduled & filters.private & filters.command(["start"], prefixes=["/"])) @app.on_message(~ filters.scheduled & filters.private & filters.command(["start"], prefixes=["/"]))
async def cmd_start(app, msg): async def cmd_start(app, msg):
try: user = col_users.find_one({"user": msg.from_user.id})
user_stage = configGet("stage", file=str(msg.from_user.id))
if user_stage != 0:
return
except FileNotFoundError:
jsonSave(jsonLoad(f"{configGet('data', 'locations')}{sep}user_default.json"), f"{configGet('data', 'locations')}{sep}users{sep}{msg.from_user.id}.json")
user_stage = configGet("stage", file=str(msg.from_user.id))
configSet(["telegram_id"], str(msg.from_user.username), file=str(msg.from_user.id))
configSet(["telegram_name"], f"{msg.from_user.first_name} {msg.from_user.last_name}", file=str(msg.from_user.id))
configSet(["telegram_phone"], str(msg.from_user.phone_number), file=str(msg.from_user.id))
configSet(["telegram_locale"], str(msg.from_user.language_code), file=str(msg.from_user.id))
logWrite(f"User {msg.from_user.id} started bot interaction") if user is None:
await msg.reply_text(locale("start", "message"), reply_markup=ReplyKeyboardMarkup(locale("welcome", "keyboard"), resize_keyboard=True))
col_users.insert_one({
"user": msg.from_user.id,
"link": None,
"label": "",
"tg_name": msg.from_user.first_name,
"tg_phone": msg.from_user.phone_number,
"tg_locale": msg.from_user.language_code,
"tg_username": msg.from_user.username
})
logWrite(f"User {msg.from_user.id} started bot interaction")
await msg.reply_text(locale("start", "message"), reply_markup=ReplyKeyboardMarkup(locale("welcome", "keyboard"), resize_keyboard=True))
# ============================================================================================================================== # ==============================================================================================================================

View File

@ -1,3 +1,4 @@
from modules.logging import logWrite
from modules.utils import configGet from modules.utils import configGet
from pyrogram.types import BotCommand, BotCommandScopeChat from pyrogram.types import BotCommand, BotCommandScopeChat
from pyrogram.errors import bad_request_400 from pyrogram.errors import bad_request_400
@ -24,16 +25,25 @@ def commands_register(app):
except bad_request_400.PeerIdInvalid: except bad_request_400.PeerIdInvalid:
pass pass
app.set_bot_commands(commands_admin_list, scope=BotCommandScopeChat(chat_id=configGet("owner"))) try:
app.set_bot_commands(commands_admin_list, scope=BotCommandScopeChat(chat_id=configGet("owner")))
except bad_request_400.PeerIdInvalid:
logWrite(f"Could not register commands for bot owner. Perhaps user has not started the bot yet.")
# Registering admin group commands # Registering admin group commands
commands_group_admin_list = [] commands_group_admin_list = []
for command in configGet("commands_group_admin"): for command in configGet("commands_group_admin"):
commands_group_admin_list.append(BotCommand(command, configGet("commands_group_admin")[command])) commands_group_admin_list.append(BotCommand(command, configGet("commands_group_admin")[command]))
app.set_bot_commands(commands_group_admin_list, scope=BotCommandScopeChat(chat_id=configGet("admin_group"))) try:
app.set_bot_commands(commands_group_admin_list, scope=BotCommandScopeChat(chat_id=configGet("admin_group")))
except bad_request_400.ChannelInvalid:
logWrite(f"Could not register commands for admin group. Bot is likely not in the group.")
# Registering destination group commands # Registering destination group commands
commands_group_destination_list = [] commands_group_destination_list = []
for command in configGet("commands_group_destination"): for command in configGet("commands_group_destination"):
commands_group_destination_list.append(BotCommand(command, configGet("commands_group_destination")[command])) commands_group_destination_list.append(BotCommand(command, configGet("commands_group_destination")[command]))
app.set_bot_commands(commands_group_destination_list, scope=BotCommandScopeChat(chat_id=configGet("destination_group"))) try:
app.set_bot_commands(commands_group_destination_list, scope=BotCommandScopeChat(chat_id=configGet("destination_group")))
except bad_request_400.ChannelInvalid:
logWrite(f"Could not register commands for destination group. Bot is likely not in the group.")

37
modules/database.py Normal file
View File

@ -0,0 +1,37 @@
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", "context", "messages", "warnings", "applications", "sponsorships"]:
if not collection in collections:
db.create_collection(collection)
col_users = db.get_collection("users")
col_context = db.get_collection("context")
col_messages = db.get_collection("messages")
col_warnings = db.get_collection("warnings")
col_applications = db.get_collection("applications")
col_sponsorships = db.get_collection("sponsorships")

View File

@ -5,95 +5,93 @@ import asyncio
from pyrogram import filters from pyrogram import filters
from pyrogram.types import ForceReply, ReplyKeyboardMarkup, Message from pyrogram.types import ForceReply, ReplyKeyboardMarkup, Message
from modules.utils import configGet, configSet, jsonLoad, jsonSave, locale, logWrite, should_quote from modules.utils import configGet, configSet, jsonLoad, jsonSave, locale, logWrite, should_quote
from modules.database import col_messages
async def message_involved(msg: Message): async def message_involved(msg: Message) -> bool:
messages = jsonLoad(f"{configGet('data', 'locations')}{sep}messages.json") message = col_messages.find_one({"destination.id": msg.reply_to_message.id, "destination.chat": msg.reply_to_message.chat.id})
for message in messages: if message is not None:
if (message["destination"]["id"] == msg.reply_to_message.id) and (message["destination"]["chat"] == msg.reply_to_message.chat.id): return True
return True
return False return False
async def message_context(msg: Message): async def message_context(msg: Message) -> tuple:
messages = jsonLoad(f"{configGet('data', 'locations')}{sep}messages.json") message = col_messages.find_one({"destination.id": msg.reply_to_message.id, "destination.chat": msg.reply_to_message.chat.id})
for message in messages: if message is not None:
if (message["destination"]["id"] == msg.reply_to_message.id) and (message["destination"]["chat"] == msg.reply_to_message.chat.id): return message["origin"]["chat"], message["origin"]["id"]
return message["origin"]["chat"], message["origin"]["id"] return 0, 0
# Any other input ============================================================================================================== # Any other input ==============================================================================================================
@app.on_message(~ filters.scheduled & filters.private) # @app.on_message(~ filters.scheduled & filters.private)
async def any_stage(app, msg): # async def any_stage(app, msg):
if msg.via_bot is None: # if msg.via_bot is None:
if (msg.reply_to_message != None) and (await message_involved(msg)): # if (msg.reply_to_message != None) and (await message_involved(msg)):
context = await message_context(msg) # context = await message_context(msg)
if msg.chat.id == configGet("admin_group") or await isAnAdmin(msg.from_user.id): # if msg.chat.id == configGet("admin_group") or await isAnAdmin(msg.from_user.id):
new_message = await (await app.get_messages(context[0], context[1])).reply_text(msg.text+locale("message_reply_notice", "message"), quote=True) # new_message = await (await app.get_messages(context[0], context[1])).reply_text(msg.text+locale("message_reply_notice", "message"), quote=True)
else: # else:
new_message = await (await app.get_messages(context[0], context[1])).reply_text(locale("message_from", "message").format(msg.from_user.first_name, msg.from_user.id)+msg.text+locale("message_reply_notice", "message"), quote=True) # new_message = await (await app.get_messages(context[0], context[1])).reply_text(locale("message_from", "message").format(msg.from_user.first_name, msg.from_user.id)+msg.text+locale("message_reply_notice", "message"), quote=True)
await msg.reply_text(locale("message_sent", "message"), quote=should_quote(msg)) # await msg.reply_text(locale("message_sent", "message"), quote=should_quote(msg))
messages = jsonLoad(f"{configGet('data', 'locations')}{sep}messages.json") # col_messages.insert_one({"origin": {"chat": msg.chat.id, "id": msg.id}, "destination": {"chat": new_message.chat.id, "id": new_message.id}})
messages.append({"origin": {"chat": msg.chat.id, "id": msg.id}, "destination": {"chat": new_message.chat.id, "id": new_message.id}}) # return
jsonSave(messages, f"{configGet('data', 'locations')}{sep}messages.json")
return
user_stage = configGet("stage", file=str(msg.from_user.id)) # user_stage = configGet("stage", file=str(msg.from_user.id))
if user_stage == 1: # if user_stage == 1:
await msg.reply_text(locale(f"question{user_stage+1}", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage+1}", "force_reply")))) # await msg.reply_text(locale(f"question{user_stage+1}", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage+1}", "force_reply"))))
logWrite(f"User {msg.from_user.id} completed stage {user_stage} of application") # logWrite(f"User {msg.from_user.id} completed stage {user_stage} of application")
configSet(["application", str(user_stage)], str(msg.text), file=str(msg.from_user.id)) # configSet(["application", str(user_stage)], str(msg.text), file=str(msg.from_user.id))
configSet(["stage"], user_stage+1, file=str(msg.from_user.id)) # configSet(["stage"], user_stage+1, file=str(msg.from_user.id))
elif user_stage == 2: # elif user_stage == 2:
try: # try:
configSet(["application", str(user_stage)], str(msg.text), file=str(msg.from_user.id)) # configSet(["application", str(user_stage)], str(msg.text), file=str(msg.from_user.id))
input_dt = datetime.strptime(msg.text, "%d.%m.%Y") # input_dt = datetime.strptime(msg.text, "%d.%m.%Y")
if datetime.now() <= input_dt: # if datetime.now() <= input_dt:
logWrite(f"User {msg.from_user.id} failed stage {user_stage} due to joking") # logWrite(f"User {msg.from_user.id} failed stage {user_stage} due to joking")
await msg.reply_text(locale("question2_joke", "message"), reply_markup=ForceReply(placeholder=str(locale("question2", "force_reply")))) # await msg.reply_text(locale("question2_joke", "message"), reply_markup=ForceReply(placeholder=str(locale("question2", "force_reply"))))
elif ((datetime.now() - input_dt).days) < ((datetime.now() - datetime.now().replace(year=datetime.now().year - configGet("age_allowed"))).days): # elif ((datetime.now() - input_dt).days) < ((datetime.now() - datetime.now().replace(year=datetime.now().year - configGet("age_allowed"))).days):
logWrite(f"User {msg.from_user.id} failed stage {user_stage} due to being underage") # logWrite(f"User {msg.from_user.id} failed stage {user_stage} due to being underage")
await msg.reply_text(locale("question2_underage", "message").format(str(configGet("age_allowed"))), reply_markup=ForceReply(placeholder=str(locale("question2", "force_reply")))) # await msg.reply_text(locale("question2_underage", "message").format(str(configGet("age_allowed"))), reply_markup=ForceReply(placeholder=str(locale("question2", "force_reply"))))
else: # else:
logWrite(f"User {msg.from_user.id} completed stage {user_stage} of application") # logWrite(f"User {msg.from_user.id} completed stage {user_stage} of application")
await msg.reply_text(locale(f"question{user_stage+1}", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage+1}", "force_reply")))) # await msg.reply_text(locale(f"question{user_stage+1}", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage+1}", "force_reply"))))
configSet(["stage"], user_stage+1, file=str(msg.from_user.id)) # configSet(["stage"], user_stage+1, file=str(msg.from_user.id))
except ValueError: # except ValueError:
logWrite(f"User {msg.from_user.id} failed stage {user_stage} due to sending invalid date format") # logWrite(f"User {msg.from_user.id} failed stage {user_stage} due to sending invalid date format")
await msg.reply_text(locale(f"question2_invalid", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage}", "force_reply")))) # await msg.reply_text(locale(f"question2_invalid", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage}", "force_reply"))))
else: # else:
if user_stage <= 9: # if user_stage <= 9:
logWrite(f"User {msg.from_user.id} completed stage {user_stage} of application") # logWrite(f"User {msg.from_user.id} completed stage {user_stage} of application")
await msg.reply_text(locale(f"question{user_stage+1}", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage+1}", "force_reply")))) # await msg.reply_text(locale(f"question{user_stage+1}", "message"), reply_markup=ForceReply(placeholder=str(locale(f"question{user_stage+1}", "force_reply"))))
configSet(["application", str(user_stage)], str(msg.text), file=str(msg.from_user.id)) # configSet(["application", str(user_stage)], str(msg.text), file=str(msg.from_user.id))
configSet(["stage"], user_stage+1, file=str(msg.from_user.id)) # configSet(["stage"], user_stage+1, file=str(msg.from_user.id))
else: # else:
if not configGet("sent", file=str(msg.from_user.id)): # if not configGet("sent", file=str(msg.from_user.id)):
if not configGet("confirmed", file=str(msg.from_user.id)): # if not configGet("confirmed", file=str(msg.from_user.id)):
configSet(["application", str(user_stage)], str(msg.text), file=str(msg.from_user.id)) # configSet(["application", str(user_stage)], str(msg.text), file=str(msg.from_user.id))
application_content = [] # application_content = []
i = 1 # i = 1
for question in configGet("application", file=str(msg.from_user.id)): # for question in configGet("application", file=str(msg.from_user.id)):
application_content.append(f"{locale('question'+str(i), 'message', 'question_titles')} {configGet('application', file=str(msg.from_user.id))[question]}") # application_content.append(f"{locale('question'+str(i), 'message', 'question_titles')} {configGet('application', file=str(msg.from_user.id))[question]}")
i += 1 # i += 1
await msg.reply_text(locale("confirm", "message").format("\n".join(application_content)), reply_markup=ReplyKeyboardMarkup(locale("confirm", "keyboard"), resize_keyboard=True)) # await msg.reply_text(locale("confirm", "message").format("\n".join(application_content)), reply_markup=ReplyKeyboardMarkup(locale("confirm", "keyboard"), resize_keyboard=True))
#configSet("sent", True, file=str(msg.from_user.id)) # #configSet("sent", True, file=str(msg.from_user.id))
#configSet("application_date", int(time()), file=str(msg.from_user.id)) # #configSet("application_date", int(time()), file=str(msg.from_user.id))
else: # else:
if not configGet("approved", file=str(msg.from_user.id)) and not configGet("refused", file=str(msg.from_user.id)): # if not configGet("approved", file=str(msg.from_user.id)) and not configGet("refused", file=str(msg.from_user.id)):
await msg.reply_text(locale("already_sent", "message")) # await msg.reply_text(locale("already_sent", "message"))
else: # else:
if not configGet("approved", file=str(msg.from_user.id)) and not configGet("refused", file=str(msg.from_user.id)): # if not configGet("approved", file=str(msg.from_user.id)) and not configGet("refused", file=str(msg.from_user.id)):
await msg.reply_text(locale("already_sent", "message")) # await msg.reply_text(locale("already_sent", "message"))
@app.on_message(~ filters.scheduled & filters.group) @app.on_message(~ filters.scheduled & filters.group)
async def message_in_group(app, msg): async def message_in_group(app, msg):

View File

@ -1,6 +1,7 @@
from app import app from app import app
from pyrogram import filters from pyrogram import filters
from pyrogram.types import ForceReply, ReplyKeyboardMarkup from pyrogram.types import ForceReply, ReplyKeyboardMarkup
from classes.holo_user import HoloUser
from modules.utils import configGet, configSet, locale, logWrite from modules.utils import configGet, configSet, locale, logWrite
# Welcome check ================================================================================================================ # Welcome check ================================================================================================================
@ -14,6 +15,8 @@ async def welcome_pass(app, msg, once_again: bool = True) -> None:
* once_again (bool, optional): Set to False if it's the first time as user applies. Defaults to True. * once_again (bool, optional): Set to False if it's the first time as user applies. Defaults to True.
""" """
holo_user = HoloUser(msg.from_user)
if not once_again: if not once_again:
await msg.reply_text(locale("privacy_notice", "message")) await msg.reply_text(locale("privacy_notice", "message"))

35
modules/scheduled.py Normal file
View File

@ -0,0 +1,35 @@
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from datetime import datetime
from os import fsdecode, listdir, sep
from app import app
from modules.utils import configGet, jsonLoad, locale, logWrite
from dateutil.relativedelta import relativedelta
from modules.database import col_applications
scheduler = AsyncIOScheduler()
# for user_file in listdir(f"{configGet('data', 'locations')}{sep}users{sep}"):
# filename = fsdecode(f"{configGet('data', 'locations')}{sep}users{sep}{user_file}")
# if filename.endswith(".json"):
# user = jsonLoad(filename)
# if isinstance(user["application"]["2"], str):
# try:
# if ".".join([((user["application"]["2"]).split("."))[0], ((user["application"]["2"]).split("."))[1]]) == datetime.now().strftime("%d.%m"):
# tg_user = await app.get_users(int(user_file.replace(".json", "")))
# await app.send_message( configGet("admin_group"), locale("birthday", "message").format(str(tg_user.first_name), str(tg_user.username), str(relativedelta(datetime.now(), datetime.strptime(user["application"]["2"], '%d.%m.%Y')).years)) )
# except AttributeError:
# continue
if configGet("enabled", "scheduler", "birthdays"):
@scheduler.scheduled_job(trigger="cron", hour=configGet("time", "scheduler", "birthdays"))
async def check_birthdays():
for entry in col_applications.find({"2": datetime.now().strftime("%d.%m.%Y")}):
tg_user = await app.get_users(entry["user"])
await app.send_message( configGet("admin_group"), locale("birthday", "message").format(str(tg_user.first_name), str(tg_user.username), str(relativedelta(datetime.now(), datetime.strptime(entry["2"], '%d.%m.%Y')).years)) ) # type: ignore
logWrite(f"Notified admins about {entry['user']}'s birthday")
logWrite("Birthdays check performed")
if configGet("enabled", "scheduler", "sponsorships"):
@scheduler.scheduled_job(trigger="cron", hour=configGet("time", "scheduler", "sponsorships"))
async def check_sponsors():
logWrite("Sponsorships check performed")

View File

@ -1,5 +1,8 @@
from typing import Any, Union from typing import Any, Union
from pyrogram.enums.chat_type import ChatType from pyrogram.enums.chat_type import ChatType
from pyrogram.types import User
from pyrogram.client import Client
from pyrogram.errors import bad_request_400
from ujson import JSONDecodeError as JSONDecodeError from ujson import JSONDecodeError as JSONDecodeError
from ujson import loads, dumps from ujson import loads, dumps
@ -49,11 +52,11 @@ def nested_set(dic, keys, value, create_missing=True):
def configSet(keys: list, value: Any, file: str = "config", create_missing=True): def configSet(keys: list, value: Any, file: str = "config", create_missing=True):
"""Set config's value to provided one """Set config's value to provided one
Args: ### Args:
* keys (list): List of keys from the highest one to target * keys (`list`): List of keys from the highest one to target
* value (Any): Needed value * value (`Any`): Needed value
* file (str, optional): File (if not config). Defaults to "config". * file (`str`, optional): File (if not config). Defaults to "config".
* create_missing (bool, optional): Create missing items on the way. Defaults to True. * create_missing (`bool`, optional): Create missing items on the way. Defaults to True.
""" """
if file == "config": if file == "config":
filepath = "" filepath = ""
@ -74,11 +77,11 @@ def configSet(keys: list, value: Any, file: str = "config", create_missing=True)
def configGet(key: str, *args: str, file: str = "config"): def configGet(key: str, *args: str, file: str = "config"):
"""Get value of the config key """Get value of the config key
Args: ### Args:
* key (str): The last key of the keys path. * key (`str`): The last key of the keys path.
* *args (str): Path to key like: dict[args][key]. * *args (`str`): Path to key like: dict[args][key].
* file (str): User ID to load. Loads config if not provided. Defaults to "config". * file (`str`): User ID to load. Loads config if not provided. Defaults to "config".
Returns: ### Returns:
* any: Value of provided key * any: Value of provided key
""" """
if file == "config": if file == "config":
@ -99,16 +102,19 @@ def configGet(key: str, *args: str, file: str = "config"):
this_key = this_key[dict_key] this_key = this_key[dict_key]
return this_key[key] return this_key[key]
def locale(key: str, *args: str, locale=configGet("locale")) -> Union[str, list, dict]: def locale(key: str, *args: str, locale: Union[str, User] = configGet("locale")) -> Any:
"""Get value of locale string """Get value of locale string
Args: ### Args:
* key (str): The last key of the locale's keys path. * key (`str`): The last key of the locale's keys path.
* *args (list): Path to key like: dict[args][key]. * *args (`list`): Path to key like: dict[args][key].
* locale (str): Locale to looked up in. Defaults to config's locale value. * locale (`Union[str, User]`): Locale to looked up in. Provide User to get his `.language_code`. Defaults to config's locale value.
Returns: ### Returns:
* any: Value of provided locale key * any: Value of provided locale key. In normal case must be `str`, `dict` or `list`.
""" """
if (locale == None): if isinstance(locale, User):
locale = locale.language_code
if locale is None:
locale = configGet("locale") locale = configGet("locale")
try: try:
@ -144,4 +150,20 @@ def killProc(pid):
p.kill() p.kill()
def should_quote(msg): def should_quote(msg):
return True if msg.chat.type is not ChatType.PRIVATE else False return True if msg.chat.type is not ChatType.PRIVATE else False
async def find_user(app: Client, query: Union[str, int]):
try:
result = await app.get_users(int(query))
if result == [] or result == None:
raise TypeError
except (TypeError, ValueError):
try:
result = await app.get_users(query)
except bad_request_400.UsernameNotOccupied:
return None
except bad_request_400.UsernameInvalid:
return None
except bad_request_400.PeerIdInvalid:
return None
return result

View File

@ -1,7 +1,9 @@
pyrogram>=2.0.59 APScheduler==3.9.1.post1
tgcrypto>=1.2.4 fastapi==0.88.0
ujson>=5.5.0 psutil==5.9.4
psutil>=5.9.2 pymongo==4.3.3
schedule Pyrogram==2.0.69
fastapi tgcrypto==1.2.5
uvicorn[standard] python_dateutil==2.8.2
starlette==0.22.0
ujson==5.6.0

View File

@ -0,0 +1,6 @@
{
"$jsonSchema": {
"required": [],
"properties": {}
}
}

6
validation/context.json Normal file
View File

@ -0,0 +1,6 @@
{
"$jsonSchema": {
"required": [],
"properties": {}
}
}

36
validation/messages.json Normal file
View File

@ -0,0 +1,36 @@
{
"$jsonSchema": {
"required": [
"origin",
"origin.chat",
"origin.id",
"destination",
"destination.chat",
"destination.id"
],
"properties": {
"origin": {
"bsonType": "object"
},
"origin.chat": {
"bsonType": ["int", "long"],
"description": "Telegram ID of message's origin chat"
},
"origin.id": {
"bsonType": ["int", "long"],
"description": "ID of message in origin chat"
},
"destination": {
"bsonType": "object"
},
"destination.chat": {
"bsonType": ["int", "long"],
"description": "Telegram ID of message's destination chat"
},
"destination.id": {
"bsonType": ["int", "long"],
"description": "ID of message in destination chat"
}
}
}
}

View File

@ -0,0 +1,6 @@
{
"$jsonSchema": {
"required": [],
"properties": {}
}
}

43
validation/users.json Normal file
View File

@ -0,0 +1,43 @@
{
"$jsonSchema": {
"required": [
"user",
"link",
"label",
"tg_name",
"tg_phone",
"tg_locale",
"tg_username"
],
"properties": {
"user": {
"bsonType": ["int", "long"],
"description": "Telegram ID of user"
},
"link": {
"bsonType": ["string", "null"],
"description": "Invite link to destination group"
},
"label": {
"bsonType": "string",
"description": "Label given by admins"
},
"tg_name": {
"bsonType": "string",
"description": "Telegram first name"
},
"tg_phone": {
"bsonType": ["string", "null"],
"description": "Telegram phone number"
},
"tg_locale": {
"bsonType": ["string", "null"],
"description": "Telegram locale"
},
"tg_username": {
"bsonType": ["string", "null"],
"description": "Telegram username"
}
}
}
}

6
validation/warnings.json Normal file
View File

@ -0,0 +1,6 @@
{
"$jsonSchema": {
"required": [],
"properties": {}
}
}