This commit is contained in:
Yuriy Yuriev
2026-07-05 16:59:33 +07:00
parent f262616aad
commit 44ce29d2e0
9 changed files with 188 additions and 31 deletions
+9 -1
View File
@@ -4,7 +4,15 @@
"Bash(pip install *)", "Bash(pip install *)",
"Bash(python -c \"import psutil; print\\('psutil ok, version:', psutil.__version__\\)\")", "Bash(python -c \"import psutil; print\\('psutil ok, version:', psutil.__version__\\)\")",
"Bash(python -c ' *)", "Bash(python -c ' *)",
"WebFetch(domain:doc.heleket.com)" "WebFetch(domain:doc.heleket.com)",
"Bash(chcp)",
"PowerShell(chcp)",
"PowerShell(python -c \"from core.logger import setup_logger; log = setup_logger\\('test'\\); log.info\\('?? @Nightbot : 15 '\\)\")",
"Read(//c/Users/user/AppData/Local/Programs/Python/**)",
"Bash(where.exe python *)",
"Bash(.venv/Scripts/python -c ' *)",
"Bash(/c/Work/PythonBot/.venv/Scripts/python.exe -c ' *)",
"PowerShell(& \"C:\\\\Work\\\\PythonBot\\\\.venv\\\\Scripts\\\\python.exe\" -c \"from core.logger import setup_logger; log = setup_logger\\('test'\\); log.info\\('test kirillicy klikov znak'\\)\")"
] ]
} }
} }
+5
View File
@@ -62,6 +62,11 @@ class AuthStorage:
data = await asyncio.to_thread(self._load_sync) data = await asyncio.to_thread(self._load_sync)
return str(user_id) in data return str(user_id) in data
async def get_all_user_ids(self) -> list[int]:
async with self._lock:
data = await asyncio.to_thread(self._load_sync)
return [int(k) for k in data.keys()]
def _load_sync(self) -> dict: def _load_sync(self) -> dict:
if not self.file_path.exists(): if not self.file_path.exists():
return {} return {}
+3
View File
@@ -28,6 +28,9 @@ def setup_logger(
datefmt='%Y-%m-%d %H:%M:%S' datefmt='%Y-%m-%d %H:%M:%S'
) )
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
console_handler = logging.StreamHandler(sys.stdout) console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(log_level) console_handler.setLevel(log_level)
console_handler.setFormatter(console_formatter) console_handler.setFormatter(console_formatter)
+114 -24
View File
@@ -69,6 +69,7 @@ class BotInterface:
self._topup_confirm: Dict[int, int] = {} # user_id -> rub amount pending confirm self._topup_confirm: Dict[int, int] = {} # user_id -> rub amount pending confirm
self._user_task_state: Dict[int, dict] = {} # шаги создания задачи пользователем self._user_task_state: Dict[int, dict] = {} # шаги создания задачи пользователем
self._admin_balance_state: Dict[int, int] = {} # admin_id -> target_user_id self._admin_balance_state: Dict[int, int] = {} # admin_id -> target_user_id
self._admin_rub_state: Dict[int, int] = {} # admin_id -> target_user_id
self._admin_msg_state: Dict[int, int] = {} # admin_id -> target_user_id self._admin_msg_state: Dict[int, int] = {} # admin_id -> target_user_id
self._menu_msg: Dict[int, int] = {} # user_id -> inline keyboard message_id self._menu_msg: Dict[int, int] = {} # user_id -> inline keyboard message_id
self._menu_top_msg: Dict[int, int] = {} # user_id -> reply keyboard message_id self._menu_top_msg: Dict[int, int] = {} # user_id -> reply keyboard message_id
@@ -1241,6 +1242,24 @@ class BotInterface:
pass pass
await callback.answer() await callback.answer()
@dp.callback_query(F.data.startswith("ubal_menu_"))
async def cb_user_balance_menu(callback: CallbackQuery):
if not await require_admin(callback):
return
target_uid = int(callback.data.replace("ubal_menu_", "", 1))
builder = InlineKeyboardBuilder()
for amount in [10, 50, 100, 500]:
builder.button(text=f"+{amount}", callback_data=f"ubal_{amount}_{target_uid}")
builder.adjust(4)
builder.row(InlineKeyboardButton(text="✏️ Другая сумма", callback_data=f"ubal_custom_{target_uid}"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"udetail_{target_uid}"))
clicks = await interface.balance_storage.get_balance(target_uid)
await callback.message.edit_text(
f"👤 {target_uid} — переходов: {clicks}\n\nДобавить переходов:",
reply_markup=builder.as_markup()
)
await callback.answer()
@dp.callback_query(F.data.startswith("ubal_")) @dp.callback_query(F.data.startswith("ubal_"))
async def cb_user_balance(callback: CallbackQuery): async def cb_user_balance(callback: CallbackQuery):
if not await require_admin(callback): if not await require_admin(callback):
@@ -1252,7 +1271,7 @@ class BotInterface:
interface._admin_balance_state[callback.from_user.id] = target_uid interface._admin_balance_state[callback.from_user.id] = target_uid
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"udetail_{target_uid}")) builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"udetail_{target_uid}"))
await callback.message.answer("Введите сумму для пополнения:", reply_markup=builder.as_markup()) await callback.message.answer("Введите количество переходов:", reply_markup=builder.as_markup())
await callback.answer() await callback.answer()
else: else:
amount = int(parts[1]) amount = int(parts[1])
@@ -1261,6 +1280,44 @@ class BotInterface:
await callback.answer(f"✅ +{amount} переходов. Баланс: {new_balance}") await callback.answer(f"✅ +{amount} переходов. Баланс: {new_balance}")
await interface._show_user_detail(callback, target_uid) await interface._show_user_detail(callback, target_uid)
@dp.callback_query(F.data.startswith("urub_menu_"))
async def cb_user_rub_menu(callback: CallbackQuery):
if not await require_admin(callback):
return
target_uid = int(callback.data.replace("urub_menu_", "", 1))
builder = InlineKeyboardBuilder()
for amount in [100, 500, 1000, 3000]:
builder.button(text=f"+{amount}", callback_data=f"urub_{amount}_{target_uid}")
builder.adjust(4)
builder.row(InlineKeyboardButton(text="✏️ Другая сумма", callback_data=f"urub_custom_{target_uid}"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"udetail_{target_uid}"))
rub = await interface.rub_storage.get_balance(target_uid)
await callback.message.edit_text(
f"👤 {target_uid} — рублей: {rub}\n\nДобавить рублей:",
reply_markup=builder.as_markup()
)
await callback.answer()
@dp.callback_query(F.data.startswith("urub_"))
async def cb_user_rub(callback: CallbackQuery):
if not await require_admin(callback):
return
parts = callback.data.split("_")
# формат: urub_{amount}_{user_id} или urub_custom_{user_id}
if parts[1] == "custom":
target_uid = int(parts[2])
interface._admin_rub_state[callback.from_user.id] = target_uid
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"udetail_{target_uid}"))
await callback.message.answer("Введите сумму в рублях:", reply_markup=builder.as_markup())
await callback.answer()
else:
amount = int(parts[1])
target_uid = int(parts[2])
new_balance = await interface.rub_storage.add_balance(target_uid, amount)
await callback.answer(f"✅ +{amount} ₽. Баланс: {new_balance}")
await interface._show_user_detail(callback, target_uid)
@dp.callback_query(F.data.startswith("utasks_")) @dp.callback_query(F.data.startswith("utasks_"))
async def cb_user_tasks_admin(callback: CallbackQuery): async def cb_user_tasks_admin(callback: CallbackQuery):
if not await require_admin(callback): if not await require_admin(callback):
@@ -1376,7 +1433,7 @@ class BotInterface:
await interface._show_user_menu(message) await interface._show_user_menu(message)
return return
# Ввод суммы баланса (admin) # Ввод количества переходов (admin)
if user_id in interface._admin_balance_state: if user_id in interface._admin_balance_state:
target_uid = interface._admin_balance_state.pop(user_id) target_uid = interface._admin_balance_state.pop(user_id)
await interface._safe_delete(message.bot, message.chat.id, message.message_id) await interface._safe_delete(message.bot, message.chat.id, message.message_id)
@@ -1385,7 +1442,21 @@ class BotInterface:
if amount <= 0: if amount <= 0:
raise ValueError raise ValueError
new_balance = await interface.balance_storage.add_balance(target_uid, amount) new_balance = await interface.balance_storage.add_balance(target_uid, amount)
await interface._send_temp(message, f"Пополнено на {amount}. Баланс: {new_balance}") await interface._send_temp(message, f"+{amount} переходов. Баланс: {new_balance}")
except (ValueError, TypeError):
await interface._send_temp(message, "❌ Введите целое положительное число")
return
# Ввод рублёвого пополнения (admin)
if user_id in interface._admin_rub_state:
target_uid = interface._admin_rub_state.pop(user_id)
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
try:
amount = int(text)
if amount <= 0:
raise ValueError
new_balance = await interface.rub_storage.add_balance(target_uid, amount)
await interface._send_temp(message, f"✅ +{amount} ₽. Баланс: {new_balance}")
except (ValueError, TypeError): except (ValueError, TypeError):
await interface._send_temp(message, "❌ Введите целое положительное число") await interface._send_temp(message, "❌ Введите целое положительное число")
return return
@@ -2389,7 +2460,7 @@ class BotInterface:
params.pending_visits += visits_count params.pending_visits += visits_count
params.total_planned_visits += visits_count params.total_planned_visits += visits_count
logger.info(f"🔗 @{username} {url}{visits_count} clicks ({calc_info}), total pending: {params.pending_visits}") logger.info(f"🔗 @{username} {url}{visits_count} clicks ({calc_info}), total pending: {params.pending_visits}")
await send_message_safe( notif_msg = await send_message_safe(
message.bot, message.chat.id, message.bot, message.chat.id,
f"🔗 @{username}: {url[:60]}\n" f"🔗 @{username}: {url[:60]}\n"
f"📊 {calc_info}\n" f"📊 {calc_info}\n"
@@ -2402,6 +2473,7 @@ class BotInterface:
# Resume > 5 мин: серия этой ссылки полностью отбрасывается # Resume > 5 мин: серия этой ссылки полностью отбрасывается
SERIES_EXPIRY = 5 * 60 SERIES_EXPIRY = 5 * 60
remaining = visits_count remaining = visits_count
successful_this_link = 0
paused_at: Optional[float] = None paused_at: Optional[float] = None
first_click = True first_click = True
@@ -2443,6 +2515,7 @@ class BotInterface:
return return
if result.success: if result.success:
params.successful_visits += 1 params.successful_visits += 1
successful_this_link += 1
remaining -= 1 remaining -= 1
if params.user_id: if params.user_id:
new_balance = await self.balance_storage.deduct(params.user_id, 1) new_balance = await self.balance_storage.deduct(params.user_id, 1)
@@ -2470,6 +2543,15 @@ class BotInterface:
if params.stopped: if params.stopped:
return return
# Серия завершена — удаляем уведомление
if notif_msg:
try:
await message.bot.delete_message(
chat_id=message.chat.id,
message_id=notif_msg.message_id,
)
except Exception:
pass
except Exception as e: except Exception as e:
logger.error(f"Process URL error: {e}") logger.error(f"Process URL error: {e}")
@@ -3070,24 +3152,34 @@ class BotInterface:
await self._send_temp(message, "Ошибка запуска задачи") await self._send_temp(message, "Ошибка запуска задачи")
async def _show_users_list(self, message: Message, edit: bool = False): async def _show_users_list(self, message: Message, edit: bool = False):
balances = await self.balance_storage.get_all() all_user_ids, all_tasks, all_clicks, all_rub = await asyncio.gather(
all_tasks = await self.task_manager.get_all_tasks() self.auth_storage.get_all_user_ids(),
self.task_manager.get_all_tasks(),
self.balance_storage.get_all(),
self.rub_storage.get_all(),
)
text = "👥 Пользователи\n\n" text = f"👥 Пользователи ({len(all_user_ids)})\n\n"
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
if not balances: if not all_user_ids:
text += "Нет пользователей с балансом." text += "Нет зарегистрированных пользователей."
else: else:
for uid_str, balance in list(balances.items())[:15]: for uid in all_user_ids[:20]:
uid = int(uid_str) clicks = all_clicks.get(str(uid), 0)
rub = all_rub.get(str(uid), 0)
active = sum( active = sum(
1 for p in all_tasks.values() 1 for p in all_tasks.values()
if p.user_id == uid and not p.completed and not p.stopped if p.user_id == uid and not p.completed and not p.stopped
) )
text += f"👤 `{uid}` — 💰 {balance} | 📋 {active} задач\n" bal_str = f"🖱{clicks}"
if rub:
bal_str += f" 🪙{rub}"
if active:
bal_str += f" 📋{active}"
text += f"👤 `{uid}` — {bal_str}\n"
builder.row(InlineKeyboardButton( builder.row(InlineKeyboardButton(
text=f"👤 {uid} (💰{balance})", text=f"👤 {uid} {bal_str}",
callback_data=f"udetail_{uid}", callback_data=f"udetail_{uid}",
)) ))
@@ -3095,25 +3187,23 @@ class BotInterface:
await self._edit_or_send(message, text, builder.as_markup(), edit) await self._edit_or_send(message, text, builder.as_markup(), edit)
async def _show_user_detail(self, callback: CallbackQuery, target_user_id: int): async def _show_user_detail(self, callback: CallbackQuery, target_user_id: int):
balance = await self.balance_storage.get_balance(target_user_id) clicks = await self.balance_storage.get_balance(target_user_id)
rub = await self.rub_storage.get_balance(target_user_id)
all_tasks = await self.task_manager.get_all_tasks() all_tasks = await self.task_manager.get_all_tasks()
user_tasks = {tid: p for tid, p in all_tasks.items() if p.user_id == target_user_id} user_tasks = {tid: p for tid, p in all_tasks.items() if p.user_id == target_user_id}
active = sum(1 for p in user_tasks.values() if not p.completed and not p.stopped) active = sum(1 for p in user_tasks.values() if not p.completed and not p.stopped)
text = ( text = (
f"👤 Пользователь {target_user_id}\n\n" f"👤 Пользователь {target_user_id}\n\n"
f"🔢 Переходов: {balance}\n" f"🖱 Переходов: {clicks}\n"
f"📋 Задач: {len(user_tasks)} (активных: {active})\n\n" f"🪙 Рублей: {rub}\n"
"Пополнить баланс:" f"📋 Задач: {len(user_tasks)} (активных: {active})\n"
) )
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for amount in [10, 50, 100, 500]:
builder.button(text=f"+{amount}", callback_data=f"ubal_{amount}_{target_user_id}") builder.row(InlineKeyboardButton(text=" Переходы", callback_data=f"ubal_menu_{target_user_id}"))
builder.adjust(4) builder.row(InlineKeyboardButton(text=" Рубли", callback_data=f"urub_menu_{target_user_id}"))
builder.row(InlineKeyboardButton(
text="✏️ Другая сумма",
callback_data=f"ubal_custom_{target_user_id}",
))
if user_tasks: if user_tasks:
builder.row(InlineKeyboardButton( builder.row(InlineKeyboardButton(
text=f"📋 Задачи ({len(user_tasks)})", text=f"📋 Задачи ({len(user_tasks)})",
+12
View File
@@ -344,6 +344,18 @@ class PaymentStorage:
payments.pop(payment_id, None) payments.pop(payment_id, None)
await asyncio.to_thread(self._save_sync, payments) await asyncio.to_thread(self._save_sync, payments)
async def pop(self, payment_id: str) -> Optional[dict]:
"""Get and mark payment as processed. Returns None if not found or already processed."""
async with self._lock:
payments = await asyncio.to_thread(self._load_sync)
data = payments.get(payment_id)
if data is None or data.get("processed"):
return None
payments[payment_id]["processed"] = True
payments[payment_id]["processed_at"] = datetime.now().isoformat(timespec="seconds")
await asyncio.to_thread(self._save_sync, payments)
return data
async def get_by_user(self, user_id: int) -> Optional[dict]: async def get_by_user(self, user_id: int) -> Optional[dict]:
async with self._lock: async with self._lock:
payments = await asyncio.to_thread(self._load_sync) payments = await asyncio.to_thread(self._load_sync)
+1 -1
View File
@@ -332,7 +332,7 @@ class BrowserService:
current = new_url current = new_url
redirect = True redirect = True
task = asyncio.create_task( task = asyncio.create_task(
page.wait_for_load_state("load", timeout=30000) page.wait_for_load_state("domcontentloaded", timeout=30000)
) )
await self._move_mouse(page, task) await self._move_mouse(page, task)
await task await task
+11
View File
@@ -5,6 +5,7 @@ Twitch чат через IRC over WebSocket (wss://irc-ws.chat.twitch.tv:443).
import asyncio import asyncio
import logging import logging
import random
import re import re
from typing import Optional, Callable, Awaitable, List from typing import Optional, Callable, Awaitable, List
from datetime import datetime from datetime import datetime
@@ -34,7 +35,13 @@ class TwitchGQLChatClient:
): ):
self.channel = channel.lower() self.channel = channel.lower()
self.target_username = target_username.lower() self.target_username = target_username.lower()
# Anonymous Twitch login (justinfanNNNNN) is shared across all clients by
# default, so parallel monitors on the same nick get kicked by Twitch
# (one connection per nick) — randomize it unless a real login is configured.
if irc_username or settings.IRC_USERNAME != "justinfan12345":
self.irc_username = irc_username or settings.IRC_USERNAME self.irc_username = irc_username or settings.IRC_USERNAME
else:
self.irc_username = f"justinfan{random.randint(10000, 99999)}"
self.irc_oauth = irc_oauth or settings.IRC_OAUTH self.irc_oauth = irc_oauth or settings.IRC_OAUTH
self._ws: Optional[aiohttp.ClientWebSocketResponse] = None self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
@@ -257,6 +264,10 @@ class TwitchGQLChatClient:
raise raise
except Exception: except Exception:
pass pass
else:
logger.info(
f"🚫 Домен не разрешён: {url} (allowed_domains={allowed_domains})"
)
return stats return stats
+7
View File
@@ -3,6 +3,7 @@ IRC сервис для мониторинга Twitch чата.
""" """
import asyncio import asyncio
import random
import re import re
import logging import logging
from typing import Optional, Callable, Awaitable, List from typing import Optional, Callable, Awaitable, List
@@ -28,7 +29,13 @@ class TwitchIRCClient:
): ):
self.channel = channel.lower() self.channel = channel.lower()
self.target_username = target_username.lower() self.target_username = target_username.lower()
# Anonymous Twitch login (justinfanNNNNN) is shared across all clients by
# default, so parallel monitors on the same nick get kicked by Twitch
# (one connection per nick) — randomize it unless a real login is configured.
if irc_username or settings.IRC_USERNAME != "justinfan12345":
self.irc_username = irc_username or settings.IRC_USERNAME self.irc_username = irc_username or settings.IRC_USERNAME
else:
self.irc_username = f"justinfan{random.randint(10000, 99999)}"
self.irc_oauth = irc_oauth or settings.IRC_OAUTH self.irc_oauth = irc_oauth or settings.IRC_OAUTH
self._reader: Optional[asyncio.StreamReader] = None self._reader: Optional[asyncio.StreamReader] = None
+24 -3
View File
@@ -11,6 +11,7 @@ import hashlib
import json import json
import logging import logging
from datetime import datetime from datetime import datetime
from pathlib import Path
from typing import Optional from typing import Optional
from aiohttp import web from aiohttp import web
@@ -74,6 +75,21 @@ class HelketWebhookServer:
await self._runner.cleanup() await self._runner.cleanup()
logger.info("Heleket webhook server stopped") logger.info("Heleket webhook server stopped")
async def _save_lost_webhook(self, payload: dict) -> None:
"""Сохраняет необработанный вебхук в файл и уведомляет админа."""
lost_file = Path("data/lost_webhooks.json")
try:
data = []
if lost_file.exists():
with open(lost_file, "r", encoding="utf-8") as f:
data = json.load(f)
data.append({**payload, "_saved_at": datetime.now().isoformat(timespec="seconds")})
with open(lost_file, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"Lost webhook saved: uuid={payload.get('uuid')}")
except Exception as e:
logger.error(f"Failed to save lost webhook: {e}")
async def _handle_health(self, request: web.Request) -> web.Response: async def _handle_health(self, request: web.Request) -> web.Response:
return web.Response(text="ok") return web.Response(text="ok")
@@ -102,9 +118,15 @@ class HelketWebhookServer:
logger.info(f"Webhook: uuid={payment_uuid} status={status} — ignored") logger.info(f"Webhook: uuid={payment_uuid} status={status} — ignored")
return web.Response(text="ok") return web.Response(text="ok")
payment = await self._payment_storage.get(payment_uuid) existing = await self._payment_storage.get(payment_uuid)
if existing and existing.get("processed"):
logger.info(f"Webhook: payment {payment_uuid} already processed — skipping duplicate")
return web.Response(text="ok")
payment = await self._payment_storage.pop(payment_uuid)
if not payment: if not payment:
logger.warning(f"Webhook: payment {payment_uuid} not found (already processed?)") logger.warning(f"Webhook: payment {payment_uuid} not found in storage")
await self._save_lost_webhook(payload)
return web.Response(text="ok") return web.Response(text="ok")
user_id = payment.get("user_id") user_id = payment.get("user_id")
@@ -147,7 +169,6 @@ class HelketWebhookServer:
} }
logger.info(f"Webhook: user={user_id} +{rub} RUB") logger.info(f"Webhook: user={user_id} +{rub} RUB")
await self._payment_storage.delete(payment_uuid)
await self._history_storage.add(user_id, history_record) await self._history_storage.add(user_id, history_record)
if chat_id: if chat_id: