This commit is contained in:
sShemet
2026-01-09 17:40:45 +05:00
parent 7561e6f411
commit c623b8c2f9
4 changed files with 115 additions and 85 deletions

View File

@@ -3,8 +3,8 @@ bot_secret = 6454033742:AAFj2rUoVb2jJ_Lew4eiIecdr7s7xbrqeNU
enabled = 1 enabled = 1
[YouTube] [YouTube]
video_id = 3xLyYdp0UFg video_id = y2fwatM1oOg
enabled = 1 enabled = 0
[Twitch] [Twitch]
channel = coulthardf1 channel = coulthardf1

View File

@@ -14,6 +14,7 @@ import tzlocal
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib import parse from urllib import parse
from datetime import timezone
# Чтение конфигурации # Чтение конфигурации
config = configparser.ConfigParser() config = configparser.ConfigParser()
@@ -23,14 +24,14 @@ config.read("config.ini")
chat_connectors = {} chat_connectors = {}
chat_comments = {} chat_comments = {}
# Telegram (опционально) # Telegram
if config.getboolean('Telegram', 'enabled', fallback=False): if config.getboolean('Telegram', 'enabled', fallback=False):
import telegram import telegram
bot = telegram.Bot(config['Telegram']['bot_secret']) bot = telegram.Bot(config['Telegram']['bot_secret'])
chat_connectors['telegram'] = bot chat_connectors['telegram'] = bot
chat_comments['tg'] = [] chat_comments['tg'] = []
# YouTube (опционально) # YouTube
if config.getboolean('YouTube', 'enabled', fallback=False): if config.getboolean('YouTube', 'enabled', fallback=False):
print('Youtube enabled') print('Youtube enabled')
import pytchat import pytchat
@@ -39,16 +40,18 @@ if config.getboolean('YouTube', 'enabled', fallback=False):
chat_connectors['youtube'] = chat chat_connectors['youtube'] = chat
chat_comments['yt'] = [] chat_comments['yt'] = []
# Twitch (опционально) # Twitch
TWITCH_CHANNEL = None
if config.getboolean('Twitch', 'enabled', fallback=False): if config.getboolean('Twitch', 'enabled', fallback=False):
sys.path.append('twitchchatirc') sys.path.append('twitchchatirc')
import twitch import twitch
twitch_channel = config['Twitch']['channel'] twitch_channel = config['Twitch']['channel']
twitch_socket = twitch.TwitchChatIRC(twitch_channel) twitch_socket = twitch.TwitchChatIRC(twitch_channel)
chat_connectors['twitch'] = twitch_socket chat_connectors['twitch'] = twitch_socket
TWITCH_CHANNEL = config['Twitch']['channel']
chat_comments['tw'] = [] chat_comments['tw'] = []
# Функции для работы с датами (оставлены как есть) # Функции для работы с датами
class DateTimeEncoder(json.JSONEncoder): class DateTimeEncoder(json.JSONEncoder):
def default(self, o): def default(self, o):
if isinstance(o, datetime): if isinstance(o, datetime):
@@ -64,10 +67,12 @@ all_comments = []
all_old_comments = [] all_old_comments = []
is_changed = False is_changed = False
overallcount = 0 overallcount = 0
last_tg_update_id = 0
# Получение комментариев из Telegram # Получение комментариев из Telegram
async def get_telegram_comments(): async def get_telegram_comments():
global chat_comments global chat_comments
global last_tg_update_id
chat_comments['tg'] = [] chat_comments['tg'] = []
if 'telegram' not in chat_connectors: if 'telegram' not in chat_connectors:
@@ -76,6 +81,7 @@ async def get_telegram_comments():
async with bot: async with bot:
try: try:
updates = await bot.get_updates( updates = await bot.get_updates(
offset=last_tg_update_id + 1,
allowed_updates=['message', 'edited_message'], allowed_updates=['message', 'edited_message'],
timeout=None timeout=None
) )
@@ -87,6 +93,11 @@ async def get_telegram_comments():
print(f'TG connection error: {e}') print(f'TG connection error: {e}')
return return
if not updates:
return
last_tg_update_id = updates[-1].update_id
for upd in updates: for upd in updates:
msg = upd.message msg = upd.message
if upd.edited_message: if upd.edited_message:
@@ -102,7 +113,9 @@ async def get_telegram_comments():
if sendr == "Group": if sendr == "Group":
sendr = 'Админ' sendr = 'Админ'
netdatetime = msg.date.replace(tzinfo=tz) try:
netdatetime = msg.date.replace(tzinfo=timezone.utc).astimezone(tz)
comm = { comm = {
'id': msg.message_id, 'id': msg.message_id,
'type': 'tg', 'type': 'tg',
@@ -111,6 +124,10 @@ async def get_telegram_comments():
'msg': msg.text 'msg': msg.text
} }
chat_comments['tg'].append(comm) chat_comments['tg'].append(comm)
except Exception as e:
print(f"Ошибка преобразования времени: {e}")
# Получение комментариев из YouTube # Получение комментариев из YouTube
def get_youtube_comments(): def get_youtube_comments():
@@ -156,16 +173,35 @@ def update_all_comments():
global all_comments global all_comments
global chat_comments global chat_comments
# Добавляем новые комментарии из всех источников # Временный список для новых комментариев
for source in ['tg', 'yt', 'tw']: new_comments = []
if source in chat_comments:
for comment in chat_comments[source]:
if comment not in all_comments:
all_comments.append(comment)
# Сортируем по дате и ограничиваем количество # Собираем новые комментарии из всех источников
for source in ['tg', 'yt', 'tw']:
if source in chat_comments and chat_comments[source]:
# Добавляем все новые комментарии из этого источника
new_comments.extend(chat_comments[source])
# Очищаем буфер этого источника
chat_comments[source] = []
# Добавляем новые комментарии к существующим
all_comments.extend(new_comments)
# Сортируем по дате
all_comments.sort(key=sort_by_date) all_comments.sort(key=sort_by_date)
all_comments = all_comments[-150:]
# Удаляем дубликаты по ID (если они есть)
seen_ids = set()
unique_comments = []
for comment in all_comments:
comment_id = (comment['id'], comment['type']) # ID уникален в рамках типа
if comment_id not in seen_ids:
seen_ids.add(comment_id)
unique_comments.append(comment)
# Ограничиваем количество и сохраняем
all_comments = unique_comments[-150:]
def make_json_object(): def make_json_object():
global all_comments global all_comments
@@ -189,10 +225,21 @@ def make_json_object():
# Ограничиваем буферы для каждого источника # Ограничиваем буферы для каждого источника
for source in ['yt', 'tg', 'tw']: for source in ['yt', 'tg', 'tw']:
if source in chat_comments and len(chat_comments[source]) > 15: if source in chat_comments and len(chat_comments[source]) > 15:
# Сортируем ВСЕ источники по дате (старые -> новые)
chat_comments[source].sort(key=lambda x: x['date'])
# Берём последние 15 (самые новые)
chat_comments[source] = chat_comments[source][-15:] chat_comments[source] = chat_comments[source][-15:]
# Добавляем приветственное сообщение каждые 10 минут # Добавляем приветственное сообщение каждые 10 минут
if int(time.time()) % 600 == 0: current_second = int(time.time())
if current_second % 600 <= 5:
# Проверяем, есть ли приветственное сообщение от Eikichi-bot в последних 20 сообщениях
has_recent_hello = any(
comment.get('sendr') == 'Eikichi-bot' and comment.get('type') == 'hello'
for comment in all_comments[-20:] # Последние 20 сообщений
)
if not has_recent_hello:
dt = datetime.now(ZoneInfo('UTC')).astimezone(tz) dt = datetime.now(ZoneInfo('UTC')).astimezone(tz)
hello_msg = { hello_msg = {
'id': random.randint(100000, 999999), 'id': random.randint(100000, 999999),
@@ -202,6 +249,7 @@ def make_json_object():
'msg': '🔥 Спасибо всем на стриме за интерес к переводу и поддержку! 🔥' 'msg': '🔥 Спасибо всем на стриме за интерес к переводу и поддержку! 🔥'
} }
all_comments.append(hello_msg) all_comments.append(hello_msg)
print(f"{datetime.now().strftime('%H:%M:%S')} Добавлено приветственное сообщение")
# Обновляем общий список комментариев # Обновляем общий список комментариев
update_all_comments() update_all_comments()
@@ -211,9 +259,11 @@ def make_json_object():
def print_all_comments(): def print_all_comments():
global all_comments global all_comments
global chat_comments
print('-' * 40) print('-' * 40)
print(all_comments) print(all_comments)
print('-' * 40) print('-' * 40)
print(chat_comments)
# HTTP сервер # HTTP сервер
class ChatServer(BaseHTTPRequestHandler): class ChatServer(BaseHTTPRequestHandler):
@@ -267,6 +317,9 @@ server_thread.start()
print('Запуск подсистем чатов...') print('Запуск подсистем чатов...')
print('...Удерживайте Ctrl+Alt+Shift+C для выхода и C для очистки консоли...') print('...Удерживайте Ctrl+Alt+Shift+C для выхода и C для очистки консоли...')
# print('Загрузка 7TV эмодзи...')
# emotes_cache = get_7tv_emotes() if TWITCH_CHANNEL else {}
poll_time = 2 # Интервал проверки в секундах poll_time = 2 # Интервал проверки в секундах

