├── .templates └── forms_module │ ├── __init__.py │ ├── data.py │ ├── meta.py │ ├── plbot │ └── handlers.py │ ├── requirements.txt │ ├── settings.py │ └── tgbot │ ├── __init__.py │ ├── _handlers.py │ ├── callback_datas │ ├── __init__.py │ └── all.py │ ├── callback_handlers │ ├── __init__.py │ ├── actions.py │ └── navigation.py │ ├── handlers │ ├── __init__.py │ ├── commands.py │ └── entering.py │ ├── states │ ├── __init__.py │ └── all.py │ └── templates │ ├── __init__.py │ └── all.py ├── README.md ├── __init__.py ├── bot.py ├── core ├── handlers.py ├── modules.py └── utils.py ├── data.py ├── install_requirements.bat ├── playerokapi ├── __init__.py ├── account.py ├── enums.py ├── exceptions.py ├── listener │ ├── events.py │ └── listener.py ├── parser.py └── types.py ├── plbot ├── playerokbot.py └── stats.py ├── requirements.txt ├── services └── updater.py ├── settings.py ├── start.bat └── tgbot ├── __init__.py ├── callback_datas ├── __init__.py ├── actions.py └── navigation.py ├── callback_handlers ├── __init__.py ├── actions.py └── navigation.py ├── handlers ├── __init__.py ├── commands.py └── entering.py ├── helpful.py ├── states ├── __init__.py └── all.py ├── telegrambot.py └── templates ├── __init__.py └── all.py /.templates/forms_module/__init__.py: -------------------------------------------------------------------------------- 1 | from core.modules import Module 2 | from logging import getLogger 3 | 4 | from playerokapi.enums import EventTypes 5 | 6 | from .plbot.handlers import on_playerok_bot_init, on_new_message 7 | from .tgbot._handlers import on_telegram_bot_init 8 | from .tgbot import router 9 | from .meta import * 10 | 11 | 12 | logger = getLogger(f"forms") 13 | _module: Module = None 14 | 15 | 16 | def set_module(new: Module): 17 | global _module 18 | _module = new 19 | 20 | def get_module(): 21 | return _module 22 | 23 | def on_module_connected(module: Module): 24 | set_module(module) 25 | logger.info(f"{PREFIX} Модуль подключен и активен") 26 | 27 | 28 | BOT_EVENT_HANDLERS = { 29 | "ON_MODULE_CONNECTED": [on_module_connected], 30 | "ON_MODULE_ENABLED": [on_module_connected], 31 | "ON_MODULE_RELOADED": [on_module_connected], 32 | "ON_PLAYEROK_BOT_INIT": [on_playerok_bot_init], 33 | "ON_TELEGRAM_BOT_INIT": [on_telegram_bot_init] 34 | } 35 | PLAYEROK_EVENT_HANDLERS = { 36 | EventTypes.NEW_MESSAGE: [on_new_message] 37 | } 38 | TELEGRAM_BOT_ROUTERS = [router] -------------------------------------------------------------------------------- /.templates/forms_module/data.py: -------------------------------------------------------------------------------- 1 | import os 2 | from data import ( 3 | Data as data, 4 | DataFile 5 | ) 6 | 7 | 8 | NEW_FORMS = DataFile( 9 | name="new_forms", 10 | path=os.path.join(os.path.dirname(__file__), "module_data", "new_forms.json"), 11 | default={} 12 | ) 13 | 14 | FORMS = DataFile( 15 | name="forms", 16 | path=os.path.join(os.path.dirname(__file__), "module_data", "forms.json"), 17 | default={} 18 | ) 19 | 20 | DATA = [NEW_FORMS, FORMS] 21 | 22 | 23 | class Data: 24 | 25 | @staticmethod 26 | def get(name: str) -> dict: 27 | return data.get(name, DATA) 28 | 29 | @staticmethod 30 | def set(name: str, new: list | dict) -> dict: 31 | return data.set(name, new, DATA) -------------------------------------------------------------------------------- /.templates/forms_module/meta.py: -------------------------------------------------------------------------------- 1 | from colorama import Fore, Style 2 | 3 | ACCENT_COLOR = Fore.YELLOW 4 | PREFIX = f"{ACCENT_COLOR}forms {Fore.LIGHTBLACK_EX}·{Fore.WHITE}" 5 | VERSION = "1.0" 6 | NAME = "forms" 7 | DESCRIPTION = "Модуль-пример для бота Playerok Universal. Позволяет заполнять и смотреть свою анкету, используя команды !заполнить, !мояанкета. /questionary в Telegram боте для управления" 8 | AUTHORS = "@alleexxeeyy" 9 | LINKS = "https://t.me/alleexxeeyy, https://t.me/alexeyproduction" -------------------------------------------------------------------------------- /.templates/forms_module/plbot/handlers.py: -------------------------------------------------------------------------------- 1 | import re 2 | import traceback 3 | from logging import getLogger 4 | from typing import TYPE_CHECKING 5 | from colorama import Fore 6 | from threading import Thread 7 | 8 | from playerokapi.listener.events import * 9 | from playerokapi.enums import * 10 | from playerokapi.account import * 11 | 12 | from ..meta import PREFIX, NAME 13 | from ..data import Data as data 14 | from ..settings import Settings as sett 15 | from ..settings import DATA 16 | from plbot.playerokbot import get_playerok_bot 17 | 18 | if TYPE_CHECKING: 19 | from plbot.playerokbot import PlayerokBot 20 | 21 | 22 | logger = getLogger(f"{NAME}.playerok") 23 | config = sett.get("config") 24 | messages = sett.get("messages") 25 | 26 | new_forms = data.get("new_forms") 27 | forms = data.get("forms") 28 | 29 | 30 | def msg(message_name: str, exclude_watermark: bool = False, **kwargs) -> str | None: 31 | return get_playerok_bot().msg(message_name, exclude_watermark, "messages", DATA, **kwargs) 32 | 33 | def is_fullname_valid(fullname: str) -> bool: 34 | pattern = r'^[А-Яа-яЁё]+ [А-Яа-яЁё]+ [А-Яа-яЁё]+$' 35 | return bool(re.match(pattern, fullname.strip())) 36 | 37 | def is_age_valid(age: str) -> bool: 38 | return age.isdigit() 39 | 40 | def is_hobby_valid(hobby: str) -> bool: 41 | pattern = r'^[А-Яа-яЁё\s\-]+$' 42 | return bool(re.match(pattern, hobby.strip())) 43 | 44 | async def handle_cmds(plbot: 'PlayerokBot', event: NewMessageEvent): 45 | global new_forms 46 | if event.message.text.lower() == "!мояанкета": 47 | form = forms.get(event.message.user.username) 48 | if not form: 49 | plbot.send_message(event.chat.id, msg("cmd_myform_error", reason="Ваша анкета не была найдена.\nИспользуйте команду !заполнить, чтобы заполнить анкету.")) 50 | return 51 | plbot.send_message(event.chat.id, msg("cmd_myform", fullname=form["fullname"], age=form["age"], hobby=form["hobby"])) 52 | elif event.message.text.lower() == "!заполнить": 53 | new_forms[event.message.user.username] = { 54 | "fullname": "", 55 | "age": "", 56 | "hobby": "", 57 | "state": "waiting_for_fullname" 58 | } 59 | plbot.send_message(event.chat.id, msg("cmd_writein")) 60 | 61 | async def handle_new_form_waiting_for_fullname(plbot: 'PlayerokBot', event: NewMessageEvent): 62 | global new_forms 63 | fullname = event.message.text.strip() 64 | if not is_fullname_valid(fullname): 65 | plbot.send_message(event.chat.id, msg("entering_fullname_error")) 66 | return 67 | fullname = " ".join([f"{part[0].upper()}{part[1:]}" for part in fullname.split(" ")]) 68 | new_forms[event.message.user.username]["fullname"] = fullname 69 | new_forms[event.message.user.username]["state"] = "waiting_for_age" 70 | if config["playerok"]["bot"]["log_states"]: 71 | logger.info(f"{PREFIX} {Fore.LIGHTWHITE_EX}{event.message.user.username} {Fore.WHITE}указал в анкете ФИО: {Fore.LIGHTWHITE_EX}{fullname}") 72 | plbot.send_message(event.chat.id, msg("enter_age")) 73 | 74 | async def handle_new_form_waiting_for_age(plbot: 'PlayerokBot', event: NewMessageEvent): 75 | global new_forms 76 | age = event.message.text.strip() 77 | if not is_age_valid(age): 78 | plbot.send_message(event.chat.id, msg("entering_age_error")) 79 | return 80 | new_forms[event.message.user.username]["age"] = int(age) 81 | new_forms[event.message.user.username]["state"] = "waiting_for_hobby" 82 | if config["playerok"]["bot"]["log_states"]: 83 | logger.info(f"{PREFIX} {Fore.LIGHTWHITE_EX}{event.message.user.username} {Fore.WHITE}указал в анкете возраст: {Fore.LIGHTWHITE_EX}{age}") 84 | plbot.send_message(event.chat.id, msg("enter_hobby")) 85 | 86 | async def handle_new_form_waiting_for_hobby(plbot: 'PlayerokBot', event: NewMessageEvent): 87 | global new_forms, forms 88 | hobby = event.message.text.strip() 89 | if not is_hobby_valid(hobby): 90 | plbot.send_message(event.chat.id, msg("entering_hobby_error")) 91 | return 92 | new_forms[event.message.user.username]["hobby"] = hobby 93 | forms[event.message.user.username] = { 94 | "fullname": new_forms[event.message.user.username]["fullname"], 95 | "age": new_forms[event.message.user.username]["age"], 96 | "hobby": new_forms[event.message.user.username]["hobby"] 97 | } 98 | del new_forms[event.message.user.username] 99 | if config["playerok"]["bot"]["log_states"]: 100 | logger.info(f"{PREFIX} {Fore.LIGHTWHITE_EX}{event.message.user.username} {Fore.WHITE}указал в анкете хобби: {Fore.LIGHTWHITE_EX}{hobby}") 101 | plbot.send_message(event.chat.id, msg("form_filled_out", fullname=forms[event.message.user.username]["fullname"], age=forms[event.message.user.username]["age"], hobby=forms[event.message.user.username]["hobby"])) 102 | 103 | async def handle_new_form(plbot: 'PlayerokBot', event: NewMessageEvent): 104 | if event.message.user.username not in new_forms: 105 | return 106 | if new_forms[event.message.user.username]["state"] == "waiting_for_fullname": 107 | await handle_new_form_waiting_for_fullname(plbot, event) 108 | elif new_forms[event.message.user.username]["state"] == "waiting_for_age": 109 | await handle_new_form_waiting_for_age(plbot, event) 110 | elif new_forms[event.message.user.username]["state"] == "waiting_for_hobby": 111 | await handle_new_form_waiting_for_hobby(plbot, event) 112 | else: 113 | return 114 | 115 | def on_playerok_bot_init(plbot: 'PlayerokBot'): 116 | 117 | def endless_loop(cycle_delay=5): 118 | global config, messages, new_forms, forms 119 | while True: 120 | if sett.get("config") != config: config = sett.get("config") 121 | if sett.get("messages") != messages: messages = sett.get("messages") 122 | if data.get("new_forms") != new_forms: data.set("new_forms", new_forms) 123 | if data.get("forms") != forms: data.set("forms", forms) 124 | time.sleep(cycle_delay) 125 | 126 | Thread(target=endless_loop, daemon=True).start() 127 | 128 | async def on_new_message(plbot: 'PlayerokBot', event: NewMessageEvent): 129 | try: 130 | if event.message.text is None: 131 | return 132 | if event.message.user.id != plbot.playerok_account.id: 133 | await handle_new_form(plbot, event) 134 | await handle_cmds(plbot, event) 135 | except Exception: 136 | logger.error(f"{PREFIX} {Fore.LIGHTRED_EX}При обработке ивента новых сообщений произошла ошибка: {Fore.WHITE}") 137 | traceback.print_exc() -------------------------------------------------------------------------------- /.templates/forms_module/requirements.txt: -------------------------------------------------------------------------------- 1 | deep-translator==1.11.4 2 | validators==0.34.0 -------------------------------------------------------------------------------- /.templates/forms_module/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from settings import ( 3 | Settings as sett, 4 | SettingsFile 5 | ) 6 | 7 | 8 | CONFIG = SettingsFile( 9 | name="config", 10 | path=os.path.join(os.path.dirname(__file__), "module_settings", "config.json"), 11 | need_restore=True, 12 | default={ 13 | "playerok": { 14 | "bot": { 15 | "log_states": True 16 | } 17 | } 18 | } 19 | ) 20 | 21 | MESSAGES = SettingsFile( 22 | name="messages", 23 | path=os.path.join(os.path.dirname(__file__), "module_settings", "messages.json"), 24 | need_restore=True, 25 | default={ 26 | "cmd_writein": { 27 | "enabled": True, 28 | "text": [ 29 | "✏️ Шаг 1. Ввод фамилии, имени, отчества", 30 | "", 31 | "💡 Например: Петров Иван Олегович", 32 | "", 33 | "Введите своё ФИО:" 34 | ] 35 | }, 36 | "entering_fullname_error": { 37 | "enabled": True, 38 | "text": [ 39 | "❌ Шаг 1. Ошибка ввода ФИО", 40 | "", 41 | "Убедитесь, что текст соответствует формату", 42 | "", 43 | "Введите ФИО снова:" 44 | ] 45 | }, 46 | "enter_age": { 47 | "enabled": True, 48 | "text": [ 49 | "✏️ Шаг 2. Ввод возраста", 50 | "", 51 | "💡 Например: 18", 52 | "", 53 | "Введите свой возраст:" 54 | ] 55 | }, 56 | "entering_age_error": { 57 | "enabled": True, 58 | "text": [ 59 | "❌ Шаг 2. Ошибка ввода возраста", 60 | "", 61 | "Убедитесь, что вы ввели числовое значение", 62 | "", 63 | "Введите возраст снова:" 64 | ] 65 | }, 66 | "enter_hobby": { 67 | "enabled": True, 68 | "text": [ 69 | "✏️ Шаг 3. Ввод хобби", 70 | "", 71 | "💡 Например: Рисование", 72 | "", 73 | "Введите своё хобби:" 74 | ] 75 | }, 76 | "entering_username_error": { 77 | "enabled": True, 78 | "text": [ 79 | "❌ Шаг 3. Ошибка ввода хобби", 80 | "", 81 | "Убедитесь, что текст соответствует формату", 82 | "", 83 | "Введите хобби снова:" 84 | ] 85 | }, 86 | "form_filled_out": { 87 | "enabled": True, 88 | "text": [ 89 | "✅ Анкета была заполнена!", 90 | "", 91 | "Ваши данные:", 92 | "・ ФИО: {fullname}", 93 | "・ Возраст: {age}", 94 | "・ Хобби: {hobby}", 95 | "", 96 | "💡 Используйте команду !мояанкета, чтобы просмотреть данные снова" 97 | ] 98 | }, 99 | "cmd_myform": { 100 | "enabled": True, 101 | "text": [ 102 | "📝 Ваша анкета", 103 | "", 104 | "・ ФИО: {fullname}", 105 | "・ Возраст: {age}", 106 | "・ Хобби: {hobby}", 107 | "", 108 | "💡 Используйте команду !заполнить, чтобы заполнить анкету заново" 109 | ] 110 | }, 111 | "cmd_myform_error": { 112 | "enabled": True, 113 | "text": [ 114 | "❌ При открытии вашей анкеты произошла ошибка", 115 | "", 116 | "{reason}" 117 | ] 118 | } 119 | } 120 | ) 121 | 122 | DATA = [CONFIG, MESSAGES] 123 | 124 | 125 | class Settings: 126 | 127 | @staticmethod 128 | def get(name: str) -> dict: 129 | return sett.get(name, DATA) 130 | 131 | @staticmethod 132 | def set(name: str, new: list | dict) -> dict: 133 | return sett.set(name, new, DATA) -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from .handlers import router as handlers_router 4 | from .callback_handlers import router as callback_handlers_router 5 | 6 | router = Router() 7 | router.include_routers(callback_handlers_router, handlers_router) -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/_handlers.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import BotCommand 2 | from tgbot.telegrambot import TelegramBot 3 | from logging import getLogger 4 | 5 | from ..meta import NAME 6 | 7 | 8 | logger = getLogger(f"{NAME}.telegram") 9 | 10 | 11 | async def on_telegram_bot_init(tgbot: TelegramBot) -> None: 12 | try: 13 | main_menu_commands = await tgbot.bot.get_my_commands() 14 | forms_menu_commands = [BotCommand(command=f"/{NAME}", description=f"📝📈 Управление модулем {NAME}")] 15 | await tgbot.bot.set_my_commands(list(main_menu_commands + forms_menu_commands)) 16 | except: 17 | pass -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/callback_datas/__init__.py: -------------------------------------------------------------------------------- 1 | from .all import * -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/callback_datas/all.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | 3 | 4 | class FORMS_MenuNavigation(CallbackData, prefix="forms_mennav"): 5 | to: str 6 | 7 | class FORMS_InstructionNavigation(CallbackData, prefix="forms_inspag"): 8 | to: str 9 | 10 | class FORMS_MessagesPagination(CallbackData, prefix="forms_mespag"): 11 | page: int 12 | 13 | class FORMS_MessagePage(CallbackData, prefix="forms_mespage"): 14 | message_id: str -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/callback_handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from .actions import router as actions_router 4 | from .navigation import router as navigation_router 5 | 6 | router = Router() 7 | router.include_routers(actions_router, navigation_router) -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/callback_handlers/actions.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, Router 2 | from aiogram.types import CallbackQuery 3 | from aiogram.fsm.context import FSMContext 4 | from aiogram.exceptions import TelegramAPIError 5 | 6 | from tgbot.helpful import throw_float_message 7 | from tgbot import templates as main_templ 8 | 9 | from .. import templates as templ 10 | from .. import callback_datas as calls 11 | from .. import states 12 | from ...settings import Settings as sett 13 | from .navigation import * 14 | 15 | 16 | router = Router() 17 | 18 | 19 | @router.callback_query(F.data == "forms_switch_log_states") 20 | async def callback_forms_switch_log_states(callback: CallbackQuery, state: FSMContext): 21 | await state.set_state(None) 22 | config = sett.get("config") 23 | config["playerok"]["bot"]["log_states"] = not config["playerok"]["bot"]["log_states"] 24 | sett.set("config", config) 25 | return await callback_menu_navigation(callback, calls.FORMS_MenuNavigation(to="settings"), state) 26 | 27 | @router.callback_query(F.data == "forms_enter_messages_page") 28 | async def callback_forms_enter_messages_page(callback: CallbackQuery, state: FSMContext): 29 | data = await state.get_data() 30 | last_page = data.get("last_page") or 0 31 | await state.set_state(states.FORMS_MessagesStates.entering_page) 32 | await throw_float_message(state=state, 33 | message=callback.message, 34 | text=templ.settings_mess_float_text(f"📃 Введите номер страницы для перехода ↓"), 35 | reply_markup=main_templ.back_kb(calls.FORMS_MessagesPagination(page=last_page).pack())) 36 | 37 | @router.callback_query(F.data == "forms_switch_message_enabled") 38 | async def callback_forms_switch_message_enabled(callback: CallbackQuery, state: FSMContext): 39 | try: 40 | data = await state.get_data() 41 | last_page = data.get("last_page") or 0 42 | message_id = data.get("message_id") 43 | if not message_id: 44 | raise Exception("❌ ID сообщения не был найден, повторите процесс с самого начала") 45 | 46 | messages = sett.get("messages") 47 | messages[message_id]["enabled"] = not messages[message_id]["enabled"] 48 | sett.set("messages", messages) 49 | return await callback_message_page(callback, calls.FORMS_MessagePage(message_id=message_id), state) 50 | except Exception as e: 51 | if e is not TelegramAPIError: 52 | data = await state.get_data() 53 | last_page = data.get("last_page") or 0 54 | await throw_float_message(state=state, 55 | message=callback.message, 56 | text=templ.settings_mess_float_text(e), 57 | reply_markup=main_templ.back_kb(calls.FORMS_MessagesPagination(page=last_page).pack())) 58 | 59 | @router.callback_query(F.data == "forms_enter_message_text") 60 | async def callback_forms_enter_message_text(callback: CallbackQuery, state: FSMContext): 61 | try: 62 | data = await state.get_data() 63 | last_page = data.get("last_page") or 0 64 | message_id = data.get("message_id") 65 | if not message_id: 66 | raise Exception("❌ ID сообщения не был найден, повторите процесс с самого начала") 67 | 68 | await state.set_state(states.FORMS_MessagePageStates.entering_message_text) 69 | messages = sett.get("messages") 70 | mess_text = "\n".join(messages[message_id]["text"]) or "❌ Не задано" 71 | await throw_float_message(state=state, 72 | message=callback.message, 73 | text=templ.settings_mess_float_text(f"💬 Введите новый текст сообщения {message_id} ↓\n┗ Текущее:
{mess_text}
"), 74 | reply_markup=main_templ.back_kb(calls.FORMS_MessagesPagination(page=last_page).pack())) 75 | except Exception as e: 76 | if e is not TelegramAPIError: 77 | data = await state.get_data() 78 | last_page = data.get("last_page") or 0 79 | await throw_float_message(state=state, 80 | message=callback.message, 81 | text=templ.settings_mess_float_text(e), 82 | reply_markup=main_templ.back_kb(calls.FORMS_MessagesPagination(page=last_page).pack())) -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/callback_handlers/navigation.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | from aiogram.types import CallbackQuery 3 | from aiogram.fsm.context import FSMContext 4 | 5 | from tgbot.helpful import throw_float_message 6 | 7 | from .. import templates as templ 8 | from .. import callback_datas as calls 9 | 10 | 11 | router = Router() 12 | 13 | 14 | @router.callback_query(calls.FORMS_MenuNavigation.filter()) 15 | async def callback_menu_navigation(callback: CallbackQuery, callback_data: calls.FORMS_MenuNavigation, state: FSMContext): 16 | await state.set_state(None) 17 | to = callback_data.to 18 | if to == "default": 19 | await throw_float_message(state, callback.message, templ.menu_text(), templ.menu_kb(), callback) 20 | elif to == "settings": 21 | await throw_float_message(state, callback.message, templ.settings_text(), templ.settings_kb(), callback) 22 | 23 | @router.callback_query(calls.FORMS_InstructionNavigation.filter()) 24 | async def callback_instruction_navgiation(callback: CallbackQuery, callback_data: calls.FORMS_InstructionNavigation, state: FSMContext): 25 | await state.set_state(None) 26 | to = callback_data.to 27 | if to == "default": 28 | await throw_float_message(state, callback.message, templ.instruction_text(), templ.instruction_kb(), callback) 29 | elif to == "commands": 30 | await throw_float_message(state, callback.message, templ.instruction_comms_text(), templ.instruction_comms_kb(), callback) 31 | 32 | 33 | @router.callback_query(calls.FORMS_MessagesPagination.filter()) 34 | async def callback_messages_pagination(callback: CallbackQuery, callback_data: calls.FORMS_MessagesPagination, state: FSMContext): 35 | await state.set_state(None) 36 | page = callback_data.page 37 | await state.update_data(last_page=page) 38 | await throw_float_message(state, callback.message, templ.settings_mess_text(), templ.settings_mess_kb(page), callback) 39 | 40 | @router.callback_query(calls.FORMS_MessagePage.filter()) 41 | async def callback_message_page(callback: CallbackQuery, callback_data: calls.FORMS_MessagePage, state: FSMContext): 42 | message_id = callback_data.message_id 43 | data = await state.get_data() 44 | await state.update_data(message_id=message_id) 45 | last_page = data.get("last_page") or 0 46 | await throw_float_message(state, callback.message, templ.settings_mess_page_text(message_id), templ.settings_mess_page_kb(message_id, last_page), callback) -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from .commands import router as commands_router 4 | from .entering import router as entering_router 5 | 6 | router = Router() 7 | router.include_routers(commands_router, entering_router) -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/handlers/commands.py: -------------------------------------------------------------------------------- 1 | from aiogram import types, Router 2 | from aiogram.filters import Command 3 | from aiogram.fsm.context import FSMContext 4 | 5 | from settings import Settings as main_sett 6 | from tgbot.helpful import throw_float_message, do_auth 7 | 8 | from .. import templates as templ 9 | 10 | 11 | router = Router() 12 | 13 | 14 | @router.message(Command("forms")) 15 | async def cmd_forms(message: types.Message, state: FSMContext): 16 | from ... import get_module 17 | await state.set_state(None) 18 | main_config = main_sett.get("config") 19 | if not get_module().enabled: 20 | return 21 | if message.from_user.id not in main_config["telegram"]["bot"]["signed_users"]: 22 | return await do_auth(message, state) 23 | await throw_float_message(state, message, templ.menu_text(), templ.menu_kb()) -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/handlers/entering.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, types, Router 2 | from aiogram.exceptions import TelegramAPIError 3 | from aiogram.fsm.context import FSMContext 4 | 5 | from tgbot.helpful import throw_float_message 6 | from tgbot import templates as main_templ 7 | 8 | from .. import templates as templ 9 | from .. import states 10 | from ...settings import Settings as sett 11 | from .. import callback_datas as calls 12 | 13 | 14 | router = Router() 15 | 16 | 17 | @router.message(states.FORMS_MessagePageStates.entering_message_text, F.text) 18 | async def handler_entering_message_text(message: types.Message, state: FSMContext): 19 | try: 20 | await state.set_state(None) 21 | if len(message.text.strip()) <= 0: 22 | raise Exception("❌ Слишком короткий текст") 23 | 24 | data = await state.get_data() 25 | messages = sett.get("messages") 26 | message_split_lines = message.text.strip().split('\n') 27 | messages[data["message_id"]]["text"] = message_split_lines 28 | sett.set("messages", messages) 29 | await throw_float_message(state=state, 30 | message=message, 31 | text=templ.settings_mess_page_float_text(f"✅ Текст сообщения {data['message_id']} был успешно изменён на
{message.text.strip()}
"), 32 | reply_markup=main_templ.back_kb(calls.FORMS_MessagePage(message_id=data.get("message_id")).pack())) 33 | except Exception as e: 34 | if e is not TelegramAPIError: 35 | data = await state.get_data() 36 | await throw_float_message(state=state, 37 | message=message, 38 | text=templ.settings_mess_page_float_text(e), 39 | reply_markup=main_templ.back_kb(calls.FORMS_MessagePage(message_id=data.get("message_id")).pack())) 40 | 41 | @router.message(states.FORMS_MessagesStates.entering_page, F.text) 42 | async def handler_entering_messages_page(message: types.Message, state: FSMContext): 43 | try: 44 | await state.set_state(None) 45 | if not message.text.strip().isdigit(): 46 | raise Exception("❌ Вы должны ввести числовое значение") 47 | 48 | await state.update_data(last_page=int(message.text.strip())-1) 49 | await throw_float_message(state=state, 50 | message=message, 51 | text=templ.settings_mess_text(), 52 | reply_markup=main_templ.settings_mess_kb(int(message.text.strip())-1)) 53 | except Exception as e: 54 | if e is not TelegramAPIError: 55 | data = await state.get_data() 56 | last_page = data.get("last_page") or 0 57 | await throw_float_message(state=state, 58 | message=message, 59 | text=main_templ.settings_mess_float_text(e), 60 | reply_markup=main_templ.back_kb(calls.FORMS_MessagesPagination(page=last_page).pack())) -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/states/__init__.py: -------------------------------------------------------------------------------- 1 | from .all import * -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/states/all.py: -------------------------------------------------------------------------------- 1 | from aiogram.fsm.state import State, StatesGroup 2 | 3 | 4 | class FORMS_MessagesStates(StatesGroup): 5 | entering_page = State() 6 | 7 | class FORMS_MessagePageStates(StatesGroup): 8 | entering_message_text = State() -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/templates/__init__.py: -------------------------------------------------------------------------------- 1 | from .all import * -------------------------------------------------------------------------------- /.templates/forms_module/tgbot/templates/all.py: -------------------------------------------------------------------------------- 1 | import math 2 | import textwrap 3 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 4 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 5 | 6 | from plbot.playerokbot import get_playerok_bot 7 | 8 | from .. import callback_datas as calls 9 | from ...settings import Settings as sett 10 | from ...meta import NAME, VERSION 11 | 12 | 13 | def menu_text(): 14 | txt = textwrap.dedent(f""" 15 | 📝 Меню {NAME} 16 | 17 | {NAME} v{VERSION} 18 | Модуль, позволяющий заполнять анкеты 19 | 20 | Ссылки: 21 | ┣ @alleexxeeyy — главный и единственный разработчик 22 | ┗ @alexey_production_bot — бот для покупки официальных модулей 23 | 24 | Перемещайтесь по разделам ниже ↓ 25 | """) 26 | return txt 27 | 28 | def menu_kb(): 29 | rows = [ 30 | [ 31 | InlineKeyboardButton(text="⚙️", callback_data=calls.FORMS_MenuNavigation(to="settings").pack()) 32 | ], 33 | [InlineKeyboardButton(text="📖 Инструкция", callback_data=calls.FORMS_InstructionNavigation(to="default").pack())], 34 | [ 35 | InlineKeyboardButton(text="👨‍💻 Разработчик", url="https://t.me/alleexxeeyy"), 36 | InlineKeyboardButton(text="🤖 Наш бот", url="https://t.me/alexey_production_bot") 37 | ] 38 | ] 39 | kb = InlineKeyboardMarkup(inline_keyboard=rows) 40 | return kb 41 | 42 | def menu_float_text(placeholder: str): 43 | txt = textwrap.dedent(f""" 44 | 📝 Меню {NAME} 45 | \n{placeholder} 46 | """) 47 | return txt 48 | 49 | 50 | def instruction_text(): 51 | txt = textwrap.dedent(f""" 52 | 📖 Инструкция {NAME} 53 | В этом разделе описаны инструкции по работе с модулем 54 | 55 | Перемещайтесь по разделам ниже ↓ 56 | """) 57 | return txt 58 | 59 | def instruction_kb(): 60 | rows = [ 61 | [InlineKeyboardButton(text="⌨️ Команды", callback_data=calls.FORMS_InstructionNavigation(to="commands").pack())], 62 | [InlineKeyboardButton(text="⬅️ Назад", callback_data=calls.FORMS_MenuNavigation(to="default").pack())] 63 | ] 64 | kb = InlineKeyboardMarkup(inline_keyboard=rows) 65 | return kb 66 | 67 | def instruction_comms_text(): 68 | txt = textwrap.dedent(f""" 69 | 📖 Инструкция {NAME} → ⌨️ Команды 70 | 71 | !мояанкета — отображает данные заполненной анкеты 72 | !заполнить — начинает процесс заполнения анкеты 73 | 74 | Выберите действие ↓ 75 | """) 76 | return txt 77 | 78 | def instruction_comms_kb(): 79 | rows = [[InlineKeyboardButton(text="⬅️ Назад", callback_data=calls.FORMS_InstructionNavigation(to="default").pack())]] 80 | kb = InlineKeyboardMarkup(inline_keyboard=rows) 81 | return kb 82 | 83 | 84 | def settings_text(): 85 | txt = textwrap.dedent(f""" 86 | ⚙️ Настройки {NAME} 87 | 88 | Перемещайтесь по разделам ниже, чтобы изменять значения параметров ↓ 89 | """) 90 | return txt 91 | 92 | def settings_kb(): 93 | config = sett.get("config") 94 | log_states = "🟢 Включено" if config["playerok"]["bot"]["log_states"] else "🔴 Выключено" 95 | rows = [ 96 | [InlineKeyboardButton(text=f"👁️ Логгировать состояния в консоль: {log_states}", callback_data="forms_switch_log_states")], 97 | [InlineKeyboardButton(text=f"💬 Сообщения", callback_data=calls.FORMS_MessagesPagination(page=0).pack())], 98 | [ 99 | InlineKeyboardButton(text="⬅️ Назад", callback_data=calls.FORMS_MenuNavigation(to="default").pack()), 100 | InlineKeyboardButton(text="🔄️ Обновить", callback_data=calls.FORMS_MenuNavigation(to="settings").pack()) 101 | ] 102 | ] 103 | kb = InlineKeyboardMarkup(inline_keyboard=rows) 104 | return kb 105 | 106 | def settings_float_text(placeholder: str): 107 | txt = textwrap.dedent(f""" 108 | ⚙️ Настройки {NAME} 109 | \n{placeholder} 110 | """) 111 | return txt 112 | 113 | 114 | def settings_mess_text(): 115 | messages = sett.get("messages") 116 | txt = textwrap.dedent(f""" 117 | ⚙️ Настройки → ✉️ Сообщения 118 | Всего {len(messages.keys())} настраиваемых сообщений в конфиге 119 | 120 | Перемещайтесь по разделам ниже. Нажмите на сообщение, чтобы перейти в его редактирование ↓ 121 | """) 122 | return txt 123 | 124 | def settings_mess_kb(page: int = 0): 125 | messages = sett.get("messages") 126 | rows = [] 127 | items_per_page = 8 128 | total_pages = math.ceil(len(messages.keys()) / items_per_page) 129 | total_pages = total_pages if total_pages > 0 else 1 130 | 131 | if page < 0: page = 0 132 | elif page >= total_pages: page = total_pages - 1 133 | 134 | start_offset = page * items_per_page 135 | end_offset = start_offset + items_per_page 136 | 137 | for mess_id, info in list(messages.items())[start_offset:end_offset]: 138 | enabled = "🟢" if info["enabled"] else "🔴" 139 | text_joined = "\n".join(info["text"]) 140 | rows.append([InlineKeyboardButton(text=f"{enabled} {mess_id} | {text_joined}", callback_data=calls.FORMS_MessagePage(message_id=mess_id).pack())]) 141 | 142 | buttons_row = [] 143 | btn_back = InlineKeyboardButton(text="←", callback_data=calls.FORMS_MessagesPagination(page=page-1).pack()) if page > 0 else InlineKeyboardButton(text="🛑", callback_data="123") 144 | buttons_row.append(btn_back) 145 | buttons_row.append(InlineKeyboardButton(text=f"{page+1}/{total_pages}", callback_data="forms_enter_messages_page")) 146 | 147 | btn_next = InlineKeyboardButton(text="→", callback_data=calls.FORMS_MessagesPagination(page=page+1).pack()) if page < total_pages - 1 else InlineKeyboardButton(text="🛑", callback_data="123") 148 | buttons_row.append(btn_next) 149 | rows.append(buttons_row) 150 | 151 | rows.append([InlineKeyboardButton(text="⬅️ Назад", callback_data=calls.FORMS_MenuNavigation(to="settings").pack()), 152 | InlineKeyboardButton(text="🔄️ Обновить", callback_data=calls.FORMS_MessagesPagination(page=page).pack())]) 153 | kb = InlineKeyboardMarkup(inline_keyboard=rows) 154 | return kb 155 | 156 | def settings_mess_float_text(placeholder: str): 157 | txt = textwrap.dedent(f""" 158 | ⚙️ Настройки → ✉️ Сообщения 159 | \n{placeholder} 160 | """) 161 | return txt 162 | 163 | 164 | def settings_mess_page_text(message_id: int): 165 | messages = sett.get("messages") 166 | enabled = "🟢 Включено" if messages[message_id]["enabled"] else "🔴Выключено" 167 | message_text = "\n".join(messages[message_id]["text"]) or "❌ Не задано" 168 | txt = textwrap.dedent(f""" 169 | ✒️ Редактирование сообщения 170 | 171 | 🆔 ID сообщения: {message_id} 172 | 💡 Состояние: {enabled} 173 | 💬 Текст сообщения:
{message_text}
174 | 175 | Выберите параметр для изменения ↓ 176 | """) 177 | return txt 178 | 179 | def settings_mess_page_kb(message_id: int, page: int = 0): 180 | messages = sett.get("messages") 181 | enabled = "🟢 Включено" if messages[message_id]["enabled"] else "🔴Выключено" 182 | message_text = "\n".join(messages[message_id]["text"]) or "❌ Не задано" 183 | rows = [ 184 | [InlineKeyboardButton(text=f"💡 Состояние: {enabled}", callback_data="forms_switch_message_enabled")], 185 | [InlineKeyboardButton(text=f"💬 Текст сообщения: {message_text}", callback_data="forms_enter_message_text")], 186 | [ 187 | InlineKeyboardButton(text="⬅️ Назад", callback_data=calls.FORMS_MessagesPagination(page=page).pack()), 188 | InlineKeyboardButton(text="🔄️ Обновить", callback_data=calls.FORMS_MessagePage(message_id=message_id).pack()) 189 | ] 190 | ] 191 | kb = InlineKeyboardMarkup(inline_keyboard=rows) 192 | return kb 193 | 194 | def settings_mess_page_float_text(placeholder: str): 195 | txt = textwrap.dedent(f""" 196 | ✒️ Редактирование сообщения 197 | \n{placeholder} 198 | """) 199 | return txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playerok Universal 2 | Современный бот-помощник для Playerok 🤖🟦 3 | 4 | ## 🧭 Навигация: 5 | - [Функционал бота](#-функционал) 6 | - [Установка бота](#%EF%B8%8F-установка) 7 | - [Полезные ссылки](#-полезные-ссылки) 8 | - [Для разработчиков](#-для-разработчиков) 9 | 10 | ## ⚡ Функционал 11 | - Система модулей 12 | - Удобный Telegram бот для настройки бота (используется aiogram 3.10.0) 13 | - Базовый функционал: 14 | - Вечный онлайн на сайте 15 | - Автоматическое восстановление предметов после их покупки с прежним статусом приоритета 16 | - Приветственное сообщение 17 | - Возможность добавления пользовательских команд 18 | - Возможность добавления пользовательской авто-выдачи на предметы 19 | - Команда `!продавец` для вызова продавца (уведомляет вас в Telegram боте, что покупателю требуется помощь) 20 | - Прочий мелкий функционал: 21 | - Добавление/удаление/редактирование водяного знака под сообщениями ботов 22 | - Возможность читать/не читать чат перед отправкой сообщения 23 | 24 | и др. 25 | 26 | ## ⬇️ Установка 27 | 1. Скачайте [последнюю Release версию](https://github.com/alleexxeeyy/playerok-universal/releases/latest) и распакуйте в любое удобное для вас место 28 | 2. Убедитесь, что у вас установлен **Python версии 3.x.x - 3.12**. Если не установлен, сделайте это, перейдя по ссылке https://www.python.org/downloads/release/python-31210 (при установке нажмите на пункт `Add to PATH`) 29 | 3. Откройте `install_requirements.bat` и дождитесь установки всех необходимых для работы библиотек, а после закройте окно 30 | 4. Чтобы запустить бота, откройте запускатор `start.bat` 31 | 5. После первого запуска вас попросят настроить бота для работы 32 | 33 | ## 📚 Для разработчиков 34 | 35 | Модульная система помогает внедрять в бота дополнительный функционал, сделанный энтузиастами. По сути, это же, что и плагины, но в более удобном формате. 36 | Вы можете создавать свой модуль, опираясь на [шаблонный](.templates/forms_module). 37 | 38 |
39 | 📌 Основные ивенты 40 | 41 | ### Ивенты бота (BOT_EVENT_HANDLERS) 42 | 43 | Ивенты, которые выполняются при определённом действии бота. 44 | 45 | | Ивент | Когда вызывается | Передающиеся аргументы | 46 | |-------|------------------|------------------------| 47 | | `ON_MODULE_CONNECTED` | При подключении модуля | `Module` | 48 | | `ON_MODULE_ENABLED` | При включении модуля | `Module` | 49 | | `ON_MODULE_DISABLED` | При выключении модуля | `Module` | 50 | | `ON_MODULE_RELOADED` | При перезагрузке модуля | `Module` | 51 | | `ON_INIT` | При инициализации бота | `-` | 52 | | `ON_PLAYEROK_BOT_INIT` | При инициализации (запуске) Playerok бота | `PlayerokBot` | 53 | | `ON_TELEGRAM_BOT_INIT` | При инициализации (запуске) Telegram бота | `TelegramBot` | 54 | 55 | ### Ивенты Playerok (PLAYEROK_EVENT_HANDLERS) 56 | 57 | Ивенты, получаемые в слушателе событий в Playerok боте. 58 | 59 | | Ивент | Когда вызывается | Передающиеся аргументы | 60 | |-------|------------------|------------------------| 61 | | `EventTypes.CHAT_INITIALIZED` | Чат инициализирован | `PlayerokBot`, `ChatInitializedEvent` | 62 | | `EventTypes.NEW_MESSAGE` | Новое сообщение в чате | `PlayerokBot`, `NewMessageEvent` | 63 | | `EventTypes.NEW_DEAL` | Создана новая сделка (когда покупатель оплатил товар) | `PlayerokBot`, `NewDealEvent` | 64 | | `EventTypes.NEW_REVIEW` | Новый отзыв по сделке | `PlayerokBot`, `NewReviewEvent` | 65 | | `EventTypes.DEAL_CONFIRMED` | Сделка подтверждена | `PlayerokBot`, `DealConfirmedEvent` | 66 | | `EventTypes.DEAL_ROLLED_BACK` | Продавец оформил возврат сделки | `PlayerokBot`, `DealRolledBackEvent` | 67 | | `EventTypes.DEAL_HAS_PROBLEM` | Пользователь сообщил о проблеме в сделке | `PlayerokBot`, `DealHasProblemEvent` | 68 | | `EventTypes.DEAL_PROBLEM_RESOLVED` | Проблема в сделке решена | `PlayerokBot`, `DealProblemResolvedEvent` | 69 | | `EventTypes.DEAL_STATUS_CHANGED` | Статус сделки изменён | `PlayerokBot`, `DealStatusChangedEvent` | 70 | | `EventTypes.ITEM_PAID` | Пользователь оплатил предмет | `PlayerokBot`, `ItemPaidEvent` | 71 | | `EventTypes.ITEM_SENT` | Предмет отправлен (продавец подтвердил выполнение сделки) | `PlayerokBot`, `ItemSentEvent` | 72 | 73 |
74 | 75 |
76 | 📁 Строение модуля 77 | 78 |
Модуль - это папка, внутри которой находятся важные компоненты. Вы можете изучить строение модуля, опираясь на [шаблонный модуль](.templates/forms_module), но стоит понимать, что это лишь пример, сделанный нами. 79 | 80 | Обязательные константы хендлеров: 81 | | Константа | Тип | Описание | 82 | |-----------|-----|----------| 83 | | `BOT_EVENT_HANDLERS` | `dict[str, list[Any]]` | В этом словаре задаются хендлеры ивентов бота | 84 | | `PLAYEROK_EVENT_HANDLERS` | `dict[EventTypes, list[Any]` | В этом словаре задаются хендлеры ивентов Playerok | 85 | | `TELEGRAM_BOT_ROUTERS` | `list[Router]` | В этом массиве задаются роутеры модульного Telegram бота | 86 | 87 | Обязательные константы метаданных: 88 | | Константа | Тип | Описание | 89 | |-----------|-----|----------| 90 | | `PREFIX` | `str` | Префикс | 91 | | `VERSION` | `str` | Версия | 92 | | `NAME` | `str` | Название | 93 | | `DESCRIPTION` | `str` | Описание | 94 | | `AUTHORS` | `str` | Авторы | 95 | | `LINKS` | `str` | Ссылки на авторов | 96 | 97 | Также, если модуль требует дополнительных зависимостей, в нём должен быть файл зависимостей **requirements.txt**, которые будут сами скачиваться при загрузке всех модулей бота. 98 | 99 | #### 🔧 Пример содержимого: 100 | Обратите внимание, что метаданные были вынесены в отдельный файл `meta.py`, но импортируются в `__init__.py`. 101 | Это сделано для избежания конфликтов импорта в дальнейшей части кода модуля. 102 | 103 | **`meta.py`**: 104 | ```python 105 | from colorama import Fore, Style 106 | 107 | PREFIX = f"{Fore.LIGHTCYAN_EX}[test module]{Fore.WHITE}" 108 | VERSION = "0.1" 109 | NAME = "test_module" 110 | DESCRIPTION = "Тестовый модуль. /test_module в Telegram боте для управления" 111 | AUTHORS = "@alleexxeeyy" 112 | LINKS = "https://t.me/alleexxeeyy, https://t.me/alexeyproduction" 113 | ``` 114 | 115 | **`__init__.py`**: 116 | ```python 117 | from playerokapi.listener.events import EventTypes 118 | from core.modules_manager import Module, disable_module 119 | 120 | from .plbot.handlers import on_playerok_bot_init, on_new_message, on_new_deal 121 | from .tgbot import router 122 | from .tgbot._handlers import on_telegram_bot_init 123 | from .meta import * 124 | 125 | 126 | _module: Module = None 127 | 128 | 129 | def set_module(module: Module): 130 | global _module 131 | _module = module 132 | 133 | def get_module(): 134 | return _module 135 | 136 | def on_module_connected(module: Module): 137 | try: 138 | set_module(module) 139 | print(f"{PREFIX} Модуль подключен и активен") 140 | except: 141 | disable_module(_module.uuid) 142 | 143 | 144 | BOT_EVENT_HANDLERS = { 145 | "ON_MODULE_CONNECTED": [on_module_connected], 146 | "ON_PLAYEROK_BOT_INIT": [on_playerok_bot_init], 147 | "ON_TELEGRAM_BOT_INIT": [on_telegram_bot_init] 148 | } 149 | PLAYEROK_EVENT_HANDLERS = { 150 | EventTypes.NEW_MESSAGE: [on_new_message], 151 | EventTypes.NEW_DEAL: [on_new_deal], 152 | # ... 153 | } 154 | TELEGRAM_BOT_ROUTERS = [router] 155 | ``` 156 | 157 |
158 | 159 |
160 | 🛠️ Полезные инструменты 161 | 162 | ### 📝 Настроенные врапперы файлов конфигурации и файлов данных 163 | Вместо того, чтобы лишний раз мучаться с файлами конфигурациями, написанием кода для управлениями ими, мы подготовили для вас готовое решение. 164 | У бота есть уже настроенные классы в файлах [`settings.py`](settings.py) и [`data.py`](data.py) 165 | 166 | #### Как это работает? 167 | Допустим, вы хотите создать файл конфигурации в своём модуле, для этого вам нужно будет создать файл `settings.py` в корне папки модуля. 168 | Содержимое `settings.py` должно быть примерно следующим: 169 | ```python 170 | import os 171 | from settings import ( 172 | Settings as sett, 173 | SettingsFile 174 | ) 175 | 176 | 177 | CONFIG = SettingsFile( 178 | name="config", # название файла конфигурации 179 | path=os.path.join(os.path.dirname(__file__), "module_settings", "config.json"), # путь к файлу конфигурации (в данном случае относительно папки модуля) 180 | need_restore=True, # нужно ли восстанавливать конфиг 181 | default={ 182 | "bool_param": True, 183 | "str_param": "qwerty", 184 | "int_param": 123 185 | } # стандартное содержимое файла 186 | ) 187 | 188 | DATA = [CONFIG] 189 | 190 | 191 | class Settings: 192 | 193 | @staticmethod 194 | def get(name: str) -> dict: 195 | return sett.get(name, DATA) 196 | 197 | @staticmethod 198 | def set(name: str, new: list | dict) -> dict: 199 | return sett.set(name, new, DATA) 200 | ``` 201 | 202 | Файл конфигурации задаётся с помощью датакласса `SettingsFile`, который в свою очередь, передаётся в массив `DATA`. 203 | 204 | Далее, получить данные из конфига или сохранить данные в конфиг можно вот так: 205 | ```python 206 | from . import settings as sett 207 | 208 | config = sett.get("config") # получаем конфиг 209 | print(config["bool_param"]) # -> True 210 | print(config["str_param"]) # -> qwerty 211 | print(config["int_param"]) # -> 123 212 | config["bool_param"] = False 213 | config["str_param"] = "uiop" 214 | config["int_param"] = 456 215 | sett.set("config", config) # задаём конфигу новое значение 216 | ``` 217 | 218 | Задавая конфигу новое значение, оно сразу записывается в его файл. Также и при получении, берутся актуальные данные из файла. 219 | 220 | Описание аргументов датакласса `SettingsFile`: 221 | | Аргумент | Описание | 222 | |----------|----------| 223 | | `name` | Название файла конфигурации, которое будем использовать при получении и записи | 224 | | `path` | Путь к файлу конфигурации | 225 | | `need_restore` | Нужно ли восстанавливать конфиг? Допустим, в стандартное значение конфига у вас добавились новые данные, а в уже созданном **ранее** файле конфигурации они отсутствуют. Если параметр включен, скрипт будет сверять текущие данные конфига со стандартными указанными, и если в текущих данных не будет того или иного ключа, который есть в стандартном значении, он автоматически добавится в конфиг. Так же, если тип значения ключа стандартного конфига не соответствует существующему (например, в файле **строковый** тип, а в стандартном значении **числовой**), также этот ключ в текущем конфиге будет заменён на стандартное значение | 226 | | `default` | Стандартное значение файла конфигурации | 227 | 228 | 229 |
Точно также устроен и файл данных, но он нужен для хранения информации, собранной самим скриптом, а не указанной пользователей. 230 | Например, вы хотите создать файл данных в своём модуле, для этого вам нужно будет создать файл `data.py` в корне папки модуля. 231 | 232 | Содержимое `data.py` должно быть примерно следующим: 233 | ```python 234 | import os 235 | from data import ( 236 | Data as data, 237 | DataFile 238 | ) 239 | 240 | 241 | LATEST_EVENTS_TIMES = DataFile( 242 | name="new_forms", # название файла данных 243 | path=os.path.join(os.path.dirname(__file__), "module_data", "new_forms.json"), # путь к файлу данных (в данном случае относительно папки модуля) 244 | default={} # стандартное содержимое файла 245 | ) 246 | 247 | DATA = [LATEST_EVENTS_TIMES] 248 | 249 | 250 | class Data: 251 | 252 | @staticmethod 253 | def get(name: str) -> dict: 254 | return data.get(name, DATA) 255 | 256 | @staticmethod 257 | def set(name: str, new: list | dict) -> dict: 258 | return data.set(name, new, DATA) 259 | ``` 260 | 261 | Здесь всё аналогично файлу конфигурации, только служит для другой задачи. 262 | 263 | 264 | ### 🔌 Удобное управление состояниями модуля 265 | Используя методы из `core/modules.py`, можно удобно включать/выключать/перезагружать текущий модуль. 266 | Для того, чтобы это сделать, нужно прежде всего получить UUID текущего запущенного модуля, который генерируется при его инициализации. 267 | 268 | Например, в файле `__init__.py` можно делать так: 269 | ```python 270 | # import ... 271 | 272 | 273 | _module: Module = None 274 | 275 | 276 | def set_module(module: Module): 277 | global _module 278 | _module = module 279 | 280 | def get_module(): 281 | return _module 282 | 283 | 284 | BOT_EVENT_HANDLERS = { 285 | "ON_MODULE_CONNECTED": [set_module], 286 | # ... 287 | } 288 | # ... 289 | ``` 290 | 291 | А потом в любом удобном месте управлять модулем: 292 | ```python 293 | from core.modules import enable_module, disable_module, reload_module 294 | 295 | from . import get_module 296 | 297 | 298 | disable_module(get_module().uuid) # выключает модуль 299 | enable_module(get_module().uuid) # включает модуль 300 | reload_module(get_module().uuid) # перезагружает модуль 301 | ``` 302 | 303 |
304 | 305 |
306 | ❗ Примечания 307 | 308 |
Функционал Telegram бота написан на библиотеке aiogram 3, система внедрения пользовательского функционала Telegram бота работает на основе роутеров, которые сливаются с основным, главным роутером бота. 309 | И так, как они сливаются воедино, могут возникнуть осложнения, если, например Callback данные имеют идентичное название. Поэтому, после написания функционала Telegram бота для модуля, лучше переименуйте 310 | эти данные уникальным образом, чтобы они не совпадали с названиями основного бота или дополнительных подключаемых модулей. 311 | 312 |
313 | 314 | 315 | ## 🔗 Полезные ссылки 316 | - Разработчик: https://github.com/alleexxeeyy (в профиле есть актуальные ссылки на все контакты для связи) 317 | - Telegram канал: https://t.me/alexeyproduction 318 | - Telegram бот для покупки официальных модулей: https://t.me/alexey_production_bot 319 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from colorama import Fore 2 | 3 | VERSION = "1.3.2.4" 4 | SKIP_UPDATES = False 5 | ACCENT_COLOR = Fore.LIGHTBLUE_EX -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import re 3 | import string 4 | import requests 5 | from threading import Thread 6 | import traceback 7 | import base64 8 | from colorama import init, Fore 9 | init() 10 | from logging import getLogger 11 | logger = getLogger(f"universal") 12 | 13 | from playerokapi.account import Account 14 | 15 | from __init__ import ACCENT_COLOR, VERSION 16 | from settings import Settings as sett 17 | from core.utils import set_title, setup_logger, install_requirements, patch_requests 18 | from core.modules import load_modules, set_modules, connect_modules 19 | from core.handlers import get_bot_event_handlers 20 | from services.updater import check_for_updates 21 | 22 | 23 | async def start_telegram_bot(): 24 | from tgbot.telegrambot import TelegramBot 25 | config = sett.get("config") 26 | tgbot = TelegramBot(config["telegram"]["api"]["token"]) 27 | await tgbot.run_bot() 28 | 29 | 30 | async def start_playerok_bot(): 31 | from plbot.playerokbot import PlayerokBot 32 | def run(): 33 | asyncio.new_event_loop().run_until_complete(PlayerokBot().run_bot()) 34 | Thread(target=run, daemon=True).start() 35 | 36 | 37 | def check_and_configure_config(): 38 | config = sett.get("config") 39 | 40 | def is_token_valid(token: str) -> bool: 41 | if not re.match(r"^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$", token): 42 | return False 43 | try: 44 | header, payload, signature = token.split('.') 45 | for part in (header, payload, signature): 46 | padding = '=' * (-len(part) % 4) 47 | base64.urlsafe_b64decode(part + padding) 48 | return True 49 | except Exception: 50 | return False 51 | 52 | def is_pl_account_working() -> bool: 53 | try: 54 | Account(token=config["playerok"]["api"]["token"], 55 | user_agent=config["playerok"]["api"]["user_agent"], 56 | requests_timeout=config["playerok"]["api"]["requests_timeout"], 57 | proxy=config["playerok"]["api"]["proxy"] or None).get() 58 | return True 59 | except: 60 | return False 61 | 62 | def is_user_agent_valid(ua: str) -> bool: 63 | if not ua or not (10 <= len(ua) <= 512): 64 | return False 65 | allowed_chars = string.ascii_letters + string.digits + string.punctuation + ' ' 66 | return all(c in allowed_chars for c in ua) 67 | 68 | def is_proxy_valid(proxy: str) -> bool: 69 | ip_pattern = r'(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)' 70 | pattern_ip_port = re.compile( 71 | rf'^{ip_pattern}\.{ip_pattern}\.{ip_pattern}\.{ip_pattern}:(\d+)$' 72 | ) 73 | pattern_auth_ip_port = re.compile( 74 | rf'^[^:@]+:[^:@]+@{ip_pattern}\.{ip_pattern}\.{ip_pattern}\.{ip_pattern}:(\d+)$' 75 | ) 76 | match = pattern_ip_port.match(proxy) 77 | if match: 78 | port = int(match.group(1)) 79 | return 1 <= port <= 65535 80 | match = pattern_auth_ip_port.match(proxy) 81 | if match: 82 | port = int(match.group(1)) 83 | return 1 <= port <= 65535 84 | return False 85 | 86 | def is_proxy_working(proxy: str, timeout: int = 10) -> bool: 87 | proxies = { 88 | "http": f"http://{proxy}", 89 | "https": f"http://{proxy}" 90 | } 91 | test_url = "https://playerok.com" 92 | try: 93 | response = requests.get(test_url, proxies=proxies, timeout=timeout) 94 | return response.status_code in [200, 403] 95 | except Exception: 96 | return False 97 | 98 | def is_tg_token_valid(token: str) -> bool: 99 | pattern = r'^\d{7,12}:[A-Za-z0-9_-]{35}$' 100 | return bool(re.match(pattern, token)) 101 | 102 | def is_tg_bot_exists() -> bool: 103 | try: 104 | response = requests.get(f"https://api.telegram.org/bot{config['telegram']['api']['token']}/getMe", timeout=5) 105 | data = response.json() 106 | return data.get("ok", False) is True and data.get("result", {}).get("is_bot", False) is True 107 | except Exception: 108 | return False 109 | 110 | def is_password_valid(password: str) -> bool: 111 | if len(password) < 6 or len(password) > 64: 112 | return False 113 | common_passwords = { 114 | "123456", "1234567", "12345678", "123456789", "password", "qwerty", 115 | "admin", "123123", "111111", "abc123", "letmein", "welcome", 116 | "monkey", "login", "root", "pass", "test", "000000", "user", 117 | "qwerty123", "iloveyou" 118 | } 119 | if password.lower() in common_passwords: 120 | return False 121 | return True 122 | 123 | while not config["playerok"]["api"]["token"]: 124 | print(f"\n{Fore.WHITE}Введите {Fore.LIGHTBLUE_EX}токен {Fore.WHITE}вашего Playerok аккаунта. Его можно узнать из Cookie-данных, воспользуйтесь расширением Cookie-Editor." 125 | f"\n {Fore.WHITE}· Пример: eyJhbGciOiJIUzI1NiIsInR5cCI1IkpXVCJ9.eyJzdWIiOiIxZWUxMzg0Ni...") 126 | token = input(f" {Fore.WHITE}↳ {Fore.LIGHTWHITE_EX}").strip() 127 | if is_token_valid(token): 128 | config["playerok"]["api"]["token"] = token 129 | sett.set("config", config) 130 | print(f"\n{Fore.GREEN}Токен успешно сохранён в конфиг.") 131 | else: 132 | print(f"\n{Fore.LIGHTRED_EX}Похоже, что вы ввели некорректный токен. Убедитесь, что он соответствует формату и попробуйте ещё раз.") 133 | 134 | print(f"\n{Fore.WHITE}Введите {Fore.LIGHTMAGENTA_EX}User Agent {Fore.WHITE}вашего браузера. Его можно скопировать на сайте {Fore.LIGHTWHITE_EX}https://whatmyuseragent.com. Или вы можете пропустить этот параметр, нажав Enter." 135 | f"\n {Fore.WHITE}· Пример: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36") 136 | user_agent = input(f" {Fore.WHITE}↳ {Fore.LIGHTWHITE_EX}").strip() 137 | if not user_agent: 138 | print(f"\n{Fore.YELLOW}Вы пропустили ввод User Agent. Учтите, что в таком случае бот может работать нестабильно.") 139 | break 140 | if is_user_agent_valid(user_agent): 141 | config["playerok"]["api"]["user_agent"] = user_agent 142 | sett.set("config", config) 143 | print(f"\n{Fore.GREEN}User Agent успешно сохранён в конфиг.") 144 | else: 145 | print(f"\n{Fore.LIGHTRED_EX}Похоже, что вы ввели некорректный User Agent. Убедитесь, что в нём нет русских символов и попробуйте ещё раз.") 146 | 147 | print(f"\n{Fore.WHITE}Введите {Fore.LIGHTBLUE_EX}IPv4 Прокси {Fore.WHITE}в формате user:password@ip:port или ip:port, если он без авторизации. Если вы не знаете что это, или не хотите устанавливать прокси - пропустите этот параметр, нажав Enter." 148 | f"\n {Fore.WHITE}· Пример: DRjcQTm3Yc:m8GnUN8Q9L@46.161.30.187:8000") 149 | proxy = input(f" {Fore.WHITE}↳ {Fore.LIGHTWHITE_EX}").strip() 150 | if not proxy: 151 | print(f"\n{Fore.WHITE}Вы пропустили ввод прокси.") 152 | break 153 | if is_proxy_valid(proxy): 154 | config["playerok"]["api"]["proxy"] = proxy 155 | sett.set("config", config) 156 | print(f"\n{Fore.GREEN}Прокси успешно сохранён в конфиг.") 157 | else: 158 | print(f"\n{Fore.LIGHTRED_EX}Похоже, что вы ввели некорректный Прокси. Убедитесь, что он соответствует формату и попробуйте ещё раз.") 159 | 160 | while not config["telegram"]["api"]["token"]: 161 | print(f"\n{Fore.WHITE}Введите {Fore.CYAN}токен вашего Telegram бота{Fore.WHITE}. Бота нужно создать у @BotFather." 162 | f"\n {Fore.WHITE}· Пример: 7257913369:AAG2KjLL3-zvvfSQFSVhaTb4w7tR2iXsJXM") 163 | token = input(f" {Fore.WHITE}↳ {Fore.LIGHTWHITE_EX}").strip() 164 | if is_tg_token_valid(token): 165 | config["telegram"]["api"]["token"] = token 166 | sett.set("config", config) 167 | print(f"\n{Fore.GREEN}Токен Telegram бота успешно сохранён в конфиг.") 168 | else: 169 | print(f"\n{Fore.LIGHTRED_EX}Похоже, что вы ввели некорректный токен. Убедитесь, что он соответствует формату и попробуйте ещё раз.") 170 | 171 | while not config["telegram"]["bot"]["password"]: 172 | print(f"\n{Fore.WHITE}Придумайте и введите {Fore.YELLOW}пароль для вашего Telegram бота{Fore.WHITE}. Бот будет запрашивать этот пароль при каждой новой попытке взаимодействия чужого пользователя с вашим Telegram ботом." 173 | f"\n {Fore.WHITE}· Пароль должен быть сложным, длиной не менее 6 и не более 64 символов.") 174 | password = input(f" {Fore.WHITE}↳ {Fore.LIGHTWHITE_EX}").strip() 175 | if is_password_valid(password): 176 | config["telegram"]["bot"]["password"] = password 177 | sett.set("config", config) 178 | print(f"\n{Fore.GREEN}Пароль успешно сохранён в конфиг.") 179 | else: 180 | print(f"\n{Fore.LIGHTRED_EX}Ваш пароль не подходит. Убедитесь, что он соответствует формату и не является лёгким и попробуйте ещё раз.") 181 | 182 | if config["playerok"]["api"]["proxy"] and not is_proxy_working(config["playerok"]["api"]["proxy"]): 183 | print(f"\n{Fore.LIGHTRED_EX}Похоже, что указанный вами прокси не работает. Пожалуйста, проверьте его и введите снова.") 184 | config["playerok"]["api"]["token"] = "" 185 | config["playerok"]["api"]["user_agent"] = "" 186 | config["playerok"]["api"]["proxy"] = "" 187 | sett.set("config", config) 188 | return check_and_configure_config() 189 | elif config["playerok"]["api"]["proxy"]: 190 | logger.info(f"{Fore.WHITE}Прокси успешно работает.") 191 | 192 | if not is_pl_account_working(): 193 | print(f"\n{Fore.LIGHTRED_EX}Не удалось подключиться к вашему Playerok аккаунту. Пожалуйста, убедитесь, что у вас указан верный токен и введите его снова.") 194 | config["playerok"]["api"]["token"] = "" 195 | config["playerok"]["api"]["user_agent"] = "" 196 | config["playerok"]["api"]["proxy"] = "" 197 | sett.set("config", config) 198 | return check_and_configure_config() 199 | else: 200 | logger.info(f"{Fore.WHITE}Playerok аккаунт успешно авторизован.") 201 | 202 | if not is_tg_bot_exists(): 203 | print(f"\n{Fore.LIGHTRED_EX}Не удалось подключиться к вашему Telegram боту. Пожалуйста, убедитесь, что у вас указан верный токен и введите его снова.") 204 | config["telegram"]["api"]["token"] = "" 205 | sett.set("config", config) 206 | return check_and_configure_config() 207 | else: 208 | logger.info(f"{Fore.WHITE}Telegram бот успешно работает.") 209 | 210 | 211 | if __name__ == "__main__": 212 | try: 213 | install_requirements("requirements.txt") # установка недостающих зависимостей, если таковые есть 214 | patch_requests() 215 | setup_logger() 216 | set_title(f"Playerok Universal v{VERSION} by @alleexxeeyy") 217 | print(f"\n\n {ACCENT_COLOR}Playerok Universal {Fore.WHITE}v{Fore.LIGHTWHITE_EX}{VERSION}" 218 | f"\n ↳ {Fore.LIGHTWHITE_EX}https://t.me/alleexxeeyy" 219 | f"\n ↳ {Fore.LIGHTWHITE_EX}https://t.me/alexeyproduction\n\n") 220 | 221 | check_for_updates() 222 | check_and_configure_config() 223 | 224 | modules = load_modules() 225 | set_modules(modules) 226 | 227 | if len(modules) > 0: 228 | connect_modules(modules) 229 | 230 | bot_event_handlers = get_bot_event_handlers() 231 | def handle_on_init(): 232 | """ 233 | Запускается при инициализации софта. 234 | Запускает за собой все хендлеры ON_INIT. 235 | """ 236 | for handler in bot_event_handlers.get("ON_INIT", []): 237 | try: 238 | handler() 239 | except Exception as e: 240 | logger.error(f"{Fore.LIGHTRED_EX}Ошибка при обработке хендлера ивента ON_INIT: {Fore.WHITE}{e}") 241 | handle_on_init() 242 | 243 | asyncio.run(start_playerok_bot()) 244 | asyncio.run(start_telegram_bot()) 245 | except Exception as e: 246 | traceback.print_exc() 247 | print(f"\n {Fore.LIGHTRED_EX}Ваш бот словил непредвиденную ошибку и был выключен." 248 | f"\n {Fore.WHITE}Пожалуйста, напишите в Telegram разработчика {Fore.LIGHTWHITE_EX}@alleexxeeyy{Fore.WHITE}, для уточнения причин") 249 | raise SystemExit(1) -------------------------------------------------------------------------------- /core/handlers.py: -------------------------------------------------------------------------------- 1 | from playerokapi.enums import EventTypes 2 | 3 | 4 | _bot_event_handlers: dict[str, list[callable]] = { 5 | "ON_MODULE_CONNECTED": [], 6 | "ON_INIT": [], 7 | "ON_PLAYEROK_BOT_INIT": [], 8 | "ON_TELEGRAM_BOT_INIT": [] 9 | } 10 | _playerok_event_handlers: dict[EventTypes, list[callable]] = { 11 | EventTypes.CHAT_INITIALIZED: [], 12 | EventTypes.NEW_MESSAGE : [], 13 | EventTypes.NEW_DEAL : [], 14 | EventTypes.NEW_REVIEW : [], 15 | EventTypes.DEAL_CONFIRMED : [], 16 | EventTypes.DEAL_ROLLED_BACK : [], 17 | EventTypes.DEAL_HAS_PROBLEM : [], 18 | EventTypes.DEAL_PROBLEM_RESOLVED : [], 19 | EventTypes.DEAL_STATUS_CHANGED : [], 20 | EventTypes.ITEM_PAID : [], 21 | EventTypes.ITEM_SENT : [] 22 | } 23 | 24 | 25 | def set_bot_event_handlers(data: dict[str, list[callable]]): 26 | """ 27 | Устанавливает новые хендлеры ивентов бота. 28 | 29 | :param data: Словарь с названиями событий и списками хендлеров. 30 | :type data: `dict[str, list[callable]]` 31 | """ 32 | global _bot_event_handlers 33 | _bot_event_handlers = data 34 | 35 | 36 | def add_bot_event_handler(event: str, handler: callable): 37 | """ 38 | Добавляет новый хендлер в ивенты бота. 39 | 40 | :param event: Название события, для которого добавляется хендлер. 41 | :type event: `str` 42 | 43 | :param handler: Вызываемый метод. 44 | :type handler: `callable` 45 | """ 46 | global _bot_event_handlers 47 | _bot_event_handlers[event].append(handler) 48 | 49 | 50 | def register_bot_event_handlers(handlers: dict[str, list[callable]]): 51 | """ 52 | Регистрирует хендлеры ивентов бота (добавляет переданные хендлеры, если их нету). 53 | 54 | :param data: Словарь с названиями событий и списками хендлеров. 55 | :type data: `dict[str, list[callable]]` 56 | """ 57 | global _bot_event_handlers 58 | for event_type, funcs in handlers.items(): 59 | if event_type not in _bot_event_handlers: 60 | _bot_event_handlers[event_type] = [] 61 | _bot_event_handlers[event_type].extend(funcs) 62 | 63 | 64 | def get_bot_event_handlers() -> dict[str, list[callable]]: 65 | """ 66 | Возвращает хендлеры ивентов бота. 67 | 68 | :return: Словарь с событиями и списками хендлеров. 69 | :rtype: `dict[str, list[callable]]` 70 | """ 71 | return _bot_event_handlers 72 | 73 | 74 | def set_playerok_event_handlers(data: dict[EventTypes, list[callable]]): 75 | """ 76 | Устанавливает новые хендлеры ивентов Playerok. 77 | 78 | :param data: Словарь с событиями и списками хендлеров. 79 | :type data: `dict[PlayerokAPI.updater.events.EventTypes, list[callable]]` 80 | """ 81 | global _playerok_event_handlers 82 | _playerok_event_handlers = data 83 | 84 | 85 | def add_playerok_event_handler(event: EventTypes, handler: callable): 86 | """ 87 | Добавляет новый хендлер в ивенты Playerok. 88 | 89 | :param event: Событие, для которого добавляется хендлер. 90 | :type event: `PlayerokAPI.updater.events.EventTypes` 91 | 92 | :param handler: Вызываемый метод. 93 | :type handler: `callable` 94 | """ 95 | global _playerok_event_handlers 96 | _playerok_event_handlers[event].append(handler) 97 | 98 | 99 | def register_playerok_event_handlers(handlers): 100 | """ 101 | Регистрирует хендлеры ивентов Playerok (добавляет переданные хендлеры, если их нету). 102 | 103 | :param data: Словарь с событиями и списками хендлеров. 104 | :type data: `dict[PlayerokAPI.updater.events.EventTypes, list[callable]]` 105 | """ 106 | global _playerok_event_handlers 107 | for event_type, funcs in handlers.items(): 108 | if event_type not in _playerok_event_handlers: 109 | _playerok_event_handlers[event_type] = [] 110 | _playerok_event_handlers[event_type].extend(funcs) 111 | 112 | 113 | def get_playerok_event_handlers() -> dict[EventTypes, list]: 114 | """ 115 | Возвращает хендлеры ивентов Playerok. 116 | 117 | :return: Словарь с событиями и списками хендлеров. 118 | :rtype: `dict[PlayerokAPI.updater.events.EventTypes, list[callable]]` 119 | """ 120 | return _playerok_event_handlers 121 | 122 | 123 | def remove_handlers(bot_event_handlers: dict[str, list[callable]], playerok_event_handlers: dict[EventTypes, list[callable]]): 124 | """ 125 | Удаляет переданные хендлеры бота и Playerok. 126 | 127 | :param bot_event_handlers: Словарь с событиями и списками хендлеров бота. 128 | :type bot_event_handlers: `dict[str, list[callable]]` 129 | 130 | :param playerok_event_handlers: Словарь с событиями и списками хендлеров Playerok. 131 | :type playerok_event_handlers: `dict[PlayerokAPI.updater.events.EventTypes, list[callable]]` 132 | """ # ДОДЕЛАТЬ 133 | global _bot_event_handlers, _playerok_event_handlers 134 | for event, funcs in bot_event_handlers.items(): 135 | if event in _bot_event_handlers: 136 | for func in funcs: 137 | if func in _bot_event_handlers[event]: 138 | _bot_event_handlers[event].remove(func) 139 | for event, funcs in playerok_event_handlers.items(): 140 | if event in _playerok_event_handlers: 141 | for func in funcs: 142 | if func in _playerok_event_handlers[event]: 143 | _playerok_event_handlers[event].remove(func) -------------------------------------------------------------------------------- /core/modules.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import os 3 | import sys 4 | import importlib 5 | import uuid 6 | from uuid import UUID 7 | from colorama import Fore 8 | from logging import getLogger 9 | logger = getLogger(f"universal.modules") 10 | 11 | from core.handlers import register_bot_event_handlers, register_playerok_event_handlers, remove_handlers 12 | from core.utils import install_requirements 13 | 14 | 15 | @dataclass 16 | class ModuleMeta: 17 | prefix: str 18 | version: str 19 | name: str 20 | description: str 21 | authors: str 22 | links: str 23 | 24 | @dataclass 25 | class Module: 26 | uuid: UUID 27 | enabled: bool 28 | meta: ModuleMeta 29 | bot_event_handlers: dict 30 | playerok_event_handlers: dict 31 | telegram_bot_routers: list 32 | _dir_name: str 33 | 34 | 35 | _loaded_modules: list[Module] = [] 36 | 37 | 38 | def set_modules(data: list[Module]): 39 | """ 40 | Устанавливает новые модули в загруженные. 41 | 42 | :param data: Массив модулей. 43 | :type data: `list[core.modules.Module]` 44 | """ 45 | global _loaded_modules 46 | _loaded_modules = data 47 | 48 | 49 | def get_modules() -> list[Module]: 50 | """ 51 | Возвращает загруженные модули. 52 | 53 | :return: Массив модулей. 54 | :rtype: `list[core.modules.Module]` 55 | """ 56 | return _loaded_modules 57 | 58 | 59 | def get_module_by_uuid(module_uuid: UUID) -> Module: 60 | """ 61 | Получает модуль по UUID. 62 | 63 | :param module_uuid: UUID модуля. 64 | :type module_uuid: `uuid.UUID` 65 | 66 | :return: Объект модуля. 67 | :rtype: `core.modules.Module` 68 | """ 69 | try: return [module for module in _loaded_modules if module.uuid == module_uuid][0] 70 | except: return None 71 | 72 | 73 | def enable_module(module_uuid: UUID) -> bool: 74 | """ 75 | Включает модуль и добавляет его хендлеры. 76 | 77 | :param module_uuid: UUID модуля. 78 | :type module_uuid: `uuid.UUID` 79 | 80 | :return: True, если модуль был включен. False, если не был включен. 81 | :rtype: `bool` 82 | """ 83 | global _loaded_modules 84 | try: 85 | module = get_module_by_uuid(module_uuid) 86 | if not module: 87 | raise Exception("Модуль не найден в загруженных") 88 | register_bot_event_handlers(module.bot_event_handlers) 89 | register_playerok_event_handlers(module.playerok_event_handlers) 90 | i = _loaded_modules.index(module) 91 | module.enabled = True 92 | _loaded_modules[i] = module 93 | logger.info(f"Модуль {Fore.LIGHTWHITE_EX}{module.meta.name} {Fore.WHITE}подключен") 94 | 95 | def handle_on_module_enabled(): 96 | """ 97 | Запускается при включении модуля. 98 | Запускает за собой все хендлеры ON_MODULE_ENABLED. 99 | """ 100 | for handler in module.bot_event_handlers.get("ON_MODULE_ENABLED", []): 101 | try: 102 | handler(module) 103 | except Exception as e: 104 | logger.error(f"{Fore.LIGHTRED_EX}Ошибка при обработке хендлера ивента ON_MODULE_ENABLED: {Fore.WHITE}{e}") 105 | handle_on_module_enabled() 106 | return True 107 | except Exception as e: 108 | logger.error(f"{Fore.LIGHTRED_EX}Ошибка при подключении модуля {module_uuid}: {Fore.WHITE}{e}") 109 | return False 110 | 111 | 112 | def disable_module(module_uuid: UUID) -> bool: 113 | """ 114 | Выключает модуль и удаляет его хендлеры. 115 | 116 | :param module_uuid: UUID модуля. 117 | :type module_uuid: `uuid.UUID` 118 | 119 | :return: True, если модуль был выключен. False, если не был выключен. 120 | :rtype: `bool` 121 | """ 122 | global _loaded_modules 123 | try: 124 | module = get_module_by_uuid(module_uuid) 125 | if not module: 126 | raise Exception("Модуль не найден в загруженных") 127 | remove_handlers(module.bot_event_handlers, module.playerok_event_handlers) 128 | i = _loaded_modules.index(module) 129 | module.enabled = False 130 | _loaded_modules[i] = module 131 | logger.info(f"Модуль {Fore.LIGHTWHITE_EX}{module.meta.name} {Fore.WHITE}отключен") 132 | 133 | def handle_on_module_disabled(): 134 | """ 135 | Запускается при выключении модуля. 136 | Запускает за собой все хендлеры ON_MODULE_DISABLED. 137 | """ 138 | for handler in module.bot_event_handlers.get("ON_MODULE_DISABLED", []): 139 | try: 140 | handler(module) 141 | except Exception as e: 142 | logger.error(f"{Fore.LIGHTRED_EX}Ошибка при обработке хендлера ивента ON_MODULE_DISABLED: {Fore.WHITE}{e}") 143 | handle_on_module_disabled() 144 | return True 145 | except Exception as e: 146 | logger.error(f"{Fore.LIGHTRED_EX}Ошибка при отключении модуля {module_uuid}: {Fore.WHITE}{e}") 147 | return False 148 | 149 | 150 | def reload_module(module_uuid: str): 151 | """ 152 | Перезагружает модуль (отгружает и импортирует снова). 153 | 154 | :param module_uuid: UUID модуля. 155 | :type module_uuid: `uuid.UUID` 156 | 157 | :return: True, если модуль был перезагружен. False, если не был перезагружен. 158 | :rtype: `bool` 159 | """ 160 | try: 161 | module = get_module_by_uuid(module_uuid) 162 | if not module: 163 | raise Exception("Модуль не найден в загруженных") 164 | if module._dir_name in sys.modules: 165 | del sys.modules[f"modules.{module._dir_name}"] 166 | mod = importlib.import_module(f"modules.{module._dir_name}") 167 | logger.info(f"Модуль {Fore.LIGHTWHITE_EX}{module.meta.name} {Fore.WHITE}перезагружен") 168 | 169 | def handle_on_module_reloaded(): 170 | """ 171 | Запускается при первом подключении модуля. 172 | Запускает за собой все хендлеры ON_MODULE_RELOADED. 173 | """ 174 | for handler in module.bot_event_handlers.get("ON_MODULE_RELOADED", []): 175 | try: 176 | handler(module) 177 | except Exception as e: 178 | logger.error(f"{Fore.LIGHTRED_EX}Ошибка при обработке хендлера ивента ON_MODULE_RELOADED: {Fore.WHITE}{e}") 179 | handle_on_module_reloaded() 180 | return mod 181 | except Exception as e: 182 | logger.error(f"{Fore.LIGHTRED_EX}Ошибка при перезагрузке модуля {module_uuid}: {Fore.WHITE}{e}") 183 | return False 184 | 185 | 186 | def load_modules() -> list[Module]: 187 | """ 188 | Загружает все модули из папки modules. 189 | 190 | :return: Массив загруженных модулей. 191 | :rtype: `list[core.modules.Module]` 192 | """ 193 | modules = [] 194 | modules_path = "modules" 195 | os.makedirs(modules_path, exist_ok=True) 196 | for name in os.listdir(modules_path): 197 | bot_event_handlers = {} 198 | playerok_event_handlers = {} 199 | telegram_bot_routers = [] 200 | module_path = os.path.join(modules_path, name) 201 | if os.path.isdir(module_path) and "__init__.py" in os.listdir(module_path): 202 | try: 203 | install_requirements(os.path.join(module_path, "requirements.txt")) 204 | module = importlib.import_module(f"modules.{name}") 205 | if hasattr(module, "BOT_EVENT_HANDLERS"): 206 | for key, funcs in module.BOT_EVENT_HANDLERS.items(): 207 | bot_event_handlers.setdefault(key, []).extend(funcs) 208 | if hasattr(module, "PLAYEROK_EVENT_HANDLERS"): 209 | for key, funcs in module.PLAYEROK_EVENT_HANDLERS.items(): 210 | playerok_event_handlers.setdefault(key, []).extend(funcs) 211 | if hasattr(module, "TELEGRAM_BOT_ROUTERS"): 212 | telegram_bot_routers.extend(module.TELEGRAM_BOT_ROUTERS) 213 | module_data = Module( 214 | uuid.uuid4(), 215 | enabled=False, 216 | meta=ModuleMeta( 217 | module.PREFIX, 218 | module.VERSION, 219 | module.NAME, 220 | module.DESCRIPTION, 221 | module.AUTHORS, 222 | module.LINKS 223 | ), 224 | bot_event_handlers=bot_event_handlers, 225 | playerok_event_handlers=playerok_event_handlers, 226 | telegram_bot_routers=telegram_bot_routers, 227 | _dir_name=name 228 | ) 229 | modules.append(module_data) 230 | except Exception as e: 231 | logger.error(f"{Fore.LIGHTRED_EX}Ошибка при загрузке модуля {name}: {Fore.WHITE}{e}") 232 | return modules 233 | 234 | 235 | def connect_modules(modules: list[Module]): 236 | """ 237 | Подключает переданные модули (при запуске бота). 238 | 239 | :param modules: Массив модулей. 240 | :type modules: `list[core.modules.Module]` 241 | """ 242 | global _loaded_modules 243 | names = [] 244 | for module in modules: 245 | try: 246 | register_bot_event_handlers(module.bot_event_handlers) 247 | register_playerok_event_handlers(module.playerok_event_handlers) 248 | i = _loaded_modules.index(module) 249 | module.enabled = True 250 | _loaded_modules[i] = module 251 | names.append(f"{Fore.YELLOW}{module.meta.name} {Fore.LIGHTWHITE_EX}{module.meta.version}") 252 | except Exception as e: 253 | logger.error(f"{Fore.LIGHTRED_EX}Ошибка при подключении модуля {module.meta.name}: {Fore.WHITE}{e}") 254 | continue 255 | logger.info(f'{Fore.LIGHTBLUE_EX}Подключено {Fore.CYAN}{len(modules)} модуля(-ей): {f"{Fore.WHITE}, ".join(names)}') 256 | 257 | def on_module_connected(): 258 | """ 259 | Запускается при первом подключении модуля. 260 | Запускает за собой все хендлеры ON_MODULE_CONNECTED. 261 | """ 262 | for module in modules: 263 | for handler in module.bot_event_handlers.get("ON_MODULE_CONNECTED", []): 264 | try: 265 | handler(module) 266 | except Exception as e: 267 | logger.error(f"{Fore.LIGHTRED_EX}Ошибка при обработке хендлера ивента ON_MODULE_CONNECTED: {Fore.WHITE}{e}") 268 | if module.enabled: 269 | on_module_connected() -------------------------------------------------------------------------------- /core/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import ctypes 4 | import logging 5 | from colorlog import ColoredFormatter 6 | from colorama import Fore 7 | import pkg_resources 8 | import subprocess 9 | import tls_requests 10 | import random 11 | import time 12 | from logging import getLogger 13 | 14 | 15 | logger = getLogger(f"universal.core") 16 | 17 | 18 | def restart(): 19 | """Перезагружает консоль.""" 20 | python = sys.executable 21 | os.execv(python, [python] + sys.argv) 22 | 23 | 24 | def set_title(title: str): 25 | """ 26 | Устанавливает заголовок консоли. 27 | 28 | :param title: Заголовок. 29 | :type title: `str` 30 | """ 31 | if sys.platform == "win32": 32 | ctypes.windll.kernel32.SetConsoleTitleW(title) 33 | elif sys.platform.startswith("linux"): 34 | sys.stdout.write(f"\x1b]2;{title}\x07") 35 | sys.stdout.flush() 36 | elif sys.platform == "darwin": 37 | sys.stdout.write(f"\x1b]0;{title}\x07") 38 | sys.stdout.flush() 39 | 40 | 41 | def setup_logger(log_file: str = "logs/latest.log"): 42 | """ 43 | Настраивает логгер. 44 | 45 | :param log_file: Путь к файлу логов. 46 | :type log_file: `str` 47 | """ 48 | class ShortLevelFormatter(ColoredFormatter): 49 | def format(self, record): 50 | record.shortLevel = record.levelname[0] 51 | return super().format(record) 52 | 53 | os.makedirs("logs", exist_ok=True) 54 | LOG_FORMAT = "%(light_black)s%(asctime)s · %(log_color)s%(shortLevel)s: %(reset)s%(white)s%(message)s" 55 | formatter = ShortLevelFormatter( 56 | LOG_FORMAT, 57 | datefmt="%d.%m.%Y %H:%M:%S", 58 | reset=True, 59 | log_colors={ 60 | 'DEBUG': 'light_blue', 61 | 'INFO': 'light_green', 62 | 'WARNING': 'yellow', 63 | 'ERROR': 'bold_red', 64 | 'CRITICAL': 'red', 65 | }, 66 | style='%' 67 | ) 68 | console_handler = logging.StreamHandler() 69 | console_handler.setFormatter(formatter) 70 | console_handler.setLevel(logging.INFO) 71 | file_handler = logging.FileHandler(log_file, encoding="utf-8") 72 | file_handler.setLevel(logging.DEBUG) 73 | file_formatter = logging.Formatter( 74 | "[%(asctime)s] %(levelname)-1s · %(name)-20s %(message)s", 75 | datefmt="%d.%m.%Y %H:%M:%S", 76 | ) 77 | file_handler.setFormatter(file_formatter) 78 | logger = logging.getLogger() 79 | logger.setLevel(logging.INFO) 80 | for handler in logger.handlers[:]: 81 | logger.removeHandler(handler) 82 | logger.addHandler(console_handler) 83 | logger.addHandler(file_handler) 84 | return logger 85 | 86 | 87 | def is_package_installed(requirement_string: str) -> bool: 88 | """ 89 | Проверяет, установлена ли библиотека. 90 | 91 | :param requirement_string: Строка пакета из файла зависимостей. 92 | :type requirement_string: `str` 93 | """ 94 | try: 95 | pkg_resources.require(requirement_string) 96 | return True 97 | except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict): 98 | return False 99 | 100 | 101 | def install_requirements(requirements_path: str): 102 | """ 103 | Устанавливает зависимости из файла. 104 | 105 | :param requirements_path: Путь к файлу зависимостей. 106 | :type requirements_path: `str` 107 | """ 108 | if not os.path.exists(requirements_path): 109 | return 110 | with open(requirements_path, "r", encoding="utf-8") as f: 111 | lines = f.readlines() 112 | missing_packages = [] 113 | for line in lines: 114 | pkg = line.strip() 115 | if not pkg or pkg.startswith("#"): 116 | continue 117 | if not is_package_installed(pkg): 118 | missing_packages.append(pkg) 119 | if missing_packages: 120 | logger.info(f"Установка недостающих зависимостей: {Fore.YELLOW}{f'{Fore.WHITE}, {Fore.YELLOW}'.join(missing_packages)}{Fore.WHITE}") 121 | subprocess.check_call([sys.executable, "-m", "pip", "install", *missing_packages]) 122 | 123 | 124 | def patch_requests(): 125 | """Патчит стандартные requests на кастомные с обработкой ошибок.""" 126 | _orig_request = tls_requests.Client.request 127 | def _request(self, method, url, **kwargs): # type: ignore 128 | for attempt in range(6): 129 | resp = _orig_request(self, method, url, **kwargs) 130 | statuses = { 131 | "429": "TOO_MANY_REQUESTS", 132 | "502": "BAD_GATEWAY", 133 | "503": "SERVICE_UNAVAIBLE" 134 | } 135 | if str(resp.status_code) not in statuses: 136 | if any([status_text for status_text in statuses.values() if status_text in resp.text]): 137 | break 138 | else: 139 | return resp 140 | retry_hdr = resp.headers.get("Retry-After") 141 | try: 142 | delay = float(retry_hdr) if retry_hdr else min(120.0, 5.0 * (2 ** attempt)) 143 | except Exception: 144 | delay = min(120.0, 5.0 * (2 ** attempt)) 145 | delay += random.uniform(0.2, 0.8) # небольшой джиттер 146 | time.sleep(delay) 147 | return resp 148 | tls_requests.Client.request = _request # type: ignore -------------------------------------------------------------------------------- /data.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class DataFile: 8 | name: str 9 | path: str 10 | default: list | dict 11 | 12 | 13 | INITIALIZED_USERS = DataFile( 14 | name="initialized_users", 15 | path="bot_data/initialized_users.json", 16 | default=[] 17 | ) 18 | 19 | DATA = [INITIALIZED_USERS] 20 | 21 | 22 | def get_json(path: str, default: dict | list) -> dict: 23 | """ 24 | Получает содержимое файла данных. 25 | Создаёт файл данных, если его нет. 26 | 27 | :param path: Путь к json файлу. 28 | :type path: `str` 29 | 30 | :param default: Стандартная структура файла. 31 | :type default: `dict` 32 | """ 33 | folder_path = os.path.dirname(path) 34 | if not os.path.exists(folder_path): 35 | os.makedirs(folder_path) 36 | try: 37 | with open(path, 'r', encoding='utf-8') as f: 38 | config = json.load(f) 39 | except: 40 | config = default 41 | with open(path, 'w', encoding='utf-8') as f: 42 | json.dump(config, f, indent=4, ensure_ascii=False) 43 | finally: 44 | return config 45 | 46 | 47 | def set_json(path: str, new: dict): 48 | """ 49 | Устанавливает новые данные в файл данных. 50 | 51 | :param path: Путь к json файлу. 52 | :type path: `str` 53 | 54 | :param new: Новые данные. 55 | :type new: `dict` 56 | """ 57 | with open(path, 'w', encoding='utf-8') as f: 58 | json.dump(new, f, indent=4, ensure_ascii=False) 59 | 60 | 61 | class Data: 62 | 63 | @staticmethod 64 | def get(name: str, data: list[DataFile] = DATA) -> dict | None: 65 | try: 66 | file = [file for file in data if file.name == name][0] 67 | return get_json(file.path, file.default) 68 | except: return None 69 | 70 | @staticmethod 71 | def set(name: str, new: list | dict, data: list[DataFile] = DATA): 72 | try: 73 | file = [file for file in data if file.name == name][0] 74 | set_json(file.path, new) 75 | except: pass -------------------------------------------------------------------------------- /install_requirements.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | pip install -r requirements.txt -------------------------------------------------------------------------------- /playerokapi/__init__.py: -------------------------------------------------------------------------------- 1 | from . import types 2 | from . import parser -------------------------------------------------------------------------------- /playerokapi/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class EventTypes(Enum): 5 | """ 6 | Типы событий. 7 | """ 8 | 9 | CHAT_INITIALIZED = 0 10 | """ Чат инициализирован. """ 11 | NEW_MESSAGE = 1 12 | """ Новое сообщение в чате. """ 13 | NEW_DEAL = 2 14 | """ Создана новая сделка (когда покупатель оплатил товар). """ 15 | NEW_REVIEW = 3 16 | """ Новый отзыв от покупателя. """ 17 | DEAL_CONFIRMED = 4 18 | """ Сделка подтверждена (покупатель подтвердил получение предмета). """ 19 | DEAL_CONFIRMED_AUTOMATICALLY = 5 20 | """ Сделка подтверждена автоматически (если покупатель долго не выходит на связь). """ 21 | DEAL_ROLLED_BACK = 6 22 | """ Продавец оформил возврат сделки. """ 23 | DEAL_HAS_PROBLEM = 7 24 | """ Пользователь сообщил о проблеме в сделке. """ 25 | DEAL_PROBLEM_RESOLVED = 8 26 | """ Проблема в сделке решена. """ 27 | DEAL_STATUS_CHANGED = 9 28 | """ Статус сделки изменён. """ 29 | ITEM_PAID = 10 30 | """ Пользователь оплатил предмет. """ 31 | ITEM_SENT = 11 32 | """ Предмет отправлен (продавец подтвердил выполнение сделки). """ 33 | 34 | 35 | class ItemLogEvents(Enum): 36 | """ 37 | События логов предмета. 38 | """ 39 | 40 | PAID = 0 41 | """ Продавец подтвердил выполнение сделки. """ 42 | SENT = 1 43 | """ Товар сделки отправлен. """ 44 | DEAL_CONFIRMED = 2 45 | """ Сделка подтверждена. """ 46 | DEAL_ROLLED_BACK = 3 47 | """ Сделка возвращена. """ 48 | PROBLEM_REPORTED = 4 49 | """ Отправлена жалоба (создана проблема). """ 50 | PROBLEM_RESOLVED = 5 51 | """ Проблема решена. """ 52 | 53 | 54 | class TransactionOperations(Enum): 55 | """ 56 | Операции транзакций. 57 | """ 58 | 59 | DEPOSIT = 0 60 | """ Пополнение. """ 61 | BUY = 1 62 | """ Оплата товара. """ 63 | SELL = 2 64 | """ Продажа товара. """ 65 | ITEM_DEFAULT_PRIORITY = 3 66 | """ Оплата бесплатного приоритета. """ 67 | ITEM_PREMIUM_PRIORITY = 4 68 | """ Оплата премиум приоритета. """ 69 | WITHDRAW = 5 70 | """ Выплата. """ 71 | MANUAL_BALANCE_INCREASE = 6 72 | """ Начисление на баланс аккаунта. """ 73 | MANUAL_BALANCE_DECREASE = 7 74 | """ Списание с баланса аккаунта. """ 75 | REFERRAL_BONUS = 8 76 | """ Приглашение друга (реферал). """ 77 | STEAM_DEPOSIT = 9 78 | """ Оплата пополнения Steam. """ 79 | 80 | 81 | class TransactionDirections(Enum): 82 | """ 83 | Операции транзакций. 84 | """ 85 | 86 | IN = 0 87 | """ Начисление. """ 88 | OUT = 1 89 | """ Списание. """ 90 | 91 | 92 | class TransactionStatuses(Enum): 93 | """ 94 | Статусы транзакций. 95 | """ 96 | 97 | PENDING = 0 98 | """ В ожидании (транзакция оплачена, но деньги за неё ещё не поступили на баланс). """ 99 | PROCESSING = 1 100 | """ В заморозке. """ 101 | CONFIRMED = 2 102 | """ Сделка транзакции подтверждена. """ 103 | ROLLED_BACK = 3 104 | """ Возврат по сделке транзакции. """ 105 | FAILED = 4 106 | """ Ошибка транзакции. """ 107 | 108 | 109 | class TransactionPaymentMethodIds(Enum): 110 | """ 111 | Айди методов транзакции. 112 | """ 113 | 114 | MIR = 0 115 | """ С помощью банковских карт МИР. """ 116 | VISA_MASTERCARD = 1 117 | """ С помощью банковских карт VISA/Mastercard. """ 118 | ERIP = 2 119 | """ С помощью ЕРИП. """ 120 | 121 | 122 | class TransactionProviderDirections(Enum): 123 | """ 124 | Направления провайдеров транзакции. 125 | """ 126 | 127 | IN = 0 128 | """ Пополнение. """ 129 | OUT = 1 130 | """ Вывод. """ 131 | 132 | 133 | class TransactionProviderIds(Enum): 134 | """ 135 | Айди провайдеров транзакции. 136 | """ 137 | 138 | LOCAL = 0 139 | """ С помощью баланса аккаунта. """ 140 | SBP = 1 141 | """ С помощью СБП. """ 142 | BANK_CARD_RU = 2 143 | """ С помощью банковской карты России. """ 144 | BANK_CARD_BY = 3 145 | """ С помощью банковской карты Беларуси. """ 146 | BANK_CARD = 4 147 | """ С помощью иностранной банковской карты. """ 148 | YMONEY = 5 149 | """ С помощью ЮMoney. """ 150 | USDT = 6 151 | """ Криптовалюта USDT (TRC20). """ 152 | PENDING_INCOME = 7 153 | """ Пополнение из замороженных средств. """ 154 | 155 | 156 | class BankCardTypes(Enum): 157 | """ 158 | Типы банковских карт. 159 | """ 160 | 161 | MIR = 0 162 | """ Банковская карта МИР. """ 163 | VISA = 1 164 | """ Банковская карта VISA. """ 165 | MASTERCARD = 2 166 | """ Банковская карта Mastercard. """ 167 | 168 | 169 | class ItemDealStatuses(Enum): 170 | """ 171 | Состояния сделки. 172 | """ 173 | 174 | PAID = 0 175 | """ Сделка оплачена. """ 176 | PENDING = 1 177 | """ Сделка в ожидании отправки товара. """ 178 | SENT = 2 179 | """ Продавец подтвердил выполнение сделки. """ 180 | CONFIRMED = 3 181 | """ Сделка подтверждена. """ 182 | ROLLED_BACK = 4 183 | """ Сделка возвращена. """ 184 | 185 | 186 | class ItemDealDirections(Enum): 187 | """ 188 | Направления сделки. 189 | """ 190 | 191 | IN = 0 192 | """ Покупка. """ 193 | OUT = 1 194 | """ Продажа. """ 195 | 196 | 197 | class GameTypes(Enum): 198 | """ 199 | Типы игр. 200 | """ 201 | 202 | GAME = 0 203 | """ Игра. """ 204 | APPLICATION = 1 205 | """ Приложение. """ 206 | 207 | 208 | class UserTypes(Enum): 209 | """ 210 | Типы пользователей. 211 | """ 212 | 213 | USER = 0 214 | """ Обычный пользователь. """ 215 | MODERATOR = 1 216 | """ Модератор. """ 217 | BOT = 2 218 | """ Бот. """ 219 | 220 | 221 | class ChatTypes(Enum): 222 | """ 223 | Типы чатов. 224 | """ 225 | 226 | PM = 0 227 | """ Приватный чат (диалог с пользователем). """ 228 | NOTIFICATIONS = 1 229 | """ Чат уведомлений. """ 230 | SUPPORT = 2 231 | """ Чат поддержки. """ 232 | 233 | 234 | class ChatStatuses(Enum): 235 | """ 236 | Статусы чатов. 237 | """ 238 | 239 | NEW = 0 240 | """ Новый чат (в нём нет ни единого прочитанного сообщения). """ 241 | FINISHED = 1 242 | """ Чат доступен, в нём сейчас можно переписываться. """ 243 | 244 | 245 | class ChatMessageButtonTypes(Enum): 246 | """ 247 | Типы кнопок сообщений. 248 | """ 249 | 250 | # TODO: Доделать все типы кнопок сообщения 251 | REDIRECT = 0 252 | """ Перенаправляет на ссылку. """ 253 | LOTTERY = 1 254 | """ Перенаправляет на розыгрыш/акцию. """ 255 | 256 | 257 | class ItemStatuses(Enum): 258 | """ 259 | Статусы предметов. 260 | """ 261 | 262 | PENDING_APPROVAL = 0 263 | """ Ожидает принятия (на проверке модерацией). """ 264 | PENDING_MODERATION = 1 265 | """ Ожидает проверки изменений модерацией. """ 266 | APPROVED = 2 267 | """ Активный (принятый модерацией). """ 268 | DECLINED = 3 269 | """ Отклонённый. """ 270 | BLOCKED = 4 271 | """ Заблокированный. """ 272 | EXPIRED = 5 273 | """ Истёкший. """ 274 | SOLD = 6 275 | """ Проданный. """ 276 | DRAFT = 7 277 | """ Черновик (если предмет не выставлен на продажу). """ 278 | 279 | 280 | class ReviewStatuses(Enum): 281 | """ 282 | Статусы отзывов. 283 | """ 284 | 285 | APPROVED = 0 286 | """ Активный. """ 287 | DELETED = 1 288 | """ Удалённый. """ 289 | 290 | 291 | class SortDirections(Enum): 292 | """ 293 | Типы сортировки. 294 | """ 295 | 296 | DESC = 0 297 | """ По убыванию. """ 298 | ASC = 1 299 | """ По возрастанию. """ 300 | 301 | 302 | class PriorityTypes(Enum): 303 | """ 304 | Типы приоритетов. 305 | """ 306 | 307 | DEFAULT = 0 308 | """ Стандартный приоритет. """ 309 | PREMIUM = 1 310 | """ Премиум приоритет. """ 311 | 312 | 313 | class GameCategoryAgreementIconTypes(Enum): 314 | """ 315 | Типы иконок соглашения покупателя в определённой категории. 316 | """ 317 | 318 | # TODO: Доделать все типы иконок соглашений 319 | RESTRICTION = 0 320 | """ Ограничение. """ 321 | CONFIRMATION = 0 322 | """ Подтверждение. """ 323 | 324 | 325 | class GameCategoryOptionTypes(Enum): 326 | """ 327 | Типы опции категории. 328 | """ 329 | 330 | # TODO: Доделать все типы опций категории 331 | SELECTOR = 0 332 | """ Выбор типа. """ 333 | SWITCH = 1 334 | """ Переключатель. """ 335 | 336 | 337 | class GameCategoryDataFieldTypes(Enum): 338 | """ 339 | Типы полей с данными категории игры. 340 | """ 341 | 342 | ITEM_DATA = 0 343 | """ Данные предмета. """ 344 | OBTAINING_DATA = 1 345 | """ Получаемые данные (после покупки предмета). """ 346 | 347 | 348 | class GameCategoryDataFieldInputTypes(Enum): 349 | """ 350 | Типы вводимых полей с данными категории игры. 351 | """ 352 | 353 | # TODO: Доделать все типы вводимых дата-полей 354 | INPUT = 0 355 | """ Вводимое значение (вводится покупателем при оформлении предмета). """ 356 | 357 | 358 | class GameCategoryAutoConfirmPeriods(Enum): 359 | """ 360 | Периоды автоматического подтверждения сделки в категории игры. 361 | """ 362 | 363 | # TODO: Доделать все периоды авто-подтверждения 364 | SEVEN_DEYS = 0 365 | """ Семь дней. """ 366 | 367 | 368 | class GameCategoryInstructionTypes(Enum): 369 | """ 370 | Типы инструкций категории. 371 | """ 372 | 373 | FOR_SELLER = 0 374 | """ Для продавца. """ 375 | FOR_BUYER = 1 376 | """ Для покупателя. """ 377 | -------------------------------------------------------------------------------- /playerokapi/exceptions.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class CloudflareDetectedException(Exception): 5 | """ 6 | Ошибка обнаружения Cloudflare защиты при отправке запроса. 7 | 8 | :param response: Объект ответа. 9 | :type response: `Response` 10 | """ 11 | 12 | def __init__(self, response: requests.Response): 13 | self.response = response 14 | self.status_code = self.response.status_code 15 | self.html_text = self.response.text 16 | 17 | def __str__(self): 18 | msg = ( 19 | f"Ошибка: CloudFlare заметил подозрительную активность при отправке запроса на сайт Playerok." 20 | f"\nКод ошибки: {self.status_code}" 21 | f"\nОтвет: {self.html_text}" 22 | ) 23 | return msg 24 | 25 | 26 | class RequestFailedError(Exception): 27 | """ 28 | Ошибка, которая возбуждается, если код ответа не равен 200. 29 | 30 | :param response: Объект ответа. 31 | :type response: `Response` 32 | """ 33 | 34 | def __init__(self, response: requests.Response): 35 | self.response = response 36 | self.status_code = self.response.status_code 37 | self.html_text = self.response.text 38 | 39 | def __str__(self): 40 | msg = ( 41 | f"Ошибка запроса к {self.response.url}" 42 | f"\nКод ошибки: {self.status_code}" 43 | f"\nОтвет: {self.html_text}" 44 | ) 45 | return msg 46 | 47 | 48 | class RequestError(Exception): 49 | """ 50 | Ошибка, которая возбуждается, если возникла ошибка при отправке запроса. 51 | 52 | :param response: Объект ответа. 53 | :type response: `Response` 54 | """ 55 | 56 | def __init__(self, response: requests.Response): 57 | self.response = response 58 | self.json = response.json() or None 59 | self.error_code = self.json["errors"][0]["extensions"]["code"] 60 | self.error_message = self.json["errors"][0]["message"] 61 | 62 | def __str__(self): 63 | msg = ( 64 | f"Ошибка запроса к {self.response.url}" 65 | f"\nКод ошибки: {self.error_code}" 66 | f"\nСообщение: {self.error_message}" 67 | ) 68 | return msg 69 | 70 | 71 | class UnauthorizedError(Exception): 72 | """ 73 | Ошибка, которая возбуждается, если не удалось подключиться к аккаунту Playerok. 74 | """ 75 | 76 | def __str__(self): 77 | return "Не удалось подключиться к аккаунту Playerok. Может вы указали неверный token?" 78 | -------------------------------------------------------------------------------- /playerokapi/listener/events.py: -------------------------------------------------------------------------------- 1 | from ..enums import EventTypes 2 | from .. import types 3 | import time 4 | 5 | 6 | class BaseEvent: 7 | """ 8 | Базовый класс события. 9 | 10 | :param event_type: Тип события. 11 | :type event_type: `PlayerokAPI.enums.EventTypes` 12 | 13 | :param chat_obj: Объект чата, в котором произошло событие. 14 | :type chat_obj: `PlayerokAPI.types.Chat` 15 | """ 16 | 17 | def __init__(self, event_type: EventTypes, chat_obj: types.Chat): 18 | self.type = event_type 19 | """ Тип события. """ 20 | self.chat = chat_obj 21 | """ Объект чата, в котором произошло событие. """ 22 | self.time = time.time() 23 | """ Время события. """ 24 | 25 | 26 | class ChatInitializedEvent(BaseEvent): 27 | """ 28 | Класс события: обнаружен чат при первом запросе Runner'а. 29 | 30 | :param chat_obj: Объект обнаруженного чата. 31 | :type chat_obj: `PlayerokAPI.types.Chat` 32 | """ 33 | 34 | def __init__(self, chat_obj: types.Chat): 35 | super(ChatInitializedEvent, self).__init__( 36 | EventTypes.CHAT_INITIALIZED, chat_obj 37 | ) 38 | self.chat: types.Chat = chat_obj 39 | """ Объект обнаруженного чата. """ 40 | 41 | 42 | class NewMessageEvent(BaseEvent): 43 | """ 44 | Класс события: новое сообщение в чате. 45 | 46 | :param message_obj: Объект полученного сообщения. 47 | :type message_obj: `PlayerokAPI.types.ChatMessage` 48 | 49 | :param chat_obj: Объект чата, в котором произошло событие. 50 | :type chat_obj: `PlayerokAPI.types.Chat` 51 | """ 52 | 53 | def __init__(self, message_obj: types.ChatMessage, chat_obj: types.Chat): 54 | super(NewMessageEvent, self).__init__(EventTypes.NEW_MESSAGE, chat_obj) 55 | self.message: types.ChatMessage = message_obj 56 | """ Объект полученного сообщения. """ 57 | 58 | 59 | class NewDealEvent(BaseEvent): 60 | """ 61 | Класс события: новая созданная сделка (когда покупатель оплатил предмет). 62 | 63 | :param deal_obj: Объект новой сделки. 64 | :type deal_obj: `PlayerokAPI.types.ItemDeal` 65 | 66 | :param chat_obj: Объект чата, в котором произошло событие. 67 | :type chat_obj: `PlayerokAPI.types.Chat` 68 | """ 69 | 70 | def __init__(self, deal_obj: types.ItemDeal, chat_obj: types.Chat): 71 | super(NewDealEvent, self).__init__(EventTypes.NEW_DEAL, chat_obj) 72 | self.deal: types.ItemDeal = deal_obj 73 | """ Объект сделки. """ 74 | 75 | 76 | class NewReviewEvent(BaseEvent): 77 | """ 78 | Класс события: новый отзыв от покупателя. 79 | 80 | :param deal_obj: Объект сделки с отзывом. 81 | :type deal_obj: `PlayerokAPI.types.ItemDeal` 82 | 83 | :param chat_obj: Объект чата, в котором произошло событие. 84 | :type chat_obj: `PlayerokAPI.types.Chat` 85 | """ 86 | 87 | def __init__(self, deal_obj: types.ItemDeal, chat_obj: types.Chat): 88 | super(NewReviewEvent, self).__init__(EventTypes.NEW_REVIEW, chat_obj) 89 | self.deal: types.ItemDeal = deal_obj 90 | """ Объект сделки. """ 91 | 92 | 93 | class DealConfirmedEvent(BaseEvent): 94 | """ 95 | Класс события: покупатель подтвердил сделку. 96 | 97 | :param deal_obj: Объект сделки. 98 | :type deal_obj: `PlayerokAPI.types.ItemDeal` 99 | 100 | :param chat_obj: Объект чата, в котором произошло событие. 101 | :type chat_obj: `PlayerokAPI.types.Chat` 102 | """ 103 | 104 | def __init__(self, deal_obj: types.ItemDeal, chat_obj: types.Chat): 105 | super(DealConfirmedEvent, self).__init__(EventTypes.DEAL_CONFIRMED, chat_obj) 106 | self.deal: types.ItemDeal = deal_obj 107 | """ Объект сделки. """ 108 | 109 | 110 | class DealRolledBackEvent(BaseEvent): 111 | """ 112 | Класс события: продавец вернул средства за сделку. 113 | 114 | :param deal_obj: Объект сделки. 115 | :type deal_obj: `PlayerokAPI.types.ItemDeal` 116 | 117 | :param chat_obj: Объект чата, в котором произошло событие. 118 | :type chat_obj: `PlayerokAPI.types.Chat` 119 | """ 120 | 121 | def __init__(self, deal_obj: types.ItemDeal, chat_obj: types.Chat): 122 | super(DealRolledBackEvent, self).__init__(EventTypes.DEAL_ROLLED_BACK, chat_obj) 123 | self.deal: types.ItemDeal = deal_obj 124 | """ Объект сделки. """ 125 | 126 | 127 | class DealHasProblemEvent(BaseEvent): 128 | """ 129 | Класс события: кто-то сообщил о проблеме в сделке. 130 | 131 | :param deal_obj: Объект сделки. 132 | :type deal_obj: `PlayerokAPI.types.ItemDeal` 133 | 134 | :param chat_obj: Объект чата, в котором произошло событие. 135 | :type chat_obj: `PlayerokAPI.types.Chat` 136 | """ 137 | 138 | def __init__(self, deal_obj: types.ItemDeal, chat_obj: types.Chat): 139 | super(DealHasProblemEvent, self).__init__(EventTypes.DEAL_HAS_PROBLEM, chat_obj) 140 | self.deal: types.ItemDeal = deal_obj 141 | """ Объект сделки. """ 142 | 143 | 144 | class DealProblemResolvedEvent(BaseEvent): 145 | """ 146 | Класс события: проблема в сделке решена. 147 | 148 | :param deal_obj: Объект сделки. 149 | :type deal_obj: `PlayerokAPI.types.ItemDeal` 150 | 151 | :param chat_obj: Объект чата, в котором произошло событие. 152 | :type chat_obj: `PlayerokAPI.types.Chat` 153 | """ 154 | 155 | def __init__(self, deal_obj: types.ItemDeal, chat_obj: types.Chat): 156 | super(DealProblemResolvedEvent, self).__init__( 157 | EventTypes.DEAL_PROBLEM_RESOLVED, chat_obj 158 | ) 159 | self.deal: types.ItemDeal = deal_obj 160 | """ Объект сделки. """ 161 | 162 | 163 | class DealStatusChangedEvent(BaseEvent): 164 | """ 165 | Класс события: статус сделки изменён. 166 | 167 | :param deal_obj: Объект сделки. 168 | :type deal_obj: `PlayerokAPI.types.ItemDeal` 169 | 170 | :param chat_obj: Объект чата, в котором произошло событие. 171 | :type chat_obj: `PlayerokAPI.types.Chat` 172 | """ 173 | 174 | def __init__(self, deal_obj: types.ItemDeal, chat_obj: types.Chat): 175 | super(DealStatusChangedEvent, self).__init__( 176 | EventTypes.DEAL_STATUS_CHANGED, chat_obj 177 | ) 178 | self.deal: types.ItemDeal = deal_obj 179 | """ Объект сделки. """ 180 | 181 | 182 | class ItemPaidEvent(BaseEvent): 183 | """ 184 | Класс события: предмет оплачен. 185 | 186 | :param deal_obj: Объект сделки. 187 | :type deal_obj: `PlayerokAPI.types.Item` 188 | 189 | :param chat_obj: Объект чата, в котором произошло событие. 190 | :type chat_obj: `PlayerokAPI.types.Chat` 191 | """ 192 | 193 | def __init__(self, deal_obj: types.ItemDeal, chat_obj: types.Chat): 194 | super(ItemPaidEvent, self).__init__(EventTypes.ITEM_PAID, chat_obj) 195 | self.deal: types.ItemDeal = deal_obj 196 | """ Объект сделки. """ 197 | 198 | 199 | class ItemSentEvent(BaseEvent): 200 | """ 201 | Класс события: предмет отправлен покупателю. 202 | 203 | :param deal_obj: Объект сделки. 204 | :type deal_obj: `PlayerokAPI.types.Item` 205 | 206 | :param chat_obj: Объект чата, в котором произошло событие. 207 | :type chat_obj: `PlayerokAPI.types.Chat` 208 | """ 209 | 210 | def __init__(self, deal_obj: types.ItemDeal, chat_obj: types.Chat): 211 | super(ItemSentEvent, self).__init__(EventTypes.ITEM_SENT, chat_obj) 212 | self.deal: types.ItemDeal = deal_obj 213 | """ Объект Сделки. """ -------------------------------------------------------------------------------- /playerokapi/listener/listener.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | from logging import getLogger 3 | import time 4 | 5 | from ..account import Account 6 | from ..types import ChatList, ChatMessage, Chat 7 | from .events import * 8 | 9 | 10 | class EventListener: 11 | """ 12 | Слушатель событий с Playerok.com. 13 | 14 | :param account: Объект аккаунта. 15 | :type account: `playerokapi.account.Account` 16 | """ 17 | 18 | def __init__(self, account: Account): 19 | self.account: Account = account 20 | """ Объект аккаунта. """ 21 | 22 | self.__logger = getLogger("playerokapi.listener") 23 | self.__review_check_deals: dict = {} # {deal_id: last_known_testimonial_id} 24 | self.__last_check_time: dict = {} # {deal_id: last_check_time} 25 | 26 | def parse_chat_event( 27 | self, chat: Chat 28 | ) -> list[ChatInitializedEvent]: 29 | """ 30 | Получает ивент с чата. 31 | 32 | :param chat: Объект чата. 33 | :type chat: `playerokapi.types.Chat` 34 | 35 | :return: Массив ивентов. 36 | :rtype: `list` of 37 | `playerokapi.listener.events.ChatInitializedEvent` 38 | """ 39 | 40 | if chat: 41 | return [ChatInitializedEvent(chat)] 42 | return [] 43 | 44 | def get_chat_events( 45 | self, chats: ChatList 46 | ) -> list[ChatInitializedEvent]: 47 | """ 48 | Получает новые ивенты чатов. 49 | 50 | :param chats: Страница чатов. 51 | :type chats: `playerokapi.types.ChatList` 52 | 53 | :return: Массив новых ивентов. 54 | :rtype: `list` of 55 | `playerokapi.listener.events.ChatInitializedEvent` 56 | """ 57 | 58 | events = [] 59 | for chat in chats.chats: 60 | this_events = self.parse_chat_event(chat=chat) 61 | for event in this_events: 62 | events.append(event) 63 | return events 64 | 65 | def parse_message_event( 66 | self, message: ChatMessage, chat: Chat 67 | ) -> list[ 68 | NewMessageEvent 69 | | NewDealEvent 70 | | ItemPaidEvent 71 | | ItemSentEvent 72 | | DealConfirmedEvent 73 | | DealRolledBackEvent 74 | | DealHasProblemEvent 75 | | DealProblemResolvedEvent 76 | | DealStatusChangedEvent 77 | ]: 78 | """ 79 | Получает ивент с сообщения. 80 | 81 | :param message: Объект сообщения. 82 | :type message: `playerokapi.types.ChatMessage` 83 | 84 | :return: Массив ивентов. 85 | :rtype: `list` of 86 | `playerokapi.listener.events.ChatInitializedEvent` \ 87 | _or_ `playerokapi.listener.events.NewMessageEvent` \ 88 | _or_ `playerokapi.listener.events.NewDealEvent` \ 89 | _or_ `playerokapi.listener.events.ItemPaidEvent` \ 90 | _or_ `playerokapi.listener.events.ItemSentEvent` \ 91 | _or_ `playerokapi.listener.events.DealConfirmedEvent` \ 92 | _or_ `playerokapi.listener.events.DealRolledBackEvent` \ 93 | _or_ `playerokapi.listener.events.DealHasProblemEvent` \ 94 | _or_ `playerokapi.listener.events.DealProblemResolvedEvent` \ 95 | _or_ `playerokapi.listener.events.DealStatusChangedEvent(message.deal)` 96 | """ 97 | 98 | if not message: 99 | return [] 100 | if message.text == "{{ITEM_PAID}}" and message.deal is not None: 101 | return [NewDealEvent(message.deal, chat), ItemPaidEvent(message.deal, chat)] 102 | elif message.text == "{{ITEM_SENT}}" and message.deal is not None: 103 | return [ItemSentEvent(message.deal, chat)] 104 | elif message.text == "{{DEAL_CONFIRMED}}" and message.deal is not None: 105 | return [ 106 | DealConfirmedEvent(message.deal, chat), 107 | DealStatusChangedEvent(message.deal, chat), 108 | ] 109 | elif message.text == "{{DEAL_ROLLED_BACK}}" and message.deal is not None: 110 | return [ 111 | DealRolledBackEvent(message.deal, chat), 112 | DealStatusChangedEvent(message.deal, chat), 113 | ] 114 | elif message.text == "{{DEAL_HAS_PROBLEM}}" and message.deal is not None: 115 | return [ 116 | DealHasProblemEvent(message.deal, chat), 117 | DealStatusChangedEvent(message.deal, chat), 118 | ] 119 | elif message.text == "{{DEAL_PROBLEM_RESOLVED}}" and message.deal is not None: 120 | return [ 121 | DealProblemResolvedEvent(message.deal, chat), 122 | DealStatusChangedEvent(message.deal, chat), 123 | ] 124 | 125 | return [NewMessageEvent(message, chat)] 126 | 127 | def _should_check_deal( 128 | self, deal_id: int, delay: int = 30 129 | ) -> bool: 130 | now = time.time() 131 | last_time = self.__last_check_time.get(deal_id, 0) 132 | if now - last_time > delay: 133 | self.__last_check_time[deal_id] = now 134 | return True 135 | return False 136 | 137 | def _check_for_new_review( 138 | self, chat: Chat 139 | ) -> NewReviewEvent | None: 140 | deal_id = chat.last_message.deal.id 141 | # проверка раз в N минут, или только если прошло время, или если что-то изменилось 142 | if not self._should_check_deal(deal_id): 143 | return 144 | deal = self.account.get_deal(deal_id) 145 | if deal.review is not None: 146 | del self.__review_check_deals[deal_id] 147 | return NewReviewEvent(deal, chat) 148 | 149 | def get_message_events( 150 | self, old_chats: ChatList, new_chats: ChatList, get_new_review_events: bool 151 | ) -> list[ 152 | NewMessageEvent 153 | | NewDealEvent 154 | | NewReviewEvent 155 | | ItemPaidEvent 156 | | ItemSentEvent 157 | | DealConfirmedEvent 158 | | DealRolledBackEvent 159 | | DealHasProblemEvent 160 | | DealProblemResolvedEvent 161 | | DealStatusChangedEvent, 162 | ]: 163 | """ 164 | Получает новые ивенты сообщений, сравнивая старые чаты с новыми полученными. 165 | 166 | :param old_chats: Старые чаты. 167 | :type old_chats: `playerokapi.types.ChatList` 168 | 169 | :param new_chats: Новые чаты. 170 | :type new_chats: `playerokapi.types.ChatList` 171 | 172 | :return: Массив новых ивентов. 173 | :rtype: `list` of 174 | `playerokapi.listener.events.ChatInitializedEvent` \ 175 | _or_ `playerokapi.listener.events.NewMessageEvent` \ 176 | _or_ `playerokapi.listener.events.NewDealEvent` \ 177 | _or_ `playerokapi.listener.events.NewReviewEvent` \ 178 | _or_ `playerokapi.listener.events.ItemPaidEvent` \ 179 | _or_ `playerokapi.listener.events.ItemSentEvent` \ 180 | _or_ `playerokapi.listener.events.DealConfirmedEvent` \ 181 | _or_ `playerokapi.listener.events.DealRolledBackEvent` \ 182 | _or_ `playerokapi.listener.events.DealHasProblemEvent` \ 183 | _or_ `playerokapi.listener.events.DealProblemResolvedEvent` \ 184 | _or_ `playerokapi.listener.events.DealStatusChangedEvent(message.deal)` 185 | """ 186 | 187 | events = [] 188 | old_chat_map = {chat.id: chat for chat in old_chats.chats} 189 | for new_chat in new_chats.chats: 190 | old_chat = old_chat_map.get(new_chat.id) 191 | 192 | if not old_chat: 193 | # если это новый чат, парсим ивенты только последнего сообщения, ведь это - покупка товара 194 | events.extend(self.parse_message_event(new_chat.last_message, new_chat)) 195 | continue 196 | 197 | if ( 198 | get_new_review_events 199 | and new_chat.last_message.deal 200 | and old_chat.last_message.deal 201 | and new_chat.last_message.deal.id in self.__review_check_deals 202 | ): 203 | new_review_event = self._check_for_new_review(new_chat) 204 | if new_review_event: events.append(new_review_event) 205 | 206 | if not new_chat.last_message or not old_chat.last_message: 207 | continue 208 | if new_chat.last_message.id == old_chat.last_message.id: 209 | continue 210 | 211 | msg_list = self.account.get_chat_messages(new_chat.id, 10) 212 | new_msgs = [] 213 | for msg in msg_list.messages: 214 | if msg.id == old_chat.last_message.id: 215 | break 216 | new_msgs.append(msg) 217 | 218 | if get_new_review_events and new_chat.last_message.deal: 219 | self.__review_check_deals[new_chat.last_message.deal.id] = None 220 | 221 | for msg in reversed(new_msgs): 222 | events.extend(self.parse_message_event(msg, new_chat)) 223 | return events 224 | 225 | def listen( 226 | self, requests_delay: int | float = 4, get_new_review_events: bool = True 227 | ) -> Generator[ 228 | ChatInitializedEvent 229 | | NewMessageEvent 230 | | NewDealEvent 231 | | NewReviewEvent 232 | | ItemPaidEvent 233 | | ItemSentEvent 234 | | DealConfirmedEvent 235 | | DealRolledBackEvent 236 | | DealHasProblemEvent 237 | | DealProblemResolvedEvent 238 | | DealStatusChangedEvent, 239 | None, 240 | None, 241 | ]: 242 | """ 243 | "Слушает" события в чатах. 244 | Бесконечно отправляет запросы, узнавая новые события из чатов. 245 | 246 | :param requests_delay: Периодичность отправления запросов (в секундах). 247 | :type requests_delay: `int` or `float` 248 | 249 | :param get_new_review_events: Нужно ли слушать новые отзывы? (отправляет больше запросов). 250 | :type get_new_review_events: `bool` 251 | 252 | :return: Полученный ивент. 253 | :rtype: `Generator` of 254 | `playerokapi.listener.events.ChatInitializedEvent` \ 255 | _or_ `playerokapi.listener.events.NewMessageEvent` \ 256 | _or_ `playerokapi.listener.events.NewDealEvent` \ 257 | _or_ `playerokapi.listener.events.NewReviewEvent` \ 258 | _or_ `playerokapi.listener.events.ItemPaidEvent` \ 259 | _or_ `playerokapi.listener.events.ItemSentEvent` \ 260 | _or_ `playerokapi.listener.events.DealConfirmedEvent` \ 261 | _or_ `playerokapi.listener.events.DealRolledBackEvent` \ 262 | _or_ `playerokapi.listener.events.DealHasProblemEvent` \ 263 | _or_ `playerokapi.listener.events.DealProblemResolvedEvent` \ 264 | _or_ `playerokapi.listener.events.DealStatusChangedEvent(message.deal)` 265 | """ 266 | 267 | chats: ChatList = None 268 | while True: 269 | try: 270 | next_chats = self.account.get_chats(10) 271 | if not chats: 272 | events = self.get_chat_events(next_chats) 273 | for event in events: 274 | yield event 275 | elif chats != next_chats: 276 | events = self.get_message_events(chats, next_chats, get_new_review_events) 277 | for event in events: 278 | yield event 279 | 280 | chats = next_chats 281 | time.sleep(requests_delay) 282 | except Exception as e: 283 | self.__logger.error(f"Ошибка при получении ивентов: {e}") 284 | time.sleep(requests_delay) 285 | continue 286 | -------------------------------------------------------------------------------- /plbot/playerokbot.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import asyncio 3 | import time 4 | from datetime import datetime 5 | import time 6 | import traceback 7 | from threading import Thread 8 | import textwrap 9 | import shutil 10 | from colorama import Fore 11 | from aiogram.types import InlineKeyboardMarkup 12 | 13 | from __init__ import VERSION 14 | from playerokapi.account import Account 15 | from playerokapi import exceptions as plapi_exceptions 16 | from playerokapi.enums import * 17 | from playerokapi.listener.events import * 18 | from playerokapi.listener.listener import EventListener 19 | from playerokapi.types import Chat, Item 20 | from core.utils import set_title 21 | from core.handlers import get_bot_event_handlers, set_bot_event_handlers, get_playerok_event_handlers, set_playerok_event_handlers 22 | from settings import DATA, Settings as sett 23 | from logging import getLogger 24 | from data import Data as data 25 | from tgbot.telegrambot import get_telegram_bot, get_telegram_bot_loop 26 | from tgbot.templates import log_text, log_new_mess_kb, log_new_deal_kb 27 | 28 | from .stats import get_stats, set_stats 29 | 30 | 31 | def get_playerok_bot() -> None | PlayerokBot: 32 | if hasattr(PlayerokBot, "instance"): 33 | return getattr(PlayerokBot, "instance") 34 | 35 | class PlayerokBot: 36 | def __new__(cls, *args, **kwargs) -> PlayerokBot: 37 | if not hasattr(cls, "instance"): 38 | cls.instance = super(PlayerokBot, cls).__new__(cls) 39 | return getattr(cls, "instance") 40 | 41 | def __init__(self): 42 | self.config = sett.get("config") 43 | self.messages = sett.get("messages") 44 | self.custom_commands = sett.get("custom_commands") 45 | self.auto_deliveries = sett.get("auto_deliveries") 46 | self.logger = getLogger(f"universal.playerok") 47 | self.playerok_account = Account(token=self.config["playerok"]["api"]["token"], 48 | user_agent=self.config["playerok"]["api"]["user_agent"], 49 | requests_timeout=self.config["playerok"]["api"]["requests_timeout"], 50 | proxy=self.config["playerok"]["api"]["proxy"] or None).get() 51 | 52 | self.initialized_users: list = data.get("initialized_users") 53 | self.stats = get_stats() 54 | 55 | self.__saved_chats: dict[str, Chat] = {} 56 | """Словарь последних запомненных чатов.\nВ формате: {`chat_id` _or_ `username`: `chat_obj`, ...}""" 57 | 58 | def get_chat_by_id(self, chat_id: str) -> Chat: 59 | """ 60 | Получает чат с пользователем из запомненных чатов по его ID. 61 | Запоминает и получает чат, если он не запомнен. 62 | 63 | :param chat_id: ID чата. 64 | :type chat_id: `str` 65 | 66 | :return: Объект чата. 67 | :rtype: `playerokapi.types.Chat` 68 | """ 69 | if chat_id in self.__saved_chats: 70 | return self.__saved_chats[chat_id] 71 | self.__saved_chats[chat_id] = self.playerok_account.get_chat(chat_id) 72 | return self.get_chat_by_id(chat_id) 73 | 74 | def get_chat_by_username(self, username: str) -> Chat: 75 | """ 76 | Получает чат с пользователем из запомненных чатов по никнейму собеседника. 77 | Запоминает и получает чат, если он не запомнен. 78 | 79 | :param username: Юзернейм собеседника чата. 80 | :type username: `str` 81 | 82 | :return: Объект чата. 83 | :rtype: `playerokapi.types.Chat` 84 | """ 85 | if username in self.__saved_chats: 86 | return self.__saved_chats[username] 87 | self.__saved_chats[username] = self.playerok_account.get_chat_by_username(username) 88 | return self.get_chat_by_username(username) 89 | 90 | def get_my_items(self, statuses: list[ItemStatuses] | None = None) -> list[types.ItemProfile]: 91 | """ 92 | Получает все предметы аккаунта. 93 | 94 | :param statuses: Статусы, с которыми нужно получать предметы, _опционально_. 95 | :type statuses: `list[playerokapi.enums.ItemStatuses]` or `None` 96 | 97 | :return: Массив предметов профиля. 98 | :rtype: `list` of `playerokapi.types.ItemProfile` 99 | """ 100 | user = self.playerok_account.get_user(self.playerok_account.id) 101 | my_items: list[types.ItemProfile] = [] 102 | next_cursor = None 103 | while True: 104 | _items = user.get_items(statuses=statuses, after_cursor=next_cursor) 105 | for _item in _items.items: 106 | if _item.id not in [item.id for item in my_items]: 107 | my_items.append(_item) 108 | if not _items.page_info.has_next_page: 109 | break 110 | next_cursor = _items.page_info.end_cursor 111 | time.sleep(0.3) 112 | return my_items 113 | 114 | 115 | def msg(self, message_name: str, exclude_watermark: bool = False, 116 | messages_config_name: str = "messages", messages_data: dict = DATA, 117 | **kwargs) -> str | None: 118 | """ 119 | Получает отформатированное сообщение из словаря сообщений. 120 | 121 | :param message_name: Наименование сообщения в словаре сообщений (ID). 122 | :type message_name: `str` 123 | 124 | :param exclude_watermark: Пропустить и не использовать водяной знак. 125 | :type exclude_watermark: `bool` 126 | 127 | :param messages_config_name: Имя файла конфигурации сообщений. 128 | :type messages_config_name: `str` 129 | 130 | :param messages_data: Словарь данных конфигурационных файлов. 131 | :type messages_data: `dict` or `None` 132 | 133 | :return: Отформатированное сообщение или None, если сообщение выключено. 134 | :rtype: `str` or `None` 135 | """ 136 | 137 | class SafeDict(dict): 138 | def __missing__(self, key): 139 | return "{" + key + "}" 140 | 141 | messages = sett.get(messages_config_name, messages_data) or {} 142 | mess: dict = messages.get(message_name, {}) 143 | if mess.get("enabled") is False: 144 | return None 145 | message_lines: list[str] = mess.get("text", []) 146 | if message_lines and message_lines: 147 | try: 148 | formatted_lines = [line.format_map(SafeDict(**kwargs)) for line in message_lines] 149 | msg = "\n".join(formatted_lines) 150 | if not exclude_watermark and self.config["playerok"]["bot"]["messages_watermark_enabled"]: 151 | msg += f'\n{self.config["playerok"]["bot"]["messages_watermark"]}' or "" 152 | return msg 153 | except: 154 | pass 155 | return f"Не удалось получить сообщение {message_name}" 156 | 157 | def send_message(self, chat_id: str, text: str | None = None, photo_file_path: str | None = None, 158 | mark_chat_as_read: bool = None, max_attempts: int = 3) -> types.ChatMessage: 159 | """ 160 | Кастомный метод отправки сообщения в чат Playerok. 161 | Пытается отправить за 3 попытки, если не удалось - выдаёт ошибку в консоль.\n 162 | Можно отправить текстовое сообщение `text` или фотографию `photo_file_path`. 163 | 164 | :param chat_id: ID чата, в который нужно отправить сообщение. 165 | :type chat_id: `str` 166 | 167 | :param text: Текст сообщения, _опционально_. 168 | :type text: `str` or `None` 169 | 170 | :param photo_file_path: Путь к файлу фотографии, _опционально_. 171 | :type photo_file_path: `str` or `None` 172 | 173 | :param mark_chat_as_read: Пометить чат, как прочитанный перед отправкой, _опционально_. 174 | :type mark_chat_as_read: `bool` 175 | 176 | :return: Объект отправленного сообщения. 177 | :rtype: `PlayerokAPI.types.ChatMessage` 178 | """ 179 | if text is None and photo_file_path is None: 180 | return None 181 | for _ in range(max_attempts): 182 | try: 183 | mark_chat_as_read = (self.config["playerok"]["bot"]["read_chat_before_sending_message_enabled"] or False) if mark_chat_as_read is None else mark_chat_as_read 184 | mess = self.playerok_account.send_message(chat_id, text, photo_file_path, mark_chat_as_read) 185 | return mess 186 | except plapi_exceptions.RequestFailedError: 187 | continue 188 | except Exception as e: 189 | self.logger.error(f"{Fore.LIGHTRED_EX}Ошибка при отправке сообщения {Fore.LIGHTWHITE_EX}«{text}» {Fore.LIGHTRED_EX}в чат {Fore.LIGHTWHITE_EX}{chat_id} {Fore.LIGHTRED_EX}: {Fore.WHITE}{e}") 190 | return 191 | text = text.replace('\n', '').strip() 192 | self.logger.error(f"{Fore.LIGHTRED_EX}Не удалось отправить сообщение {Fore.LIGHTWHITE_EX}«{text}» {Fore.LIGHTRED_EX}в чат {Fore.LIGHTWHITE_EX}{chat_id}") 193 | 194 | def log_to_tg(self, text: str, kb: InlineKeyboardMarkup | None = None): 195 | """ 196 | Отправляет сообщение всем авторизованным в боте пользователям. 197 | 198 | :param text: Текст сообщения. 199 | :type text: `str` 200 | 201 | :param kb: Клавиатура сообщения, _опционально_. 202 | :type kb: `aiogram.types.InlineKeyboardMarkup` or `None` 203 | """ 204 | asyncio.run_coroutine_threadsafe(get_telegram_bot().log_event(text, kb), get_telegram_bot_loop()) 205 | 206 | async def restore_last_sold_item(self, item: Item): 207 | """ 208 | Восстанавливает последний проданный предмет. 209 | 210 | :param item: Объект предмета, который нужно восстановить. 211 | :type item: `playerokapi.types.Item` 212 | """ 213 | try: 214 | profile = self.playerok_account.get_user(id=self.playerok_account.id) 215 | items = profile.get_items(count=24, statuses=[ItemStatuses.SOLD]).items 216 | _item = [profile_item for profile_item in items if profile_item.name == item.name] 217 | if len(_item) <= 0: return 218 | try: item: types.MyItem = self.playerok_account.get_item(_item[0].id) 219 | except: item = _item[0] 220 | 221 | priority_statuses = self.playerok_account.get_item_priority_statuses(item.id, item.price) 222 | priority_status = None 223 | for status in priority_statuses: 224 | if isinstance(item, types.MyItem) and item.priority: 225 | if status.type.name == item.priority.name: 226 | priority_status = status 227 | elif status.type is PriorityTypes.DEFAULT: 228 | priority_status = status 229 | if priority_status: break 230 | 231 | new_item = self.playerok_account.publish_item(item.id, priority_status.id) 232 | if new_item.status is ItemStatuses.PENDING_APPROVAL or new_item.status is ItemStatuses.APPROVED: 233 | self.logger.info(f"{Fore.LIGHTWHITE_EX}«{item.name}» {Fore.WHITE}— {Fore.YELLOW}товар восстановлен") 234 | else: 235 | self.logger.error(f"{Fore.LIGHTRED_EX}Не удалось восстановить предмет «{new_item.name}». Его статус: {Fore.WHITE}{new_item.status.name}") 236 | except Exception as e: 237 | self.logger.error(f"{Fore.LIGHTRED_EX}При восстановлении предмета «{item.name}» произошла ошибка: {Fore.WHITE}{e}") 238 | 239 | def log_new_message(self, message: types.ChatMessage, chat: types.Chat): 240 | plbot = get_playerok_bot() 241 | try: chat_user = [user.username for user in chat.users if user.id != plbot.playerok_account.id][0] 242 | except: chat_user = message.user.username 243 | ch_header = f"Новое сообщение в чате с {chat_user}:" 244 | self.logger.info(f"{Fore.LIGHTBLUE_EX}{ch_header.replace(chat_user, f'{Fore.LIGHTCYAN_EX}{chat_user}')}") 245 | self.logger.info(f"{Fore.LIGHTBLUE_EX}│ {Fore.LIGHTWHITE_EX}{message.user.username}:") 246 | max_width = shutil.get_terminal_size((80, 20)).columns - 40 247 | longest_line_len = 0 248 | text = "" 249 | if message.text is not None: text = message.text 250 | elif message.file is not None: text = f"{Fore.LIGHTMAGENTA_EX}Изображение {Fore.WHITE}({message.file.url})" 251 | for raw_line in text.split("\n"): 252 | if not raw_line.strip(): 253 | self.logger.info(f"{Fore.LIGHTBLUE_EX}│") 254 | continue 255 | wrapped_lines = textwrap.wrap(raw_line, width=max_width) 256 | for wrapped in wrapped_lines: 257 | self.logger.info(f"{Fore.LIGHTBLUE_EX}│ {Fore.WHITE}{wrapped}") 258 | longest_line_len = max(longest_line_len, len(wrapped.strip())) 259 | underline_len = max(len(ch_header)-1, longest_line_len+2) 260 | self.logger.info(f"{Fore.LIGHTBLUE_EX}└{'─'*underline_len}") 261 | 262 | def log_new_deal(self, deal: types.ItemDeal): 263 | self.logger.info(f"{Fore.YELLOW}───────────────────────────────────────") 264 | self.logger.info(f"{Fore.YELLOW}Новая сделка {deal.id}:") 265 | self.logger.info(f" · Покупатель: {Fore.LIGHTWHITE_EX}{deal.user.username}") 266 | self.logger.info(f" · Товар: {Fore.LIGHTWHITE_EX}{deal.item.name}") 267 | self.logger.info(f" · Сумма: {Fore.LIGHTWHITE_EX}{deal.item.price}₽") 268 | self.logger.info(f"{Fore.YELLOW}───────────────────────────────────────") 269 | 270 | def log_new_review(self, deal: types.ItemDeal): 271 | self.logger.info(f"{Fore.YELLOW}───────────────────────────────────────") 272 | self.logger.info(f"{Fore.YELLOW}Новый отзыв по сделке {deal.id}:") 273 | self.logger.info(f" · Оценка: {Fore.LIGHTYELLOW_EX}{'★' * deal.review.rating or 5} ({deal.review.rating or 5})") 274 | self.logger.info(f" · Текст: {Fore.LIGHTWHITE_EX}{deal.review.text}") 275 | self.logger.info(f" · Оставил: {Fore.LIGHTWHITE_EX}{deal.review.user.username}") 276 | self.logger.info(f" · Дата: {Fore.LIGHTWHITE_EX}{datetime.fromisoformat(deal.review.created_at).strftime('%d.%m.%Y %H:%M:%S')}") 277 | self.logger.info(f"{Fore.YELLOW}───────────────────────────────────────") 278 | 279 | def log_deal_status_changed(self, deal: types.ItemDeal): 280 | status = "Неизвестный" 281 | if deal.status is ItemDealStatuses.PAID: status = "Оплачен" 282 | elif deal.status is ItemDealStatuses.PENDING: status = "В ожидании отправки" 283 | elif deal.status is ItemDealStatuses.SENT: status = "Продавец подтвердил выполнение" 284 | elif deal.status is ItemDealStatuses.CONFIRMED: status = "Покупатель подтвердил сделку" 285 | elif deal.status is ItemDealStatuses.ROLLED_BACK: status = "Возврат" 286 | self.logger.info(f"{Fore.WHITE}───────────────────────────────────────") 287 | self.logger.info(f"{Fore.WHITE}Статус сделки {Fore.LIGHTWHITE_EX}{deal.id} {Fore.WHITE}изменился:") 288 | self.logger.info(f" · Статус: {Fore.LIGHTWHITE_EX}{status}") 289 | self.logger.info(f" · Покупатель: {Fore.LIGHTWHITE_EX}{deal.user.username}") 290 | self.logger.info(f" · Товар: {Fore.LIGHTWHITE_EX}{deal.item.name}") 291 | self.logger.info(f" · Сумма: {Fore.LIGHTWHITE_EX}{deal.item.price}₽") 292 | self.logger.info(f"{Fore.WHITE}───────────────────────────────────────") 293 | 294 | def log_new_problem(self, deal: types.ItemDeal): 295 | self.logger.info(f"{Fore.YELLOW}───────────────────────────────────────") 296 | self.logger.info(f"{Fore.YELLOW}Новая жалоба в сделке {deal.id}:") 297 | self.logger.info(f" · Оставил: {Fore.LIGHTWHITE_EX}{deal.user.username}") 298 | self.logger.info(f" · Товар: {Fore.LIGHTWHITE_EX}{deal.item.name}") 299 | self.logger.info(f" · Сумма: {Fore.LIGHTWHITE_EX}{deal.item.price}₽") 300 | self.logger.info(f"{Fore.YELLOW}───────────────────────────────────────") 301 | 302 | async def run_bot(self): 303 | self.logger.info(f"{Fore.GREEN}Playerok бот запущен и активен") 304 | self.logger.info("") 305 | self.logger.info(f"{Fore.LIGHTBLUE_EX}───────────────────────────────────────") 306 | self.logger.info(f"{Fore.LIGHTBLUE_EX}Информация об аккаунте:") 307 | self.logger.info(f" · ID: {Fore.LIGHTWHITE_EX}{self.playerok_account.id}") 308 | self.logger.info(f" · Никнейм: {Fore.LIGHTWHITE_EX}{self.playerok_account.username}") 309 | self.logger.info(f" · Баланс: {Fore.LIGHTWHITE_EX}{self.playerok_account.profile.balance.value}₽") 310 | self.logger.info(f" · Доступно: {Fore.LIGHTWHITE_EX}{self.playerok_account.profile.balance.available}₽") 311 | self.logger.info(f" · В ожидании: {Fore.LIGHTWHITE_EX}{self.playerok_account.profile.balance.pending_income}₽") 312 | self.logger.info(f" · Заморожено: {Fore.LIGHTWHITE_EX}{self.playerok_account.profile.balance.frozen}₽") 313 | self.logger.info(f" · Активные продажи: {Fore.LIGHTWHITE_EX}{self.playerok_account.profile.stats.deals.outgoing.total - self.playerok_account.profile.stats.deals.outgoing.finished}") 314 | self.logger.info(f" · Активные покупки: {Fore.LIGHTWHITE_EX}{self.playerok_account.profile.stats.deals.incoming.total - self.playerok_account.profile.stats.deals.incoming.finished}") 315 | self.logger.info(f"{Fore.LIGHTBLUE_EX}───────────────────────────────────────") 316 | self.logger.info("") 317 | if self.config["playerok"]["api"]["proxy"]: 318 | user, password = self.config["playerok"]["api"]["proxy"].split("@")[0].split(":") if "@" in self.config["playerok"]["api"]["proxy"] else self.config["playerok"]["api"]["proxy"] 319 | ip, port = self.config["playerok"]["api"]["proxy"].split("@")[1].split(":") if "@" in self.config["playerok"]["api"]["proxy"] else self.config["playerok"]["api"]["proxy"] 320 | self.logger.info(f"{Fore.LIGHTBLUE_EX}───────────────────────────────────────") 321 | self.logger.info(f"{Fore.LIGHTBLUE_EX}Информация о прокси:") 322 | self.logger.info(f" · IP: {Fore.LIGHTWHITE_EX}{ip}:{port}") 323 | self.logger.info(f" · Юзер: {(f'{Fore.LIGHTWHITE_EX}{user[:3]}' + '*' * 5) if user else f'Без авторизации'}") 324 | self.logger.info(f" · Пароль: {(f'{Fore.LIGHTWHITE_EX}{password[:3]}' + '*' * 5) if password else f'Без авторизации'}") 325 | self.logger.info(f"{Fore.LIGHTBLUE_EX}───────────────────────────────────────") 326 | self.logger.info("") 327 | 328 | def on_playerok_bot_init(plbot: PlayerokBot): 329 | self.stats.bot_launch_time = datetime.now() 330 | 331 | def endless_loop(): 332 | while True: 333 | try: 334 | balance = self.playerok_account.profile.balance.value if self.playerok_account.profile.balance is not None else "?" 335 | set_title(f"Playerok Universal v{VERSION} | {self.playerok_account.username}: {balance}₽") 336 | if plbot.stats != get_stats(): set_stats(plbot.stats) 337 | if data.get("initialized_users") != self.initialized_users: data.set("initialized_users", self.initialized_users) 338 | if sett.get("config") != self.config: self.config = sett.get("config") 339 | if sett.get("messages") != self.messages: self.messages = sett.get("messages") 340 | if sett.get("custom_commands") != self.custom_commands: self.custom_commands = sett.get("custom_commands") 341 | if sett.get("auto_deliveries") != self.auto_deliveries: self.auto_deliveries = sett.get("auto_deliveries") 342 | except Exception: 343 | self.logger.error(f"{Fore.LIGHTRED_EX}В бесконечном цикле произошла ошибка: {Fore.WHITE}{e}") 344 | time.sleep(3) 345 | 346 | Thread(target=endless_loop, daemon=True).start() 347 | 348 | bot_event_handlers = get_bot_event_handlers() 349 | bot_event_handlers["ON_PLAYEROK_BOT_INIT"].insert(0, on_playerok_bot_init) 350 | set_bot_event_handlers(bot_event_handlers) 351 | 352 | async def on_new_message(plbot: PlayerokBot, event: NewMessageEvent): 353 | try: 354 | this_chat = event.chat 355 | self.log_new_message(event.message, event.chat) 356 | if self.config["playerok"]["bot"]["tg_logging_enabled"] and (self.config["playerok"]["bot"]["tg_logging_events"]["new_user_message"] or self.config["playerok"]["bot"]["tg_logging_events"]["new_system_message"]): 357 | if event.message.user.username != self.playerok_account.username: 358 | do = False 359 | if self.config["playerok"]["bot"]["tg_logging_events"]["new_user_message"] and event.message.user.username not in ["Playerok.com", "Поддержка"]: do = True 360 | if self.config["playerok"]["bot"]["tg_logging_events"]["new_system_message"] and event.message.user.username in ["Playerok.com", "Поддержка"]: do = True 361 | if do: 362 | text = f"{event.message.user.username}: {event.message.text or ''}" 363 | if event.message.file: 364 | text += f' {event.message.file.filename}' 365 | self.log_to_tg(text=log_text(f'💬 Новое сообщение в чате', text.strip()), 366 | kb=log_new_mess_kb(event.message.user.username)) 367 | 368 | if event.message.user is not None: 369 | if event.message.user.id != self.playerok_account.id and event.chat.id not in [self.playerok_account.system_chat_id, self.playerok_account.support_chat_id]: 370 | if event.message.user.id not in self.initialized_users: 371 | self.send_message(this_chat.id, self.msg("first_message", username=event.message.user.username)) 372 | self.initialized_users.append(event.message.user.id) 373 | 374 | if self.config["playerok"]["bot"]["custom_commands_enabled"]: 375 | if event.message.text in self.custom_commands.keys(): 376 | msg = "\n".join(self.custom_commands[event.message.text]) + (f'\n{self.config["playerok"]["bot"]["messages_watermark"]}' if self.config["playerok"]["bot"]["messages_watermark_enabled"] else "") 377 | self.send_message(this_chat.id, msg) 378 | if str(event.message.text).lower() == "!команды" or str(event.message.text).lower() == "!commands": 379 | self.send_message(this_chat.id, self.msg("cmd_commands")) 380 | if str(event.message.text).lower() == "!продавец" or str(event.message.text).lower() == "!seller": 381 | asyncio.run_coroutine_threadsafe(get_telegram_bot().call_seller(event.message.user.username, this_chat.id), get_telegram_bot_loop()) 382 | self.send_message(this_chat.id, self.msg("cmd_seller")) 383 | except Exception: 384 | self.logger.error(f"{Fore.LIGHTRED_EX}При обработке ивента новых сообщений произошла ошибка: {Fore.WHITE}") 385 | traceback.print_exc() 386 | 387 | async def on_new_deal(plbot: PlayerokBot, event: NewDealEvent): 388 | try: 389 | this_chat = event.chat 390 | self.log_new_deal(event.deal) 391 | if self.config["playerok"]["bot"]["tg_logging_enabled"] and self.config["playerok"]["bot"]["tg_logging_events"]["new_deal"]: 392 | self.log_to_tg(text=log_text(f'📋 Новая сделка', f"Покупатель: {event.deal.user.username}\nПредмет: {event.deal.item.name}\nСумма: {event.deal.item.price or '?'}₽"), 393 | kb=log_new_deal_kb(event.deal.user.username, event.deal.id)) 394 | self.send_message(this_chat.id, self.msg("new_deal", deal_item_name=event.deal.item.name, deal_item_price=event.deal.item.price)) 395 | if self.config["playerok"]["bot"]["auto_deliveries_enabled"]: 396 | for auto_delivery in self.auto_deliveries: 397 | for phrase in auto_delivery["keyphrases"]: 398 | if phrase.lower() in event.deal.item.name.lower() or event.deal.item.name.lower() == phrase.lower(): 399 | self.send_message(this_chat.id, "\n".join(auto_delivery["message"])) 400 | break 401 | if self.config["playerok"]["bot"]["auto_complete_deals_enabled"]: 402 | if event.deal.user.id != self.playerok_account.id: 403 | self.playerok_account.update_deal(event.deal.id, ItemDealStatuses.SENT) 404 | except Exception: 405 | self.logger.error(f"{Fore.LIGHTRED_EX}При обработке ивента новой сделки произошла ошибка: {Fore.WHITE}") 406 | traceback.print_exc() 407 | 408 | async def on_new_review(plbot: PlayerokBot, event: NewReviewEvent): 409 | try: 410 | self.log_new_review(event.deal) 411 | if self.config["playerok"]["bot"]["tg_logging_enabled"] and self.config["playerok"]["bot"]["tg_logging_events"]["new_review"]: 412 | self.log_to_tg(text=log_text(f'💬✨ Новый отзыв по сделке', f"Оценка: {'⭐' * event.deal.review.rating}\nОставил: {event.deal.review.creator.username}\nТекст: {event.deal.review.text}\nДата: {datetime.fromisoformat(event.deal.review.created_at).strftime('%d.%m.%Y %H:%M:%S')}"), 413 | kb=log_new_mess_kb(event.deal.user.username)) 414 | except Exception: 415 | self.logger.error(f"{Fore.LIGHTRED_EX}При обработке ивента новых отзывов произошла ошибка: {Fore.WHITE}") 416 | traceback.print_exc() 417 | 418 | async def on_item_paid(plbot: PlayerokBot, event: ItemPaidEvent): 419 | try: 420 | if self.config["playerok"]["bot"]["auto_restore_items_enabled"]: 421 | await self.restore_last_sold_item(event.deal.item) 422 | except Exception: 423 | self.logger.error(f"{Fore.LIGHTRED_EX}При обработке ивента новых сообщений произошла ошибка: {Fore.WHITE}") 424 | traceback.print_exc() 425 | 426 | async def on_new_problem(plbot: PlayerokBot, event: ItemPaidEvent): 427 | try: 428 | self.log_new_problem(event.deal) 429 | if self.config["playerok"]["bot"]["tg_logging_enabled"] and self.config["playerok"]["bot"]["tg_logging_events"]["new_problem"]: 430 | self.log_to_tg(text=log_text(f'🤬 Новая жалоба в сделке', f"Покупатель: {event.deal.user.username}\nПредмет: {event.deal.item.name}"), 431 | kb=log_new_mess_kb(event.deal.user.username)) 432 | except Exception: 433 | self.logger.error(f"{Fore.LIGHTRED_EX}При обработке ивента новых сообщений произошла ошибка: {Fore.WHITE}") 434 | traceback.print_exc() 435 | 436 | async def on_deal_status_changed(plbot: PlayerokBot, event: DealStatusChangedEvent): 437 | try: 438 | this_chat = event.chat 439 | if event.deal.user.id != self.playerok_account.id: 440 | self.log_deal_status_changed(event.deal) 441 | status = "Неизвестный" 442 | if event.deal.status is ItemDealStatuses.PAID: status = "Оплачен" 443 | elif event.deal.status is ItemDealStatuses.PENDING: status = "В ожидании отправки" 444 | elif event.deal.status is ItemDealStatuses.SENT: status = "Продавец подтвердил выполнение" 445 | elif event.deal.status is ItemDealStatuses.CONFIRMED: status = "Покупатель подтвердил сделку" 446 | elif event.deal.status is ItemDealStatuses.ROLLED_BACK: status = "Возврат" 447 | if self.config["playerok"]["bot"]["tg_logging_enabled"] and self.config["playerok"]["bot"]["tg_logging_events"]["deal_status_changed"]: 448 | self.log_to_tg(log_text(f'🔄️📋 Статус сделки изменился', f"Новый статус: {status}")) 449 | if event.deal.status is ItemDealStatuses.PENDING: 450 | self.send_message(this_chat.id, self.msg("deal_pending", deal_id=event.deal.id, deal_item_name=event.deal.item.name, deal_item_price=event.deal.item.price)) 451 | if event.deal.status is ItemDealStatuses.SENT: 452 | self.send_message(this_chat.id, self.msg("deal_sent", deal_id=event.deal.id, deal_item_name=event.deal.item.name, deal_item_price=event.deal.item.price)) 453 | if event.deal.status is ItemDealStatuses.CONFIRMED: 454 | self.send_message(this_chat.id, self.msg("deal_confirmed", deal_id=event.deal.id, deal_item_name=event.deal.item.name, deal_item_price=event.deal.item.price)) 455 | self.stats.deals_completed += 1 456 | self.stats.earned_money += round(event.deal.transaction.value or 0, 2) 457 | elif event.deal.status is ItemDealStatuses.ROLLED_BACK: 458 | self.send_message(this_chat.id, self.msg("deal_confirmed", deal_id=event.deal.id, deal_item_name=event.deal.item.name, deal_item_price=event.deal.item.price)) 459 | self.stats.deals_refunded += 1 460 | except Exception: 461 | self.logger.error(f"{Fore.LIGHTRED_EX}При обработке ивента смены статуса сделки произошла ошибка: {Fore.WHITE}") 462 | traceback.print_exc() 463 | 464 | playerok_event_handlers = get_playerok_event_handlers() 465 | playerok_event_handlers[EventTypes.NEW_MESSAGE].insert(0, on_new_message) 466 | playerok_event_handlers[EventTypes.NEW_DEAL].insert(0, on_new_deal) 467 | playerok_event_handlers[EventTypes.NEW_REVIEW].insert(0, on_new_review) 468 | playerok_event_handlers[EventTypes.DEAL_STATUS_CHANGED].insert(0, on_deal_status_changed) 469 | playerok_event_handlers[EventTypes.DEAL_HAS_PROBLEM].insert(0, on_new_problem) 470 | playerok_event_handlers[EventTypes.ITEM_PAID].insert(0, on_item_paid) 471 | set_playerok_event_handlers(playerok_event_handlers) 472 | 473 | bot_event_handlers = get_bot_event_handlers() 474 | def handle_on_playerok_bot_init(): 475 | """ 476 | Запускается при инициализации FunPay бота. 477 | Запускает за собой все хендлеры ON_PLAYEROK_BOT_INIT 478 | """ 479 | for handler in bot_event_handlers.get("ON_PLAYEROK_BOT_INIT", []): 480 | try: 481 | handler(self) 482 | except Exception as e: 483 | self.logger.error(f"{Fore.LIGHTRED_EX}Ошибка при обработке хендлера ивента ON_PLAYEROK_BOT_INIT: {Fore.WHITE}{e}") 484 | handle_on_playerok_bot_init() 485 | 486 | self.logger.info(f"Слушатель событий запущен") 487 | listener = EventListener(self.playerok_account) 488 | for event in listener.listen(requests_delay=self.config["playerok"]["api"]["listener_requests_delay"]): 489 | playerok_event_handlers = get_playerok_event_handlers() # чтобы каждый раз брать свежие хендлеры, ибо модули могут отключаться/включаться 490 | if event.type in playerok_event_handlers: 491 | for handler in playerok_event_handlers[event.type]: 492 | try: 493 | await handler(self, event) 494 | except Exception as e: 495 | self.logger.error(f"{Fore.LIGHTRED_EX}Ошибка при обработке хендлера {handler} в ивенте {event.type.name}: {Fore.WHITE}{e}") -------------------------------------------------------------------------------- /plbot/stats.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class Stats: 5 | def __init__( 6 | self, 7 | bot_launch_time, 8 | deals_completed, 9 | deals_refunded, 10 | earned_money 11 | ): 12 | self.bot_launch_time: datetime = bot_launch_time 13 | self.deals_completed: int = deals_completed 14 | self.deals_refunded: int = deals_refunded 15 | self.earned_money: int = earned_money 16 | 17 | 18 | _stats = Stats( 19 | bot_launch_time=None, 20 | deals_completed=0, 21 | deals_refunded=0, 22 | earned_money=0 23 | ) 24 | 25 | def get_stats() -> Stats: 26 | global _stats 27 | return _stats 28 | 29 | def set_stats(new): 30 | global _stats 31 | _stats = new -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bs4==0.0.2 2 | wrapper-tls-requests==1.1.4 3 | requests==2.32.3 4 | requests-toolbelt==0.10.1 5 | validators==0.34.0 6 | asyncio==3.4.3 7 | aiogram==3.10.0 8 | lxml==5.2.2 9 | certifi==2025.1.31 10 | urllib3==1.26.16 11 | colorama==0.4.6 12 | colorlog==6.9.0 13 | beautifulsoup4==4.12.3 14 | setuptools==75.8.0 15 | tqdm==4.67.1 -------------------------------------------------------------------------------- /services/updater.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import zipfile 4 | import io 5 | import shutil 6 | from colorama import Fore 7 | from logging import getLogger 8 | 9 | from __init__ import VERSION, SKIP_UPDATES 10 | from core.utils import restart 11 | 12 | 13 | REPO = "alleexxeeyy/playerok-universal" 14 | logger = getLogger(f"universal.updater") 15 | 16 | 17 | def check_for_updates(): 18 | """ 19 | Проверяет проект GitHub на наличие новых обновлений. 20 | Если вышел новый релиз - скачивает и устанавливает обновление. 21 | """ 22 | try: 23 | response = requests.get(f"https://api.github.com/repos/{REPO}/releases") 24 | if response.status_code != 200: 25 | raise Exception(f"Ошибка запроса к GitHub API: {response.status_code}") 26 | releases = response.json() 27 | latest_release = releases[0] 28 | versions = [release["tag_name"] for release in releases] 29 | if VERSION not in versions: 30 | logger.info(f"Вашей версии {Fore.LIGHTWHITE_EX}{VERSION} {Fore.WHITE}нету в релизах репозитория. Последняя версия: {Fore.LIGHTWHITE_EX}{latest_release['tag_name']}") 31 | return 32 | elif VERSION == latest_release["tag_name"]: 33 | logger.info(f"У вас установлена последняя версия: {Fore.LIGHTWHITE_EX}{VERSION}") 34 | return 35 | logger.info(f"{Fore.YELLOW}Доступна новая версия: {Fore.LIGHTWHITE_EX}{latest_release['tag_name']}") 36 | if SKIP_UPDATES: 37 | logger.info(f"Пропускаю установку обновления. Если вы хотите автоматически скачивать обновления, измените значение " 38 | f"{Fore.LIGHTWHITE_EX}SKIP_UPDATES{Fore.WHITE} на {Fore.YELLOW}False {Fore.WHITE}в файле инициализации {Fore.LIGHTWHITE_EX}(__init__.py)") 39 | return 40 | logger.info(f"Скачиваю обновление: {Fore.LIGHTWHITE_EX}{latest_release['html_url']}") 41 | bytes = download_update(latest_release) 42 | if bytes: 43 | if install_update(latest_release, bytes): 44 | logger.info(f"{Fore.YELLOW}Обновление {Fore.LIGHTWHITE_EX}{latest_release['tag_name']} {Fore.YELLOW}было успешно установлено.") 45 | restart() 46 | except Exception as e: 47 | logger.error(f"{Fore.LIGHTRED_EX}При проверке на наличие обновлений произошла ошибка: {Fore.WHITE}{e}") 48 | 49 | 50 | def download_update(release_info: dict) -> bytes: 51 | """ 52 | Получает файлы обновления. 53 | 54 | :param release_info: Информация о GitHub релизе. 55 | :type release_info: `dict` 56 | 57 | :return: Содержимое файлов. 58 | :rtype: `bytes` 59 | """ 60 | try: 61 | logger.info(f"Загружаю обновление {release_info['tag_name']}...") 62 | zip_url = release_info['zipball_url'] 63 | zip_response = requests.get(zip_url) 64 | if zip_response.status_code != 200: 65 | raise Exception(f"При скачивании архива обновления произошла ошибка: {zip_response.status_code}") 66 | return zip_response.content 67 | except Exception as e: 68 | logger.error(f"{Fore.LIGHTRED_EX}При скачивании обновления произошла ошибка: {Fore.WHITE}{e}") 69 | return False 70 | 71 | 72 | def install_update(release_info: dict, content: bytes) -> bool: 73 | """ 74 | Устанавливает файлы обновления в текущий проект. 75 | 76 | :param release_info: Информация о GitHub релизе. 77 | :type release_info: `dict` 78 | 79 | :param content: Содержимое файлов. 80 | :type content: `bytes` 81 | """ 82 | temp_dir = ".temp_update" 83 | try: 84 | logger.info(f"Устанавливаю обновление {release_info['tag_name']}...") 85 | with zipfile.ZipFile(io.BytesIO(content), 'r') as zip_ref: 86 | zip_ref.extractall(temp_dir) 87 | archive_root = None 88 | for item in os.listdir(temp_dir): 89 | if os.path.isdir(os.path.join(temp_dir, item)): 90 | archive_root = os.path.join(temp_dir, item) 91 | break 92 | if not archive_root: 93 | raise Exception("В архиве нет корневой папки!") 94 | for root, _, files in os.walk(archive_root): 95 | for file in files: 96 | src = os.path.join(root, file) 97 | dst = os.path.join('.', os.path.relpath(src, archive_root)) 98 | os.makedirs(os.path.dirname(dst), exist_ok=True) 99 | shutil.copy2(src, dst) 100 | return True 101 | except Exception as e: 102 | logger.error(f"{Fore.LIGHTRED_EX}При установке обновления произошла ошибка: {Fore.WHITE}{e}") 103 | return False 104 | finally: 105 | if os.path.exists(temp_dir): 106 | shutil.rmtree(temp_dir, ignore_errors=True) -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import copy 4 | from dataclasses import dataclass 5 | 6 | 7 | @dataclass 8 | class SettingsFile: 9 | name: str 10 | path: str 11 | need_restore: bool 12 | default: list | dict 13 | 14 | 15 | CONFIG = SettingsFile( 16 | name="config", 17 | path="bot_settings/config.json", 18 | need_restore=True, 19 | default={ 20 | "playerok": { 21 | "api": { 22 | "token": "", 23 | "user_agent": "", 24 | "proxy": "", 25 | "requests_timeout": 30, 26 | "listener_requests_delay": 4 27 | }, 28 | "bot": { 29 | "messages_watermark_enabled": True, 30 | "messages_watermark": "©️ 𝗣𝗹𝗮𝘆𝗲𝗿𝗼𝗸 𝗨𝗻𝗶𝘃𝗲𝗿𝘀𝗮𝗹", 31 | "read_chat_before_sending_message_enabled": True, 32 | "first_message_enabled": True, 33 | "custom_commands_enabled": True, 34 | "auto_deliveries_enabled": True, 35 | "auto_restore_items_enabled": True, 36 | "auto_restore_items_priority_status": "DEFAULT", 37 | "auto_complete_deals_enabled": True, 38 | "tg_logging_enabled": True, 39 | "tg_logging_chat_id": "", 40 | "tg_logging_events": { 41 | "new_user_message": True, 42 | "new_system_message": True, 43 | "new_deal": True, 44 | "new_review": True, 45 | "new_problem": True, 46 | "deal_status_changed": True, 47 | } 48 | } 49 | }, 50 | "telegram": { 51 | "api": { 52 | "token": "" 53 | }, 54 | "bot": { 55 | "password": "", 56 | "signed_users": [] 57 | } 58 | } 59 | } 60 | ) 61 | 62 | MESSAGES = SettingsFile( 63 | name="messages", 64 | path="bot_settings/messages.json", 65 | need_restore=True, 66 | default={ 67 | "first_message": { 68 | "enabled": True, 69 | "text": [ 70 | "👋 Привет, {username}, я бот-помощник 𝗣𝗹𝗮𝘆𝗲𝗿𝗼𝗸 𝗨𝗻𝗶𝘃𝗲𝗿𝘀𝗮𝗹", 71 | "", 72 | "💡 Если вы хотите поговорить с продавцом, напишите команду !продавец, чтобы я пригласил его в этот диалог", 73 | "", 74 | "Чтобы узнать все мои команды, напишите !команды" 75 | ] 76 | }, 77 | "cmd_error": { 78 | "enabled": True, 79 | "text": [ 80 | "❌ При вводе команды произошла ошибка: {error}" 81 | ] 82 | }, 83 | "cmd_commands": { 84 | "enabled": True, 85 | "text": [ 86 | "🕹️ Основные команды:", 87 | "・ !продавец — уведомить и позвать продавца в этот чат" 88 | ] 89 | }, 90 | "cmd_seller": { 91 | "enabled": True, 92 | "text": [ 93 | "💬 Продавец был вызван в этот чат. Ожидайте, пока он подключиться к диалогу..." 94 | ] 95 | }, 96 | "new_deal": { 97 | "enabled": False, 98 | "text": [ 99 | "📋 Спасибо за покупку «{deal_item_name}» в количестве {deal_amount} шт.", 100 | "" 101 | "Продавца сейчас может не быть на месте, чтобы позвать его, используйте команду !продавец." 102 | ] 103 | }, 104 | "deal_pending": { 105 | "enabled": False, 106 | "text": [ 107 | "⌛ Отправьте нужные данные, чтобы я смог выполнить ваш заказ" 108 | ] 109 | }, 110 | "deal_sent": { 111 | "enabled": False, 112 | "text": [ 113 | "✅ Я подтвердил выполнение вашего заказа! Если вы не получили купленный товар - напишите это в чате" 114 | ] 115 | }, 116 | "deal_confirmed": { 117 | "enabled": False, 118 | "text": [ 119 | "🌟 Спасибо за успешную сделку. Буду рад, если оставите отзыв. Жду вас в своём магазине в следующий раз, удачи!" 120 | ] 121 | }, 122 | "deal_refunded": { 123 | "enabled": False, 124 | "text": [ 125 | "📦 Заказ был возвращён. Надеюсь эта сделка не принесла вам неудобств. Жду вас в своём магазине в следующий раз, удачи!" 126 | ] 127 | } 128 | } 129 | ) 130 | 131 | CUSTOM_COMMANDS = SettingsFile( 132 | name="custom_commands", 133 | path="bot_settings/custom_commands.json", 134 | need_restore=False, 135 | default={} 136 | ) 137 | 138 | AUTO_DELIVERIES = SettingsFile( 139 | name="auto_deliveries", 140 | path="bot_settings/auto_deliveries.json", 141 | need_restore=False, 142 | default=[] 143 | ) 144 | 145 | DATA = [CONFIG, MESSAGES, CUSTOM_COMMANDS, AUTO_DELIVERIES] 146 | 147 | 148 | def validate_config(config, default): 149 | """ 150 | Проверяет структуру конфига на соответствие стандартному шаблону. 151 | 152 | :param config: Текущий конфиг. 153 | :type config: `dict` 154 | 155 | :param default: Стандартный шаблон конфига. 156 | :type default: `dict` 157 | 158 | :return: True если структура валидна, иначе False. 159 | :rtype: bool 160 | """ 161 | for key, value in default.items(): 162 | if key not in config: 163 | return False 164 | if type(config[key]) is not type(value): 165 | return False 166 | if isinstance(value, dict) and isinstance(config[key], dict): 167 | if not validate_config(config[key], value): 168 | return False 169 | return True 170 | 171 | 172 | def restore_config(config: dict, default: dict): 173 | """ 174 | Восстанавливает недостающие параметры в конфиге из стандартного шаблона. 175 | И удаляет параметры из конфига, которых нету в стандартном шаблоне. 176 | 177 | :param config: Текущий конфиг. 178 | :type config: `dict` 179 | 180 | :param default: Стандартный шаблон конфига. 181 | :type default: `dict` 182 | 183 | :return: Восстановленный конфиг. 184 | :rtype: `dict` 185 | """ 186 | config = copy.deepcopy(config) 187 | 188 | def check_default(config, default): 189 | for key, value in dict(default).items(): 190 | if key not in config: 191 | config[key] = value 192 | elif type(value) is not type(config[key]): 193 | config[key] = value 194 | elif isinstance(value, dict) and isinstance(config[key], dict): 195 | check_default(config[key], value) 196 | return config 197 | 198 | config = check_default(config, default) 199 | return config 200 | 201 | 202 | def get_json(path: str, default: dict, need_restore: bool = True) -> dict: 203 | """ 204 | Получает данные файла настроек. 205 | Создаёт файл настроек, если его нет. 206 | Добавляет новые данные, если такие есть. 207 | 208 | :param path: Путь к json файлу. 209 | :type path: `str` 210 | 211 | :param default: Стандартный шаблон файла. 212 | :type default: `dict` 213 | 214 | :param need_restore: Нужно ли сделать проверку на целостность конфига. 215 | :type need_restore: `bool` 216 | """ 217 | folder_path = os.path.dirname(path) 218 | if not os.path.exists(folder_path): 219 | os.makedirs(folder_path) 220 | try: 221 | with open(path, 'r', encoding='utf-8') as f: 222 | config = json.load(f) 223 | if need_restore: 224 | new_config = restore_config(config, default) 225 | if config != new_config: 226 | config = new_config 227 | with open(path, 'w', encoding='utf-8') as f: 228 | json.dump(config, f, indent=4, ensure_ascii=False) 229 | except: 230 | config = default 231 | with open(path, 'w', encoding='utf-8') as f: 232 | json.dump(config, f, indent=4, ensure_ascii=False) 233 | finally: 234 | return config 235 | 236 | 237 | def set_json(path: str, new: dict): 238 | """ 239 | Устанавливает новые данные в файл настроек. 240 | 241 | :param path: Путь к json файлу. 242 | :type path: `str` 243 | 244 | :param new: Новые данные. 245 | :type new: `dict` 246 | """ 247 | with open(path, 'w', encoding='utf-8') as f: 248 | json.dump(new, f, indent=4, ensure_ascii=False) 249 | 250 | 251 | class Settings: 252 | 253 | @staticmethod 254 | def get(name: str, data: list[SettingsFile] = DATA) -> dict | None: 255 | try: 256 | file = [file for file in data if file.name == name][0] 257 | return get_json(file.path, file.default, file.need_restore) 258 | except: return None 259 | 260 | @staticmethod 261 | def set(name: str, new: list | dict, data: list[SettingsFile] = DATA): 262 | try: 263 | file = [file for file in data if file.name == name][0] 264 | set_json(file.path, new) 265 | except: pass -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | python bot.py 3 | pause -------------------------------------------------------------------------------- /tgbot/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from .handlers import router as handlers_router 4 | from .callback_handlers import router as callback_handlers_router 5 | 6 | router = Router() 7 | router.include_routers(callback_handlers_router, handlers_router) -------------------------------------------------------------------------------- /tgbot/callback_datas/__init__.py: -------------------------------------------------------------------------------- 1 | from .navigation import * 2 | from .actions import * -------------------------------------------------------------------------------- /tgbot/callback_datas/actions.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | 3 | class RememberUsername(CallbackData, prefix="rech"): 4 | name: str 5 | do: str 6 | 7 | class RememberDealId(CallbackData, prefix="rede"): 8 | de_id: str 9 | do: str -------------------------------------------------------------------------------- /tgbot/callback_datas/navigation.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | from uuid import UUID 3 | 4 | 5 | 6 | class MenuNavigation(CallbackData, prefix="menpag"): 7 | to: str 8 | 9 | 10 | class SettingsNavigation(CallbackData, prefix="sepag"): 11 | to: str 12 | 13 | class BotSettingsNavigation(CallbackData, prefix="bspag"): 14 | to: str 15 | 16 | class ItemsSettingsNavigation(CallbackData, prefix="ispag"): 17 | to: str 18 | 19 | class InstructionNavigation(CallbackData, prefix="inspag"): 20 | to: str 21 | 22 | 23 | class ModulesPagination(CallbackData, prefix="modpag"): 24 | page: int 25 | 26 | class ModulePage(CallbackData, prefix="modpage"): 27 | uuid: UUID 28 | 29 | 30 | class CustomCommandsPagination(CallbackData, prefix="cucopag"): 31 | page: int 32 | 33 | class CustomCommandPage(CallbackData, prefix="cucopage"): 34 | command: str 35 | 36 | 37 | class AutoDeliveriesPagination(CallbackData, prefix="audepag"): 38 | page: int 39 | 40 | class AutoDeliveryPage(CallbackData, prefix="audepage"): 41 | index: int 42 | 43 | 44 | class MessagesPagination(CallbackData, prefix="messpag"): 45 | page: int 46 | 47 | class MessagePage(CallbackData, prefix="messpage"): 48 | message_id: str -------------------------------------------------------------------------------- /tgbot/callback_handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from .navigation import router as navigation_router 4 | from .actions import router as actions_router 5 | 6 | router = Router() 7 | 8 | router.include_routers(navigation_router, actions_router) -------------------------------------------------------------------------------- /tgbot/callback_handlers/navigation.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | from aiogram.types import CallbackQuery 3 | from aiogram.fsm.context import FSMContext 4 | 5 | from .. import templates as templ 6 | from .. import callback_datas as calls 7 | from ..helpful import throw_float_message 8 | 9 | router = Router() 10 | 11 | 12 | 13 | @router.callback_query(calls.MenuNavigation.filter()) 14 | async def callback_menu_navigation(callback: CallbackQuery, callback_data: calls.MenuNavigation, state: FSMContext): 15 | await state.set_state(None) 16 | to = callback_data.to 17 | if to == "default": 18 | await throw_float_message(state, callback.message, templ.menu_text(), templ.menu_kb(), callback) 19 | elif to == "stats": 20 | await throw_float_message(state, callback.message, templ.stats_text(), templ.stats_kb(), callback) 21 | elif to == "profile": 22 | await throw_float_message(state, callback.message, templ.profile_text(), templ.profile_kb(), callback) 23 | 24 | @router.callback_query(calls.InstructionNavigation.filter()) 25 | async def callback_instruction_navgiation(callback: CallbackQuery, callback_data: calls.InstructionNavigation, state: FSMContext): 26 | await state.set_state(None) 27 | to = callback_data.to 28 | if to == "default": 29 | await throw_float_message(state, callback.message, templ.instruction_text(), templ.instruction_kb(), callback) 30 | elif to == "commands": 31 | await throw_float_message(state, callback.message, templ.instruction_comms_text(), templ.instruction_comms_kb(), callback) 32 | 33 | @router.callback_query(calls.SettingsNavigation.filter()) 34 | async def callback_settings_navigation(callback: CallbackQuery, callback_data: calls.SettingsNavigation, state: FSMContext): 35 | await state.set_state(None) 36 | to = callback_data.to 37 | if to == "default": 38 | await throw_float_message(state, callback.message, templ.settings_text(), templ.settings_kb(), callback) 39 | elif to == "auth": 40 | await throw_float_message(state, callback.message, templ.settings_auth_text(), templ.settings_auth_kb(), callback) 41 | elif to == "conn": 42 | await throw_float_message(state, callback.message, templ.settings_conn_text(), templ.settings_conn_kb(), callback) 43 | elif to == "items": 44 | await throw_float_message(state, callback.message, templ.settings_items_text(), templ.settings_items_kb(), callback) 45 | elif to == "logger": 46 | await throw_float_message(state, callback.message, templ.settings_logger_text(), templ.settings_logger_kb(), callback) 47 | elif to == "other": 48 | await throw_float_message(state, callback.message, templ.settings_other_text(), templ.settings_other_kb(), callback) 49 | 50 | 51 | @router.callback_query(calls.CustomCommandsPagination.filter()) 52 | async def callback_custom_commands_pagination(callback: CallbackQuery, callback_data: calls.CustomCommandsPagination, state: FSMContext): 53 | await state.set_state(None) 54 | page = callback_data.page 55 | await state.update_data(last_page=page) 56 | await throw_float_message(state, callback.message, templ.settings_comm_text(), templ.settings_comm_kb(page), callback) 57 | 58 | @router.callback_query(calls.CustomCommandPage.filter()) 59 | async def callback_custom_command_page(callback: CallbackQuery, callback_data: calls.CustomCommandPage, state: FSMContext): 60 | await state.set_state(None) 61 | command = callback_data.command 62 | data = await state.get_data() 63 | await state.update_data(custom_command=command) 64 | last_page = data.get("last_page") or 0 65 | await throw_float_message(state, callback.message, templ.settings_comm_page_text(command), templ.settings_comm_page_kb(command, last_page), callback) 66 | 67 | 68 | @router.callback_query(calls.AutoDeliveriesPagination.filter()) 69 | async def callback_auto_delivery_pagination(callback: CallbackQuery, callback_data: calls.AutoDeliveriesPagination, state: FSMContext): 70 | try: 71 | await state.set_state(None) 72 | page = callback_data.page 73 | await state.update_data(last_page=page) 74 | await throw_float_message(state, callback.message, templ.settings_deliv_text(), templ.settings_deliv_kb(page), callback) 75 | except: 76 | import traceback 77 | traceback.print_exc() 78 | 79 | @router.callback_query(calls.AutoDeliveryPage.filter()) 80 | async def callback_custom_command_page(callback: CallbackQuery, callback_data: calls.AutoDeliveryPage, state: FSMContext): 81 | try: 82 | await state.set_state(None) 83 | index = callback_data.index 84 | data = await state.get_data() 85 | await state.update_data(auto_delivery_index=index) 86 | last_page = data.get("last_page") or 0 87 | await throw_float_message(state, callback.message, templ.settings_deliv_page_text(index), templ.settings_deliv_page_kb(index, last_page), callback) 88 | except: 89 | import traceback 90 | traceback.print_exc() 91 | 92 | 93 | @router.callback_query(calls.MessagesPagination.filter()) 94 | async def callback_messages_pagination(callback: CallbackQuery, callback_data: calls.MessagesPagination, state: FSMContext): 95 | await state.set_state(None) 96 | page = callback_data.page 97 | await state.update_data(last_page=page) 98 | await throw_float_message(state, callback.message, templ.settings_mess_text(), templ.settings_mess_kb(page), callback) 99 | 100 | @router.callback_query(calls.MessagePage.filter()) 101 | async def callback_message_page(callback: CallbackQuery, callback_data: calls.MessagePage, state: FSMContext): 102 | await state.set_state(None) 103 | message_id = callback_data.message_id 104 | data = await state.get_data() 105 | await state.update_data(message_id=message_id) 106 | last_page = data.get("last_page") or 0 107 | await throw_float_message(state, callback.message, templ.settings_mess_page_text(message_id), templ.settings_mess_page_kb(message_id, last_page), callback) 108 | 109 | 110 | @router.callback_query(calls.ModulesPagination.filter()) 111 | async def callback_modules_pagination(callback: CallbackQuery, callback_data: calls.ModulesPagination, state: FSMContext): 112 | await state.set_state(None) 113 | page = callback_data.page 114 | await state.update_data(last_page=page) 115 | await throw_float_message(state, callback.message, templ.modules_text(), templ.modules_kb(page), callback) 116 | 117 | @router.callback_query(calls.ModulePage.filter()) 118 | async def callback_module_page(callback: CallbackQuery, callback_data: calls.ModulePage, state: FSMContext): 119 | await state.set_state(None) 120 | module_uuid = callback_data.uuid 121 | data = await state.get_data() 122 | await state.update_data(module_uuid=module_uuid) 123 | last_page = data.get("last_page") or 0 124 | await throw_float_message(state, callback.message, templ.module_page_text(module_uuid), templ.module_page_kb(module_uuid, last_page), callback) -------------------------------------------------------------------------------- /tgbot/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from .commands import router as commands_router 4 | from .entering import router as entering_router 5 | 6 | router = Router() 7 | 8 | router.include_routers(commands_router, entering_router) -------------------------------------------------------------------------------- /tgbot/handlers/commands.py: -------------------------------------------------------------------------------- 1 | from aiogram import types, Router 2 | from aiogram.filters import Command 3 | from aiogram.fsm.context import FSMContext 4 | 5 | from .. import templates as templ 6 | from ..helpful import throw_float_message, do_auth 7 | from settings import Settings as sett 8 | 9 | router = Router() 10 | 11 | 12 | @router.message(Command("start")) 13 | async def handler_start(message: types.Message, state: FSMContext): 14 | await state.set_state(None) 15 | config = sett.get("config") 16 | if message.from_user.id not in config["telegram"]["bot"]["signed_users"]: 17 | return await do_auth(message, state) 18 | await throw_float_message(state=state, 19 | message=message, 20 | text=templ.menu_text(), 21 | reply_markup=templ.menu_kb()) -------------------------------------------------------------------------------- /tgbot/handlers/entering.py: -------------------------------------------------------------------------------- 1 | import re 2 | from aiogram import types, Router, F 3 | from aiogram.fsm.context import FSMContext 4 | from aiogram.exceptions import TelegramAPIError 5 | 6 | from settings import Settings as sett 7 | 8 | from .. import templates as templ 9 | from .. import states 10 | from .. import callback_datas as calls 11 | from ..helpful import throw_float_message 12 | 13 | 14 | router = Router() 15 | 16 | 17 | def is_eng_str(str: str): 18 | pattern = r'^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};:\'",.<>/?\\|`~ ]+$' 19 | return bool(re.match(pattern, str)) 20 | 21 | 22 | @router.message(states.ActionsStates.entering_message_text, F.text) 23 | async def handler_entering_password(message: types.Message, state: FSMContext): 24 | try: 25 | from plbot.playerokbot import get_playerok_bot 26 | await state.set_state(None) 27 | if len(message.text.strip()) <= 0: 28 | raise Exception("❌ Слишком короткий текст") 29 | 30 | data = await state.get_data() 31 | plbot = get_playerok_bot() 32 | username = data.get("username") 33 | chat = plbot.get_chat_by_username(username) 34 | plbot.send_message(chat_id=chat.id, text=message.text.strip()) 35 | 36 | await throw_float_message(state=state, 37 | message=message, 38 | text=templ.do_action_text(f"✅ Пользователю {username} было отправлено сообщение:
{message.text.strip()}
"), 39 | reply_markup=templ.destroy_kb()) 40 | except Exception as e: 41 | if e is not TelegramAPIError: 42 | await throw_float_message(state=state, 43 | message=message, 44 | text=templ.do_action_text(e), 45 | reply_markup=templ.destroy_kb()) 46 | 47 | 48 | @router.message(states.SystemStates.entering_password, F.text) 49 | async def handler_entering_password(message: types.Message, state: FSMContext): 50 | try: 51 | await state.set_state(None) 52 | config = sett.get("config") 53 | if message.text.strip() != config["telegram"]["bot"]["password"]: 54 | raise Exception("❌ Неверный ключ-пароль.") 55 | 56 | config["telegram"]["bot"]["signed_users"].append(message.from_user.id) 57 | sett.set("config", config) 58 | 59 | await throw_float_message(state=state, 60 | message=message, 61 | text=templ.menu_text(), 62 | reply_markup=templ.menu_kb()) 63 | except Exception as e: 64 | if e is not TelegramAPIError: 65 | await throw_float_message(state=state, 66 | message=message, 67 | text=templ.sign_text(e), 68 | reply_markup=templ.destroy_kb()) 69 | 70 | @router.message(states.MessagesStates.entering_page, F.text) 71 | async def handler_entering_messages_page(message: types.Message, state: FSMContext): 72 | try: 73 | await state.set_state(None) 74 | if not message.text.strip().isdigit(): 75 | raise Exception("❌ Вы должны ввести числовое значение") 76 | 77 | await state.update_data(last_page=int(message.text.strip())-1) 78 | await throw_float_message(state=state, 79 | message=message, 80 | text=templ.settings_mess_text(), 81 | reply_markup=templ.settings_mess_kb(int(message.text)-1)) 82 | except Exception as e: 83 | if e is not TelegramAPIError: 84 | data = await state.get_data() 85 | await throw_float_message(state=state, 86 | message=message, 87 | text=templ.settings_mess_float_text(e), 88 | reply_markup=templ.back_kb(calls.MessagesPagination(page=data.get("last_page") or 0).pack())) 89 | 90 | @router.message(states.MessagePageStates.entering_message_text, F.text) 91 | async def handler_entering_message_text(message: types.Message, state: FSMContext): 92 | try: 93 | await state.set_state(None) 94 | if len(message.text.strip()) <= 0: 95 | raise Exception("❌ Слишком короткий текст") 96 | 97 | data = await state.get_data() 98 | messages = sett.get("messages") 99 | message_split_lines = message.text.strip().split('\n') 100 | messages[data["message_id"]]["text"] = message_split_lines 101 | sett.set("messages", messages) 102 | await throw_float_message(state=state, 103 | message=message, 104 | text=templ.settings_mess_page_float_text(f"✅ Текст сообщения {data['message_id']} был успешно изменён на
{message.text.strip()}
"), 105 | reply_markup=templ.back_kb(calls.MessagePage(message_id=data.get("message_id")).pack())) 106 | except Exception as e: 107 | if e is not TelegramAPIError: 108 | data = await state.get_data() 109 | await throw_float_message(state=state, 110 | message=message, 111 | text=templ.settings_mess_page_float_text(e), 112 | reply_markup=templ.back_kb(calls.MessagePage(message_id=data.get("message_id")).pack())) 113 | 114 | @router.message(states.SettingsStates.entering_token, F.text) 115 | async def handler_entering_token(message: types.Message, state: FSMContext): 116 | try: 117 | await state.set_state(None) 118 | if len(message.text.strip()) <= 3 or len(message.text.strip()) >= 500: 119 | raise Exception("❌ Слишком короткое или длинное значение") 120 | 121 | config = sett.get("config") 122 | config["playerok"]["api"]["token"] = message.text.strip() 123 | sett.set("config", config) 124 | await throw_float_message(state=state, 125 | message=message, 126 | text=templ.settings_auth_float_text(f"✅ Токен был успешно изменён на {message.text.strip()}"), 127 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="auth").pack())) 128 | except Exception as e: 129 | if e is not TelegramAPIError: 130 | await throw_float_message(state=state, 131 | message=message, 132 | text=templ.settings_auth_float_text(e), 133 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="auth").pack())) 134 | 135 | @router.message(states.SettingsStates.entering_user_agent, F.text) 136 | async def handler_entering_user_agent(message: types.Message, state: FSMContext): 137 | try: 138 | await state.set_state(None) 139 | if len(message.text.strip()) <= 3: 140 | raise Exception("❌ Слишком короткое значение") 141 | 142 | config = sett.get("config") 143 | config["playerok"]["api"]["user_agent"] = message.text.strip() 144 | sett.set("config", config) 145 | await throw_float_message(state=state, 146 | message=message, 147 | text=templ.settings_auth_float_text(f"✅ user_agent был успешно изменён на {message.text.strip()}"), 148 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="auth").pack())) 149 | except Exception as e: 150 | if e is not TelegramAPIError: 151 | await throw_float_message(state=state, 152 | message=message, 153 | text=templ.settings_auth_float_text(e), 154 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="auth").pack())) 155 | 156 | @router.message(states.SettingsStates.entering_proxy, F.text) 157 | async def handler_entering_proxy(message: types.Message, state: FSMContext): 158 | try: 159 | await state.set_state(None) 160 | if len(message.text.strip()) <= 3: 161 | raise Exception("❌ Слишком короткое значение") 162 | if not is_eng_str(message.text.strip()): 163 | raise Exception("❌ Некорректный прокси") 164 | 165 | config = sett.get("config") 166 | config["playerok"]["api"]["proxy"] = message.text.strip() 167 | sett.set("config", config) 168 | await throw_float_message(state=state, 169 | message=message, 170 | text=templ.settings_auth_float_text(f"✅ Прокси был успешно изменён на {message.text.strip()}"), 171 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="conn").pack())) 172 | except Exception as e: 173 | if e is not TelegramAPIError: 174 | await throw_float_message(state=state, 175 | message=message, 176 | text=templ.settings_auth_float_text(e), 177 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="conn").pack())) 178 | 179 | @router.message(states.SettingsStates.entering_requests_timeout, F.text) 180 | async def handler_entering_requests_timeout(message: types.Message, state: FSMContext): 181 | try: 182 | await state.set_state(None) 183 | if not message.text.strip().isdigit(): 184 | raise Exception("❌ Вы должны ввести числовое значение") 185 | if int(message.text.strip()) < 0: 186 | raise Exception("❌ Слишком низкое значение") 187 | 188 | config = sett.get("config") 189 | config["playerok"]["api"]["requests_timeout"] = int(message.text.strip()) 190 | sett.set("config", config) 191 | await throw_float_message(state=state, 192 | message=message, 193 | text=templ.settings_conn_float_text(f"✅ Таймаут запросов был успешно изменён на {message.text.strip()}"), 194 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="conn").pack())) 195 | except Exception as e: 196 | if e is not TelegramAPIError: 197 | await throw_float_message(state=state, 198 | message=message, 199 | text=templ.settings_conn_float_text(e), 200 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="conn").pack())) 201 | 202 | @router.message(states.SettingsStates.entering_listener_requests_delay, F.text) 203 | async def handler_entering_listener_requests_delay(message: types.Message, state: FSMContext): 204 | try: 205 | await state.set_state(None) 206 | if not message.text.strip().isdigit(): 207 | raise Exception("❌ Вы должны ввести числовое значение") 208 | if int(message.text.strip()) < 0: 209 | raise Exception("❌ Слишком низкое значение") 210 | 211 | config = sett.get("config") 212 | config["playerok"]["api"]["listener_requests_delay"] = int(message.text.strip()) 213 | sett.set("config", config) 214 | await throw_float_message(state=state, 215 | message=message, 216 | text=templ.settings_conn_float_text(f"✅ Периодичность запросов была успешна изменена на {message.text.strip()}"), 217 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="conn").pack())) 218 | except Exception as e: 219 | if e is not TelegramAPIError: 220 | await throw_float_message(state=state, 221 | message=message, 222 | text=templ.settings_conn_float_text(e), 223 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="conn").pack())) 224 | 225 | 226 | @router.message(states.SettingsStates.entering_tg_logging_chat_id, F.text) 227 | async def handler_entering_tg_logging_chat_id(message: types.Message, state: FSMContext): 228 | try: 229 | await state.set_state(None) 230 | if len(message.text.strip()) < 0: 231 | raise Exception("❌ Слишком низкое значение") 232 | 233 | if message.text.strip().isdigit(): chat_id = "-100" + str(message.text.strip()).replace("-100", "") 234 | else: chat_id = "@" + str(message.text.strip()).replace("@", "") 235 | 236 | config = sett.get("config") 237 | config["playerok"]["bot"]["tg_logging_chat_id"] = chat_id 238 | sett.set("config", config) 239 | await throw_float_message(state=state, 240 | message=message, 241 | text=templ.settings_logger_float_text(f"✅ ID чата для логов было успешно изменено на {chat_id}"), 242 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="logger").pack())) 243 | except Exception as e: 244 | if e is not TelegramAPIError: 245 | await throw_float_message(state=state, 246 | message=message, 247 | text=templ.settings_logger_float_text(e), 248 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="logger").pack())) 249 | 250 | 251 | @router.message(states.CustomCommandsStates.entering_page, F.text) 252 | async def handler_entering_custom_commands_page(message: types.Message, state: FSMContext): 253 | try: 254 | await state.set_state(None) 255 | if not message.text.strip().isdigit(): 256 | raise Exception("❌ Вы должны ввести числовое значение") 257 | 258 | await state.update_data(last_page=int(message.text.strip())-1) 259 | await throw_float_message(state=state, 260 | message=message, 261 | text=templ.settings_comm_text(), 262 | reply_markup=templ.settings_comm_kb(page=int(message.text)-1)) 263 | except Exception as e: 264 | if e is not TelegramAPIError: 265 | data = await state.get_data() 266 | await throw_float_message(state=state, 267 | message=message, 268 | text=templ.settings_comm_float_text(e), 269 | reply_markup=templ.back_kb(calls.CustomCommandsPagination(page=data.get("last_page") or 0).pack())) 270 | 271 | @router.message(states.CustomCommandsStates.entering_new_custom_command, F.text) 272 | async def handler_entering_custom_command(message: types.Message, state: FSMContext): 273 | try: 274 | await state.set_state(None) 275 | if len(message.text.strip()) <= 0 or len(message.text.strip()) >= 32: 276 | raise Exception("❌ Слишком короткая или длинная команда") 277 | 278 | data = await state.get_data() 279 | await state.update_data(new_custom_command=message.text.strip()) 280 | await state.set_state(states.CustomCommandsStates.entering_new_custom_command_answer) 281 | await throw_float_message(state=state, 282 | message=message, 283 | text=templ.adding_new_comm_float_text(f"💬 Введите ответ для команды {message.text.strip()} ↓"), 284 | reply_markup=templ.back_kb(calls.CustomCommandsPagination(page=data.get("last_page") or 0).pack())) 285 | except Exception as e: 286 | if e is not TelegramAPIError: 287 | data = await state.get_data() 288 | await throw_float_message(state=state, 289 | message=message, 290 | text=templ.adding_new_comm_float_text(e), 291 | reply_markup=templ.back_kb(calls.CustomCommandsPagination(page=data.get("last_page") or 0).pack())) 292 | 293 | @router.message(states.CustomCommandsStates.entering_new_custom_command_answer, F.text) 294 | async def handler_entering_new_custom_command_answer(message: types.Message, state: FSMContext): 295 | try: 296 | await state.set_state(None) 297 | if len(message.text.strip()) <= 0: 298 | raise Exception("❌ Слишком короткий ответ") 299 | 300 | data = await state.get_data() 301 | await state.update_data(new_custom_command_answer=message.text.strip()) 302 | await throw_float_message(state=state, 303 | message=message, 304 | text=templ.adding_new_comm_float_text(f"➕ Подтвердите добавление новой команды {data['new_custom_command']} ↓"), 305 | reply_markup=templ.confirm_kb(confirm_cb="add_new_custom_command", cancel_cb=calls.CustomCommandsPagination(page=data.get("last_page") or 0).pack())) 306 | except Exception as e: 307 | if e is not TelegramAPIError: 308 | data = await state.get_data() 309 | await throw_float_message(state=state, 310 | message=message, 311 | text=templ.adding_new_comm_float_text(e), 312 | reply_markup=templ.back_kb(calls.CustomCommandsPagination(page=data.get("last_page") or 0).pack())) 313 | 314 | @router.message(states.CustomCommandPageStates.entering_custom_command_answer, F.text) 315 | async def handler_entering_custom_command_answer(message: types.Message, state: FSMContext): 316 | try: 317 | await state.set_state(None) 318 | if len(message.text.strip()) <= 0: 319 | raise Exception("❌ Слишком короткий текст") 320 | 321 | data = await state.get_data() 322 | custom_commands = sett.get("custom_commands") 323 | custom_commands[data["custom_command"]] = message.text.strip().split('\n') 324 | sett.set("custom_commands", custom_commands) 325 | await throw_float_message(state=state, 326 | message=message, 327 | text=templ.settings_comm_float_text(f"✅ Текст ответа команды {data['custom_command']} был успешно изменён на:
{message.text.strip()}
"), 328 | reply_markup=templ.back_kb(calls.CustomCommandPage(command=data["custom_command"]).pack())) 329 | except Exception as e: 330 | if e is not TelegramAPIError: 331 | data = await state.get_data() 332 | await throw_float_message(state=state, 333 | message=message, 334 | text=templ.settings_comm_float_text(e), 335 | reply_markup=templ.back_kb(calls.CustomCommandPage(command=data["custom_command"]).pack())) 336 | 337 | 338 | @router.message(states.AutoDeliveriesStates.entering_page, F.text) 339 | async def handler_entering_auto_deliveries_page(message: types.Message, state: FSMContext): 340 | try: 341 | await state.set_state(None) 342 | if not message.text.strip().isdigit(): 343 | raise Exception("❌ Вы должны ввести числовое значение") 344 | 345 | await state.update_data(last_page=int(message.text.strip())-1) 346 | await throw_float_message(state=state, 347 | message=message, 348 | text=templ.settings_deliv_float_text(f"📃 Введите номер страницы для перехода ↓"), 349 | reply_markup=templ.settings_deliv_kb(int(message.text)-1)) 350 | except Exception as e: 351 | if e is not TelegramAPIError: 352 | data = await state.get_data() 353 | await throw_float_message(state=state, 354 | message=message, 355 | text=templ.settings_deliv_float_text(e), 356 | reply_markup=templ.back_kb(calls.AutoDeliveriesPagination(page=data.get("last_page") or 0).pack())) 357 | 358 | @router.message(states.AutoDeliveriesStates.entering_new_auto_delivery_keyphrases, F.text) 359 | async def handler_entering_new_auto_delivery_keyphrases(message: types.Message, state: FSMContext): 360 | try: 361 | await state.set_state(None) 362 | if len(message.text.strip()) <= 0: 363 | raise Exception("❌ Слишком короткое значение") 364 | 365 | data = await state.get_data() 366 | keyphrases = [phrase.strip() for phrase in message.text.strip().split(",")] 367 | await state.update_data(new_auto_delivery_keyphrases=keyphrases) 368 | 369 | await state.set_state(states.AutoDeliveriesStates.entering_new_auto_delivery_message) 370 | await throw_float_message(state=state, 371 | message=message, 372 | text=templ.adding_new_deliv_float_text(f"💬 Введите сообщение авто-выдачи, которое будет писаться после покупки лота ↓"), 373 | reply_markup=templ.back_kb(calls.AutoDeliveriesPagination(page=data.get("last_page") or 0).pack())) 374 | except Exception as e: 375 | if e is not TelegramAPIError: 376 | data = await state.get_data() 377 | await throw_float_message(state=state, 378 | message=message, 379 | text=templ.settings_new_deliv_float_text(e), 380 | reply_markup=templ.back_kb(calls.AutoDeliveriesPagination(page=data.get("last_page") or 0).pack())) 381 | 382 | @router.message(states.AutoDeliveriesStates.entering_new_auto_delivery_message, F.text) 383 | async def handler_entering_new_auto_delivery_message(message: types.Message, state: FSMContext): 384 | try: 385 | if len(message.text.strip()) <= 0: 386 | raise Exception("❌ Слишком короткое значение") 387 | 388 | data = await state.get_data() 389 | await state.update_data(new_auto_delivery_message=message.text.strip()) 390 | keyphrases = ", ".join(data.get("new_auto_delivery_keyphrases")) 391 | await throw_float_message(state=state, 392 | message=message, 393 | text=templ.adding_new_deliv_float_text(f"➕ Подтвердите добавление авто-выдачи с ключевыми фразами {keyphrases}"), 394 | reply_markup=templ.confirm_kb(confirm_cb="add_new_auto_delivery", cancel_cb=calls.AutoDeliveriesPagination(page=data.get("last_page") or 0).pack())) 395 | except Exception as e: 396 | if e is not TelegramAPIError: 397 | data = await state.get_data() 398 | await throw_float_message(state=state, 399 | message=message, 400 | text=templ.settings_new_deliv_float_text(e), 401 | reply_markup=templ.back_kb(calls.AutoDeliveriesPagination(page=data.get("last_page") or 0).pack())) 402 | 403 | @router.message(states.AutoDeliveryPageStates.entering_auto_delivery_keyphrases, F.text) 404 | async def handler_entering_auto_delivery_keyphrases(message: types.Message, state: FSMContext): 405 | try: 406 | await state.set_state(None) 407 | if len(message.text.strip()) <= 0: 408 | raise Exception("❌ Слишком короткое значение") 409 | 410 | data = await state.get_data() 411 | auto_deliveries = sett.get("auto_deliveries") 412 | keyphrases = [phrase.strip() for phrase in message.text.strip().split(",")] 413 | auto_deliveries[data.get("auto_delivery_index")]["keyphrases"] = keyphrases 414 | sett.set("auto_deliveries", auto_deliveries) 415 | 416 | keyphrases = ", ".join(keyphrases) 417 | await throw_float_message(state=state, 418 | message=message, 419 | text=templ.settings_deliv_page_float_text(f"✅ Ключевые фразы были успешно изменены на: {keyphrases}"), 420 | reply_markup=templ.back_kb(calls.AutoDeliveryPage(index=data.get("auto_delivery_index")).pack())) 421 | except Exception as e: 422 | if e is not TelegramAPIError: 423 | data = await state.get_data() 424 | await throw_float_message(state=state, 425 | message=message, 426 | text=templ.settings_deliv_page_float_text(e), 427 | reply_markup=templ.back_kb(calls.AutoDeliveryPage(index=data.get("auto_delivery_index")).pack())) 428 | 429 | @router.message(states.AutoDeliveryPageStates.entering_auto_delivery_message, F.text) 430 | async def handler_entering_auto_delivery_message(message: types.Message, state: FSMContext): 431 | try: 432 | await state.set_state(None) 433 | if len(message.text.strip()) <= 0: 434 | raise Exception("❌ Слишком короткий текст") 435 | 436 | data = await state.get_data() 437 | auto_deliveries = sett.get("auto_deliveries") 438 | auto_deliveries[data.get("auto_delivery_index")]["message"] = message.text.strip().splitlines() 439 | sett.set("auto_deliveries", auto_deliveries) 440 | 441 | await throw_float_message(state=state, 442 | message=message, 443 | text=templ.settings_deliv_page_float_text(f"✅ Сообщение авто-выдачи было успешно изменено на:
{message.text.strip()}
"), 444 | reply_markup=templ.back_kb(calls.AutoDeliveryPage(index=data.get("auto_delivery_index")).pack())) 445 | except Exception as e: 446 | if e is not TelegramAPIError: 447 | data = await state.get_data() 448 | await throw_float_message(state=state, 449 | message=message, 450 | text=templ.settings_deliv_page_float_text(e), 451 | reply_markup=templ.back_kb(calls.AutoDeliveryPage(index=data.get("auto_delivery_index")).pack())) 452 | 453 | 454 | @router.message(states.SettingsStates.entering_messages_watermark, F.text) 455 | async def handler_entering_messages_watermark(message: types.Message, state: FSMContext): 456 | try: 457 | await state.set_state(None) 458 | data = await state.get_data() 459 | if len(message.text.strip()) <= 0 or len(message.text.strip()) >= 150: 460 | raise Exception("❌ Слишком короткое или длинное значение") 461 | 462 | config = sett.get("config") 463 | config["playerok"]["bot"]["messages_watermark"] = message.text.strip() 464 | sett.set("config", config) 465 | await throw_float_message(state=state, 466 | message=message, 467 | text=templ.settings_other_float_text(f"✅ Водяной знак сообщений был успешно изменён на {message.text.strip()}"), 468 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="other").pack())) 469 | except Exception as e: 470 | if e is not TelegramAPIError: 471 | await throw_float_message(state=state, 472 | message=message, 473 | text=templ.settings_other_float_text(e), 474 | reply_markup=templ.back_kb(calls.SettingsNavigation(to="other").pack())) -------------------------------------------------------------------------------- /tgbot/helpful.py: -------------------------------------------------------------------------------- 1 | from aiogram.fsm.context import FSMContext 2 | from aiogram.types import InlineKeyboardMarkup, Message, CallbackQuery 3 | from aiogram.exceptions import TelegramAPIError 4 | 5 | from . import templates as templ 6 | 7 | 8 | async def do_auth(message: Message, state: FSMContext): 9 | """ 10 | Начинает процесс авторизации в боте (запрашивает пароль, указанный в конфиге). 11 | 12 | :param message: Исходное сообщение. 13 | :type message: `aiogram.types.Message` 14 | 15 | :param state: Исходное состояние. 16 | :type state: `aiogram.fsm.context.FSMContext` 17 | """ 18 | from . import states 19 | await state.set_state(states.SystemStates.entering_password) 20 | return await throw_float_message(state=state, 21 | message=message, 22 | text=templ.sign_text('🔑 Введите ключ-пароль, указанный вами в конфиге бота ↓\n\nЕсли вы забыли, его можно посмотреть напрямую в конфиге по пути bot_settings/config.json, параметр password в разделе telegram.bot'), 23 | reply_markup=templ.destroy_kb()) 24 | 25 | async def throw_float_message(state: FSMContext, message: Message, text: str, 26 | reply_markup: InlineKeyboardMarkup = None, 27 | callback: CallbackQuery = None, 28 | send: bool = False) -> Message | None: 29 | """ 30 | Изменяет плавающее сообщение (изменяет текст акцентированного сообщения) или родительское сообщение бота, переданное в аргумент `message`.\n 31 | Если не удалось найти акцентированное сообщение, или это сообщения - команда, отправит новое акцентированное сообщение. 32 | 33 | :param state: Состояние бота. 34 | :type state: `aiogram.fsm.context.FSMContext` 35 | 36 | :param message: Переданный в handler объект сообщения. 37 | :type message: `aiogram.types.Message` 38 | 39 | :param text: Текст сообщения. 40 | :type text: `str` 41 | 42 | :param reply_markup: Клавиатура сообщения, _опционально_. 43 | :type reply_markup: `aiogram.typesInlineKeyboardMarkup.` 44 | 45 | :param callback: CallbackQuery хендлера, для ответа пустой AnswerCallbackQuery, _опционально_. 46 | :type callback: `aiogram.types.CallbackQuery` or `None` 47 | 48 | :param send: Отправить ли новое акцентированное сообщение, _опционально_. 49 | :type send: `bool` 50 | """ 51 | from .telegrambot import get_telegram_bot 52 | try: 53 | bot = get_telegram_bot().bot 54 | data = await state.get_data() 55 | accent_message_id = message.message_id 56 | if message.from_user and message.from_user.id != bot.id: 57 | accent_message_id = data.get("accent_message_id") 58 | mess = None 59 | new_mess_cond = False 60 | 61 | if not send: 62 | if message.text is not None: 63 | new_mess_cond = message.from_user.id != bot.id and message.text.startswith('/') 64 | 65 | if accent_message_id is not None and not new_mess_cond: 66 | try: 67 | if message.from_user.id != bot.id: 68 | await bot.delete_message(message.chat.id, message.message_id) 69 | mess = await bot.edit_message_text(text=text, reply_markup=reply_markup, 70 | chat_id=message.chat.id, message_id=accent_message_id, parse_mode="HTML") 71 | except TelegramAPIError as e: 72 | if "message to edit not found" in e.message.lower(): 73 | accent_message_id = None 74 | elif "message is not modified" in e.message.lower(): 75 | await bot.answer_callback_query(callback.id, show_alert=False, cache_time=0) 76 | pass 77 | else: 78 | raise e 79 | if callback: 80 | await bot.answer_callback_query(callback.id, show_alert=False, cache_time=0) 81 | if accent_message_id is None or new_mess_cond or send: 82 | mess = await bot.send_message(chat_id=message.chat.id, text=text, 83 | reply_markup=reply_markup, parse_mode="HTML") 84 | except Exception as e: 85 | import traceback 86 | traceback.print_exc() 87 | try: 88 | mess = await bot.edit_message_text(chat_id=message.chat.id, reply_markup=templ.destroy_kb(), 89 | text=templ.error_text(e), message_id=accent_message_id, parse_mode="HTML") 90 | except Exception as e: 91 | mess = await bot.send_message(chat_id=message.chat.id, reply_markup=templ.destroy_kb(), 92 | text=templ.error_text(e), parse_mode="HTML") 93 | finally: 94 | if mess: await state.update_data(accent_message_id=mess.message_id) 95 | return mess -------------------------------------------------------------------------------- /tgbot/states/__init__.py: -------------------------------------------------------------------------------- 1 | from .all import * -------------------------------------------------------------------------------- /tgbot/states/all.py: -------------------------------------------------------------------------------- 1 | from aiogram.fsm.state import State, StatesGroup 2 | 3 | 4 | class ActionsStates(StatesGroup): 5 | entering_message_text = State() 6 | 7 | class SystemStates(StatesGroup): 8 | entering_password = State() 9 | 10 | 11 | class SettingsStates(StatesGroup): 12 | entering_token = State() 13 | entering_user_agent = State() 14 | entering_requests_timeout = State() 15 | entering_listener_requests_delay = State() 16 | entering_proxy = State() 17 | entering_tg_logging_chat_id = State() 18 | entering_messages_watermark = State() 19 | 20 | 21 | class MessagesStates(StatesGroup): 22 | entering_page = State() 23 | 24 | class MessagePageStates(StatesGroup): 25 | entering_message_text = State() 26 | 27 | 28 | class CustomCommandsStates(StatesGroup): 29 | entering_page = State() 30 | entering_new_custom_command = State() 31 | entering_new_custom_command_answer = State() 32 | 33 | class CustomCommandPageStates(StatesGroup): 34 | entering_custom_command_answer = State() 35 | 36 | 37 | class AutoDeliveriesStates(StatesGroup): 38 | entering_page = State() 39 | entering_new_auto_delivery_keyphrases = State() 40 | entering_new_auto_delivery_message = State() 41 | 42 | class AutoDeliveryPageStates(StatesGroup): 43 | entering_auto_delivery_keyphrases = State() 44 | entering_auto_delivery_message = State() 45 | 46 | 47 | class ActiveOrdersStates(StatesGroup): 48 | entering_page = State() -------------------------------------------------------------------------------- /tgbot/telegrambot.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import asyncio 3 | from colorama import Fore 4 | import textwrap 5 | from aiogram import Bot, Dispatcher 6 | from aiogram.types import BotCommand, InlineKeyboardMarkup 7 | import logging 8 | 9 | from settings import Settings as sett 10 | from core.modules import get_modules 11 | from core.handlers import get_bot_event_handlers 12 | 13 | from . import router as main_router 14 | from . import templates as templ 15 | 16 | 17 | logger = logging.getLogger(f"universal.telegram") 18 | 19 | 20 | def get_telegram_bot_loop() -> None | asyncio.AbstractEventLoop: 21 | if hasattr(TelegramBot, "loop"): 22 | return getattr(TelegramBot, "loop") 23 | 24 | def get_telegram_bot() -> None | TelegramBot: 25 | if hasattr(TelegramBot, "instance"): 26 | return getattr(TelegramBot, "instance") 27 | 28 | class TelegramBot: 29 | def __new__(cls, *args, **kwargs) -> TelegramBot: 30 | if not hasattr(cls, "instance"): 31 | cls.instance = super(TelegramBot, cls).__new__(cls) 32 | cls.loop = asyncio.get_running_loop() 33 | return getattr(cls, "instance") 34 | 35 | def __init__(self, bot_token: str): 36 | self.bot_token = bot_token 37 | logging.getLogger("aiogram").setLevel(logging.CRITICAL) 38 | logging.getLogger("aiogram.event").setLevel(logging.CRITICAL) 39 | self.bot = Bot(token=self.bot_token) 40 | self.dp = Dispatcher() 41 | 42 | for module in get_modules(): 43 | for router in module.telegram_bot_routers: 44 | main_router.include_router(router) 45 | self.dp.include_router(main_router) 46 | 47 | async def set_main_menu(self): 48 | try: 49 | main_menu_commands = [BotCommand(command="/start", description="🏠 Главное меню")] 50 | await self.bot.set_my_commands(main_menu_commands) 51 | except: 52 | pass 53 | 54 | async def set_short_description(self): 55 | try: 56 | short_description = textwrap.dedent(f""" 57 | Playerok Universal — Современный бот-помощник для Playerok 🟦 58 | ┕ Канал — @alexeyproduction 59 | ┕ Бот — @alexey_production_bot 60 | """) 61 | await self.bot.set_my_short_description(short_description=short_description) 62 | except: 63 | pass 64 | 65 | async def set_description(self): 66 | try: 67 | description = textwrap.dedent(f""" 68 | Playerok Universal — Бесплатный современный бот-помощник для Playerok 🟦 69 | 70 | 🟢 Вечный онлайн 71 | ♻️ Авто-восстановление товаров 72 | 📦 Авто-выдача 73 | 🕹️ Команды 74 | 💬 Вызов продавца в чат 75 | 76 | ⬇️ Скачать бота: https://github.com/alleexxeeyy/playerok-universal 77 | 78 | 📣 Канал — @alexeyproduction 79 | 🤖 Бот — @alexey_production_bot 80 | 🧑‍💻 Автор — @alleexxeeyy 81 | """) 82 | await self.bot.set_my_description(description=description) 83 | except: 84 | pass 85 | 86 | async def run_bot(self): 87 | await self.set_main_menu() 88 | await self.set_short_description() 89 | await self.set_description() 90 | 91 | bot_event_handlers = get_bot_event_handlers() 92 | async def handle_on_telegram_bot_init(): 93 | """ 94 | Запускается преред инициализацией Telegram бота. 95 | Запускает за собой все хендлеры ON_TELEGRAM_BOT_INIT. 96 | """ 97 | for handler in bot_event_handlers.get("ON_TELEGRAM_BOT_INIT", []): 98 | try: 99 | await handler(self) 100 | except Exception as e: 101 | logger.error(f"{Fore.LIGHTRED_EX}{Fore.LIGHTRED_EX}Ошибка при обработке хендлера ивента ON_TELEGRAM_BOT_INIT: {Fore.WHITE}{e}") 102 | await handle_on_telegram_bot_init() 103 | 104 | me = await self.bot.get_me() 105 | logger.info(f"{Fore.LIGHTBLUE_EX}Telegram бот {Fore.CYAN}@{me.username} {Fore.LIGHTBLUE_EX}запущен и активен") 106 | await self.dp.start_polling(self.bot, skip_updates=True, handle_signals=False) 107 | 108 | async def call_seller(self, calling_name: str, chat_id: int | str): 109 | """ 110 | Пишет админу в Telegram с просьбой о помощи от заказчика. 111 | 112 | :param calling_name: Никнейм покупателя. 113 | :type calling_name: `str` 114 | 115 | :param chat_id: ID чата с заказчиком. 116 | :type chat_id: `int` or `str` 117 | """ 118 | config = sett.get("config") 119 | for user_id in config["telegram"]["bot"]["signed_users"]: 120 | await self.bot.send_message(chat_id=user_id, 121 | text=templ.call_seller_text(calling_name, f"https://playerok.com/chats/{chat_id}"), 122 | reply_markup=templ.destroy_kb(), 123 | parse_mode="HTML") 124 | 125 | async def log_event(self, text: str, kb: InlineKeyboardMarkup | None = None): 126 | """ 127 | Логирует событие в чат TG бота. 128 | 129 | :param text: Текст лога. 130 | :type text: `str` 131 | 132 | :param kb: Клавиатура с кнопками. 133 | :type kb: `aiogram.types.InlineKeyboardMarkup` or `None` 134 | """ 135 | config = sett.get("config") 136 | chat_id = config["playerok"]["bot"]["tg_logging_chat_id"] 137 | if not chat_id: 138 | for user_id in config["telegram"]["bot"]["signed_users"]: 139 | await self.bot.send_message(chat_id=user_id, text=text, reply_markup=kb, parse_mode="HTML") 140 | else: 141 | await self.bot.send_message(chat_id=chat_id, text=f'{text}\nПереключите чат логов на чат с ботом, чтобы отображалась меню с действиями', reply_markup=None, parse_mode="HTML") 142 | 143 | 144 | if __name__ == "__main__": 145 | config = sett.get("config") 146 | asyncio.run(TelegramBot(config["telegram"]["api"]["token"]).run_bot()) -------------------------------------------------------------------------------- /tgbot/templates/__init__.py: -------------------------------------------------------------------------------- 1 | from .all import * --------------------------------------------------------------------------------