├── .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 *
--------------------------------------------------------------------------------