diff --git a/config.ini b/config.ini index d89c5b7..fc67302 100644 --- a/config.ini +++ b/config.ini @@ -2,11 +2,13 @@ bot_secret = 6454033742:AAFj2rUoVb2jJ_Lew4eiIecdr7s7xbrqeNU enabled = 1 -[Youtube] -video_id = SsC29jqiNto +[YouTube] +video_id = 3xLyYdp0UFg +enabled = 1 [Twitch] -channel = wanderbraun +channel = coulthardf1 +enabled = 1 [Alerts] app_id = 12554 diff --git a/huy.py b/huy.py index b7e68a8..98f67df 100644 --- a/huy.py +++ b/huy.py @@ -29,7 +29,7 @@ import twitch # Reading Configs config = configparser.ConfigParser() -config.read("E:/Games/cgi-bin/config.ini") +config.read("config.ini") alerts = DonationAlertsAPI( diff --git a/huySeek.py b/huySeek.py index 4806314..201801a 100644 --- a/huySeek.py +++ b/huySeek.py @@ -6,373 +6,281 @@ import os import sys import threading import asyncio +import socket +import emoji from datetime import datetime, timedelta import time +import tzlocal from zoneinfo import ZoneInfo from http.server import BaseHTTPRequestHandler, HTTPServer -import pytchat -import telegram -from donationalerts import DonationAlertsAPI, Scopes from urllib import parse -import emoji -sys.path.append('twitchchatirc') -import twitch -# ==================== -# Конфигурация -# ==================== -class Config: - def __init__(self): - self.config = configparser.ConfigParser() - self.config.read("config.ini") - - # Настройки - self.timezone = ZoneInfo('Asia/Yekaterinburg') - self.poll_interval = 2 # seconds - self.max_comments_per_source = 15 - self.max_total_comments = 150 - self.hello_message_interval = 600 # seconds - - # API keys - self.alerts_app_id = self.config['Alerts']['app_id'] - self.alerts_api_key = self.config['Alerts']['api_key'] - self.telegram_enabled = self.config['Telegram']['enabled'] - self.telegram_bot_secret = self.config['Telegram']['bot_secret'] - self.youtube_video_id = self.config['Youtube']['video_id'] - self.twitch_channel = self.config['Twitch']['channel'] +# Чтение конфигурации +config = configparser.ConfigParser() +config.read("config.ini") -# ==================== -# Модели данных -# ==================== -class Comment: - def __init__(self, comment_id, comment_type, sender, message, date, amount=None): - self.id = comment_id - self.type = comment_type - self.sender = sender - self.message = message - self.date = date - self.amount = amount - - def to_dict(self): - return { - 'id': self.id, - 'type': self.type, - 'sender': self.sender, - 'message': emoji.emojize(self.message), - 'date': self.date, - 'amount': self.amount - } +# Инициализация модулей на основе конфигурации +chat_connectors = {} +chat_comments = {} -# ==================== -# Сервисы -# ==================== -class DonationAlertsService: - def __init__(self, config): - self.config = config - self.api = DonationAlertsAPI( - config.alerts_app_id, - config.alerts_api_key, - "http://127.0.0.1:8008/login", - [Scopes.USER_SHOW] - ) - self.access_token = None - - def get_donations(self): - if not self.access_token: - print('Waiting for Alerts auth...') - return [] - - try: - donations = self.api.donations.get(self.access_token.access_token) - return self._process_donations(donations.items) - except Exception as e: - print(f'Alerts error: {str(e)}') - return [] - - def _process_donations(self, items): - result = [] - for item in items: - if not item.is_shown: - continue - - sender = item.username or 'Аноним' - message = item.message or '---' - amount = self._format_amount(item.amount, item.currency) - date = self._parse_donation_date(item.shown_at) - - result.append( - Comment( - comment_id=item.id, - comment_type='donate', - sender=sender, - message=message, - date=date, - amount=amount - ) - ) - return result - - def _format_amount(self, amount, currency): - if amount != int(amount): - return f"{format(amount, '.2f')} {currency}" - return f"{int(amount)} {currency}" - - def _parse_donation_date(self, date_str): - dt = datetime.fromisoformat(date_str).astimezone(self.config.timezone) - # Фильтр донатов за последний месяц - if dt > datetime.now(self.config.timezone) - timedelta(days=30): - return dt - return None +# Telegram (опционально) +if config.getboolean('Telegram', 'enabled', fallback=False): + import telegram + bot = telegram.Bot(config['Telegram']['bot_secret']) + chat_connectors['telegram'] = bot + chat_comments['tg'] = [] -class TelegramService: - def __init__(self, config): - self.config = config - self.bot = telegram.Bot(config.telegram_bot_secret) if config.telegram_enabled else None - - async def get_messages(self): - if not self.bot: - return [] - - try: - updates = await self.bot.get_updates( - allowed_updates=['message', 'edited_message'], - timeout=None - ) - return self._process_updates(updates) - except Exception as e: - print(f'Telegram error: {str(e)}') - return [] - - def _process_updates(self, updates): - messages = [] - for upd in updates: - msg = upd.edited_message if upd.edited_message else upd.message - - if not hasattr(msg, 'text') or not msg.text: - continue - - sender = self._get_sender_name(msg.from_user) - date = msg.date.replace(tzinfo=self.config.timezone) - - messages.append( - Comment( - comment_id=msg.message_id, - comment_type='tg', - sender=sender, - message=msg.text, - date=date - ) - ) - return messages - - def _get_sender_name(self, user): - if user.first_name and user.last_name: - return f"{user.first_name} {user.last_name}" - return user.first_name or "Админ" +# YouTube (опционально) +if config.getboolean('YouTube', 'enabled', fallback=False): + print('Youtube enabled') + import pytchat + video_id = config['YouTube']['video_id'] + chat = pytchat.create(video_id=video_id) + chat_connectors['youtube'] = chat + chat_comments['yt'] = [] -class YouTubeService: - def __init__(self, config): - self.config = config - self.chat = pytchat.create(video_id=config.youtube_video_id) - - def get_messages(self): - try: - items = self.chat.get() - if not hasattr(items, 'items'): - print('YT reconnecting...') - self.chat = pytchat.create(video_id=self.config.youtube_video_id) - return [] - - return self._process_messages(items.items) - except Exception as e: - print(f'YouTube error: {str(e)}') - return [] - - def _process_messages(self, items): - messages = [] - for item in items: - date = datetime.fromisoformat(item.datetime).replace(tzinfo=self.config.timezone) - messages.append( - Comment( - comment_id=item.id, - comment_type='yt', - sender=item.author.name, - message=item.message, - date=date - ) - ) - return messages +# Twitch (опционально) +if config.getboolean('Twitch', 'enabled', fallback=False): + sys.path.append('twitchchatirc') + import twitch + twitch_channel = config['Twitch']['channel'] + twitch_socket = twitch.TwitchChatIRC(twitch_channel) + chat_connectors['twitch'] = twitch_socket + chat_comments['tw'] = [] -class TwitchService: - def __init__(self, config): - self.config = config - self.socket = twitch.TwitchChatIRC(config.twitch_channel) - - def get_messages(self): - # Предполагаем, что сообщения добавляются в self.socket.all_messages - # в отдельном потоке (как в оригинальном коде) - return getattr(self.socket, 'all_messages', []) - -# ==================== -# Ядро приложения -# ==================== -class ChatAggregator: - def __init__(self): - self.config = Config() - self.services = { - 'alerts': DonationAlertsService(self.config), - 'telegram': TelegramService(self.config), - # 'youtube': YouTubeService(self.config), - 'twitch': TwitchService(self.config) - } - self.comments = [] - self.lock = threading.Lock() - self.hello_message = Comment( - comment_id=random.randint(100000, 999999), - comment_type='hello', - sender='Eikichi-bot', - message='🔥 Спасибо всем на стриме за интерес к переводу и поддержку! 🔥', - date=datetime.now(self.config.timezone) - ) - - def run(self): - # Запуск сервера в отдельном потоке - server_thread = threading.Thread(target=self._run_server) - server_thread.daemon = True - server_thread.start() - - # Запуск Twitch в отдельном потоке - twitch_thread = threading.Thread(target=self.services['twitch'].socket.listen) - twitch_thread.daemon = True - twitch_thread.start() - - print('System started. Press Ctrl+Shift+Alt+C to exit.') - - # Главный цикл - while True: - if keyboard.is_pressed('Ctrl+Shift+Alt+C'): - sys.exit(0) - - if int(time.time()) % self.config.poll_interval == 0: - self.update_comments() - time.sleep(1) - - def update_comments(self): - # Сбор сообщений со всех сервисов - new_comments = [] - - # DonationAlerts - new_comments.extend(self.services['alerts'].get_donations()) - - # YouTube - # new_comments.extend(self.services['youtube'].get_messages()) - - # Telegram - if self.config.telegram_enabled: - try: - telegram_comments = asyncio.run(self.services['telegram'].get_messages()) - new_comments.extend(telegram_comments) - except Exception as e: - print(f'Telegram error: {e}') - - # Twitch - new_comments.extend(self.services['twitch'].get_messages()) - - # Добавление приветственного сообщения - if int(time.time()) % self.config.hello_message_interval == 0: - new_comments.append(self.hello_message) - - # Обновление основного списка комментариев - with self.lock: - # Добавление новых уникальных комментариев - existing_ids = {c.id for c in self.comments} - self.comments.extend( - c for c in new_comments - if c.id not in existing_ids - ) - - # Сортировка по дате - self.comments.sort(key=lambda x: x.date) - - # Ограничение общего количества - self.comments = self.comments[-self.config.max_total_comments:] - - print(f"{datetime.now().strftime('%H:%M:%S')} Updated. Total comments: {len(self.comments)}") - - def get_comments_json(self): - with self.lock: - return json.dumps( - [c.to_dict() for c in self.comments], - cls=DateTimeEncoder, - ensure_ascii=False - ) - - def _run_server(self): - class Handler(BaseHTTPRequestHandler): - def __init__(self, *args, **kwargs): - self.aggregator = kwargs.pop('aggregator') - super().__init__(*args, **kwargs) - - def _set_headers(self): - self.send_response(200) - self.send_header('Content-type', 'application/json; charset=utf-8') - self.send_header("Access-Control-Allow-Origin", "*") - self.end_headers() - - def do_GET(self): - if self.path == '/alert_auth': - self.send_response(301) - self.send_header('Location', self.aggregator.services['alerts'].api.authorize.login()) - self.end_headers() - elif self.path.startswith('/login'): - self._handle_alerts_login() - else: - self._set_headers() - self.wfile.write(self.aggregator.get_comments_json().encode('utf-8')) - - def _handle_alerts_login(self): - url = dict(parse.parse_qsl(parse.urlsplit(self.path).query)) - code = url.get('code') - if code: - try: - self.aggregator.services['alerts'].access_token = ( - self.aggregator.services['alerts'].api.authorize.get_access_token(code) - ) - print("DonationAlerts authorized successfully!") - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(b'Authorization successful! You can close this page.') - except Exception as e: - print(f'Alerts auth error: {str(e)}') - self.send_error(500, 'Authorization failed') - else: - self.send_error(400, 'Missing code parameter') - - def log_message(self, format, *args): - return - - server_address = ('127.0.0.1', 8008) - httpd = HTTPServer(server_address, lambda *args: Handler(*args, aggregator=self)) - print(f'Starting HTTP server on port 8008...') - httpd.serve_forever() - -# ==================== -# Вспомогательные классы -# ==================== +# Функции для работы с датами (оставлены как есть) class DateTimeEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, datetime): return o.isoformat() - return super().default(o) + if isinstance(o, bytes): + return list(o) + return json.JSONEncoder.default(self, o) -# ==================== -# Точка входа -# ==================== -if __name__ == '__main__': - print('\n--- Telegram/YouTube/Twitch Chat Aggregator ---\n') - print('For DonationAlerts authorization visit: http://127.0.0.1:8008/alert_auth') - print('Press Ctrl+Shift+Alt+C to exit\n') +LOCAL_TIMEZONE = tzlocal.get_localzone_name() +tz = ZoneInfo('Asia/Yekaterinburg') + +all_comments = [] +all_old_comments = [] +is_changed = False +overallcount = 0 + +# Получение комментариев из Telegram +async def get_telegram_comments(): + global chat_comments + chat_comments['tg'] = [] + + if 'telegram' not in chat_connectors: + return + + async with bot: + try: + updates = await bot.get_updates( + allowed_updates=['message', 'edited_message'], + timeout=None + ) + except telegram.error.TimedOut: + print('TG connection timeout') + time.sleep(5) + return + except Exception as e: + print(f'TG connection error: {e}') + return + + for upd in updates: + msg = upd.message + if upd.edited_message: + msg = upd.edited_message + + if not hasattr(msg, 'text') or not msg.text: + continue + + sendr = msg.from_user.first_name + if msg.from_user.last_name: + sendr = sendr + ' ' + msg.from_user.last_name + + if sendr == "Group": + sendr = 'Админ' + + netdatetime = msg.date.replace(tzinfo=tz) + comm = { + 'id': msg.message_id, + 'type': 'tg', + 'date': netdatetime, + 'sendr': sendr, + 'msg': msg.text + } + chat_comments['tg'].append(comm) + +# Получение комментариев из YouTube +def get_youtube_comments(): + global chat_comments - aggregator = ChatAggregator() - aggregator.run() \ No newline at end of file + if 'youtube' not in chat_connectors: + return + + chat = chat_connectors['youtube'] + itms = chat.get() + + if not hasattr(itms, 'items'): + print('YT has no items attribute! (Empty?). Reconnecting...') + time.sleep(5) + video_id = config['YouTube']['video_id'] + chat_connectors['youtube'] = pytchat.create(video_id=video_id) + return + + for c in itms.items: + dt = datetime.fromisoformat(c.datetime).replace(tzinfo=tz) + comm = { + 'id': c.id, + 'type': 'yt', + 'date': dt, + 'sendr': c.author.name, + 'msg': emoji.emojize(c.message) + } + chat_comments['yt'].append(comm) + +# Получение комментариев из Twitch +def get_twitch_comments(): + global chat_comments + + if 'twitch' not in chat_connectors: + return + + chat_comments['tw'] = chat_connectors['twitch'].all_messages + +def sort_by_date(e): + return e['date'] + +def update_all_comments(): + global all_comments + global chat_comments + + # Добавляем новые комментарии из всех источников + for source in ['tg', 'yt', 'tw']: + if source in chat_comments: + for comment in chat_comments[source]: + if comment not in all_comments: + all_comments.append(comment) + + # Сортируем по дате и ограничиваем количество + all_comments.sort(key=sort_by_date) + all_comments = all_comments[-150:] + +def make_json_object(): + global all_comments + global chat_comments + global overallcount + + # Получаем комментарии из всех включенных источников + if 'youtube' in chat_connectors: + get_youtube_comments() + + if 'twitch' in chat_connectors: + get_twitch_comments() + + if 'telegram' in chat_connectors: + try: + asyncio.run(get_telegram_comments()) + except Exception as e: + print(f'TG error: {e}') + time.sleep(2) + + # Ограничиваем буферы для каждого источника + for source in ['yt', 'tg', 'tw']: + if source in chat_comments and len(chat_comments[source]) > 15: + chat_comments[source] = chat_comments[source][-15:] + + # Добавляем приветственное сообщение каждые 10 минут + if int(time.time()) % 600 == 0: + dt = datetime.now(ZoneInfo('UTC')).astimezone(tz) + hello_msg = { + 'id': random.randint(100000, 999999), + 'type': 'hello', + 'date': dt, + 'sendr': 'Eikichi-bot', + 'msg': '🔥 Спасибо всем на стриме за интерес к переводу и поддержку! 🔥' + } + all_comments.append(hello_msg) + + # Обновляем общий список комментариев + update_all_comments() + + overallcount += 1 + print(f"{datetime.now().strftime('%H:%M:%S')} Проверка чатов... {len(all_comments)} элементов ({overallcount})") + +def print_all_comments(): + global all_comments + print('-' * 40) + print(all_comments) + print('-' * 40) + +# HTTP сервер +class ChatServer(BaseHTTPRequestHandler): + def _set_headers(self): + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + + def do_HEAD(self): + self._set_headers() + + def log_message(self, format, *args): + # Отключаем логи сервера + return + + def do_GET(self): + if self.path == '/': + self._set_headers() + self.wfile.write(json.dumps(all_comments, cls=DateTimeEncoder).encode('utf-8')) + else: + self.send_error(404, "Not Found") + +def run_server(port=8008): + server_address = ('', port) + httpd = HTTPServer(server_address, ChatServer) + print(f'Starting HTTP сервер на http://127.0.0.1:{port} ...') + print() + try: + httpd.serve_forever() + except KeyboardInterrupt: + pass + httpd.server_close() + sys.exit(1) + +# Основная часть программы +print() +print('--- Парсер чатов Telegram/YouTube/Twitch --- Sergey Shemet (C) 2025 ---') +print() + +# Запускаем Twitch в отдельном потоке если включен +if 'twitch' in chat_connectors: + twitch_thread = threading.Thread(target=twitch_socket.listen, name='twitchirc') + twitch_thread.daemon = True + twitch_thread.start() + +# Запускаем HTTP сервер в отдельном потоке +server_thread = threading.Thread(target=run_server, name='HTTPServer') +server_thread.daemon = True +server_thread.start() + +print('Запуск подсистем чатов...') +print('...Удерживайте Ctrl+Alt+Shift+C для выхода и C для очистки консоли...') + +poll_time = 2 # Интервал проверки в секундах + +while True: + # Обработка горячих клавиш + if keyboard.is_pressed('Ctrl+Shift+Alt+c'): + sys.exit(1) + if keyboard.is_pressed('Ctrl+Shift+Alt+z'): + print_all_comments() + if keyboard.is_pressed('c'): + os.system("cls") + + # Периодическая проверка чатов + this_second = int(time.time()) + if this_second % poll_time == 0: + make_json_object() + time.sleep(1) \ No newline at end of file diff --git a/index.html b/index.html index 6ab8e1f..2906ecf 100644 --- a/index.html +++ b/index.html @@ -1,80 +1,143 @@
-' + element['amount'] + "
"; + } else { + nameBlock.innerHTML = element["sendr"]; + } + + // Создаем блок сообщения + const msgBlock = document.createElement("div"); + msgBlock.className = "msgline"; + msgBlock.innerHTML = element["msg"]; + msgBlock.id = element["id"]; + + // Добавляем подсветку если есть ключевые слова + if (containsSpecialKeywords(element["msg"])) { + msgBlock.classList.add("highlight-message"); + } + + // Стиль для донатов + if (element["type"] == "donate") { + msgBlock.style.backgroundColor = "#0000FF20"; + } + + // Создаем строку чата + const row = document.createElement("div"); + row.className = "chatRow"; + row.setAttribute("name", element["id"]); + + row.appendChild(nameBlock); + row.appendChild(msgBlock); + + // ВСЕГДА вставляем новое сообщение перед якорем (внизу) + chatwin.insertBefore(row, anchor); + + // Прокручиваем сразу к новому сообщению + scrollToBottom(); + }); + + // Ограничиваем количество сообщений (200) + removeOldMessages(200); } +// Функция прокрутки в самый низ function scrollToBottom() { - chatwin.scrollTop = chatwin.scrollHeight; + // Используем setTimeout чтобы прокрутка происходила после добавления DOM элемента + setTimeout(() => { + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); + }, 10); } -function removeOldMessages() { - const children = Array.from(chatwin.children); - while (children.length > 15) { - chatwin.removeChild(children[0]); +// Удаление старых сообщений +function removeOldMessages(maxMessages = 200) { + const messageRows = document.getElementsByClassName("chatRow"); + const rowsToRemove = messageRows.length - maxMessages; + + if (rowsToRemove > 0) { + for (let i = 0; i < rowsToRemove; i++) { + if (messageRows[0]) { + messageRows[0].remove(); + } + } } } -function createChatLine() { - const nm = document.createElement("div"); - nm.className = "nameline " + randomChat(); - nm.textContent = randomNick(); - - const msg = document.createElement("div"); - msg.className = "msgline"; - msg.textContent = randomMessage(); - - const rw = document.createElement("div"); - rw.className = "chatRow"; - rw.appendChild(nm); - rw.appendChild(msg); - - chatwin.appendChild(rw); - scrollToBottom(); - removeOldMessages(); -} - -function createNewLine(json) { - json.forEach(element => { - const existing = document.getElementById(element.id); - if (existing) { - existing.querySelector('.msgline').textContent = element.msg; - return; - } - - const nm = document.createElement("div"); - nm.className = "nameline " + element.type; - nm.textContent = element.sendr; - - if (element.type === "donate") { - nm.innerHTML += `${element.amount}
`; - } - - const msg = document.createElement("div"); - msg.className = "msgline"; - msg.textContent = element.msg; - msg.id = element.id; - - if (element.type === "donate") { - msg.style.backgroundColor = "#0000FF20"; - } - - const rw = document.createElement("div"); - rw.className = "chatRow"; - rw.appendChild(nm); - rw.appendChild(msg); - - chatwin.appendChild(rw); - scrollToBottom(); - removeOldMessages(); - }); -} - +// Запрос новых сообщений с сервера function requestNewLines() { fetch("http://localhost:8008/") - .then(response => response.json()) - .then(data => createNewLine(data)) - .catch(error => console.error('Ошибка:', error)); + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + createNewLine(data); + }) + .catch(error => { + console.error('Error fetching chat messages:', error); + + // Показываем сообщение об ошибке только если его нет + const errorRow = document.getElementById("error-message"); + if (!errorRow) { + const errorDiv = document.createElement("div"); + errorDiv.id = "error-message"; + errorDiv.className = "chatRow error"; + errorDiv.innerHTML = '