// Встраивается в страницы Outlook/Teams до загрузки их скриптов (add_init_script). // Ловит события двумя путями: // 1) уведомления самого клиента (window.Notification и ServiceWorkerRegistration.showNotification); // 2) изменения карточек в списке писем/чатов (MutationObserver), как страховка. // Всё отправляется в Python через window.__uniEmit(json). // Конфиг (селекторы) подставляется в window.__UNI_CFG перед этим скриптом. (() => { 'use strict'; if (window.top !== window) return; // только верхний фрейм if (window.__uniWatcher) return; const CFG = window.__UNI_CFG || {}; const SRC = (CFG.sources || []).find((s) => location.host.includes(s.host)); if (!SRC) return; // логин-страницы и прочее — пропускаем window.__uniWatcher = true; const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim(); const emit = (ev) => { ev.source = SRC.name; ev.url = location.href; ev.ts = Date.now(); try { if (typeof window.__uniEmit === 'function') window.__uniEmit(JSON.stringify(ev)); } catch (e) { /* binding недоступен — игнор */ } }; // ---------- 1. Уведомления ---------- class UniNotification extends EventTarget { constructor(title, opts) { super(); opts = opts || {}; this.title = String(title || ''); this.body = String(opts.body || ''); this.tag = String(opts.tag || ''); this.data = opts.data; this.icon = opts.icon || ''; this.onclick = this.onclose = this.onshow = this.onerror = null; emit({ kind: 'notification', title: norm(this.title), text: norm(this.body), tag: this.tag }); setTimeout(() => { try { if (this.onshow) this.onshow(new Event('show')); this.dispatchEvent(new Event('show')); } catch (e) {} }, 0); } close() { try { if (this.onclose) this.onclose(new Event('close')); } catch (e) {} } static get permission() { return 'granted'; } static requestPermission(cb) { if (typeof cb === 'function') cb('granted'); return Promise.resolve('granted'); } static get maxActions() { return 2; } } try { Object.defineProperty(window, 'Notification', { value: UniNotification, writable: true, configurable: true }); } catch (e) { window.Notification = UniNotification; } if (window.ServiceWorkerRegistration && ServiceWorkerRegistration.prototype.showNotification) { ServiceWorkerRegistration.prototype.showNotification = function (title, opts) { opts = opts || {}; emit({ kind: 'notification', title: norm(title), text: norm(opts.body), tag: String(opts.tag || '') }); return Promise.resolve(); }; } // Клиенты часто не показывают уведомления, если вкладка в фокусе. if (CFG.fakeBlur !== false) { try { Document.prototype.hasFocus = function () { return false; }; } catch (e) {} } // ---------- 2. Карточки в списках ---------- const seen = new Set(); function leafMatches(sel) { const nodes = Array.from(document.querySelectorAll(sel)); // оставляем самые «внутренние» совпадения: группа-папка с чатами внутри не нужна return nodes.filter((n) => !nodes.some((m) => m !== n && n.contains(m))); } function readRows() { if (!SRC.rowSelector || !document.body) return []; const nodes = leafMatches(SRC.rowSelector).slice(0, SRC.maxRows || 25); return nodes.map((n) => { let id = ''; for (const a of SRC.idAttrs || []) { const v = n.getAttribute(a); if (v) { id = v; break; } } const lines = String(n.innerText || n.textContent || '').split('\n').map(norm).filter(Boolean); return { id, lines: lines.slice(0, 10), aria: norm(n.getAttribute('aria-label')).slice(0, 600) }; }).filter((r) => r.lines.length || r.aria); } // Строки вида «12:45», «Yesterday», «5 мин» меняются сами по себе — не считаем их изменением. const TIME_RE = /^(\d{1,2}[:.]\d{2}(\s?[AP]M)?|\d{1,2}[./]\d{1,2}([./]\d{2,4})?|\d+\s?\S{0,5}|now|teraz|сейчас)$/i; const INLINE_TIME_RE = /\b\d{1,2}[:.]\d{2}(\s?[AP]M)?\b/gi; const sigOf = (r) => r.id + '|' + r.lines.filter((l) => !TIME_RE.test(l)).map((l) => l.replace(INLINE_TIME_RE, '')).join('|'); function scan(force) { let rows; try { rows = readRows(); } catch (e) { return 0; } let n = 0; for (const r of rows) { const sig = sigOf(r); if (!force && seen.has(sig)) continue; seen.add(sig); n++; emit({ kind: 'row', id: r.id, lines: r.lines, aria: r.aria }); } if (seen.size > 3000) seen.clear(); return n; } let timer = null; let lastRun = 0; const schedule = () => { if (timer) return; const wait = Math.max(1200, 2500 - (Date.now() - lastRun)); timer = setTimeout(() => { timer = null; lastRun = Date.now(); scan(false); }, wait); }; const start = () => { new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true, characterData: true }); schedule(); }; if (document.body) start(); else document.addEventListener('DOMContentLoaded', start, { once: true }); // Для Python: принудительный пересмотр и отладка window.__uniScan = (force) => scan(!!force); window.__uniDebug = () => ({ source: SRC.name, title: document.title, url: location.href, rows: readRows().slice(0, 5) }); })();