View File

@@ -30,7 +30,7 @@ body {
/* color: white; */ /* color: white; */
height: 100%; height: 100%;
min-width: 150px; min-width: 150px;
max-width: 400px; max-width: 450px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -43,6 +43,8 @@ body {
flex-direction: row; flex-direction: row;
margin-bottom: 1px; margin-bottom: 1px;
animation: fadeInUp 0.3s ease-out; animation: fadeInUp 0.3s ease-out;
min-height: 0;
} }
@keyframes fadeInUp { @keyframes fadeInUp {
@@ -67,6 +69,13 @@ body {
font-weight: bold; font-weight: bold;
flex-direction: column; flex-direction: column;
vertical-align: auto; vertical-align: auto;
flex: 0 0 25%; /* Фиксируем ширину - не растягивается и не сжимается */
min-width: 0; /* Важно для работы переноса текста в flex-контейнере */
max-width: 25%; /* Ограничиваем максимальную ширину */
word-wrap: break-word; /* Перенос длинных слов */
overflow-wrap: break-word; /* Современная версия */
word-break: break-word; /* Разрыв слов если нужно */
} }
.highlight-message { .highlight-message {
@@ -75,9 +84,19 @@ body {
} }
.emote-7tv {
height: 1.4em;
width: auto;
vertical-align: middle;
display: inline-block;
margin: 0 2px;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
}
.yt { .yt {
background-image: url('data:image/svg+xml,<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"><g fill="%239C92AC" fill-opacity="0.4" fill-rule="evenodd"><path d="M0 40L40 0H20L0 20M40 40V20L20 40"/></g></svg>'); background-image: url('data:image/svg+xml,<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"><g fill="%239C92AC" fill-opacity="0.3" fill-rule="evenodd"><path d="M0 40L40 0H20L0 20M40 40V20L20 40"/></g></svg>');
background-color: #E53935A0; background-color: #E53935A0;
} }
@@ -106,6 +125,11 @@ body {
padding: 5px; padding: 5px;
width: 75%; width: 75%;
color: #fff; color: #fff;
flex: 1; /* Занимает всё оставшееся пространство */
min-width: 0; /* КРИТИЧЕСКИ ВАЖНО для работы текста в flex */
word-wrap: break-word;
overflow-wrap: break-word;
} }
</style> </style>

View File

@@ -1,28 +1,17 @@
const chatwin = document.getElementById("chatwin"); const chatwin = document.getElementById("chatwin");
const anchor = document.getElementById("anchor"); const anchor = document.getElementById("anchor");
const specialKeywords = ['sergshemet', 'sergeyshemet', 'sshemet', 'серг', 'серег', 'серёг', 'админ'];
// Массив ключевых слов для выделения (без учета регистра)
const specialKeywords = ['sergshemet', 'sergeyshemet', 'sshemet', 'сергей', 'серёга', 'админ'];
// Функция для проверки содержит ли сообщение ключевые слова
function containsSpecialKeywords(text) { function containsSpecialKeywords(text) {
const lowerText = text.toLowerCase(); const lowerText = text.toLowerCase();
return specialKeywords.some(keyword => lowerText.includes(keyword.toLowerCase())); return specialKeywords.some(keyword => lowerText.includes(keyword.toLowerCase()));
} }
// Создание новой строки чата из JSON данных
function createNewLine(json) { function createNewLine(json) {
// Сортируем сообщения по дате (самые старые первыми)
json.sort((a, b) => new Date(a.date) - new Date(b.date)); json.sort((a, b) => new Date(a.date) - new Date(b.date));
json.forEach(element => { json.forEach(element => {
// Проверяем, существует ли уже элемент с таким ID
let existingMsg = document.getElementById(element["id"]); let existingMsg = document.getElementById(element["id"]);
if (existingMsg) { if (existingMsg) {
// Обновляем текст существующего сообщения
existingMsg.innerHTML = element["msg"]; existingMsg.innerHTML = element["msg"];
// Проверяем на ключевые слова для подсветки
if (containsSpecialKeywords(element["msg"])) { if (containsSpecialKeywords(element["msg"])) {
existingMsg.classList.add("highlight-message"); existingMsg.classList.add("highlight-message");
} else { } else {
@@ -30,62 +19,39 @@ function createNewLine(json) {
} }
return; return;
} }
// Создаем блок имени
const nameBlock = document.createElement("div"); const nameBlock = document.createElement("div");
nameBlock.className = "nameline " + element["type"]; nameBlock.className = "nameline " + element["type"];
// Для донатов добавляем сумму
if (element["type"] == "donate") { if (element["type"] == "donate") {
nameBlock.innerHTML = element["sendr"] + '<br><p style="color: red">' + element['amount'] + "</p>"; nameBlock.innerHTML = element["sendr"] + '<br><p style="color: red">' + element['amount'] + "</p>";
} else { } else {
nameBlock.innerHTML = element["sendr"]; nameBlock.innerHTML = element["sendr"];
} }
// Создаем блок сообщения
const msgBlock = document.createElement("div"); const msgBlock = document.createElement("div");
msgBlock.className = "msgline"; msgBlock.className = "msgline";
msgBlock.innerHTML = element["msg"]; msgBlock.innerHTML = element["msg"];
msgBlock.id = element["id"]; msgBlock.id = element["id"];
// Добавляем подсветку если есть ключевые слова
if (containsSpecialKeywords(element["msg"])) { if (containsSpecialKeywords(element["msg"])) {
msgBlock.classList.add("highlight-message"); msgBlock.classList.add("highlight-message");
} }
// Стиль для донатов
if (element["type"] == "donate") { if (element["type"] == "donate") {
msgBlock.style.backgroundColor = "#0000FF20"; msgBlock.style.backgroundColor = "#0000FF20";
} }
// Создаем строку чата
const row = document.createElement("div"); const row = document.createElement("div");
row.className = "chatRow"; row.className = "chatRow";
row.setAttribute("name", element["id"]); row.setAttribute("name", element["id"]);
row.appendChild(nameBlock); row.appendChild(nameBlock);
row.appendChild(msgBlock); row.appendChild(msgBlock);
// ВСЕГДА вставляем новое сообщение перед якорем (внизу)
chatwin.insertBefore(row, anchor); chatwin.insertBefore(row, anchor);
// Прокручиваем сразу к новому сообщению
scrollToBottom(); scrollToBottom();
}); });
// Ограничиваем количество сообщений (200)
removeOldMessages(200); removeOldMessages(200);
} }
// Функция прокрутки в самый низ
function scrollToBottom() { function scrollToBottom() {
// Используем setTimeout чтобы прокрутка происходила после добавления DOM элемента
setTimeout(() => { setTimeout(() => {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}, 10); }, 10);
} }
// Удаление старых сообщений
function removeOldMessages(maxMessages = 200) { function removeOldMessages(maxMessages = 200) {
const messageRows = document.getElementsByClassName("chatRow"); const messageRows = document.getElementsByClassName("chatRow");
const rowsToRemove = messageRows.length - maxMessages; const rowsToRemove = messageRows.length - maxMessages;
@@ -98,8 +64,6 @@ function removeOldMessages(maxMessages = 200) {
} }
} }
} }
// Запрос новых сообщений с сервера
function requestNewLines() { function requestNewLines() {
fetch("http://localhost:8008/") fetch("http://localhost:8008/")
.then(response => { .then(response => {
@@ -113,33 +77,22 @@ function requestNewLines() {
}) })
.catch(error => { .catch(error => {
console.error('Error fetching chat messages:', error); console.error('Error fetching chat messages:', error);
// Показываем сообщение об ошибке только если его нет
const errorRow = document.getElementById("error-message"); const errorRow = document.getElementById("error-message");
if (!errorRow) { if (!errorRow) {
const errorDiv = document.createElement("div"); const errorDiv = document.createElement("div");
errorDiv.id = "error-message"; errorDiv.id = "error-message";
errorDiv.className = "chatRow error"; errorDiv.className = "chatRow error";
errorDiv.innerHTML = '<div class="nameline hello">Ошибка</div><div class="msgline">Не удалось подключиться к серверу чата</div>'; errorDiv.innerHTML = '<div class="nameline hello">Maya</div><div class="msgline">Тацуя, демоны отключили сервер чата!</div>';
chatwin.insertBefore(errorDiv, anchor); chatwin.insertBefore(errorDiv, anchor);
scrollToBottom(); scrollToBottom();
} }
}); });
} }
// Инициализация чата
function initChat() { function initChat() {
// Прокручиваем сразу вниз при загрузке
scrollToBottom(); scrollToBottom();
// Запрашиваем сообщения сразу при загрузке
requestNewLines(); requestNewLines();
// Запускаем периодический опрос
setInterval(requestNewLines, 1000); setInterval(requestNewLines, 1000);
} }
// Запускаем чат когда DOM загружен
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initChat); document.addEventListener('DOMContentLoaded', initChat);
} else { } else {