feat support bot

This commit is contained in:
Yuriy Yuriev
2026-05-21 00:17:58 +07:00
parent 98c6e08e46
commit 28d23b8de9
4 changed files with 189 additions and 50 deletions
+41 -46
View File
@@ -176,6 +176,11 @@ class TwitchIRCClient:
start_time = datetime.now()
last_message_time = datetime.now()
last_ping_time = datetime.now()
ping_interval = 20 # пинговать каждые 20 сек
pong_timeout = 10 # ждать PONG 10 сек, иначе реконнект
waiting_pong = False
ping_sent_at: Optional[float] = None
drain_errors = 0
# Если задача стартует на паузе — ждём resume до подключения к IRC
@@ -200,66 +205,54 @@ class TwitchIRCClient:
logger.info(f"▶️ #{self.channel} resumed, reconnecting")
await self.connect()
last_message_time = datetime.now()
last_ping_time = datetime.now()
waiting_pong = False
continue
# Нет соединения — ждём перед повтором
if not self._connected:
await asyncio.sleep(5)
await self.connect()
last_ping_time = datetime.now()
waiting_pong = False
continue
# === ПЕРЕПОДКЛЮЧЕНИЕ КАЖДЫЕ 30 СЕКУНД ===
if (datetime.now() - last_message_time).seconds > 30:
logger.info("🔄 Reconnecting (30s)...")
now = datetime.now()
# Сначала читаем всё что осталось (с retry)
drain_ok = False
for attempt in range(3):
try:
while True:
line = await asyncio.wait_for(self._reader.readline(), timeout=1.0)
if not line:
break
decoded = line.decode('utf-8', errors='ignore').strip()
if 'PRIVMSG' in decoded:
msg = self._parse_message(decoded)
if msg:
stats["messages"] += 1
for url in self._extract_urls(msg['message']):
if self._is_domain_allowed(url, allowed_domains):
stats["links_found"] += 1
try:
await on_url_found(url, msg['display_name'])
except Exception as e:
logger.warning(f"URL callback error: {e}")
drain_ok = True
break
except asyncio.TimeoutError:
drain_ok = True
break
except Exception as e:
logger.warning(f"Error draining IRC data (attempt {attempt + 1}/3): {e}")
await asyncio.sleep(2)
# === PONG не пришёл вовремя — соединение мёртвое, реконнект ===
if waiting_pong and ping_sent_at and (now.timestamp() - ping_sent_at) > pong_timeout:
logger.warning(f"🔌 #{self.channel} PONG timeout, reconnecting...")
waiting_pong = False
self._connected = False
continue
if not drain_ok:
# === Отправляем PING каждые ping_interval секунд ===
if not waiting_pong and (now - last_ping_time).seconds >= ping_interval:
try:
await self._send("PING :tmi.twitch.tv")
ping_sent_at = now.timestamp()
waiting_pong = True
last_ping_time = now
except Exception:
self._connected = False
continue
# === Реконнект если вообще нет активности > 5 минут (failsafe) ===
if (now - last_message_time).seconds > 300:
logger.info("🔄 No activity 5min, reconnecting...")
try:
await self.disconnect()
except Exception:
pass
await asyncio.sleep(1)
connected = await self.connect()
if not connected:
drain_errors += 1
logger.error(f"IRC drain failed 3 times, total failures: {drain_errors}")
if drain_errors >= 3:
logger.error("IRC drain failed 3 consecutive times — pausing task")
if on_drain_fail:
await on_drain_fail()
if drain_errors >= 3 and on_drain_fail:
await on_drain_fail()
return stats
else:
drain_errors = 0
# Переподключаемся
try:
await self.disconnect()
except Exception as e:
logger.warning(f"Disconnect error: {e}")
await asyncio.sleep(1)
await self.connect()
stats["reconnects"] += 1
last_message_time = datetime.now()
continue
@@ -280,6 +273,8 @@ class TwitchIRCClient:
continue
if line.startswith('PONG'):
last_message_time = datetime.now()
waiting_pong = False # подтверждение что соединение живо
continue
msg = self._parse_message(line)