Чатмонитор UMXO КЛАН ЛС облегчённый

Чатмонитор + Медведь/Волк + ЛС/клан через 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})();