Чатмонитор + Медведь/Волк + ЛС/клан через Telegram, облегчённая версия без cht.php/main.php, без авто-арены и без Telegram-команд
Size
37.8 KB
Version
3.1-lite
Created
Apr 2, 2026
Updated
13 days ago
1// ==UserScript==
2// @name Чатмонитор UMXO КЛАН ЛС облегчённый
3// @namespace https://asteriagame.com
4// @version 3.1-lite
5// @description Чатмонитор + Медведь/Волк + ЛС/клан через Telegram, облегчённая версия без cht.php/main.php, без авто-арены и без Telegram-команд
6// @author Ты + ChatGPT
7// @match https://asteriagame.com/main_frame.php
8// @match https://asteriagame.com/cht_iframe.php*
9// @grant GM_xmlhttpRequest
10// @grant GM_setValue
11// @grant GM_getValue
12// @connect api.telegram.org
13// @run-at document-end
14// ==/UserScript==
15
16// @require https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js
17
18(function () {
19 'use strict';
20
21 const currentPath = location.pathname;
22 const currentUrl = new URL(location.href);
23 const currentMode = currentUrl.searchParams.get('mode');
24
25 const ls = {
26 get(key, def) {
27 try {
28 const raw = localStorage.getItem(key);
29 return raw === null ? def : (JSON.parse(raw) ?? def);
30 } catch {
31 return def;
32 }
33 },
34 set(key, value) {
35 localStorage.setItem(key, JSON.stringify(value));
36 },
37 remove(key) {
38 localStorage.removeItem(key);
39 }
40 };
41
42 const BEAR_CONFIG = {
43 id: 67208246,
44 ACTION_FORM_URL: 'https://asteriagame.com/action_form.php',
45 OPENER_SUCCESS_FUNCTION: "frames['main_frame'].frames['main'].frames['user_iframe'].updateCurrentIframe();",
46 };
47
48 const WOLF_CONFIG = {
49 id: 80534876,
50 ACTION_FORM_URL: 'https://asteriagame.com/action_form.php',
51 OPENER_SUCCESS_FUNCTION: "frames['main_frame'].frames['main'].frames['user_iframe'].updateCurrentIframe();",
52 };
53
54 const CHAT_MONITOR_CONFIG = {
55 telegramBotToken: '7715183221:AAHag1_ICYSW8BK4UUvydXBw399yZzGDgTs',
56 telegramChatId: '454733844',
57 targetPlayer: 'umxo',
58 triggers: [
59 'Вы вмешались',
60 'В Обители появился Призрачный Админ!',
61 'на локации « Хаотическая битва»',
62 'награду',
63 'СЮДА',
64 'славы',
65 'Удачи!',
66 'Знак Дозорного',
67 'umxo выиграл',
68 'смола',
69 'Развоплощение',
70 'Центридо',
71 'Инкарнум',
72 'В Рахдарии замечены',
73 'Магия куба призвала Демона!',
74 'Встречайте!',
75 'выдано',
76 ],
77 triggerForPet: 'Начался',
78 petTriggerKey: 'petTriggerTs',
79 processedIdsLimit: 500
80 };
81
82 const TELEGRAM_CONFIG = {
83 telegramBotToken: '7715183221:AAHag1_ICYSW8BK4UUvydXBw399yZzGDgTs',
84 telegramChatId: '454733844',
85 telegramUpdateIdKey: 'lastTelegramUpdateId_LS_v6',
86 telegramReplyStorageKey: 'telegramReplyStorage_LS_v6',
87 telegramPollingInterval: 5000
88 };
89
90 const CLAN_TELEGRAM_CONFIG = {
91 telegramBotToken: '8350668539:AAEv8syPJfn_qtbLTYWlQk6rauLmQRBTIds',
92 telegramChatId: '454733844',
93 telegramUpdateIdKey: 'lastClanTelegramUpdateId_LS_v1',
94 telegramReplyStorageKey: 'clanTelegramReplyStorage_LS_v1',
95 telegramPollingInterval: 5000
96 };
97
98 let cachedLastUpdateId = null;
99 let cachedClanLastUpdateId = null;
100
101 (async () => {
102 cachedLastUpdateId = await GM_getValue(TELEGRAM_CONFIG.telegramUpdateIdKey, 0);
103 cachedClanLastUpdateId = await GM_getValue(CLAN_TELEGRAM_CONFIG.telegramUpdateIdKey, 0);
104 })();
105
106 function sendToTelegram(message, replyToMessageId = null) {
107 return new Promise((resolve, reject) => {
108 const url = `https://api.telegram.org/bot${TELEGRAM_CONFIG.telegramBotToken}/sendMessage`;
109 const payload = {
110 chat_id: TELEGRAM_CONFIG.telegramChatId,
111 text: message,
112 parse_mode: 'HTML'
113 };
114 if (replyToMessageId) {
115 payload.reply_to_message_id = replyToMessageId;
116 }
117
118 GM_xmlhttpRequest({
119 method: 'POST',
120 url,
121 headers: { 'Content-Type': 'application/json' },
122 data: JSON.stringify(payload),
123 onload: (response) => {
124 if (response.status === 200) {
125 try {
126 const result = JSON.parse(response.responseText);
127 if (result.ok && result.result?.message_id) {
128 resolve(result.result.message_id);
129 return;
130 }
131 } catch {}
132 reject();
133 } else if (response.status === 429) {
134 try {
135 const retryAfter = JSON.parse(response.responseText).parameters?.retry_after || 1;
136 setTimeout(() => {
137 sendToTelegram(message, replyToMessageId).then(resolve).catch(reject);
138 }, retryAfter * 1000);
139 } catch {
140 reject();
141 }
142 } else {
143 reject();
144 }
145 },
146 onerror: () => reject()
147 });
148 });
149 }
150
151 function sendToClanTelegram(message, replyToMessageId = null) {
152 return new Promise((resolve, reject) => {
153 const url = `https://api.telegram.org/bot${CLAN_TELEGRAM_CONFIG.telegramBotToken}/sendMessage`;
154 const payload = {
155 chat_id: CLAN_TELEGRAM_CONFIG.telegramChatId,
156 text: message,
157 parse_mode: 'HTML'
158 };
159 if (replyToMessageId) {
160 payload.reply_to_message_id = replyToMessageId;
161 }
162
163 GM_xmlhttpRequest({
164 method: 'POST',
165 url,
166 headers: { 'Content-Type': 'application/json' },
167 data: JSON.stringify(payload),
168 onload: (response) => {
169 if (response.status === 200) {
170 try {
171 const result = JSON.parse(response.responseText);
172 if (result.ok && result.result?.message_id) {
173 resolve(result.result.message_id);
174 return;
175 }
176 } catch {}
177 reject();
178 } else if (response.status === 429) {
179 try {
180 const retryAfter = JSON.parse(response.responseText).parameters?.retry_after || 1;
181 setTimeout(() => {
182 sendToClanTelegram(message, replyToMessageId).then(resolve).catch(reject);
183 }, retryAfter * 1000);
184 } catch {
185 reject();
186 }
187 } else {
188 reject();
189 }
190 },
191 onerror: () => reject()
192 });
193 });
194 }
195
196 function sendChatMessage(msgText, channelTalk = 1, locId = 5) {
197 const body = new URLSearchParams({
198 json_mode_on: '1',
199 object: 'chat',
200 action: 'send',
201 msg_text: msgText,
202 channel_talk: String(channelTalk),
203 loc_id: String(locId),
204 msg_id: '0',
205 private: '0',
206 complain: '',
207 complain_nick: '',
208 stime: '0'
209 });
210
211 GM_xmlhttpRequest({
212 method: 'POST',
213 url: 'https://asteriagame.com/entry_point.php?object=chat&action=send&json_mode_on=1',
214 headers: {
215 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
216 'X-Requested-With': 'XMLHttpRequest',
217 'Accept': 'application/json, text/javascript, */*; q=0.01'
218 },
219 data: body.toString(),
220 onload: () => {},
221 onerror: () => {}
222 });
223 }
224
225 function sendPrivateViaFetch(playerName, text) {
226 sendChatMessage(`prv[${playerName}] ${text}`, 1, 5);
227 }
228
229 function sendClanMessage(text, toNick = null) {
230 const msgText = toNick ? `to[${toNick}] ${text}` : text;
231 sendChatMessage(msgText, 64, 122);
232 }
233
234 function performBearAction() {
235 fetch(
236 `${BEAR_CONFIG.ACTION_FORM_URL}?${Math.random()}&no_confirm=1&artifact_id=${BEAR_CONFIG.id}` +
237 `&in[param_success][opener_success_function]=${encodeURIComponent(BEAR_CONFIG.OPENER_SUCCESS_FUNCTION)}` +
238 `&in[param_success][window_reload]=0` +
239 `&in[param_success][url_close]=user_iframe.php%3Fgroup%3D1%26external%3D1`,
240 { method: 'GET', credentials: 'include', headers: { Accept: '*/*' } }
241 ).catch(() => {});
242 }
243
244 function performWOLFAction() {
245 fetch(
246 `${WOLF_CONFIG.ACTION_FORM_URL}?${Math.random()}&no_confirm=1&artifact_id=${WOLF_CONFIG.id}` +
247 `&in[param_success][opener_success_function]=${encodeURIComponent(WOLF_CONFIG.OPENER_SUCCESS_FUNCTION)}` +
248 `&in[param_success][window_reload]=0` +
249 `&in[param_success][url_close]=user_iframe.php%3Fgroup%3D1%26external%3D1`,
250 { method: 'GET', credentials: 'include', headers: { Accept: '*/*' } }
251 ).catch(() => {});
252 }
253
254 let replyStorage = ls.get(TELEGRAM_CONFIG.telegramReplyStorageKey, {});
255 let clanReplyStorage = ls.get(CLAN_TELEGRAM_CONFIG.telegramReplyStorageKey, {});
256
257 (function cleanupReplyStorage() {
258 const now = Date.now();
259 for (const key in replyStorage) {
260 if (now - Number(key) > 24 * 60 * 60 * 1000) delete replyStorage[key];
261 }
262 for (const key in clanReplyStorage) {
263 if (now - Number(key) > 24 * 60 * 60 * 1000) delete clanReplyStorage[key];
264 }
265 ls.set(TELEGRAM_CONFIG.telegramReplyStorageKey, replyStorage);
266 ls.set(CLAN_TELEGRAM_CONFIG.telegramReplyStorageKey, clanReplyStorage);
267 })();
268
269 function findPlayerByTelegramMessageId(telegramMessageId) {
270 return replyStorage[telegramMessageId];
271 }
272
273 function storePlayerForTelegramMessageId(telegramMessageId, playerName) {
274 replyStorage[telegramMessageId] = playerName;
275 ls.set(TELEGRAM_CONFIG.telegramReplyStorageKey, replyStorage);
276 }
277
278 function findClanPlayerByTelegramMessageId(telegramMessageId) {
279 return clanReplyStorage[telegramMessageId];
280 }
281
282 function storeClanPlayerForTelegramMessageId(telegramMessageId, playerName) {
283 clanReplyStorage[telegramMessageId] = playerName;
284 ls.set(CLAN_TELEGRAM_CONFIG.telegramReplyStorageKey, clanReplyStorage);
285 }
286
287 function extractPlayerFromTelegramNotification(telegramText) {
288 const lines = telegramText.trim().split('\n');
289 const messageLine = lines[lines.length - 1];
290 const match = messageLine.match(/\[\d{2}:\d{2}:\d{2}\]\s+(.+?)\s+→/);
291 if (match && match[1]) return match[1].trim();
292
293 const oldMatch = messageLine.match(/^ЛС от ([^\s:]+)/);
294 if (oldMatch) return oldMatch[1].trim();
295
296 return null;
297 }
298
299 // === УЛУЧШЕННЫЕ ФУНКЦИИ ДЛЯ КЛАНА ===
300
301function extractClanSender(telegramText) {
302 if (!telegramText) return null;
303 const text = telegramText.trim();
304
305 // Вариант 1: Ник с пробелами до символа →
306 let match = text.match(/^(.+?)\s*→\s*[^:]+?\s*:\s*(.*)$/);
307 if (match && match[1]) return match[1].trim();
308
309 // Вариант 2: Ник с пробелами до двоеточия
310 match = text.match(/^([^\s:→]+(?:\s+[^\s:→]+)*?)\s*:\s*(.*)$/);
311 if (match && match[1]) return match[1].trim();
312
313 // Вариант 3: Просто первая строка до первого двоеточия (самый надёжный fallback)
314 const colonIndex = text.indexOf(':');
315 if (colonIndex > 0) {
316 const possibleNick = text.substring(0, colonIndex).trim();
317 if (possibleNick && possibleNick.length < 40) { // защита от мусора
318 return possibleNick;
319 }
320 }
321
322 return null;
323}
324
325function storeClanPlayerForTelegramMessageId(telegramMessageId, playerName) {
326 clanReplyStorage[telegramMessageId] = playerName;
327 ls.set(CLAN_TELEGRAM_CONFIG.telegramReplyStorageKey, clanReplyStorage);
328}
329
330 function notifyTelegramAboutIncomingMessage(sender, recipient, messageText) {
331 if (!sender || !recipient) return;
332
333 const telegramMessage =
334 `[${new Date().toLocaleTimeString('ru-RU', {
335 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
336 })}] ${sender} → ${recipient}: ${messageText}`;
337
338 sendToTelegram(telegramMessage)
339 .then((sentMessageId) => {
340 if (sentMessageId) storePlayerForTelegramMessageId(sentMessageId, sender);
341 })
342 .catch(() => {});
343 }
344
345 async function getTelegramUpdates() {
346 if (cachedLastUpdateId === null) {
347 cachedLastUpdateId = await GM_getValue(TELEGRAM_CONFIG.telegramUpdateIdKey, 0);
348 }
349
350 const offset = cachedLastUpdateId > 0 ? cachedLastUpdateId + 1 : null;
351 let url = `https://api.telegram.org/bot${TELEGRAM_CONFIG.telegramBotToken}/getUpdates?timeout=2`;
352 if (offset) url += `&offset=${offset}`;
353
354 GM_xmlhttpRequest({
355 method: 'GET',
356 url,
357 onload: (response) => {
358 if (response.status === 200) {
359 try {
360 const result = JSON.parse(response.responseText);
361 if (result.ok && result.result.length > 0) {
362 let maxProcessedUpdateId = cachedLastUpdateId;
363 const processedOriginalMessageIds = new Set();
364 const processedTelegramIds = new Set();
365
366 for (const update of result.result) {
367 const currentUpdateId = update.update_id;
368
369 if (update.message && update.message.text && String(update.message.chat.id) === String(TELEGRAM_CONFIG.telegramChatId)) {
370 const messageText = update.message.text.trim();
371 const messageId = update.message.message_id || 0;
372 const msgKey = `${currentUpdateId}_${messageId}_${messageText.length}_${messageText.substring(0, 40)}`;
373
374 if (processedTelegramIds.has(msgKey)) continue;
375 processedTelegramIds.add(msgKey);
376 if (processedTelegramIds.size > 600) {
377 processedTelegramIds.delete(processedTelegramIds.values().next().value);
378 }
379
380 if (messageText.includes('выполнена UMXO')) continue;
381
382 const replyToMessage = update.message.reply_to_message;
383
384 if (replyToMessage) {
385 const originalMessageId = replyToMessage.message_id;
386 if (processedOriginalMessageIds.has(originalMessageId)) continue;
387 processedOriginalMessageIds.add(originalMessageId);
388
389 const playerName =
390 findPlayerByTelegramMessageId(originalMessageId) ||
391 extractPlayerFromTelegramNotification(replyToMessage.text || '');
392
393 if (playerName) {
394 sendPrivateViaFetch(playerName, messageText);
395 } else {
396 sendToTelegram('❌ Не удалось определить получателя. Ответьте на сообщение из игры.');
397 }
398 } else {
399 const colonIndex = messageText.indexOf(':');
400 if (colonIndex !== -1) {
401 const playerName = messageText.substring(0, colonIndex).trim();
402 const playerMessage = messageText.substring(colonIndex + 1).trim();
403
404 if (playerName && playerMessage) {
405 sendPrivateViaFetch(playerName, playerMessage);
406 } else {
407 sendToTelegram('❌ Неверный формат. Используйте: <code>ИМЯ: сообщение</code>');
408 }
409 }
410 }
411 }
412
413 if (currentUpdateId > maxProcessedUpdateId) {
414 maxProcessedUpdateId = currentUpdateId;
415 }
416 }
417
418 if (maxProcessedUpdateId > cachedLastUpdateId) {
419 cachedLastUpdateId = maxProcessedUpdateId;
420 GM_setValue(TELEGRAM_CONFIG.telegramUpdateIdKey, maxProcessedUpdateId);
421 }
422 }
423 } catch {}
424
425 setTimeout(getTelegramUpdates, TELEGRAM_CONFIG.telegramPollingInterval);
426 } else if (response.status === 429) {
427 try {
428 const retryAfter = JSON.parse(response.responseText).parameters?.retry_after || 1;
429 setTimeout(getTelegramUpdates, retryAfter * 1000);
430 } catch {
431 setTimeout(getTelegramUpdates, TELEGRAM_CONFIG.telegramPollingInterval);
432 }
433 } else {
434 setTimeout(getTelegramUpdates, TELEGRAM_CONFIG.telegramPollingInterval);
435 }
436 },
437 onerror: () => setTimeout(getTelegramUpdates, TELEGRAM_CONFIG.telegramPollingInterval)
438 });
439 }
440
441 async function getClanTelegramUpdates() {
442 if (cachedClanLastUpdateId === null) {
443 cachedClanLastUpdateId = await GM_getValue(CLAN_TELEGRAM_CONFIG.telegramUpdateIdKey, 0);
444 }
445
446 const offset = cachedClanLastUpdateId > 0 ? cachedClanLastUpdateId + 1 : null;
447 let url = `https://api.telegram.org/bot${CLAN_TELEGRAM_CONFIG.telegramBotToken}/getUpdates?timeout=2`;
448 if (offset) url += `&offset=${offset}`;
449
450 GM_xmlhttpRequest({
451 method: 'GET',
452 url,
453 onload: (response) => {
454 if (response.status === 200) {
455 try {
456 const result = JSON.parse(response.responseText);
457 if (result.ok && result.result.length > 0) {
458 let maxProcessedUpdateId = cachedClanLastUpdateId;
459 const processedClanTelegramIds = new Set();
460
461 for (const update of result.result) {
462 const currentUpdateId = update.update_id;
463
464 if (update.message && update.message.text && String(update.message.chat.id) === String(CLAN_TELEGRAM_CONFIG.telegramChatId)) {
465 const messageText = update.message.text.trim();
466 const messageId = update.message.message_id || 0;
467 const msgKey = `${currentUpdateId}_${messageId}_${messageText.length}_${messageText.substring(0, 40)}`;
468
469 if (processedClanTelegramIds.has(msgKey)) continue;
470 processedClanTelegramIds.add(msgKey);
471 if (processedClanTelegramIds.size > 600) {
472 processedClanTelegramIds.delete(processedClanTelegramIds.values().next().value);
473 }
474
475 const replyToMessage = update.message.reply_to_message;
476
477 if (replyToMessage) {
478 const originalMessageId = replyToMessage.message_id;
479
480 // Сначала пытаемся найти по хранилищу (самый надёжный способ)
481 let targetNick = findClanPlayerByTelegramMessageId(originalMessageId);
482
483 // Если не нашли — пытаемся вытащить из текста сообщения
484 if (!targetNick) {
485 targetNick = extractClanSender(replyToMessage.text || '');
486 }
487
488 if (targetNick) {
489 sendClanMessage(messageText, targetNick);
490 // Дополнительно сохраняем на будущее
491 if (originalMessageId) {
492 storeClanPlayerForTelegramMessageId(originalMessageId, targetNick);
493 }
494 } else {
495 sendToClanTelegram('❌ Не удалось определить получателя для реплая.\n\nПопробуй ответить в формате:\n<code>Ник: сообщение</code>');
496 }
497} else {
498 const colonIndex = messageText.indexOf(':');
499 if (colonIndex !== -1) {
500 const targetNick = messageText.substring(0, colonIndex).trim();
501 const msg = messageText.substring(colonIndex + 1).trim();
502
503 if (targetNick && msg) {
504 sendClanMessage(msg, targetNick);
505 } else {
506 sendToClanTelegram('❌ Неверный формат приватного сообщения.');
507 }
508 } else {
509 sendClanMessage(messageText);
510 }
511 }
512 }
513
514 if (currentUpdateId > maxProcessedUpdateId) {
515 maxProcessedUpdateId = currentUpdateId;
516 }
517 }
518
519 if (maxProcessedUpdateId > cachedClanLastUpdateId) {
520 cachedClanLastUpdateId = maxProcessedUpdateId;
521 GM_setValue(CLAN_TELEGRAM_CONFIG.telegramUpdateIdKey, maxProcessedUpdateId);
522 }
523 }
524 } catch {}
525
526 setTimeout(getClanTelegramUpdates, CLAN_TELEGRAM_CONFIG.telegramPollingInterval);
527 } else if (response.status === 429) {
528 try {
529 const retryAfter = JSON.parse(response.responseText).parameters?.retry_after || 1;
530 setTimeout(getClanTelegramUpdates, retryAfter * 1000);
531 } catch {
532 setTimeout(getClanTelegramUpdates, CLAN_TELEGRAM_CONFIG.telegramPollingInterval);
533 }
534 } else {
535 setTimeout(getClanTelegramUpdates, CLAN_TELEGRAM_CONFIG.telegramPollingInterval);
536 }
537 },
538 onerror: () => setTimeout(getClanTelegramUpdates, CLAN_TELEGRAM_CONFIG.telegramPollingInterval)
539 });
540 }
541
542 function createLimitedProcessedIds(limit) {
543 const set = new Set();
544 const queue = [];
545
546 return {
547 has(id) {
548 return set.has(id);
549 },
550 add(id) {
551 if (!id || set.has(id)) return;
552 set.add(id);
553 queue.push(id);
554
555 while (queue.length > limit) {
556 const oldest = queue.shift();
557 set.delete(oldest);
558 }
559 }
560 };
561 }
562
563 const processedIds = createLimitedProcessedIds(CHAT_MONITOR_CONFIG.processedIdsLimit);
564
565 function handleChatIframe() {
566 if (currentMode !== 'text') return;
567
568 function checkMessage(msgElement) {
569 if (!msgElement || msgElement.nodeType !== 1) return;
570
571 const msgId = msgElement.getAttribute('msg_id') || msgElement.id || null;
572 if (msgId && processedIds.has(msgId)) return;
573
574 const msgTxtSpan = msgElement.querySelector('.msgtxt');
575 const msgText = msgTxtSpan ? msgTxtSpan.textContent.trim() : '';
576 if (!msgText) {
577 if (msgId) processedIds.add(msgId);
578 return;
579 }
580
581 let stime = '—';
582 const stimeAttr = msgElement.getAttribute('stime');
583 if (stimeAttr) {
584 stime = new Date(parseInt(stimeAttr, 10) * 1000).toLocaleTimeString('ru-RU', {
585 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
586 });
587 } else {
588 const tsLink = msgElement.querySelector('.timestamp[data-ts]');
589 if (tsLink) {
590 const ts = tsLink.getAttribute('data-ts');
591 if (ts) {
592 stime = new Date(parseInt(ts, 10) * 1000).toLocaleTimeString('ru-RU', {
593 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
594 });
595 }
596 }
597 }
598
599 let sender = 'Система';
600 let isTargeted = false;
601 let toUserList = '';
602 let msgData = null;
603
604 const rawJson = msgElement.getAttribute('original-msg-object');
605 if (rawJson) {
606 try {
607 msgData = JSON.parse(rawJson);
608 if (msgData.user_nick) sender = msgData.user_nick;
609
610 const toUserNicks = msgData.to_user_nicks || {};
611 const toUserNames = Object.values(toUserNicks);
612 if (toUserNames.includes(CHAT_MONITOR_CONFIG.targetPlayer)) {
613 isTargeted = true;
614 }
615 if (toUserNames.length > 0) {
616 toUserList = toUserNames.join(', ');
617 }
618 } catch {}
619 } else {
620 const senderLink = msgElement.querySelector('a[onclick*="userToTag"]');
621 if (senderLink) {
622 const onclick = senderLink.getAttribute('onclick') || '';
623 const match = onclick.match(/userToTag\('([^']+)'\)/);
624 if (match && match[1]) sender = match[1];
625 }
626 }
627
628 const hasTrigger = CHAT_MONITOR_CONFIG.triggers.some((trigger) => msgText.includes(trigger));
629 const hasPetTrigger = msgText.includes(CHAT_MONITOR_CONFIG.triggerForPet);
630
631 if ((hasTrigger && !hasPetTrigger) || isTargeted) {
632 let fullMessage = `[${stime}] ${sender}`;
633 if (isTargeted && toUserList) fullMessage += ` → ${toUserList}`;
634 fullMessage += `: ${msgText}`;
635 sendToTelegram(fullMessage).catch(() => {});
636 }
637
638 if (msgData && msgData.channel === 64 && sender !== CHAT_MONITOR_CONFIG.targetPlayer) {
639 let fullMessage = sender;
640 if (toUserList) fullMessage += ` → ${toUserList}`;
641 fullMessage += `: ${msgText}`;
642
643 sendToClanTelegram(fullMessage)
644 .then((sentMessageId) => {
645 if (sentMessageId) {
646 storeClanPlayerForTelegramMessageId(sentMessageId, sender);
647 }
648 })
649 .catch(() => {});
650 }
651
652 if (hasPetTrigger) {
653 ls.set(CHAT_MONITOR_CONFIG.petTriggerKey, Date.now());
654 }
655
656 if (msgId) processedIds.add(msgId);
657 }
658
659 function processInitialMessages(container) {
660 const messages = Array.from(container.children).filter((el) =>
661 el.classList?.contains('cml_def') ||
662 el.classList?.contains('cml_spc') ||
663 el.classList?.contains('cml_loc') ||
664 el.hasAttribute?.('msg_id')
665 );
666 messages.forEach(checkMessage);
667 }
668
669 function bindObserver() {
670 const target =
671 document.getElementById('content') ||
672 document.querySelector('#content') ||
673 document.body;
674
675 if (!target) {
676 setTimeout(bindObserver, 500);
677 return;
678 }
679
680 // Обработка уже существующих сообщений
681 const existingMessages = target.querySelectorAll('div[msg_id], div.cml_def, div.cml_spc, div.cml_loc');
682 existingMessages.forEach(checkMessage);
683
684 const observer = new MutationObserver((mutations) => {
685 for (const mutation of mutations) {
686 for (const node of mutation.addedNodes) {
687 if (!node || node.nodeType !== 1) continue;
688
689 // Прямое сообщение
690 if (
691 node.hasAttribute?.('msg_id') ||
692 node.classList?.contains('cml_def') ||
693 node.classList?.contains('cml_spc') ||
694 node.classList?.contains('cml_loc')
695 ) {
696 checkMessage(node);
697 continue;
698 }
699
700 // Ищем сообщения внутри добавленного узла (включая глубокие)
701 const nestedMessages = node.querySelectorAll?.(
702 'div[msg_id], div.cml_def, div.cml_spc, div.cml_loc'
703 );
704 if (nestedMessages?.length) {
705 nestedMessages.forEach(checkMessage);
706 }
707 }
708 }
709 });
710
711 // subtree: true нужен, но цель — только контейнер чата
712 observer.observe(target, { childList: true, subtree: true });
713}
714
715 bindObserver();
716 }
717
718 function handleMainFrame() {
719 getTelegramUpdates();
720 getClanTelegramUpdates();
721
722 const style = document.createElement('style');
723 style.textContent = `
724 #bear-wolf-container {
725 position: fixed;
726 top: 73px;
727 left: 310px;
728 z-index: 9999;
729 display: flex;
730 flex-direction: row;
731 gap: 8px;
732 }
733 .tool-wrapper {
734 display: flex;
735 align-items: center;
736 gap: 6px;
737 padding: 2px 6px;
738 border: none;
739 border-radius: 3px;
740 font-weight: bold;
741 cursor: pointer;
742 color: white;
743 background-color: #ff4444;
744 transition: background-color .25s;
745 width: auto;
746 min-width: 110px;
747 font-size: 11px;
748 }
749 .tool-wrapper.bear { background-color: #ff4444; }
750 .tool-wrapper.wolf { background-color: #4444ff; }
751 .tool-wrapper.active { background-color: #44ff44 !important; }
752 .tool-wrapper label {
753 color: #fff;
754 cursor: pointer;
755 user-select: none;
756 font-size: 10px;
757 }
758 .tool-wrapper input[type="checkbox"] {
759 margin: 0;
760 accent-color: #fff;
761 }
762 .tool-name {
763 flex: 1;
764 text-align: center;
765 font-size: 12px;
766 }
767 `;
768 document.head.appendChild(style);
769
770 const whenBodyReady = (fn) => {
771 if (document.body) {
772 fn();
773 return;
774 }
775 const obs = new MutationObserver(() => {
776 if (document.body) {
777 obs.disconnect();
778 fn();
779 }
780 });
781 obs.observe(document.documentElement, { childList: true, subtree: true });
782 };
783
784 whenBodyReady(() => {
785 if (document.getElementById('bear-wolf-container')) return;
786
787 const container = document.createElement('div');
788 container.id = 'bear-wolf-container';
789 document.body.appendChild(container);
790
791 const ACTIVE_MODE_KEY = 'petActiveMode';
792
793 const createPetButton = (petName, colorClass, actionFn) => {
794 const wrapper = document.createElement('div');
795 wrapper.className = `tool-wrapper ${colorClass}`;
796 wrapper.dataset.pet = petName;
797
798 const nameSpan = document.createElement('span');
799 nameSpan.className = 'tool-name';
800 nameSpan.textContent = petName === 'bear' ? 'Медведь' : 'Волк';
801
802 const autoChk = document.createElement('input');
803 autoChk.type = 'checkbox';
804 autoChk.id = `${petName}-auto`;
805
806 const autoLbl = document.createElement('label');
807 autoLbl.htmlFor = `${petName}-auto`;
808 autoLbl.textContent = 'auto';
809
810 const instChk = document.createElement('input');
811 instChk.type = 'checkbox';
812 instChk.id = `${petName}-inst`;
813
814 const instLbl = document.createElement('label');
815 instLbl.htmlFor = `${petName}-inst`;
816 instLbl.textContent = 'inst';
817
818 wrapper.append(nameSpan, autoChk, autoLbl, instChk, instLbl);
819
820 const myAutoMode = `${petName}-auto`;
821 const myInstMode = `${petName}-inst`;
822 const activeMode = ls.get(ACTIVE_MODE_KEY, null);
823
824 if (activeMode === myAutoMode) {
825 autoChk.checked = true;
826 wrapper.classList.add('active');
827 }
828 if (activeMode === myInstMode) {
829 instChk.checked = true;
830 wrapper.classList.add('active');
831 }
832
833 const handleCheck = (mode) => (e) => {
834 const checked = e.target.checked;
835
836 document.querySelectorAll('#bear-wolf-container input[type="checkbox"]').forEach((chk) => {
837 if (chk !== e.target) chk.checked = false;
838 });
839
840 document.querySelectorAll('#bear-wolf-container .tool-wrapper').forEach((el) => {
841 el.classList.remove('active');
842 });
843
844 if (checked) {
845 ls.set(ACTIVE_MODE_KEY, mode);
846 wrapper.classList.add('active');
847 } else {
848 ls.remove(ACTIVE_MODE_KEY);
849 }
850
851 maybeStartPolling();
852 };
853
854 autoChk.addEventListener('change', handleCheck(myAutoMode));
855 instChk.addEventListener('change', handleCheck(myInstMode));
856
857 wrapper.addEventListener('click', (e) => {
858 if (e.target.tagName === 'INPUT' || e.target.tagName === 'LABEL') return;
859 actionFn();
860 });
861
862 return wrapper;
863 };
864
865 container.append(
866 createPetButton('bear', 'bear', performBearAction),
867 createPetButton('wolf', 'wolf', performWOLFAction)
868 );
869
870 let pollingInterval = null;
871
872 function startPolling() {
873 if (pollingInterval) return;
874 pollingInterval = setInterval(() => {
875 const ts = ls.get(CHAT_MONITOR_CONFIG.petTriggerKey, null);
876 if (!ts) return;
877
878 const age = Date.now() - ts;
879 const mode = ls.get(ACTIVE_MODE_KEY, null);
880
881 if (!mode) {
882 ls.remove(CHAT_MONITOR_CONFIG.petTriggerKey);
883 return;
884 }
885
886 let shouldFire = false;
887 let delay = 0;
888
889 if (mode.endsWith('-inst') && age <= 6500) {
890 shouldFire = true;
891 delay = 120;
892 } else if (mode.endsWith('-auto') && age >= 3400 && age <= 10500) {
893 shouldFire = true;
894 delay = 380;
895 }
896
897 if (shouldFire) {
898 ls.remove(CHAT_MONITOR_CONFIG.petTriggerKey);
899 const action = mode.startsWith('bear') ? performBearAction : performWOLFAction;
900 setTimeout(action, delay);
901 } else if (age > 13000) {
902 ls.remove(CHAT_MONITOR_CONFIG.petTriggerKey);
903 }
904 }, 1000); // было 350
905 }
906
907 function stopPolling() {
908 if (pollingInterval) {
909 clearInterval(pollingInterval);
910 pollingInterval = null;
911 }
912 }
913
914 function maybeStartPolling() {
915 const mode = ls.get(ACTIVE_MODE_KEY, null);
916 if (mode) startPolling();
917 else stopPolling();
918 }
919
920 maybeStartPolling();
921 });
922 }
923
924 if (currentPath === '/main_frame.php') {
925 handleMainFrame();
926 }
927
928 if (currentPath.startsWith('/cht_iframe.php')) {
929 handleChatIframe();
930 }
931})();