fix
This commit is contained in:
@@ -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'\\)\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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
@@ -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)})",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user