(function () { "use strict"; var ROOT_SELECTOR = "#espresso-activity-root, [data-espresso-activity-root]"; // Отчёт работает только на служебной странице чистки Espresso. var ALLOWED_PATH = "/pages/chistka"; var ROOT_TEXT_MARKER = "ESPRESSO_ACTIVITY_ROOT"; var ROOT_TEXT_MARKER_PATTERN = /(?:\[hideprofile\]\s*)?(?:\[size=0\]\s*)?ESPRESSO_ACTIVITY_ROOT\s*(?:\[\/size\])?/i; var ROOT_PLACEHOLDER_HINT = "espresso-activity-root"; var ROOT_PLACEHOLDER_SELECTORS = [ ".post-content", ".postmsg", ".post-message", ".post-body", ".entry-content", ".box-post", ".html-content", ".html-post-box" ]; var ROOT_PLACEHOLDER_HTML_PATTERN = /(?:\[html\]\s*)?<\s*div\b[\s\S]*?(?:id\s*=\s*(?:"|"|')espresso-activity-root(?:"|"|')|data-espresso-activity-root\b)[\s\S]*?<\s*\/\s*div\s*>\s*(?:\s*)?(?:\[\/html\])?/i; var ROOT_PLACEHOLDER_TEXT_PATTERN = /(?:\[html\]\s*)?<\s*div\b[\s\S]*?(?:id\s*=\s*(?:"|')espresso-activity-root(?:"|')|data-espresso-activity-root\b)[\s\S]*?<\s*\/\s*div\s*>\s*(?:\[\/html\])?/i; var ALLOWED_HOST = "myespresso.rusff.me"; var STYLE_ID = "mantra-espresso-activity-topic-style"; var STYLE_MARKER = "mantra-espresso-activity-topic-mounted"; var SORTABLE_KEYS = { cleanupRisk: true, lastGamePostAt: true, lastForumPostAt: true, recentCount30d: true, snsCount: true }; var FRAME_TIMEOUT_MS = 20000; var AUTO_REFRESH_STALE_MS = 15 * 60 * 1000; var LOCK_TIMEOUT_MS = 20 * 60 * 1000; var REPORT_WINDOW_DAYS = 30; var REPORT_WINDOW_MS = REPORT_WINDOW_DAYS * 24 * 60 * 60 * 1000; // Сообщение хотя бы раз в 14 дней. var APPEARANCE_WINDOW_DAYS = 14; var APPEARANCE_WINDOW_MS = APPEARANCE_WINDOW_DAYS * 24 * 60 * 60 * 1000; // Игровая активность: один не-первый пост за два месяца. var GAME_ACTIVITY_WINDOW_DAYS = 60; var GAME_ACTIVITY_WINDOW_MS = GAME_ACTIVITY_WINDOW_DAYS * 24 * 60 * 60 * 1000; // Альтернатива игре: 50 любых SNS-сообщений за месяц. var SNS_WINDOW_DAYS = 30; var SNS_WINDOW_MS = SNS_WINDOW_DAYS * 24 * 60 * 60 * 1000; var CLEANUP_GAME_POSTS_REQUIRED = 1; var CLEANUP_SNS_REQUIRED = 50; var ADMIN_GROUP_IDS = [1, 2]; var ANKETA_USER_GROUP_IDS = [4, 6]; var ANKETA_WAIT_DAYS = 6; var NEW_ANKETA_FORUM_IDS = [9]; var DONE_ANKETA_FORUM_IDS = [18, 19]; var ANKETA_FORUM_IDS = NEW_ANKETA_FORUM_IDS.concat(DONE_ANKETA_FORUM_IDS); var GAME_FORUM_IDS = [14, 15]; var SNS_FORUM_IDS = [20]; var SNS_HTML_DETECT_FORUM_IDS = []; var SNS_POST_SELECTOR = ""; var IGNORE_TOPIC_STARTERS_IN_GAME_FORUMS = true; var EXCLUDED_USER_IDS = [36,123,192,305,318,373,64,160,234,254,326,347,469,445,429,376,407,488,323,141,391,441,44,427,440,80,428]; var API = { pageLimit: 200, maxSkip: 2000, topicFallbackBatch: 100, topicListPageLimit: 100 }; var ROSTER = { userListPath: "/userlist.php", rowSelector: ".container tbody tr", userLinkSelector: ".usersname a[href*='profile.php?id=']" }; var REGISTRY = { pageId: "espresso_activity_report_registry", editPath: "/admin_pages.php?edit_page=", formSelector: "form#editpage, form[action*='admin_pages.php?edit_page='], form.adcontainer", contentSelector: "textarea[name='content'], #page-content", saveSelector: "input[name='save'], button[name='save'], input[type='submit'], button[type='submit']", softLimit: 58000, startMarker: "", endMarker: "" }; var root = null; var rootObserver = null; var rootEventsBound = false; var autoRefreshTimer = 0; var state = { status: "loading", error: "", message: "", topicId: 0, userId: 0, isAdmin: false, registry: createEmptyRegistry(), sort: { key: "cleanupRisk", direction: "desc" }, sync: { running: false, done: 0, total: 0, error: "", message: "", snsSummary: "" }, anketa: { running: false, error: "", users: [], checkedAt: 0, open: false } }; if ( (!ALLOWED_HOST || String(location.hostname || "").toLowerCase() === ALLOWED_HOST) && String(location.pathname || "").replace(/\/+$/, "") === ALLOWED_PATH ) { bootWhenRootReady(); } function bootWhenRootReady() { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", mountWhenRootAvailable); return; } mountWhenRootAvailable(); } function mountWhenRootAvailable() { root = document.querySelector(ROOT_SELECTOR) || createRootFromPlaceholder(); if (root) { if (root.getAttribute("data-" + STYLE_MARKER) === "1") { return; } root.setAttribute("data-" + STYLE_MARKER, "1"); injectStyles(); bindRootEvents(); bootstrap(); return; } if (!document.body || rootObserver || !window.MutationObserver) { return; } rootObserver = new MutationObserver(function () { if (!document.querySelector(ROOT_SELECTOR) && !createRootFromPlaceholder()) { return; } rootObserver.disconnect(); rootObserver = null; mountWhenRootAvailable(); }); rootObserver.observe(document.body, { childList: true, subtree: true }); } function createRootFromPlaceholder() { var host = findRootPlaceholderHost() || document.querySelector("#pun-main"); var created; if (!host || !document.createElement || document.querySelector(ROOT_SELECTOR)) { return null; } created = document.createElement("div"); created.id = "espresso-activity-root"; created.setAttribute("data-espresso-activity-root", "1"); replaceRootPlaceholder(host, created); return created; } function findRootPlaceholderHost() { var nodes = []; var index; var found; if (!document.body || document.body.querySelector(ROOT_SELECTOR)) { return null; } ROOT_PLACEHOLDER_SELECTORS.forEach(function (selector) { nodes = nodes.concat(Array.prototype.slice.call(document.querySelectorAll(selector))); }); nodes = dedupeRootNodes(nodes); for (index = 0; index < nodes.length; index += 1) { found = findDeepestRootPlaceholder(nodes[index]); if (found) { return found; } } return null; } function findDeepestRootPlaceholder(node) { var children; var index; var found; if (!node || node.nodeType !== 1 || !nodeContainsRootPlaceholder(node)) { return null; } children = Array.prototype.slice.call(node.children || []); for (index = 0; index < children.length; index += 1) { found = findDeepestRootPlaceholder(children[index]); if (found) { return found; } } return node; } function nodeContainsRootPlaceholder(node) { var html = node.innerHTML || ""; var textValue = node.textContent || ""; var source = (html + " " + textValue).toLowerCase(); return ( ROOT_TEXT_MARKER_PATTERN.test(html) || ROOT_TEXT_MARKER_PATTERN.test(textValue) || ROOT_PLACEHOLDER_TEXT_PATTERN.test(html) || ROOT_PLACEHOLDER_TEXT_PATTERN.test(textValue) || ROOT_PLACEHOLDER_HTML_PATTERN.test(html) || (source.indexOf(ROOT_PLACEHOLDER_HINT) !== -1 && /(?:<|<)\s*div\b/i.test(source)) ); } function replaceRootPlaceholder(host, rootNode) { var html = host.innerHTML || ""; var textValue = host.textContent || ""; var nextHtml = html.replace(ROOT_TEXT_MARKER_PATTERN, "").replace(ROOT_PLACEHOLDER_TEXT_PATTERN, "").replace(ROOT_PLACEHOLDER_HTML_PATTERN, ""); if (nextHtml !== html) { host.innerHTML = nextHtml; host.appendChild(rootNode); return; } if (ROOT_TEXT_MARKER_PATTERN.test(textValue) || ROOT_PLACEHOLDER_TEXT_PATTERN.test(textValue)) { host.textContent = ""; } host.appendChild(rootNode); } function dedupeRootNodes(nodes) { var seen = []; return nodes.filter(function (node) { if (!node || seen.indexOf(node) !== -1) { return false; } seen.push(node); return true; }); } function injectStyles() { var style; if (document.getElementById(STYLE_ID)) return; style = document.createElement("style"); style.id = STYLE_ID; style.textContent = [ "#espresso-activity-root .espresso-activity{box-sizing:border-box;font:12px/1.45 var(--font,Arial,sans-serif);color:var(--text,#454545)}", "#espresso-activity-root .espresso-activity *,#espresso-activity-root .espresso-activity *:before,#espresso-activity-root .espresso-activity *:after{box-sizing:border-box}", "#espresso-activity-root .espresso-activity__head,#espresso-activity-root .espresso-activity__actions,#espresso-activity-root .espresso-activity__meta,#espresso-activity-root .espresso-activity__footer,#espresso-activity-root .espresso-activity-modal__head,#espresso-activity-root .espresso-activity-modal__actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}", "#espresso-activity-root .espresso-activity__head,#espresso-activity-root .espresso-activity-modal__head{justify-content:space-between;margin-bottom:14px}", "#espresso-activity-root .espresso-activity__title,#espresso-activity-root .espresso-activity-modal__title{margin:0;color:var(--link1,#c06e74);font:500 12px/1.2 var(--font,Arial,sans-serif);text-transform:lowercase}", "#espresso-activity-root .espresso-activity__chip,#espresso-activity-root .espresso-activity__badge,#espresso-activity-root .espresso-activity__notice,#espresso-activity-root .espresso-activity__progress{padding:8px;border:1px solid var(--border,#dfdfdf);border-radius:12px;background:var(--container,#f4f4f4)}", "#espresso-activity-root .espresso-activity__user,#espresso-activity-root .espresso-activity__cell{display:flex;flex-direction:column;align-items:flex-start;gap:6px;min-width:150px}", "#espresso-activity-root .espresso-activity__badges{display:flex;flex-direction:column;align-items:flex-start;gap:5px;width:100%}", "#espresso-activity-root .espresso-activity__badge{display:inline-flex;width:auto;max-width:100%;line-height:1.15;white-space:normal}", "#espresso-activity-root .espresso-activity__button,#espresso-activity-root .espresso-activity-modal__button,#espresso-activity-root .espresso-activity-modal__close{padding:7px 14px;border:0;border-radius:20px;background:var(--button,#c06e74);color:var(--button-text,#fff);font:400 12px/1 var(--font,Arial,sans-serif);cursor:pointer}", "#espresso-activity-root .espresso-activity__button:hover,#espresso-activity-root .espresso-activity-modal__button:hover{background:var(--hover,#a95860);color:var(--hover-text,#fff)}", "#espresso-activity-root .espresso-activity__button[disabled]{opacity:.55}", "#espresso-activity-root .espresso-activity__anketa-list[hidden]{display:none!important}", "#espresso-activity-root .espresso-activity__table-wrap,#espresso-activity-root .espresso-activity-modal__table-wrap{overflow:auto;border:1px solid var(--border,#dfdfdf);border-radius:12px}", "#espresso-activity-root .espresso-activity__table{width:100%;min-width:860px;border-collapse:collapse}", "#espresso-activity-root .espresso-activity__table th,#espresso-activity-root .espresso-activity__table td{padding:10px;border-bottom:1px solid var(--border,#dfdfdf);vertical-align:top;text-align:left}", "#espresso-activity-root .espresso-activity__table th{color:var(--link1,#c06e74)}", "#espresso-activity-root .espresso-activity__header-row>td{padding:12px 14px;vertical-align:middle;background:var(--container,#f4f4f4);color:var(--text,#454545);font:500 12px/1.1 var(--font,Arial,sans-serif)}", "#espresso-activity-root .espresso-activity__header-row>td:first-child{min-width:180px;color:var(--link1,#c06e74)}", "#espresso-activity-root .espresso-activity__sort{display:flex!important;align-items:center;justify-content:center;gap:6px;width:100%;min-height:32px;margin:0!important;padding:0!important;border:0!important;border-radius:0!important;background:transparent!important;color:var(--link1,#c06e74)!important;font:500 12px/1.1 var(--font,Arial,sans-serif)!important;text-align:center;text-transform:none!important;box-shadow:none!important;cursor:pointer}", "#espresso-activity-root .espresso-activity__sort:hover,#espresso-activity-root .espresso-activity__sort:focus{color:var(--hover,#a95860)!important;outline:0}", "#espresso-activity-root .espresso-activity__th-title{display:block;max-width:150px}", "#espresso-activity-root .espresso-activity__sort-indicator{display:inline-block;flex:0 0 auto;font-size:12px;line-height:1}", "#espresso-activity-root .espresso-activity__user-link,#espresso-activity-root .espresso-activity__topic-link{color:var(--link1,#c06e74);text-decoration:none}", "#espresso-activity-root .espresso-activity-modal{position:fixed;inset:0;z-index:999999;display:flex;align-items:center;justify-content:center;padding:18px;background:rgba(0,0,0,.45)}", "#espresso-activity-root .espresso-activity-modal__dialog{width:min(760px,calc(100vw - 32px));max-height:calc(100vh - 32px);overflow:auto;padding:18px;border-radius:12px;background:var(--container,#f4f4f4);color:var(--text,#454545)}", "#espresso-activity-root .espresso-activity-modal__preview{max-height:520px;overflow:auto;padding:10px;border:1px solid var(--border,#dfdfdf)}", "#espresso-activity-root .espresso-activity-modal__html{width:100%;min-height:160px;padding:10px;border:1px solid var(--border,#dfdfdf)}" ].join(""); document.head.appendChild(style); } function bindRootEvents() { if (rootEventsBound || !root) { return; } rootEventsBound = true; root.addEventListener("click", function (event) { var origin = event.target && event.target.nodeType === 1 ? event.target : event.target && event.target.parentElement ? event.target.parentElement : null; var button = origin ? origin.closest("[data-activity-action]") : null; var action; if (!button) { return; } action = button.getAttribute("data-activity-action"); if (action === "refresh-now") { refreshRegistry({ mode: "refresh" }); return; } if (action === "rebuild-now") { refreshRegistry({ mode: "rebuild" }); return; } if (action === "save-cleanup-list") { showCleanupConfirmModal(); return; } if (action === "show-previous-report") { showPreviousReportModal(); return; } if (action === "show-appearance-list") { showAppearanceRiskModal(); return; } if (action === "check-no-anketa") { checkNoAnketa(); return; } if (action === "toggle-anketa") { state.anketa.open = !state.anketa.open; render(); return; } if (action === "toggle-sort") { toggleSort(button.getAttribute("data-activity-sort-key")); } }); } async function bootstrap() { state.topicId = getTopicId(); state.userId = getUserId(); state.isAdmin = isCurrentUserAdmin(); state.status = "loading"; state.error = ""; state.message = "\u0417\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c \u043a\u044d\u0448 \u043e\u0442\u0447\u0435\u0442\u0430..."; render(); if (!state.isAdmin) { state.status = "restricted"; state.message = "\u041e\u0442\u0447\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430\u043c \u0444\u043e\u0440\u0443\u043c\u0430."; render(); return; } try { state.registry = await loadRegistryFromAdminPage(); state.status = "ready"; state.message = ""; render(); if (shouldAutoRefresh(state.registry)) { scheduleAutoRefresh(); } } catch (error) { state.status = "error"; state.error = normalizeWhitespace(error && error.message) || "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0440\u0435\u0435\u0441\u0442\u0440 \u043e\u0442\u0447\u0435\u0442\u0430."; render(); } } function scheduleAutoRefresh() { window.clearTimeout(autoRefreshTimer); autoRefreshTimer = window.setTimeout(function () { refreshRegistry({ mode: "refresh" }); }, 80); } function shouldAutoRefresh(registry) { var lastSuccess = toInteger(registry && registry.lastSuccessAt); var lastAttempt = toInteger(registry && registry.lastAttemptAt); var hasRows = !!getDisplayOrder(registry).length; var now = Date.now(); if (!hasRows) { return true; } if (!lastSuccess) { return true; } if (registry.lastError && now - lastAttempt > 60 * 1000) { return true; } return now - lastSuccess >= AUTO_REFRESH_STALE_MS; } async function refreshRegistry(options) { var settings = options || {}; var lockToken = ""; var workingRegistry = null; var roster; var now; var ids; var index; var userId; var gameCutoffMs; var snsCutoffMs; var topicMetaCache = {}; var snsPostCache = {}; var snsAggregate = { byUser: {}, warning: "" }; if (!state.isAdmin || state.sync.running) { return; } state.sync.running = true; state.sync.done = 0; state.sync.total = 0; state.sync.error = ""; state.sync.message = settings.mode === "rebuild" ? "\u041f\u043e\u0434\u0433\u043e\u0442\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u043f\u043e\u043b\u043d\u0443\u044e \u043f\u0435\u0440\u0435\u0441\u0431\u043e\u0440\u043a\u0443..." : "\u041f\u043e\u0434\u0433\u043e\u0442\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0444\u043e\u043d\u043e\u0432\u043e\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435..."; state.sync.snsSummary = ""; render(); try { workingRegistry = await acquireRegistryLock(); lockToken = workingRegistry.lock && workingRegistry.lock.token ? String(workingRegistry.lock.token) : ""; roster = await loadRoster(); now = Date.now(); workingRegistry.periodStartedAt = getReportPeriodStart(workingRegistry, now); gameCutoffMs = now - GAME_ACTIVITY_WINDOW_MS; snsCutoffMs = now - SNS_WINDOW_MS; mergeRosterIntoRegistry(workingRegistry, roster, now); ids = workingRegistry.order.filter(function (value) { return EXCLUDED_USER_IDS.indexOf(toInteger(value)) === -1; }); state.sync.message = "\u0418\u0449\u0435\u043c \u00ab\u0441\u043d\u0441\u00bb \u0432 \u0442\u0435\u043c\u0430\u0445 \u0444\u043e\u0440\u0443\u043c\u043e\u0432 " + SNS_FORUM_IDS.join(", ") + "..."; state.sync.done = 0; state.sync.total = 0; render(); snsAggregate = await buildSnsAggregate(now, snsPostCache, snsCutoffMs); state.sync.snsSummary = formatSnsSummary(snsAggregate); state.sync.error = snsAggregate.warning || ""; state.sync.total = ids.length; state.sync.message = settings.mode === "rebuild" ? "\u041f\u0435\u0440\u0435\u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u043c \u0432\u0441\u0435 \u0441\u0442\u0440\u043e\u043a\u0438..." : "\u0421\u0432\u0435\u0440\u044f\u0435\u043c \u043a\u044d\u0448 \u0441 \u0444\u043e\u0440\u0443\u043c\u043e\u043c..."; render(); for (index = 0; index < ids.length; index += 1) { userId = toInteger(ids[index]); workingRegistry.users[userId] = await syncUserActivity(workingRegistry.users[userId], now, gameCutoffMs, topicMetaCache, snsAggregate, snsPostCache); state.sync.done = index + 1; state.registry = cloneJson(workingRegistry); if (state.sync.done === 1 || state.sync.done === state.sync.total || state.sync.done % 5 === 0) { render(); } } workingRegistry.updatedAt = now; workingRegistry.lastAttemptAt = now; workingRegistry.lastSuccessAt = now; workingRegistry.lastError = ""; workingRegistry.lock = null; await saveRegistryToAdminPage(workingRegistry); state.registry = workingRegistry; state.status = "ready"; state.message = ""; state.sync.message = settings.mode === "rebuild" ? "\u041f\u043e\u043b\u043d\u0430\u044f \u043f\u0435\u0440\u0435\u0441\u0431\u043e\u0440\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430." : "\u0424\u043e\u043d\u043e\u0432\u043e\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."; if (state.sync.snsSummary) { state.sync.message += " " + state.sync.snsSummary; } render(); } catch (error) { state.sync.error = normalizeWhitespace(error && error.message) || "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043e\u0442\u0447\u0435\u0442."; state.status = "ready"; if (lockToken) { try { await clearRegistryLockIfOwned(lockToken); } catch (clearError) { // Best effort lock cleanup. } } render(); } finally { state.sync.running = false; render(); } } async function saveCleanupReport() { var registry; var now = Date.now(); var snapshot; if (!state.isAdmin || state.sync.running) { return; } state.sync.running = true; state.sync.error = ""; state.sync.message = "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u043c \u043e\u0442\u0447\u0435\u0442 \u0438 \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u0441\u043f\u0438\u0441\u043e\u043a \u0447\u0438\u0441\u0442\u043a\u0438..."; state.sync.done = 0; state.sync.total = 0; render(); try { registry = await loadRegistryFromAdminPage(); registry.periodStartedAt = getReportPeriodStart(registry, now); snapshot = buildPeriodSnapshot(registry, now); registry.reportHistory = [snapshot].concat(normalizeReportHistory(registry.reportHistory, registry.previousReport)).slice(0, 3); registry.previousReport = snapshot; registry.periodStartedAt = now; registry.updatedAt = now; registry.lastAttemptAt = now; registry.lastSuccessAt = now; registry.lastError = ""; resetPeriodMetrics(registry); await saveRegistryToAdminPage(registry); state.registry = registry; state.status = "ready"; state.sync.message = "\u041e\u0442\u0447\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d. \u041d\u043e\u0432\u044b\u0439 \u043f\u0435\u0440\u0438\u043e\u0434 \u043d\u0430\u0447\u0430\u0442."; showCleanupReportModal(snapshot, getCleanupListTitle(snapshot)); render(); } catch (error) { state.sync.error = normalizeWhitespace(error && error.message) || "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043e\u0442\u0447\u0435\u0442."; render(); } finally { state.sync.running = false; render(); } } function showPreviousReportModal() { var registry = normalizeRegistry(state.registry); var history = normalizeReportHistory(registry.reportHistory, registry.previousReport); showReportHistoryModal(history); } function showAppearanceRiskModal() { var now = Date.now(); var registry = normalizeRegistry(state.registry); var appearanceUsers = getAppearanceRiskUsers(registry, now); showCleanupReportModal(normalizePeriodSnapshot({ createdAt: now, periodStartedAt: now - APPEARANCE_WINDOW_MS, periodEndedAt: now, rowCount: getDisplayOrder(registry).length, cleanupCount: 0, appearanceCount: appearanceUsers.length, cleanupHtml: buildAppearanceHtml(appearanceUsers), cleanupUsers: [], appearanceUsers: appearanceUsers }), "\u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0440\u0430\u0437 \u0432 7 \u0434\u043d\u0435\u0439"); } function getCleanupListTitle(report) { return "\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u043b\u044f \u0447\u0438\u0441\u0442\u043a\u0438 \u0437\u0430 \u043f\u0435\u0440\u0438\u043e\u0434 " + getReportPeriodLabel(report); } async function acquireRegistryLock() { var registry = await loadRegistryFromAdminPage(); var now = Date.now(); var activeLock = registry.lock && registry.lock.token && now - toInteger(registry.lock.acquiredAt) < LOCK_TIMEOUT_MS; if (activeLock && toInteger(registry.lock.userId) !== state.userId) { throw new Error("\u0420\u0435\u0435\u0441\u0442\u0440 \u0443\u0436\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0434\u0440\u0443\u0433\u043e\u0439 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440. \u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0438\u043d\u0443\u0442 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430."); } registry.lock = { token: createToken(), userId: state.userId, topicId: state.topicId, acquiredAt: now }; registry.lastAttemptAt = now; await saveRegistryToAdminPage(registry); return registry; } async function clearRegistryLockIfOwned(token) { var registry; if (!token) { return; } registry = await loadRegistryFromAdminPage(); if (registry.lock && String(registry.lock.token || "") === String(token)) { registry.lock = null; await saveRegistryToAdminPage(registry); } } async function syncUserActivity(record, now, cutoffMs, topicMetaCache, snsAggregate, snsPostCache) { var next = normalizeUserRecord(record, record.userId); var report = await fetchUserActivityReport(next.userId, cutoffMs, topicMetaCache, snsPostCache || {}); var lastForumPost = report.latestForumPost; var lastGamePost = report.latestGamePost; var snsEntry = snsAggregate && snsAggregate.byUser ? snsAggregate.byUser[next.userId] : null; next.status = next.status === "missing" ? "missing" : "active"; next.lastSyncAt = now; next.syncStatus = report.partial ? "partial" : "ok"; next.lastError = report.warning || ""; next.recentCount30d = report.recentCount30d; next.recentCount30dExact = report.recentCount30dExact; next.lastForumPostAt = lastForumPost ? lastForumPost.postedAt : 0; next.lastForumPostId = lastForumPost ? lastForumPost.id : 0; next.lastForumTopicId = lastForumPost ? lastForumPost.topicId : 0; next.lastForumTopicSubject = lastForumPost ? getTopicLabel(lastForumPost.topicId, lastForumPost.subject) : ""; next.lastGamePostAt = lastGamePost ? lastGamePost.postedAt : 0; next.lastGamePostId = lastGamePost ? lastGamePost.id : 0; next.lastGameTopicId = lastGamePost ? lastGamePost.topicId : 0; next.lastGameTopicSubject = lastGamePost ? getTopicLabel(lastGamePost.topicId, lastGamePost.subject) : ""; next.snsCount = snsEntry ? Math.max(0, toInteger(snsEntry.count)) : 0; next.snsTopicIds = snsEntry && Array.isArray(snsEntry.topicIds) ? snsEntry.topicIds.slice() : []; return next; } async function checkNoAnketa() { var users; var anketaNames; var cutoffMs; if (state.anketa.running) { return; } state.anketa.running = true; state.anketa.error = ""; state.sync.message = "Проверяем анкеты..."; render(); try { users = await fetchUsersByGroups(ANKETA_USER_GROUP_IDS); anketaNames = await fetchAnketaUsernames(); cutoffMs = Date.now() - ANKETA_WAIT_DAYS * 24 * 60 * 60 * 1000; state.anketa.users = users.filter(function (user) { return ( user.userId > 0 && EXCLUDED_USER_IDS.indexOf(user.userId) === -1 && user.registeredAt > 0 && user.registeredAt <= cutoffMs && !anketaNames[normalizeUsernameKey(user.username)] ); }).sort(function (left, right) { return normalizeWhitespace(left.username).localeCompare(normalizeWhitespace(right.username), "ru"); }); state.anketa.checkedAt = Date.now(); state.anketa.open = true; } catch (error) { state.anketa.error = normalizeWhitespace(error && error.message) || "Не удалось проверить анкеты."; } finally { state.anketa.running = false; state.sync.message = ""; render(); } } async function fetchUsersByGroups(groupIds) { var payload = await fetchForumJson("users.get", { group_id: groupIds.join(","), limit: 500 }); var source = payload && payload.response && Array.isArray(payload.response.users) ? payload.response.users : extractResponseItems(payload); return source.map(function (item) { return { userId: toInteger(item && (item.user_id || item.id)), username: normalizeWhitespace(item && (item.username || item.name)), registeredAt: normalizeTimestamp(item && (item.registered || item.registered_at || item.reg_date)) }; }).filter(function (item) { return item.userId > 0 && item.username; }); } async function fetchAnketaUsernames() { var result = {}; var forumIndex; var skip; var payload; var items; var index; var username; for (forumIndex = 0; forumIndex < ANKETA_FORUM_IDS.length; forumIndex += 1) { skip = 0; while (true) { payload = await fetchForumJson("topic.get", { forum_id: ANKETA_FORUM_IDS[forumIndex], limit: 100, skip: skip, fields: "id,username,user_name,author" }); items = extractResponseItems(payload); if (!items.length) { break; } for (index = 0; index < items.length; index += 1) { username = normalizeUsernameKey(items[index] && (items[index].username || items[index].user_name || items[index].author)); if (username) { result[username] = true; } } if (items.length < 100) { break; } skip += items.length; if (skip >= API.maxSkip) { break; } } } return result; } function normalizeUsernameKey(value) { return normalizeWhitespace(value).toLocaleLowerCase(); } async function fetchUserActivityReport(userId, cutoffMs, topicMetaCache, snsPostCache) { var latestForumPost = null; var latestGamePost = null; var recentPosts = []; var recentCount30dExact = true; var skip = 0; var stop = false; var partial = false; var posts; var normalizedPosts; var index; var post; var unresolvedPosts; while (!stop) { posts = await fetchUserPostsPage(userId, skip); normalizedPosts = posts.map(normalizePostItem).filter(function (item) { return item.id > 0; }); if (!normalizedPosts.length) { break; } if (!latestForumPost) { latestForumPost = normalizedPosts[0]; } for (index = 0; index < normalizedPosts.length; index += 1) { post = normalizedPosts[index]; if (post.postedAt >= cutoffMs) { recentPosts.push(post); } else { stop = true; break; } } if (normalizedPosts.length < API.pageLimit) { break; } skip += normalizedPosts.length; if (skip >= API.maxSkip) { partial = true; recentCount30dExact = false; break; } } unresolvedPosts = recentPosts.filter(function (item) { return item.topicId > 0 && item.forumId <= 0; }); if (unresolvedPosts.length) { await fillTopicMetaForPosts(unresolvedPosts, topicMetaCache); } if (IGNORE_TOPIC_STARTERS_IN_GAME_FORUMS) { await fillTopicMetaForPosts(recentPosts.filter(function (item) { return item.topicId > 0 && GAME_FORUM_IDS.indexOf(item.forumId) !== -1 && item.topicFirstPostId <= 0; }), topicMetaCache); } for (index = 0; index < recentPosts.length; index += 1) { post = recentPosts[index]; if (await isEligibleGamePost(post, snsPostCache || {})) { latestGamePost = post; break; } } return { latestForumPost: latestForumPost, latestGamePost: latestGamePost, recentCount30d: recentPosts.length, recentCount30dExact: recentCount30dExact, partial: partial, warning: buildUserWarning(partial, recentCount30dExact, latestGamePost) }; } async function buildSnsAggregate(now, snsPostCache, cutoffMs) { var topics = await fetchRecentSnsTopics(cutoffMs); var byUser = {}; var failedReasons = {}; var forumStats = {}; var postsChecked = 0; var matchesFound = 0; var topicIndex; var topicForumId; var topicStats; var posts; var topicScan; var postIndex; var post; var bucket; if (!topics.length) { accumulateReasonCount(failedReasons, "no-topics"); } for (topicIndex = 0; topicIndex < topics.length; topicIndex += 1) { topicForumId = toInteger(topics[topicIndex] && topics[topicIndex].forumId); topicStats = ensureForumStats(forumStats, topicForumId); topicStats.topics += 1; if (topicIndex === 0 || topicIndex === topics.length - 1 || topicIndex % 5 === 0) { state.sync.message = "\u0418\u0449\u0435\u043c \u00ab\u0441\u043d\u0441\u00bb \u0432 \u0442\u0435\u043c\u0430\u0445: " + (topicIndex + 1) + "/" + topics.length; state.sync.done = topicIndex + 1; state.sync.total = topics.length; render(); } if (SNS_HTML_DETECT_FORUM_IDS.indexOf(topicForumId) !== -1) { topicScan = await fetchRecentSnsPostsByTopicPages(topics[topicIndex], cutoffMs, snsPostCache, failedReasons, topicStats); posts = topicScan.matches; postsChecked += topicScan.postsChecked; } else { try { posts = await fetchRecentPostsByTopic(topics[topicIndex], cutoffMs); } catch (error) { accumulateReasonCount(failedReasons, "topic-posts-fetch-failed"); continue; } } for (postIndex = 0; postIndex < posts.length; postIndex += 1) { post = posts[postIndex]; if (toInteger(post.forumId) === 8) { ensureForumStats(forumStats, post && post.forumId).posts += 1; postsChecked += 1; if (post.topicFirstPostId > 0 && post.id === post.topicFirstPostId) { ensureForumStats(forumStats, 8).firstPostsSkipped += 1; continue; } if (!post || post.userId <= 0) { accumulateReasonCount(failedReasons, "sns-user-not-found"); continue; } } else { if (!post || post.userId <= 0) { accumulateReasonCount(failedReasons, "sns-user-not-found"); continue; } } bucket = byUser[post.userId]; ensureForumStats(forumStats, post && post.forumId).matches += 1; matchesFound += 1; if (!bucket) { bucket = { count: 0, topicIds: [] }; byUser[post.userId] = bucket; } bucket.count += 1; if (bucket.topicIds.indexOf(post.topicId) === -1) { bucket.topicIds.push(post.topicId); } } } return { byUser: byUser, warning: buildSnsWarning(failedReasons), forumStats: forumStats, topicsChecked: topics.length, postsChecked: postsChecked, matchesFound: matchesFound }; } async function fetchRecentSnsTopics(cutoffMs) { var topics = []; var seen = {}; var forumIndex; var forumTopics; var topicIndex; var topic; for (forumIndex = 0; forumIndex < SNS_FORUM_IDS.length; forumIndex += 1) { forumTopics = await fetchRecentTopicsByForum(SNS_FORUM_IDS[forumIndex], cutoffMs); for (topicIndex = 0; topicIndex < forumTopics.length; topicIndex += 1) { topic = forumTopics[topicIndex]; if (!topic || topic.id <= 0 || seen[topic.id]) { continue; } seen[topic.id] = true; topics.push(topic); } } return topics; } async function fetchRecentTopicsByForum(forumId, cutoffMs) { var topics = []; var skip = 0; var stop = false; var payload; var items; var limit = API.topicListPageLimit; var index; var topic; while (!stop) { payload = await fetchForumJson("topic.get", { forum_id: forumId, limit: limit, skip: skip, fields: "id,forum_id,subject,last_post_date,last_post_id,first_post_id", sort_by: "last_post", sort_dir: "desc" }); items = extractResponseItems(payload).map(normalizeTopicItem); await fillMissingTopicFirstPostIds(items); if (!items.length) { break; } for (index = 0; index < items.length; index += 1) { topic = items[index]; if (topic.lastPostAt && topic.lastPostAt < cutoffMs) { stop = true; break; } topics.push(topic); } if (items.length < limit) { break; } skip += items.length; if (skip >= API.maxSkip) { break; } } return topics; } async function fetchRecentPostsByTopic(topic, cutoffMs) { var posts = []; var skip = 0; var stop = false; var payload; var items; var limit = API.pageLimit; var index; var post; var topicId = toInteger(topic && topic.id); var forumId = toInteger(topic && topic.forumId); var firstPostId = toInteger(topic && topic.firstPostId); while (!stop) { payload = await fetchForumJson("post.get", { topic_id: topicId, sort_by: "id", sort_dir: "desc", limit: limit, skip: skip, fields: "id,user_id,username,posted,topic_id,forum_id,subject" }); items = extractResponseItems(payload).map(normalizePostItem).filter(function (item) { return item.id > 0; }); if (!items.length) { break; } for (index = 0; index < items.length; index += 1) { post = items[index]; post.topicId = post.topicId || topicId; post.forumId = post.forumId || forumId; post.topicFirstPostId = post.topicFirstPostId || firstPostId; if (post.postedAt >= cutoffMs) { posts.push(post); } else { stop = true; break; } } if (items.length < limit) { break; } skip += items.length; if (skip >= API.maxSkip) { break; } } return posts; } async function fetchRecentSnsPostsByTopicPages(topic, cutoffMs, snsPostCache, failedReasons, stats) { var topicId = toInteger(topic && topic.id); var cacheKey = "sns-topic-pages:" + topicId + ":" + cutoffMs; var cached = snsPostCache[cacheKey]; var firstUrl; var firstPage; var firstDoc; var queue; var seenPageUrls = {}; var seenPostIds = {}; var matches = []; var postsSeen = 0; var pageIndex; var page; var doc; var discoveredUrls; var pageResult; if (cached) { return cached; } if (!topicId) { return { matches: [], postsChecked: 0 }; } firstUrl = buildTopicUrl(topicId); pushLimitedValue(stats.checkedTopicIds, topicId, 5); try { firstPage = await fetchPage(firstUrl); firstDoc = parseHtmlDocument(firstPage.html); } catch (error) { accumulateReasonCount(failedReasons, "topic-pages-fetch-failed"); return { matches: [], postsChecked: 0 }; } queue = [firstUrl].concat(collectTopicPageUrls(firstDoc, topicId)); queue = queue.filter(function (url, index, list) { return !!url && list.indexOf(url) === index; }).sort(compareTopicPageUrls); queue.forEach(function (url) { seenPageUrls[url] = true; }); for (pageIndex = 0; pageIndex < queue.length; pageIndex += 1) { if (pageIndex === 0) { page = firstPage; doc = firstDoc; } else { try { page = await fetchPage(queue[pageIndex]); doc = parseHtmlDocument(page.html); } catch (error) { accumulateReasonCount(failedReasons, "topic-pages-fetch-failed"); continue; } } stats.pages += 1; pageResult = collectSnsPostsFromTopicDocument(doc, topic, cutoffMs, seenPostIds, stats); postsSeen += pageResult.postsChecked; matches = matches.concat(pageResult.matches); discoveredUrls = collectTopicPageUrls(doc, topicId); discoveredUrls.forEach(function (url) { if (!url || seenPageUrls[url]) { return; } seenPageUrls[url] = true; queue.push(url); }); queue.sort(compareTopicPageUrls); } snsPostCache[cacheKey] = { matches: matches, postsChecked: postsSeen }; return snsPostCache[cacheKey]; } function collectSnsPostsFromTopicDocument(doc, topic, cutoffMs, seenPostIds, stats) { var matches = []; var postsChecked = 0; Array.prototype.slice.call(doc.querySelectorAll(".post[id^='p'], .post[data-posted]")).forEach(function (postNode) { var postId = extractPostIdFromNode(postNode); var postedAt = normalizeTimestamp(postNode && postNode.getAttribute ? postNode.getAttribute("data-posted") : ""); var detection; var userId; if (!postId || seenPostIds[postId]) { return; } seenPostIds[postId] = true; if (!postedAt || postedAt < cutoffMs) { return; } postsChecked += 1; stats.posts += 1; stats.htmlChecks += 1; pushLimitedValue(stats.checkedPostIds, postId, 5); if (toInteger(topic && topic.firstPostId) > 0 && postId === toInteger(topic && topic.firstPostId)) { stats.firstPostsSkipped += 1; return; } detection = detectSnsSignatureInPostNode(postNode); if (!detection.matched) { stats.selectorMissing += 1; return; } if (detection.mode === "raw-marker") { stats.rawMarkerMatches += 1; } userId = extractUserIdFromPostNode(postNode); matches.push({ id: postId, userId: userId, topicId: toInteger(topic && topic.id), forumId: toInteger(topic && topic.forumId), subject: normalizeWhitespace(topic && topic.subject), postedAt: postedAt, topicFirstPostId: toInteger(topic && topic.firstPostId) }); }); return { matches: matches, postsChecked: postsChecked }; } function detectSnsSignatureInPostNode(postNode) { var contentNode = getPostContentNode(postNode); var html; if (!postNode || postNode.nodeType !== 1) { return { matched: false, mode: "none" }; } if ( postNode.matches && postNode.matches(SNS_POST_SELECTOR) || !!postNode.querySelector(SNS_POST_SELECTOR) ) { return { matched: true, mode: "selector" }; } html = String((contentNode && contentNode.innerHTML) || postNode.innerHTML || ""); if (looksLikeInstachatMarkup(html)) { return { matched: true, mode: "raw-marker" }; } return { matched: false, mode: "none" }; } function getPostContentNode(postNode) { if (!postNode || !postNode.querySelector) { return null; } return postNode.querySelector(".post-content, [id$='-content'], .html-content, .html-post-box") || postNode; } function looksLikeInstachatMarkup(html) { var source = String(html || ""); if (!source) { return false; } if (/\babyssalchat-wrapper\b/i.test(source) || /\babyssalchat-(?:header|avatar-wrapper|center-info|name|status|body|footer|send-button)\b/i.test(source)) { return true; } if (/class\s*=\s*["'][^"']*\babyssalchat-wrapper\b/i.test(source)) { return true; } if (/<div[^>]+class\s*=\s*["'][^"']*\babyssalchat-wrapper\b/i.test(source)) { return true; } return false; } function collectTopicPageUrls(doc, topicId) { var urls = []; Array.prototype.slice.call(doc.querySelectorAll("a[href*='viewtopic.php?id=']")).forEach(function (link) { var normalized = normalizeTopicPageUrl(link && link.getAttribute ? link.getAttribute("href") : "", topicId); if (!normalized || urls.indexOf(normalized) !== -1) { return; } urls.push(normalized); }); return urls; } function normalizeTopicPageUrl(href, topicId) { var url; var normalized; var rawPage; try { url = new URL(String(href || ""), location.origin); } catch (error) { return ""; } if (!/\/viewtopic\.php$/i.test(url.pathname) || toInteger(url.searchParams.get("id")) !== toInteger(topicId)) { return ""; } normalized = new URL(location.origin + "/viewtopic.php"); normalized.searchParams.set("id", String(toInteger(topicId))); rawPage = url.searchParams.get("p") || url.searchParams.get("page") || url.searchParams.get("start") || ""; if (normalizeWhitespace(rawPage)) { normalized.searchParams.set("p", String(toInteger(rawPage) || rawPage)); } return normalized.toString(); } function compareTopicPageUrls(left, right) { return getTopicPageOrder(left) - getTopicPageOrder(right); } function getTopicPageOrder(url) { var parsed; try { parsed = new URL(String(url || ""), location.origin); return toInteger(parsed.searchParams.get("p") || parsed.searchParams.get("page") || parsed.searchParams.get("start") || 1) || 1; } catch (error) { return 1; } } function extractPostIdFromNode(node) { var value = node && node.getAttribute ? node.getAttribute("id") : ""; var match = String(value || "").match(/^p(\d+)$/i); return match ? toInteger(match[1]) : 0; } async function detectSnsPost(post, snsPostCache) { var cacheKey = String(post && post.id || ""); var urls; var urlIndex; var page; var doc; var result; if (!post || !post.id || !post.topicId) { return { ok: false, matched: false, reason: "invalid-post" }; } if (snsPostCache[cacheKey]) { return snsPostCache[cacheKey]; } urls = buildPostLookupUrls(post.topicId, post.id); for (urlIndex = 0; urlIndex < urls.length; urlIndex += 1) { try { page = await fetchPage(urls[urlIndex]); doc = parseHtmlDocument(page.html); result = detectSnsPostInDocument(doc, post.id); if (result.ok || result.reason !== "post-node-not-found") { snsPostCache[cacheKey] = result; return snsPostCache[cacheKey]; } } catch (error) { // Try the next fallback URL. } } snsPostCache[cacheKey] = { ok: false, matched: false, reason: "fetch-failed" }; return snsPostCache[cacheKey]; } function detectSnsPostInDocument(doc, postId) { var contentNode = doc ? doc.querySelector('#p' + postId + '-content') : null; var postNode = contentNode ? getPostContainerNode(contentNode) : findPostNode(doc, postId); var snsNodes; var index; var userId; var detection; if (postNode) { userId = extractUserIdFromPostNode(postNode); detection = detectSnsSignatureInPostNode(postNode); return { ok: true, matched: detection.matched, reason: detection.matched ? detection.mode : "selector-missing", userId: userId }; } snsNodes = Array.prototype.slice.call(doc.querySelectorAll(SNS_POST_SELECTOR)); if (!snsNodes.length) { return { ok: true, matched: false, reason: "selector-missing" }; } for (index = 0; index < snsNodes.length; index += 1) { if (nodeLooksRelatedToPost(snsNodes[index], postId)) { return { ok: true, matched: true, reason: "matched-by-hint", userId: extractUserIdFromPostNode(getPostContainerNode(snsNodes[index])) }; } } if (snsNodes.length === 1) { return { ok: true, matched: true, reason: "matched-single-node", userId: extractUserIdFromPostNode(getPostContainerNode(snsNodes[0])) }; } return { ok: false, matched: false, reason: "post-node-not-found" }; } function buildSnsWarning(failedReasons) { var parts = []; if (failedReasons["fetch-failed"]) { parts.push("\u043d\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u043b\u0438\u0441\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u043f\u043e\u0441\u0442\u043e\u0432: " + failedReasons["fetch-failed"]); } if (failedReasons["post-node-not-found"]) { parts.push("\u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u043f\u043e\u0441\u0442\u0430: " + failedReasons["post-node-not-found"]); } if (failedReasons["topic-posts-fetch-failed"]) { parts.push("\u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u043f\u043e\u0441\u0442\u044b \u0432 \u0442\u0435\u043c\u0430\u0445: " + failedReasons["topic-posts-fetch-failed"]); } if (failedReasons["topic-pages-fetch-failed"]) { parts.push("\u043d\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u043b\u0438\u0441\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0442\u0435\u043c: " + failedReasons["topic-pages-fetch-failed"]); } if (failedReasons["invalid-post"]) { parts.push("\u043d\u0435\u043f\u043e\u043b\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e \u043f\u043e\u0441\u0442\u0443: " + failedReasons["invalid-post"]); } if (failedReasons["sns-user-not-found"]) { parts.push("\u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u0430 \u0441\u043d\u0441-\u043f\u043e\u0441\u0442\u0430: " + failedReasons["sns-user-not-found"]); } if (failedReasons["unknown"]) { parts.push("\u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: " + failedReasons["unknown"]); } return parts.length ? "\u041a\u043e\u043b\u043e\u043d\u043a\u0430 \u00ab\u0441\u043d\u0441\u00bb: " + parts.join("; ") + "." : ""; } function accumulateReasonCount(map, key) { var name = normalizeWhitespace(key) || "unknown"; map[name] = (toInteger(map[name]) || 0) + 1; } function findPostNode(doc, postId) { var selectors = [ '#p' + postId + '-content', "#p" + postId, '[id="p' + postId + '"]', '[id="post-' + postId + '"]', '[data-post-id="' + postId + '"]', '[data-id="' + postId + '"]', '[data-post="' + postId + '"]', '[id*="p' + postId + '"]', 'a[name="p' + postId + '"]', 'a[id="p' + postId + '"]', 'a[href*="pid=' + postId + '"]', 'a[href*="#p' + postId + '"]' ]; var index; var node; if (!doc || !postId) { return null; } for (index = 0; index < selectors.length; index += 1) { node = doc.querySelector(selectors[index]); if (!node) { continue; } return getPostContainerNode(node); } return null; } function nodeLooksRelatedToPost(node, postId) { var current = node; var guard = 0; var postToken = "p" + postId; var pidToken = "pid=" + postId; while (current && current.nodeType === 1 && guard < 12) { if ( String(current.id || "").indexOf(postToken) !== -1 || String(current.getAttribute && current.getAttribute("name") || "").indexOf(postToken) !== -1 || String(current.getAttribute && current.getAttribute("data-post-id") || "") === String(postId) || String(current.getAttribute && current.getAttribute("data-id") || "") === String(postId) || String(current.getAttribute && current.getAttribute("data-post") || "") === String(postId) || String(current.getAttribute && current.getAttribute("href") || "").indexOf(pidToken) !== -1 || String(current.getAttribute && current.getAttribute("href") || "").indexOf("#" + postToken) !== -1 ) { return true; } current = current.parentElement; guard += 1; } return false; } function extractUserIdFromPostNode(node) { var current = node; var profileLink; var href; var guard = 0; while (current && current.nodeType === 1 && guard < 8) { if (toInteger(current.getAttribute && current.getAttribute("data-user-id"))) { return toInteger(current.getAttribute("data-user-id")); } current = current.parentElement; guard += 1; } profileLink = node && node.querySelector ? node.querySelector('.pl-email.profile a[href*="profile.php?id="], a[href*="profile.php?id="]') : null; href = profileLink && profileLink.getAttribute ? profileLink.getAttribute("href") : ""; return parseProfileIdFromHref(href); } function getPostContainerNode(node) { var current = node; var selectors = ".post, .postbody, .mainpost, article, li, tr, .post-box, .post-wrap"; while (current && current !== current.ownerDocument && current.nodeType === 1) { if (current.matches && current.matches(selectors)) { return current; } current = current.parentElement; } return node && node.nodeType === 1 ? node : null; } function formatSnsSummary(report) { var forumIds; var parts; if (!report || typeof report !== "object") { return ""; } parts = [ "\u0421\u041d\u0421: \u0442\u0435\u043c " + Math.max(0, toInteger(report.topicsChecked)) + ", \u043f\u043e\u0441\u0442\u043e\u0432 " + Math.max(0, toInteger(report.postsChecked)) + ", \u0441\u043e\u0432\u043f\u0430\u0434\u0435\u043d\u0438\u0439 " + Math.max(0, toInteger(report.matchesFound)) + "." ]; forumIds = Object.keys(report.forumStats || {}).map(toInteger).filter(Boolean).sort(function (left, right) { return left - right; }); forumIds.forEach(function (forumId) { var stats = report.forumStats[forumId]; parts.push( "\u0424\u043e\u0440\u0443\u043c " + forumId + ": \u0442\u0435\u043c " + Math.max(0, toInteger(stats.topics)) + ", \u0441\u0442\u0440\u0430\u043d\u0438\u0446 " + Math.max(0, toInteger(stats.pages)) + ", \u043f\u043e\u0441\u0442\u043e\u0432 " + Math.max(0, toInteger(stats.posts)) + ", \u0437\u0430\u0441\u0447\u0438\u0442\u0430\u043d\u043e " + Math.max(0, toInteger(stats.matches)) + (stats.htmlChecks ? ", HTML-\u043f\u0440\u043e\u0432\u0435\u0440\u043e\u043a " + Math.max(0, toInteger(stats.htmlChecks)) : "") + (stats.rawMarkerMatches ? ", raw-\u043c\u0430\u0440\u043a\u0435\u0440\u043e\u0432 " + Math.max(0, toInteger(stats.rawMarkerMatches)) : "") + (stats.selectorMissing ? ", \u0431\u0435\u0437 " + SNS_POST_SELECTOR + " " + Math.max(0, toInteger(stats.selectorMissing)) : "") + (stats.firstPostsSkipped ? ", \u043f\u0440\u043e\u043f\u0443\u0449\u0435\u043d\u043e \u043f\u0435\u0440\u0432\u044b\u0445 " + Math.max(0, toInteger(stats.firstPostsSkipped)) : "") + (stats.checkedTopicIds && stats.checkedTopicIds.length ? ", topic_id " + stats.checkedTopicIds.join(",") : "") + (stats.checkedPostIds && stats.checkedPostIds.length ? ", post_id " + stats.checkedPostIds.join(",") : "") + "." ); }); return parts.join(" "); } function ensureForumStats(map, forumId) { var id = toInteger(forumId) || 0; if (!map[id]) { map[id] = { topics: 0, pages: 0, posts: 0, htmlChecks: 0, rawMarkerMatches: 0, firstPostsSkipped: 0, matches: 0, selectorMissing: 0, checkedTopicIds: [], checkedPostIds: [] }; } return map[id]; } function pushLimitedValue(list, value, limit) { var numeric = toInteger(value); if (!Array.isArray(list) || !numeric || list.indexOf(numeric) !== -1 || list.length >= limit) { return; } list.push(numeric); } function buildUserWarning(partial, recentCount30dExact, latestGamePost) { if (!partial) { return ""; } if (!recentCount30dExact) { if (!latestGamePost) { return "API \u043e\u0431\u0440\u0435\u0437\u0430\u043b \u0432\u044b\u0431\u043e\u0440\u043a\u0443 \u043f\u043e \u043f\u043e\u0441\u0442\u0430\u043c. \u0423 \u0438\u0433\u0440\u043e\u043a\u0430 \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0437\u0430 30 \u0434\u043d\u0435\u0439, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0438\u0433\u0440\u043e\u0432\u043e\u0439 \u043f\u043e\u0441\u0442 \u043c\u043e\u0433 \u043e\u0441\u0442\u0430\u0442\u044c\u0441\u044f \u0437\u0430 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u043c\u0438 \u043e\u043a\u043d\u0430."; } return "API \u043e\u0431\u0440\u0435\u0437\u0430\u043b \u0432\u044b\u0431\u043e\u0440\u043a\u0443 \u043f\u043e \u043f\u043e\u0441\u0442\u0430\u043c. \u0421\u0447\u0435\u0442\u0447\u0438\u043a \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0437\u0430 30 \u0434\u043d\u0435\u0439 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u043c."; } return "\u0427\u0430\u0441\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e."; } async function fetchUserPostsPage(userId, skip) { var payload = await fetchForumJson("post.get", { user_id: userId, sort_by: "id", sort_dir: "desc", limit: API.pageLimit, skip: skip, fields: "id,user_id,username,posted,topic_id,forum_id,subject" }); return extractResponseItems(payload); } async function fillTopicMetaForPosts(posts, topicMetaCache) { var unresolvedIds = []; var topicMap; posts.forEach(function (post) { if (!post || post.topicId <= 0) { return; } if (topicMetaCache[post.topicId]) { post.forumId = topicMetaCache[post.topicId].forumId; post.subject = post.subject || topicMetaCache[post.topicId].subject; post.topicFirstPostId = post.topicFirstPostId || topicMetaCache[post.topicId].firstPostId; return; } if (unresolvedIds.indexOf(post.topicId) === -1) { unresolvedIds.push(post.topicId); } }); if (!unresolvedIds.length) { return; } topicMap = await fetchTopicMetaMap(unresolvedIds); Object.keys(topicMap).forEach(function (topicId) { topicMetaCache[topicId] = topicMap[topicId]; }); posts.forEach(function (post) { var meta = topicMetaCache[post.topicId]; if (!meta) { return; } post.forumId = post.forumId || meta.forumId; post.subject = post.subject || meta.subject; post.topicFirstPostId = post.topicFirstPostId || meta.firstPostId; }); } async function fetchTopicMetaMap(topicIds) { var map = {}; var queue = topicIds.slice(); var batch; var payload; var topics; var index; var topic; while (queue.length) { batch = queue.splice(0, API.topicFallbackBatch); try { payload = await fetchForumJson("topic.get", { topic_id: batch.join(","), fields: "id,forum_id,subject,first_post_id" }); topics = extractResponseItems(payload).map(normalizeTopicItem); await fillMissingTopicFirstPostIds(topics); if (!topics.length && batch.length > 1) { topics = await fetchTopicMetaMapOneByOne(batch); } } catch (error) { topics = await fetchTopicMetaMapOneByOne(batch); } for (index = 0; index < topics.length; index += 1) { topic = topics[index]; if (topic.id > 0) { map[topic.id] = topic; } } } return map; } async function fetchTopicMetaMapOneByOne(topicIds) { var topics = []; var index; var payload; var items; for (index = 0; index < topicIds.length; index += 1) { try { payload = await fetchForumJson("topic.get", { topic_id: topicIds[index], fields: "id,forum_id,subject,first_post_id" }); items = extractResponseItems(payload).map(normalizeTopicItem); await fillMissingTopicFirstPostIds(items); if (items.length) { topics.push(items[0]); } } catch (error) { // Ignore per-topic lookup failures here and keep the cache sparse. } } return topics; } async function fillMissingTopicFirstPostIds(topics) { var index; for (index = 0; index < topics.length; index += 1) { if (!topics[index] || topics[index].id <= 0 || topics[index].firstPostId > 0) { continue; } topics[index].firstPostId = await fetchTopicFirstPostId(topics[index].id); } } async function fetchTopicFirstPostId(topicId) { var payload; var posts; try { payload = await fetchForumJson("post.get", { topic_id: topicId, sort_by: "id", sort_dir: "asc", limit: 1, fields: "id" }); posts = extractResponseItems(payload).map(normalizePostItem); return posts.length ? posts[0].id : 0; } catch (error) { return 0; } } async function loadRoster() { try { return await loadRosterFromUserList(); } catch (userListError) { return loadRosterFromOrderedUsersApi(); } } async function loadRosterFromUserList() { var firstPage = await fetchPage(ROSTER.userListPath); var firstDoc = parseHtmlDocument(firstPage.html); var queue = collectUserListPageUrls(firstDoc).sort(compareUserListUrls); var seenPageUrls = {}; var seenUserIds = {}; var order = []; var byId = {}; var currentPage; var currentDoc; var discoveredUrls; var index; queue.forEach(function (url) { seenPageUrls[url] = true; }); appendRosterPage(firstDoc, seenUserIds, order, byId); for (index = 0; index < queue.length; index += 1) { currentPage = await fetchPage(queue[index]); currentDoc = parseHtmlDocument(currentPage.html); appendRosterPage(currentDoc, seenUserIds, order, byId); discoveredUrls = collectUserListPageUrls(currentDoc); discoveredUrls.forEach(function (url) { if (seenPageUrls[url]) { return; } seenPageUrls[url] = true; queue.push(url); }); queue.sort(compareUserListUrls); } if (!order.length) { throw new Error("\u0421\u043f\u0438\u0441\u043e\u043a \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432 \u043d\u0435 \u0432\u0435\u0440\u043d\u0443\u043b \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f."); } return { order: order, byId: byId }; } function appendRosterPage(doc, seenUserIds, order, byId) { var rows = Array.prototype.slice.call(doc.querySelectorAll(ROSTER.rowSelector)); var count = 0; rows.forEach(function (row) { var link = row.querySelector(ROSTER.userLinkSelector); var userId = parseProfileIdFromHref(link && link.getAttribute("href")); var username = normalizeWhitespace(link ? link.textContent : ""); var profileUrl; if (!userId || !username || seenUserIds[userId]) { return; } seenUserIds[userId] = true; profileUrl = toAbsoluteUrl(link && link.getAttribute("href")) || buildProfileUrl(userId); order.push(userId); byId[userId] = { userId: userId, username: username, profileUrl: profileUrl }; count += 1; }); return count; } function collectUserListPageUrls(doc) { var urls = []; Array.prototype.slice.call(doc.querySelectorAll("a[href*='userlist.php']")).forEach(function (link) { var normalized = normalizeUserListUrl(link.getAttribute("href")); var pageNumber; if (!normalized) { return; } pageNumber = getUserListPageNumber(normalized); if (pageNumber <= 1 || urls.indexOf(normalized) !== -1) { return; } urls.push(normalized); }); return urls; } function normalizeUserListUrl(href) { var url; try { url = new URL(String(href || ""), location.origin); } catch (error) { return ""; } if (!/\/userlist\.php$/i.test(url.pathname)) { return ""; } return url.toString(); } function getUserListPageNumber(url) { var parsed; try { parsed = new URL(String(url || ""), location.origin); return toInteger(parsed.searchParams.get("p") || parsed.searchParams.get("page") || 1) || 1; } catch (error) { return 1; } } function compareUserListUrls(left, right) { return getUserListPageNumber(left) - getUserListPageNumber(right); } async function loadRosterFromOrderedUsersApi() { var skip = 0; var order = []; var byId = {}; var seen = {}; var payload; var items; var index; var item; var userId; var username; var limitReached = false; while (skip <= 500) { payload = await fetchForumJson("users.orderedList", { sort_by: "user_id", sort_dir: "asc", skip: skip, limit: 50, fields: "user_id,username" }); items = extractResponseItems(payload); if (!items.length) { break; } for (index = 0; index < items.length; index += 1) { item = items[index]; userId = toInteger(item.user_id || item.id); username = normalizeWhitespace(item.username || item.name); if (!userId || !username || seen[userId]) { continue; } seen[userId] = true; order.push(userId); byId[userId] = { userId: userId, username: username, profileUrl: buildProfileUrl(userId) }; } if (items.length < 50) { break; } skip += 50; if (skip > 500) { limitReached = true; } } if (!order.length) { throw new Error("\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432 \u043d\u0438 \u0438\u0437 userlist.php, \u043d\u0438 \u0447\u0435\u0440\u0435\u0437 API users.orderedList."); } if (limitReached) { throw new Error("API users.orderedList \u0443\u043f\u0435\u0440\u0441\u044f \u0432 \u043b\u0438\u043c\u0438\u0442 skip=500. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 userlist.php \u043a\u0430\u043a \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 roster."); } return { order: order, byId: byId }; } function mergeRosterIntoRegistry(registry, roster, now) { var currentIds = {}; var missingIds = []; roster.order.forEach(function (userId) { var current = roster.byId[userId]; var existing = normalizeUserRecord(registry.users[userId], userId); currentIds[userId] = true; existing.userId = userId; existing.username = current.username; existing.profileUrl = current.profileUrl; existing.status = "active"; existing.rosterSeenAt = now; existing.missingAt = 0; registry.users[userId] = existing; }); Object.keys(registry.users).forEach(function (key) { var userId = toInteger(key); var existing; if (!userId || currentIds[userId]) { return; } existing = normalizeUserRecord(registry.users[userId], userId); existing.status = "missing"; existing.missingAt = existing.missingAt || now; registry.users[userId] = existing; missingIds.push(userId); }); missingIds.sort(function (left, right) { var leftName = normalizeWhitespace(registry.users[left] && registry.users[left].username).toLowerCase(); var rightName = normalizeWhitespace(registry.users[right] && registry.users[right].username).toLowerCase(); if (leftName < rightName) { return -1; } if (leftName > rightName) { return 1; } return left - right; }); registry.order = roster.order.slice().concat(missingIds.filter(function (value) { return roster.order.indexOf(value) === -1; })); } function createEmptyRegistry() { return normalizeRegistry({ version: 1, updatedAt: 0, lastAttemptAt: 0, lastSuccessAt: 0, lastError: "", periodStartedAt: 0, previousReport: null, reportHistory: [], lock: null, order: [], users: {} }); } function normalizeRegistry(source) { var registry = source && typeof source === "object" ? source : {}; var normalized = { version: toInteger(registry.version) || 1, updatedAt: toInteger(registry.updatedAt), lastAttemptAt: toInteger(registry.lastAttemptAt), lastSuccessAt: toInteger(registry.lastSuccessAt), lastError: normalizeWhitespace(registry.lastError), periodStartedAt: toInteger(registry.periodStartedAt), previousReport: normalizePeriodSnapshot(registry.previousReport), reportHistory: normalizeReportHistory(registry.reportHistory, registry.previousReport), lock: normalizeLock(registry.lock), order: [], users: {} }; (Array.isArray(registry.order) ? registry.order : []).forEach(function (value) { var id = toInteger(value); if (!id || normalized.order.indexOf(id) !== -1) { return; } normalized.order.push(id); }); Object.keys(registry.users || {}).forEach(function (key) { var id = toInteger(key); if (!id) { return; } normalized.users[id] = normalizeUserRecord(registry.users[key], id); if (normalized.order.indexOf(id) === -1) { normalized.order.push(id); } }); return normalized; } function normalizePeriodSnapshot(source) { var report = source && typeof source === "object" ? source : null; var users; if (!report) { return null; } users = Array.isArray(report.cleanupUsers) ? report.cleanupUsers.map(normalizeSnapshotUser).filter(function (user) { return !!user.userId; }) : []; return { createdAt: toInteger(report.createdAt), periodStartedAt: toInteger(report.periodStartedAt), periodEndedAt: toInteger(report.periodEndedAt), rowCount: Math.max(0, toInteger(report.rowCount)), cleanupCount: Math.max(0, toInteger(report.cleanupCount || users.length)), appearanceCount: Math.max(0, toInteger(report.appearanceCount)), cleanupHtml: normalizeHtmlBlock(report.cleanupHtml), allUsers: Array.isArray(report.allUsers) ? report.allUsers.map(normalizeSnapshotUser).filter(function (user) { return !!user.userId; }) : [], cleanupUsers: users, appearanceUsers: Array.isArray(report.appearanceUsers) ? report.appearanceUsers.map(normalizeSnapshotUser).filter(function (user) { return !!user.userId; }) : [] }; } function normalizeReportHistory(source, fallbackReport) { var list = Array.isArray(source) ? source.slice() : []; var fallback = normalizePeriodSnapshot(fallbackReport); var seen = {}; var normalized = []; if (!list.length && fallback && fallback.createdAt) { list.push(fallback); } list.forEach(function (report) { var snapshot = normalizePeriodSnapshot(report); var key; if (!snapshot || !snapshot.createdAt) { return; } key = String(snapshot.createdAt); if (seen[key]) { return; } seen[key] = true; normalized.push(snapshot); }); normalized.sort(function (left, right) { return toInteger(right.createdAt) - toInteger(left.createdAt); }); return normalized.slice(0, 3); } function normalizeSnapshotUser(source) { var record = source && typeof source === "object" ? source : {}; return { userId: toInteger(record.userId), username: normalizeWhitespace(record.username), profileUrl: normalizeWhitespace(record.profileUrl), recentCount30d: Math.max(0, toInteger(record.recentCount30d)), snsCount: Math.max(0, toInteger(record.snsCount)), lastGamePostAt: toInteger(record.lastGamePostAt), lastForumPostAt: toInteger(record.lastForumPostAt) }; } function getReportPeriodStart(registry, now) { var fallback = now - REPORT_WINDOW_MS; var stored = toInteger(registry && registry.periodStartedAt); if (!stored) { return fallback; } return stored; } function getReportCutoffMs(registry, now) { return Math.max(now - REPORT_WINDOW_MS, getReportPeriodStart(registry, now)); } function buildPeriodSnapshot(registry, now) { var normalized = normalizeRegistry(registry); var allUsers = getSnapshotUsers(normalized); var cleanupUsers = getCleanupUsers(normalized); var periodStartedAt = getReportPeriodStart(normalized, now); return normalizePeriodSnapshot({ createdAt: now, periodStartedAt: periodStartedAt, periodEndedAt: now, rowCount: getDisplayOrder(normalized).length, cleanupCount: cleanupUsers.length, appearanceCount: 0, cleanupHtml: buildMonthlyCleanupHtml(cleanupUsers), allUsers: allUsers, cleanupUsers: cleanupUsers, appearanceUsers: [] }); } function createEmptyPeriodSnapshot(now) { return normalizePeriodSnapshot({ createdAt: now, periodStartedAt: 0, periodEndedAt: 0, rowCount: 0, cleanupCount: 0, appearanceCount: 0, cleanupHtml: "

\u0421\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u043e\u0433\u043e \u043e\u0442\u0447\u0435\u0442\u0430 \u043f\u043e\u043a\u0430 \u043d\u0435\u0442.

", allUsers: [], cleanupUsers: [], appearanceUsers: [] }); } function getCleanupUsers(registry) { var normalized = normalizeRegistry(registry); return getDisplayOrder(normalized).map(function (userId) { return normalizeUserRecord(normalized.users[userId], userId); }).filter(function (user) { return user.userId && user.status !== "missing" && !isCleanupSafe(user); }).map(function (user) { return normalizeSnapshotUser({ userId: user.userId, username: user.username || ("id " + user.userId), profileUrl: user.profileUrl || buildProfileUrl(user.userId), recentCount30d: user.recentCount30d, snsCount: user.snsCount, lastGamePostAt: user.lastGamePostAt, lastForumPostAt: user.lastForumPostAt }); }); } function getSnapshotUsers(registry) { var normalized = normalizeRegistry(registry); return getDisplayOrder(normalized).map(function (userId) { var user = normalizeUserRecord(normalized.users[userId], userId); return normalizeSnapshotUser({ userId: user.userId, username: user.username || ("id " + user.userId), profileUrl: user.profileUrl || buildProfileUrl(user.userId), recentCount30d: user.recentCount30d, snsCount: user.snsCount, lastGamePostAt: user.lastGamePostAt, lastForumPostAt: user.lastForumPostAt }); }).filter(function (user) { return !!user.userId; }); } function getAppearanceRiskUsers(registry, now) { var normalized = normalizeRegistry(registry); return getDisplayOrder(normalized).map(function (userId) { return normalizeUserRecord(normalized.users[userId], userId); }).filter(function (user) { return user.userId && user.status !== "missing" && !isAppearanceSafe(user, now); }).map(function (user) { return normalizeSnapshotUser({ userId: user.userId, username: user.username || ("id " + user.userId), profileUrl: user.profileUrl || buildProfileUrl(user.userId), recentCount30d: user.recentCount30d, snsCount: user.snsCount, lastGamePostAt: user.lastGamePostAt, lastForumPostAt: user.lastForumPostAt }); }); } function buildMonthlyCleanupHtml(cleanupUsers) { var monthlyList = Array.isArray(cleanupUsers) ? cleanupUsers : []; var chunks = []; if (!monthlyList.length) { return "

\u0412 \u0441\u043f\u0438\u0441\u043a\u0435 \u0447\u0438\u0441\u0442\u043a\u0438 \u043d\u0438\u043a\u043e\u0433\u043e \u043d\u0435\u0442.

"; } chunks.push("

1) \u041d\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u044b \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438: 1 \u043f\u043e\u0441\u0442 / 25 SNS / 100 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0432 \u043c\u0435\u0441\u044f\u0446

"); chunks.push(buildCleanupUserListHtml(monthlyList)); return chunks.join("\n"); } function buildAppearanceHtml(appearanceUsers) { var list = Array.isArray(appearanceUsers) ? appearanceUsers : []; if (!list.length) { return "

\u0412 \u0441\u043f\u0438\u0441\u043a\u0435 \u043f\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u043c \u043d\u0438\u043a\u043e\u0433\u043e \u043d\u0435\u0442.

"; } return [ "

2) \u041d\u0435 \u043e\u0441\u0442\u0430\u0432\u043b\u044f\u043b\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 7 \u0434\u043d\u0435\u0439

", buildMessageRiskUserListHtml(list) ].join("\n"); } function buildCleanupUserListHtml(users) { var list = Array.isArray(users) ? users : []; if (!list.length) { return "

\u041d\u0438\u043a\u043e\u0433\u043e.

"; } return list.map(function (user) { var href = user.profileUrl || buildProfileUrl(user.userId); var name = user.username || ("id " + user.userId); return ' ' + escapeHtml(name) + "
"; }).join("\n"); } function buildMessageRiskUserListHtml(users) { var list = Array.isArray(users) ? users : []; if (!list.length) { return "

\u041d\u0438\u043a\u043e\u0433\u043e.

"; } return list.map(function (user) { var href = user.profileUrl || buildProfileUrl(user.userId); var name = user.username || ("id " + user.userId); return '
' + escapeHtml(name) + "
"; }).join("\n"); } function resetPeriodMetrics(registry) { Object.keys(registry.users || {}).forEach(function (key) { var user = normalizeUserRecord(registry.users[key], key); user.recentCount30d = 0; user.recentCount30dExact = true; user.lastGamePostAt = 0; user.lastGamePostId = 0; user.lastGameTopicId = 0; user.lastGameTopicSubject = ""; user.snsCount = 0; user.snsTopicIds = []; registry.users[key] = user; }); } function normalizeUserRecord(source, userId) { var record = source && typeof source === "object" ? source : {}; return { userId: toInteger(userId || record.userId), username: normalizeWhitespace(record.username), profileUrl: normalizeWhitespace(record.profileUrl), status: normalizeUserStatus(record.status), rosterSeenAt: toInteger(record.rosterSeenAt), missingAt: toInteger(record.missingAt), lastSyncAt: toInteger(record.lastSyncAt), syncStatus: normalizeSyncStatus(record.syncStatus), lastError: normalizeWhitespace(record.lastError), recentCount30d: Math.max(0, toInteger(record.recentCount30d)), recentCount30dExact: record.recentCount30dExact !== false, lastForumPostAt: toInteger(record.lastForumPostAt), lastForumPostId: toInteger(record.lastForumPostId), lastForumTopicId: toInteger(record.lastForumTopicId), lastForumTopicSubject: normalizeWhitespace(record.lastForumTopicSubject), lastGamePostAt: toInteger(record.lastGamePostAt), lastGamePostId: toInteger(record.lastGamePostId), lastGameTopicId: toInteger(record.lastGameTopicId), lastGameTopicSubject: normalizeWhitespace(record.lastGameTopicSubject), snsCount: Math.max(0, toInteger(record.snsCount)), snsTopicIds: Array.isArray(record.snsTopicIds) ? record.snsTopicIds.map(toInteger).filter(Boolean) : [] }; } function normalizeLock(source) { if (!source || typeof source !== "object") { return null; } if (!source.token) { return null; } return { token: String(source.token), userId: toInteger(source.userId), topicId: toInteger(source.topicId), acquiredAt: toInteger(source.acquiredAt) }; } function normalizeUserStatus(value) { var status = normalizeWhitespace(value).toLowerCase(); if (status === "missing") { return "missing"; } return "active"; } function normalizeSyncStatus(value) { var status = normalizeWhitespace(value).toLowerCase(); if (status === "partial") { return "partial"; } if (status === "error") { return "error"; } return "ok"; } function getRegistryEditUrl() { return location.origin + REGISTRY.editPath + encodeURIComponent(REGISTRY.pageId); } async function loadRegistryFromAdminPage() { var page = await fetchPage(getRegistryEditUrl()); var doc = parseHtmlDocument(page.html); return readRegistryFromAdminDoc(doc); } async function saveRegistryToAdminPage(registry) { var form; var field; var submitter; var submittedDoc = null; var nextSignature = getRegistrySignature(registry); await withHiddenFrame(getRegistryEditUrl(), async function (frame, doc) { var readyDoc = await waitForRegistryFormReady(frame, doc); var formState = getRegistryFormState(readyDoc); var nextContent; form = formState.form; field = formState.field; if (!form || !field) { throw new Error("\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430 \u0442\u0435\u0445\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0440\u0435\u0435\u0441\u0442\u0440\u0430 " + REGISTRY.pageId + ". \u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u0435\u0435 \u0432 admin_pages."); } nextContent = buildRegistryContent(getFieldValue(field), registry); if (nextContent.length > REGISTRY.softLimit) { throw new Error( "\u0420\u0435\u0435\u0441\u0442\u0440 \u0432\u044b\u0440\u043e\u0441 \u0434\u043e " + nextContent.length + " \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u0438 \u043f\u0440\u0438\u0431\u043b\u0438\u0437\u0438\u043b\u0441\u044f \u043a \u043b\u0438\u043c\u0438\u0442\u0443 admin_pages. \u041d\u0443\u0436\u043d\u043e \u0435\u0449\u0435 \u0441\u0438\u043b\u044c\u043d\u0435\u0435 \u0441\u0436\u0430\u0442\u044c \u0445\u0440\u0430\u043d\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u0448\u0430\u0440\u0434\u044b." ); } setRegistryFieldValues(formState, nextContent); submitter = form.querySelector(REGISTRY.saveSelector); submittedDoc = await submitFrameForm(frame, form, submitter); return submittedDoc; }); await confirmSavedRegistry(submittedDoc, nextSignature); } function readRegistryFromAdminDoc(doc) { var formState = getRegistryFormState(doc); var form = formState.form; var field = formState.field; if (!form || !field) { throw new Error("\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430 \u0442\u0435\u0445\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0440\u0435\u0435\u0441\u0442\u0440\u0430 " + REGISTRY.pageId + ". \u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u0435\u0435 \u0432 admin_pages."); } return parseRegistryContent(getFieldValue(field)); } function getRegistryFormState(doc) { var form = doc ? doc.querySelector(REGISTRY.formSelector) : null; var fields = form ? uniqueNodes(Array.prototype.slice.call(form.querySelectorAll(REGISTRY.contentSelector))) : []; var field = chooseRegistryContentField(fields); return { form: form, field: field, fields: fields }; } function chooseRegistryContentField(fields) { var namedContentField = null; var index; var field; for (index = 0; index < fields.length; index += 1) { field = fields[index]; if (!namedContentField && String(field.getAttribute("name") || "").toLowerCase() === "content") { namedContentField = field; } if (getFieldValue(field).indexOf(REGISTRY.startMarker) !== -1) { return field; } } return namedContentField || fields[0] || null; } function setRegistryFieldValues(formState, value) { var fields = formState.fields && formState.fields.length ? formState.fields : [formState.field]; fields.forEach(function (field) { if (!field) { return; } setFieldValue(field, value); dispatchFieldEvent(field, "input"); dispatchFieldEvent(field, "change"); }); } function dispatchFieldEvent(field, type) { var event; var view = field && field.ownerDocument ? field.ownerDocument.defaultView : window; if (!field || !field.dispatchEvent) { return; } try { event = new view.Event(type, { bubbles: true, cancelable: true }); } catch (error) { event = field.ownerDocument.createEvent("Event"); event.initEvent(type, true, true); } field.dispatchEvent(event); } function waitForRegistryFormReady(frame, initialDoc) { return new Promise(function (resolve, reject) { var startedAt = Date.now(); function check() { var doc = initialDoc; var formState; try { doc = frame.contentDocument || (frame.contentWindow && frame.contentWindow.document) || initialDoc; } catch (error) { doc = initialDoc; } formState = getRegistryFormState(doc); if (formState.form && formState.field) { resolve(doc); return; } if (Date.now() - startedAt >= FRAME_TIMEOUT_MS) { reject(new Error("\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430 \u0442\u0435\u0445\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0440\u0435\u0435\u0441\u0442\u0440\u0430 " + REGISTRY.pageId + ". \u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u0435\u0435 \u0432 admin_pages.")); return; } window.setTimeout(check, 120); } check(); }); } async function confirmSavedRegistry(submittedDoc, expectedSignature) { var confirmed = readConfirmedRegistryFromDoc(submittedDoc, expectedSignature); var attempt; var page; var doc; if (confirmed) { return confirmed; } for (attempt = 1; attempt <= 3; attempt += 1) { if (attempt > 1) { await sleep(350); } page = await fetchPage(getRegistryEditUrl()); doc = parseHtmlDocument(page.html); confirmed = readConfirmedRegistryFromDoc(doc, expectedSignature); if (confirmed) { return confirmed; } } throw new Error("\u0420\u0435\u0435\u0441\u0442\u0440 \u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0442\u0435\u0445\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 " + REGISTRY.pageId + " \u0432 admin_pages."); } function readConfirmedRegistryFromDoc(doc, expectedSignature) { var registry; if (!doc) { return null; } try { registry = readRegistryFromAdminDoc(doc); if (getRegistrySignature(registry) === expectedSignature) { return registry; } } catch (error) { return null; } return null; } function getRegistrySignature(registry) { return JSON.stringify(packRegistryForStorage(normalizeRegistry(registry))); } function parseRegistryContent(content) { var source = String(content == null ? "" : content); var startIndex = source.indexOf(REGISTRY.startMarker); var endIndex; var jsonText; var parsed; if (!normalizeWhitespace(source)) { return createEmptyRegistry(); } if (startIndex !== -1) { endIndex = source.indexOf(REGISTRY.endMarker, startIndex + REGISTRY.startMarker.length); if (endIndex === -1) { throw new Error("\u0412 \u0440\u0435\u0435\u0441\u0442\u0440\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u044e\u0449\u0438\u0439 \u043c\u0430\u0440\u043a\u0435\u0440."); } jsonText = source.slice(startIndex + REGISTRY.startMarker.length, endIndex).trim(); } else { jsonText = source.trim(); } if (!normalizeWhitespace(jsonText)) { return createEmptyRegistry(); } try { parsed = JSON.parse(jsonText); return normalizeRegistry(unpackRegistryFromStorage(parsed)); } catch (error) { throw new Error("\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0441\u043f\u0430\u0440\u0441\u0438\u0442\u044c JSON \u0440\u0435\u0435\u0441\u0442\u0440\u0430 " + REGISTRY.pageId + "."); } } function buildRegistryContent(content, registry) { var source = String(content == null ? "" : content); var block = REGISTRY.startMarker + "\n" + stringifyRegistry(registry) + "\n" + REGISTRY.endMarker; var startIndex = source.indexOf(REGISTRY.startMarker); var endIndex; var prefix; var suffix; if (startIndex !== -1) { endIndex = source.indexOf(REGISTRY.endMarker, startIndex + REGISTRY.startMarker.length); if (endIndex === -1) { return block; } prefix = source.slice(0, startIndex).replace(/\s*$/, ""); suffix = source.slice(endIndex + REGISTRY.endMarker.length).replace(/^\s*/, ""); return (prefix ? prefix + "\n\n" : "") + block + (suffix ? "\n\n" + suffix : ""); } if (!normalizeWhitespace(source)) { return block; } return source.replace(/\s*$/, "") + "\n\n" + block; } function stringifyRegistry(registry) { return JSON.stringify(packRegistryForStorage(normalizeRegistry(registry))); } function unpackRegistryFromStorage(source) { var data = source && typeof source === "object" ? source : {}; var unpacked; if (!isPackedRegistry(data)) { return data; } unpacked = { version: toInteger(data.v) || 2, updatedAt: toInteger(data.u), lastAttemptAt: toInteger(data.a), lastSuccessAt: toInteger(data.s), lastError: normalizeWhitespace(data.e), periodStartedAt: toInteger(data.ps), previousReport: unpackPeriodSnapshotFromStorage(data.p), reportHistory: unpackReportHistoryFromStorage(data.g), lock: unpackLockFromStorage(data.l), order: Array.isArray(data.o) ? data.o.slice() : [], users: {} }; Object.keys(data.r || {}).forEach(function (key) { var id = toInteger(key); if (!id) { return; } unpacked.users[id] = unpackUserRecordFromStorage(data.r[key], id); }); return unpacked; } function isPackedRegistry(source) { return !!(source && typeof source === "object" && source.r && !source.users); } function packRegistryForStorage(registry) { var packedUsers = {}; Object.keys(registry.users || {}).forEach(function (key) { var id = toInteger(key); if (!id) { return; } packedUsers[id] = trimPackedUserRecord([ normalizeWhitespace(registry.users[id] && registry.users[id].username), normalizeUserStatus(registry.users[id] && registry.users[id].status) === "missing" ? 1 : 0, encodeSyncStatus(normalizeSyncStatus(registry.users[id] && registry.users[id].syncStatus)), Math.max(0, toInteger(registry.users[id] && registry.users[id].recentCount30d)), registry.users[id] && registry.users[id].recentCount30dExact === false ? 0 : 1, toInteger(registry.users[id] && registry.users[id].lastForumPostAt), toInteger(registry.users[id] && registry.users[id].lastForumPostId), toInteger(registry.users[id] && registry.users[id].lastForumTopicId), normalizeWhitespace(registry.users[id] && registry.users[id].lastForumTopicSubject), toInteger(registry.users[id] && registry.users[id].lastGamePostAt), toInteger(registry.users[id] && registry.users[id].lastGamePostId), toInteger(registry.users[id] && registry.users[id].lastGameTopicId), normalizeWhitespace(registry.users[id] && registry.users[id].lastGameTopicSubject), Math.max(0, toInteger(registry.users[id] && registry.users[id].snsCount)), Array.isArray(registry.users[id] && registry.users[id].snsTopicIds) ? registry.users[id].snsTopicIds.map(toInteger).filter(Boolean) : [], normalizeWhitespace(registry.users[id] && registry.users[id].lastError) ]); }); return { v: 2, u: toInteger(registry.updatedAt), a: toInteger(registry.lastAttemptAt), s: toInteger(registry.lastSuccessAt), e: normalizeWhitespace(registry.lastError), ps: toInteger(registry.periodStartedAt), p: packPeriodSnapshotForStorage(registry.previousReport), g: packReportHistoryForStorage(registry.reportHistory), l: packLockForStorage(registry.lock), o: Array.isArray(registry.order) ? registry.order.slice() : [], r: packedUsers }; } function packPeriodSnapshotForStorage(report) { var normalized = normalizePeriodSnapshot(report); if (!normalized || !normalized.createdAt) { return null; } return { c: toInteger(normalized.createdAt), b: toInteger(normalized.periodStartedAt), e: toInteger(normalized.periodEndedAt), n: Math.max(0, toInteger(normalized.rowCount)), x: Math.max(0, toInteger(normalized.cleanupCount)), y: Math.max(0, toInteger(normalized.appearanceCount)), h: normalizeHtmlBlock(normalized.cleanupHtml), z: normalized.allUsers.map(function (user) { return trimPackedSnapshotUser([ toInteger(user.userId), normalizeWhitespace(user.username), normalizeWhitespace(user.profileUrl), Math.max(0, toInteger(user.recentCount30d)), Math.max(0, toInteger(user.snsCount)), toInteger(user.lastGamePostAt), toInteger(user.lastForumPostAt) ]); }), u: normalized.cleanupUsers.map(function (user) { return trimPackedSnapshotUser([ toInteger(user.userId), normalizeWhitespace(user.username), normalizeWhitespace(user.profileUrl), Math.max(0, toInteger(user.recentCount30d)), Math.max(0, toInteger(user.snsCount)), toInteger(user.lastGamePostAt), toInteger(user.lastForumPostAt) ]); }), q: normalized.appearanceUsers.map(function (user) { return trimPackedSnapshotUser([ toInteger(user.userId), normalizeWhitespace(user.username), normalizeWhitespace(user.profileUrl), Math.max(0, toInteger(user.recentCount30d)), Math.max(0, toInteger(user.snsCount)), toInteger(user.lastGamePostAt), toInteger(user.lastForumPostAt) ]); }) }; } function packReportHistoryForStorage(history) { return normalizeReportHistory(history).map(packPeriodSnapshotForStorage).filter(Boolean).slice(0, 3); } function unpackReportHistoryFromStorage(source) { return normalizeReportHistory(Array.isArray(source) ? source.map(unpackPeriodSnapshotFromStorage) : []); } function unpackPeriodSnapshotFromStorage(source) { var data = source && typeof source === "object" ? source : null; if (!data) { return null; } return normalizePeriodSnapshot({ createdAt: toInteger(data.c || data.createdAt), periodStartedAt: toInteger(data.b || data.periodStartedAt), periodEndedAt: toInteger(data.e || data.periodEndedAt), rowCount: toInteger(data.n || data.rowCount), cleanupCount: toInteger(data.x || data.cleanupCount), appearanceCount: toInteger(data.y || data.appearanceCount), cleanupHtml: normalizeHtmlBlock(data.h || data.cleanupHtml), allUsers: Array.isArray(data.z) ? data.z.map(unpackSnapshotUserFromStorage) : data.allUsers, cleanupUsers: Array.isArray(data.u) ? data.u.map(unpackSnapshotUserFromStorage) : data.cleanupUsers, appearanceUsers: Array.isArray(data.q) ? data.q.map(unpackSnapshotUserFromStorage) : data.appearanceUsers }); } function unpackSnapshotUserFromStorage(source) { var values = Array.isArray(source) ? source : []; return { userId: toInteger(values[0]), username: normalizeWhitespace(values[1]), profileUrl: normalizeWhitespace(values[2]), recentCount30d: Math.max(0, toInteger(values[3])), snsCount: Math.max(0, toInteger(values[4])), lastGamePostAt: toInteger(values[5]), lastForumPostAt: toInteger(values[6]) }; } function trimPackedSnapshotUser(values) { var defaults = [0, "", "", 0, 0, 0, 0]; var lastIndex = values.length - 1; while (lastIndex > 0 && isPackedFieldDefault(values[lastIndex], defaults[lastIndex])) { lastIndex -= 1; } return values.slice(0, lastIndex + 1); } function unpackUserRecordFromStorage(source, userId) { var values = Array.isArray(source) ? source : []; return { userId: toInteger(userId), username: normalizeWhitespace(values[0]), status: values[1] ? "missing" : "active", syncStatus: decodeSyncStatus(values[2]), recentCount30d: Math.max(0, toInteger(values[3])), recentCount30dExact: values.length >= 5 ? values[4] !== 0 : true, lastForumPostAt: toInteger(values[5]), lastForumPostId: toInteger(values[6]), lastForumTopicId: toInteger(values[7]), lastForumTopicSubject: normalizeWhitespace(values[8]), lastGamePostAt: toInteger(values[9]), lastGamePostId: toInteger(values[10]), lastGameTopicId: toInteger(values[11]), lastGameTopicSubject: normalizeWhitespace(values[12]), snsCount: Math.max(0, toInteger(values[13])), snsTopicIds: Array.isArray(values[14]) ? values[14].map(toInteger).filter(Boolean) : [], lastError: normalizeWhitespace(values[15]) }; } function trimPackedUserRecord(values) { var defaults = ["", 0, 0, 0, 1, 0, 0, 0, "", 0, 0, 0, "", 0, [], ""]; var lastIndex = values.length - 1; while (lastIndex > 0 && isPackedFieldDefault(values[lastIndex], defaults[lastIndex])) { lastIndex -= 1; } return values.slice(0, lastIndex + 1); } function isPackedFieldDefault(value, defaultValue) { if (Array.isArray(defaultValue)) { return Array.isArray(value) && !value.length && !defaultValue.length; } return value === defaultValue; } function packLockForStorage(lock) { var normalized = normalizeLock(lock); if (!normalized) { return 0; } return [ String(normalized.token || ""), toInteger(normalized.userId), toInteger(normalized.topicId), toInteger(normalized.acquiredAt) ]; } function unpackLockFromStorage(source) { var values = Array.isArray(source) ? source : []; if (!values.length || !values[0]) { return null; } return { token: String(values[0]), userId: toInteger(values[1]), topicId: toInteger(values[2]), acquiredAt: toInteger(values[3]) }; } function encodeSyncStatus(status) { if (status === "partial") { return 1; } if (status === "error") { return 2; } return 0; } function decodeSyncStatus(code) { if (toInteger(code) === 1) { return "partial"; } if (toInteger(code) === 2) { return "error"; } return "ok"; } function buildApiUrl(method, params) { var searchParams = new URLSearchParams(); searchParams.set("method", method); Object.keys(params || {}).forEach(function (key) { if (params[key] !== undefined && params[key] !== null && params[key] !== "") { searchParams.set(key, String(params[key])); } }); return "/api.php?" + searchParams.toString(); } async function fetchForumJson(method, params) { var response = await fetch(buildApiUrl(method, params), { credentials: "same-origin", headers: { Accept: "application/json, text/javascript, */*; q=0.01" } }); var text = await response.text(); var payload; try { payload = JSON.parse(text); } catch (error) { throw new Error("API \u0444\u043e\u0440\u0443\u043c\u0430 \u0432\u0435\u0440\u043d\u0443\u043b \u043d\u0435 JSON."); } if (!response.ok) { throw new Error("API \u0444\u043e\u0440\u0443\u043c\u0430 \u0432\u0435\u0440\u043d\u0443\u043b \u043a\u043e\u0434 " + response.status + "."); } if (payload && payload.error) { throw new Error( normalizeWhitespace( (payload.error && payload.error.message) || payload.message || payload.error || "\u041e\u0448\u0438\u0431\u043a\u0430 API \u0444\u043e\u0440\u0443\u043c\u0430." ) ); } return payload; } function extractResponseItems(payload) { var response = payload && (payload.response || payload.result || payload.data); var keys; var index; if (Array.isArray(response)) { return response; } if (response && Array.isArray(response.items)) { return response.items; } if (response && typeof response === "object") { keys = Object.keys(response); for (index = 0; index < keys.length; index += 1) { if (Array.isArray(response[keys[index]])) { return response[keys[index]]; } } return [response]; } return []; } function parseHtmlDocument(html) { return new DOMParser().parseFromString(String(html == null ? "" : html), "text/html"); } function buildNoCacheUrl(url) { var result; try { result = new URL(String(url || ""), location.origin); result.searchParams.set("_activityts", String(Date.now()) + Math.random().toString(36).slice(2, 8)); return result.toString(); } catch (error) { return String(url || ""); } } async function fetchPage(url) { var requestUrl = buildNoCacheUrl(url); var response = await fetch(requestUrl, { cache: "no-store", credentials: "include" }); var bytes; var html; if (!response.ok) { throw new Error("\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443: " + url + " (" + response.status + ")."); } bytes = await response.arrayBuffer(); html = decodeHtmlBytes(bytes, response.headers && response.headers.get ? response.headers.get("content-type") : ""); return { url: response.url || requestUrl, html: html }; } function decodeHtmlBytes(buffer, contentType) { var bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer || 0); var charset = detectCharsetFromContentType(contentType) || detectCharsetFromHtmlMeta(bytes) || "utf-8"; var decoder; try { decoder = new TextDecoder(charset); } catch (error) { decoder = new TextDecoder("utf-8"); } return decoder.decode(bytes); } function detectCharsetFromContentType(contentType) { var match = String(contentType || "").match(/charset\s*=\s*["']?\s*([a-z0-9_-]+)/i); return match ? normalizeCharsetLabel(match[1]) : ""; } function detectCharsetFromHtmlMeta(bytes) { var sampleLength = Math.min(bytes.length, 4096); var ascii = ""; var index; var code; var match; for (index = 0; index < sampleLength; index += 1) { code = bytes[index]; ascii += code >= 32 && code <= 126 ? String.fromCharCode(code) : " "; } match = ascii.match(/charset\s*=\s*["']?\s*([a-z0-9_-]+)/i); return match ? normalizeCharsetLabel(match[1]) : ""; } function normalizeCharsetLabel(value) { var label = normalizeWhitespace(value).toLowerCase(); if (!label) { return ""; } if (label === "cp1251" || label === "windows1251" || label === "win-1251") { return "windows-1251"; } if (label === "utf8") { return "utf-8"; } return label; } function sleep(ms) { return new Promise(function (resolve) { window.setTimeout(resolve, ms); }); } function createHiddenFrame() { var frame = document.createElement("iframe"); frame.setAttribute("aria-hidden", "true"); frame.tabIndex = -1; frame.style.position = "fixed"; frame.style.left = "-9999px"; frame.style.top = "0"; frame.style.width = "1px"; frame.style.height = "1px"; frame.style.opacity = "0"; frame.style.pointerEvents = "none"; document.body.appendChild(frame); return frame; } function waitForFrameLoad(frame) { return new Promise(function (resolve, reject) { var timerId; function cleanup() { window.clearTimeout(timerId); frame.removeEventListener("load", handleLoad); } function handleLoad() { var doc = frame.contentDocument || (frame.contentWindow && frame.contentWindow.document) || null; var href = ""; try { href = doc && doc.location ? String(doc.location.href || "") : ""; } catch (error) { href = ""; } if (!href || /^about:blank$/i.test(href)) { return; } cleanup(); resolve(doc); } timerId = window.setTimeout(function () { cleanup(); reject(new Error("\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0441\u043a\u0440\u044b\u0442\u043e\u0433\u043e \u0444\u0440\u0435\u0439\u043c\u0430.")); }, FRAME_TIMEOUT_MS); frame.addEventListener("load", handleLoad); }); } function loadFrameUrl(frame, url) { return new Promise(function (resolve, reject) { var startedAt = Date.now(); frame.src = buildNoCacheUrl(url); function check() { var doc = null; var href = ""; var readyState = ""; try { doc = frame.contentDocument || (frame.contentWindow && frame.contentWindow.document) || null; href = doc && doc.location ? String(doc.location.href || "") : ""; readyState = doc && doc.readyState ? String(doc.readyState) : ""; } catch (error) { doc = null; href = ""; readyState = ""; } if (doc && href && !/^about:blank$/i.test(href) && (!readyState || readyState === "interactive" || readyState === "complete")) { resolve(doc); return; } if (Date.now() - startedAt >= FRAME_TIMEOUT_MS) { reject(new Error("\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0441\u043a\u0440\u044b\u0442\u043e\u0433\u043e \u0444\u0440\u0435\u0439\u043c\u0430.")); return; } window.setTimeout(check, 120); } check(); }); } function nativeSubmitForm(form) { var view = form && form.ownerDocument ? form.ownerDocument.defaultView : window; var submitMethod = view && view.HTMLFormElement && view.HTMLFormElement.prototype ? view.HTMLFormElement.prototype.submit : null; if (typeof submitMethod !== "function") { throw new Error("\u0411\u0440\u0430\u0443\u0437\u0435\u0440 \u043d\u0435 \u0434\u0430\u043b \u043d\u0430\u0442\u0438\u0432\u043d\u043e \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0444\u043e\u0440\u043c\u0443 \u0440\u0435\u0435\u0441\u0442\u0440\u0430."); } submitMethod.call(form); } async function submitFrameForm(frame, form, submitter) { var pending = waitForFrameLoad(frame); if (form.requestSubmit) { form.requestSubmit(submitter || undefined); } else if (submitter && typeof submitter.click === "function") { submitter.click(); } else { nativeSubmitForm(form); } return pending; } async function withHiddenFrame(url, worker) { var frame = createHiddenFrame(); var doc; try { doc = await loadFrameUrl(frame, url); return await worker(frame, doc); } finally { if (frame.parentNode) { frame.parentNode.removeChild(frame); } } } function getFieldValue(field) { var tagName; var directValue; var attrValue; var textValue; if (!field) { return ""; } directValue = field.value; if (typeof directValue === "string") { return directValue; } attrValue = field.getAttribute && field.getAttribute("value"); if (typeof attrValue === "string") { return attrValue; } tagName = String(field.tagName || "").toLowerCase(); if (tagName === "textarea" || tagName === "input" || tagName === "select") { return ""; } textValue = field.textContent; return typeof textValue === "string" ? textValue : ""; } function setFieldValue(field, value) { var nextValue = String(value == null ? "" : value); var tagName = String(field && field.tagName || "").toLowerCase(); if (!field) { return; } if (typeof field.value === "string") { field.value = nextValue; } if (field.setAttribute && (tagName === "input" || tagName === "textarea")) { field.setAttribute("value", nextValue); } if (tagName !== "input" && tagName !== "textarea" && tagName !== "select") { field.textContent = nextValue; } } function uniqueNodes(nodes) { var seen = []; return (nodes || []).filter(function (node) { if (!node || seen.indexOf(node) !== -1) { return false; } seen.push(node); return true; }); } function render() { var registry = state.registry || createEmptyRegistry(); var order = getDisplayOrder(registry); if (!root) { return; } root.innerHTML = [ '
', buildHeaderMarkup(registry, order.length), buildNoticeMarkup(), buildProgressMarkup(), buildAnketaMarkup(), buildTableMarkup(registry, order), buildFooterMarkup(registry), "
" ].join(""); } function buildHeaderMarkup(registry, rowCount) { var periodStart = getReportPeriodStart(registry, Date.now()); var periodEnd = registry.lastSuccessAt || Date.now(); return [ '
', '
', '

\u041e\u0442\u0447\u0435\u0442 \u043f\u043e \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438 \u0438\u0433\u0440\u043e\u043a\u043e\u0432

', '
', ' \u0421\u0442\u0440\u043e\u043a: ' + rowCount + "", ' \u0418\u0433\u0440\u043e\u0432\u044b\u0435 \u0444\u043e\u0440\u0443\u043c\u044b: ' + GAME_FORUM_IDS.join(", ") + "", ' SNS-\u0444\u043e\u0440\u0443\u043c\u044b: ' + SNS_FORUM_IDS.join(", ") + "", ' \u041e\u043a\u043d\u043e: ' + REPORT_WINDOW_DAYS + " \u0434\u043d\u0435\u0439", ' \u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f: 1 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 / ' + APPEARANCE_WINDOW_DAYS + " \u0434\u043d\u0435\u0439", ' \u0427\u0438\u0441\u0442\u043a\u0430: 1 \u0438\u0433\u0440\u043e\u0432\u043e\u0439 / ' + GAME_ACTIVITY_WINDOW_DAYS + " \u0434\u043d\u0435\u0439 \u0438\u043b\u0438 " + CLEANUP_SNS_REQUIRED + " SNS / " + SNS_WINDOW_DAYS + " \u0434\u043d\u0435\u0439", ' \u041f\u0435\u0440\u0438\u043e\u0434: ' + escapeHtml(formatDate(periodStart) + " - " + formatDate(periodEnd)) + "", ' \u041a\u044d\u0448: ' + escapeHtml(formatDateTime(registry.lastSuccessAt) || "\u043d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445") + "", "
", "
", '
', ' ', ' ', ' ', ' ', ' ', "
", "
" ].join(""); } function buildNoticeMarkup() { var chunks = []; if (state.status === "loading" && state.message) { chunks.push('

' + escapeHtml(state.message) + "

"); } if (state.status === "restricted") { chunks.push('

' + escapeHtml(state.message) + "

"); } if (state.status === "error") { chunks.push('

' + escapeHtml(state.error) + "

"); } if (state.sync.error) { chunks.push('

' + escapeHtml(state.sync.error) + "

"); } return chunks.join(""); } function buildProgressMarkup() { var lines = []; if (!state.sync.running && !state.sync.message) { return ""; } if (state.sync.message) { lines.push(escapeHtml(state.sync.message)); } if (state.sync.running) { lines.push(" " + state.sync.done + "/" + state.sync.total); } return '

' + lines.join("") + "

"; } function buildAnketaMarkup() { var users = state.anketa && Array.isArray(state.anketa.users) ? state.anketa.users : []; var isOpen = !!(state.anketa && state.anketa.open); var countLabel = state.anketa && state.anketa.checkedAt ? " · найдено: " + users.length : ""; var body = users.length ? '
' + users.map(function (user) { return '' + escapeHtml(user.username) + 'зарегистрирован: ' + escapeHtml(formatDate(user.registeredAt)) + ''; }).join("") + "
" : state.anketa && state.anketa.checkedAt ? 'подходящих пользователей не найдено' : 'проверка ещё не запускалась'; return [ '
', '
', '
не подана анкета в течение ' + ANKETA_WAIT_DAYS + ' дней после регистрации
группы: ' + ANKETA_USER_GROUP_IDS.join(", ") + ' · разделы анкет: ' + ANKETA_FORUM_IDS.join(", ") + countLabel + '
', '
", "
", state.anketa && state.anketa.error ? '

' + escapeHtml(state.anketa.error) + "

" : "", '
' + body + "
", "
" ].join(""); } function buildTableMarkup(registry, order) { var body = order.length ? order.map(function (userId) { return buildUserRowMarkup(registry.users[userId]); }).join("") : '
\u041f\u043e\u043a\u0430 \u043d\u0435\u0442 \u0441\u0442\u0440\u043e\u043a. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0435\u0439\u0447\u0430\u0441\u00bb, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0442\u0430\u0431\u043b\u0438\u0446\u0443.
'; return [ '
', ' ', " ", " ", ' ', buildSortableHeaderCell("cleanupRisk", "\u0410\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c"), buildSortableHeaderCell("lastGamePostAt", "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u0438\u0433\u0440\u043e\u0432\u043e\u0439 \u043f\u043e\u0441\u0442"), buildSortableHeaderCell("lastForumPostAt", "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435"), buildSortableHeaderCell("recentCount30d", "\u0412\u0441\u0435\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0437\u0430 " + REPORT_WINDOW_DAYS + " \u0434\u043d\u0435\u0439"), buildSortableHeaderCell("snsCount", "\u0441\u043d\u0441"), " ", body, " ", "
\u0418\u0433\u0440\u043e\u043a
", "
" ].join(""); } function buildSortableHeaderCell(key, label) { var normalizedKey = normalizeSortKey(key); var isActive = state.sort.key === normalizedKey; var direction = isActive ? state.sort.direction : ""; var indicator = direction === "asc" ? "↑" : direction === "desc" ? "↓" : "↕"; return [ "", ' ", "" ].join(""); } function buildUserRowMarkup(record) { var user = normalizeUserRecord(record, record && record.userId); var rowClass = []; var badges = []; var now = Date.now(); if (user.status === "missing") { rowClass.push("espresso-activity__row--missing"); badges.push('\u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 userlist'); } badges.push(buildAppearanceBadge(user, now)); badges.push(buildCleanupBadge(user)); if (user.syncStatus === "partial") { badges.push('\u0447\u0430\u0441\u0442\u0438\u0447\u043d\u044b\u0439 \u043f\u0435\u0440\u0435\u0441\u0447\u0435\u0442'); } if (user.lastError) { rowClass.push("espresso-activity__row--error"); badges.push('\u043d\u0443\u0436\u043d\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430'); } return [ '', " ", '
', ' ' + escapeHtml(user.username || ("id " + user.userId)) + "", (badges.length ? '
' + badges.join("") + "
" : ""), (user.lastError ? ' ' + escapeHtml(user.lastError) + "" : ""), "
", " ", " " + buildActivityCellMarkup(user) + "", " " + buildGameCellMarkup(user) + "", " " + buildForumCellMarkup(user) + "", " " + buildCountCellMarkup(user) + "", " " + buildSnsCellMarkup(user) + "", "" ].join(""); } function buildGameCellMarkup(user) { if (!user.lastGamePostAt) { return [ '
', ' \u043d\u0435\u0442 \u0438\u0433\u0440\u043e\u0432\u044b\u0445 \u043f\u043e\u0441\u0442\u043e\u0432 \u0437\u0430 ' + GAME_ACTIVITY_WINDOW_DAYS + ' \u0434\u043d\u0435\u0439', "
" ].join(""); } return [ '
', ' ' + escapeHtml(formatDateTime(user.lastGamePostAt)) + "", ' ' + escapeHtml(user.lastGameTopicSubject || getTopicLabel(user.lastGameTopicId, "")) + "", "
" ].join(""); } function buildForumCellMarkup(user) { if (!user.lastForumPostAt) { return [ '
', ' \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e', "
" ].join(""); } return [ '
', ' ' + escapeHtml(formatDateTime(user.lastForumPostAt)) + "", ' ' + escapeHtml(user.lastForumTopicSubject || getTopicLabel(user.lastForumTopicId, "")) + "", "
" ].join(""); } function buildCountCellMarkup(user) { var prefix = user.recentCount30dExact ? "" : "\u2265"; return [ '
', ' ' + prefix + escapeHtml(String(Math.max(0, user.recentCount30d))) + "", "
" ].join(""); } function buildActivityCellMarkup(user) { var cleanupOk = isCleanupSafe(user); var appearanceOk = isAppearanceSafe(user, Date.now()); return [ '
', ' ' + (cleanupOk ? "\u0430\u043a\u0442\u0438\u0432\u0435\u043d" : "\u0432 \u0447\u0438\u0441\u0442\u043a\u0443") + "", ' \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f: ' + (appearanceOk ? "\u043e\u043a" : "\u0440\u0438\u0441\u043a") + "", "
" ].join(""); } function buildSnsCellMarkup(user) { var topicIds = Array.isArray(user.snsTopicIds) ? user.snsTopicIds.filter(Boolean) : []; var links = topicIds.slice(0, 12).map(function (topicId) { return '\u0442\u0435\u043c\u0430 #' + escapeHtml(String(topicId)) + ""; }).join(""); return [ '
', ' ' + escapeHtml(String(Math.max(0, user.snsCount))) + "", links, topicIds.length > 12 ? ' \u0438 \u0435\u0449\u0435 ' + escapeHtml(String(topicIds.length - 12)) + "" : "", "
" ].join(""); } function buildAppearanceBadge(user, now) { var ok = isAppearanceSafe(user, now); return '\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f: ' + (ok ? "\u043e\u043a" : "\u0440\u0438\u0441\u043a") + ""; } function buildCleanupBadge(user) { var ok = isCleanupSafe(user); return '\u0447\u0438\u0441\u0442\u043a\u0430: ' + (ok ? "\u043e\u043a" : "\u0440\u0438\u0441\u043a") + ""; } function isAppearanceSafe(user, now) { return !!(user && user.lastForumPostAt && now - user.lastForumPostAt <= APPEARANCE_WINDOW_MS); } function isCleanupSafe(user) { return !!( user && ( (user.lastGamePostAt && Date.now() - user.lastGamePostAt <= GAME_ACTIVITY_WINDOW_MS) || Math.max(0, toInteger(user.snsCount)) >= CLEANUP_SNS_REQUIRED ) ); } function buildFooterMarkup(registry) { var parts = []; if (registry.updatedAt) { parts.push("\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435: " + formatDateTime(registry.updatedAt)); } if (registry.lastAttemptAt) { parts.push("\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u044f\u044f \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438: " + formatDateTime(registry.lastAttemptAt)); } if (registry.reportHistory && registry.reportHistory.length) { parts.push("\u041f\u0440\u043e\u0448\u043b\u044b\u0445 \u043f\u0435\u0440\u0438\u043e\u0434\u043e\u0432: " + registry.reportHistory.length + " / 3"); } if (registry.lock && registry.lock.token) { parts.push("\u0420\u0435\u0435\u0441\u0442\u0440 \u0441\u0435\u0439\u0447\u0430\u0441 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c #" + registry.lock.userId + "."); } return parts.length ? '" : ""; } function showCleanupConfirmModal() { var overlay = document.getElementById("espresso-activity-confirm-modal"); if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); } overlay = document.createElement("div"); overlay.id = "espresso-activity-confirm-modal"; overlay.className = "espresso-activity-modal"; overlay.innerHTML = [ '" ].join(""); (root || document.body).appendChild(overlay) overlay.addEventListener("click", function (event) { var action = event.target && event.target.getAttribute ? event.target.getAttribute("data-activity-confirm-action") : ""; if (event.target === overlay || action === "cancel") { overlay.parentNode.removeChild(overlay); return; } if (action === "continue") { overlay.parentNode.removeChild(overlay); saveCleanupReport(); } }); } function showCleanupReportModal(report, title) { var normalized = normalizePeriodSnapshot(report) || createEmptyPeriodSnapshot(Date.now()); var overlay = document.getElementById("espresso-activity-cleanup-modal"); var period = normalized.periodStartedAt && normalized.periodEndedAt ? formatDate(normalized.periodStartedAt) + " - " + formatDate(normalized.periodEndedAt) : "\u043d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445"; var heading = normalizeWhitespace(title) || "\u0421\u043f\u0438\u0441\u043e\u043a \u0447\u0438\u0441\u0442\u043a\u0438"; var metaParts = [ "\u041f\u0435\u0440\u0438\u043e\u0434: " + escapeHtml(period) + "" ]; if (normalized.cleanupCount) { metaParts.push("\u041f\u043e \u043c\u0435\u0441\u044f\u0447\u043d\u043e\u0439 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438: " + escapeHtml(String(normalized.cleanupCount)) + ""); } if (normalized.appearanceCount) { metaParts.push("\u0411\u043e\u043b\u044c\u0448\u0435 7 \u0434\u043d\u0435\u0439 \u0431\u0435\u0437 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439: " + escapeHtml(String(normalized.appearanceCount)) + ""); } if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); } overlay = document.createElement("div"); overlay.id = "espresso-activity-cleanup-modal"; overlay.className = "espresso-activity-modal"; overlay.innerHTML = [ '" ].join(""); (root || document.body).appendChild(overlay) overlay.addEventListener("click", function (event) { var closeButton = event.target && event.target.closest ? event.target.closest("[data-activity-modal-close='1']") : null; var copyButton = event.target && event.target.closest ? event.target.closest("[data-activity-copy-html='1']") : null; if (closeButton) { overlay.parentNode.removeChild(overlay); return; } if (copyButton) { copyReportHtmlFromButton(copyButton); } }); } function showReportHistoryModal(history) { var reports = normalizeReportHistory(history); var overlay = document.getElementById("espresso-activity-cleanup-modal"); if (!reports.length) { reports = [createEmptyPeriodSnapshot(Date.now())]; } if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); } overlay = document.createElement("div"); overlay.id = "espresso-activity-cleanup-modal"; overlay.className = "espresso-activity-modal"; overlay.innerHTML = [ '" ].join(""); (root || document.body).appendChild(overlay) overlay.addEventListener("click", function (event) { var target = event.target; var tabIndex = target && target.getAttribute ? target.getAttribute("data-activity-history-tab") : ""; var closeButton = target && target.closest ? target.closest("[data-activity-modal-close='1']") : null; var tabButton = target && target.closest ? target.closest("[data-activity-history-tab]") : null; tabIndex = tabButton ? tabButton.getAttribute("data-activity-history-tab") : ""; if (closeButton) { overlay.parentNode.removeChild(overlay); return; } if (tabIndex !== "") { activateReportHistoryTab(overlay, toInteger(tabIndex)); return; } }); } function copyReportHtmlFromButton(button) { var panel = button.closest(".espresso-activity-modal__panel") || button.closest(".espresso-activity-modal__dialog"); var activePanel = panel && panel.querySelector ? panel.querySelector(".espresso-activity-modal__panel:not([hidden])") : null; var field = activePanel ? activePanel.querySelector(".espresso-activity-modal__html") : panel ? panel.querySelector(".espresso-activity-modal__html") : null; var text = field ? field.value : ""; if (!field) { return; } copyTextToClipboard(text).then(function () { setCopyButtonState(button, "\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u043e"); }).catch(function () { try { field.focus(); field.select(); } catch (error) {} setCopyButtonState(button, "\u0412\u044b\u0434\u0435\u043b\u0435\u043d\u043e"); }); } function copyTextToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text); } return new Promise(function (resolve, reject) { var textarea = document.createElement("textarea"); var ok; textarea.value = text; textarea.setAttribute("readonly", "readonly"); textarea.style.position = "fixed"; textarea.style.left = "-9999px"; textarea.style.top = "-9999px"; document.body.appendChild(textarea); textarea.focus(); textarea.select(); try { ok = document.execCommand("copy"); } catch (error) { ok = false; } textarea.parentNode.removeChild(textarea); if (ok) { resolve(); } else { reject(new Error("copy failed")); } }); } function setCopyButtonState(button, label) { var original = button.getAttribute("data-activity-copy-label") || button.textContent; button.setAttribute("data-activity-copy-label", original); button.textContent = label; window.clearTimeout(toInteger(button.getAttribute("data-activity-copy-timer"))); button.setAttribute("data-activity-copy-timer", window.setTimeout(function () { button.textContent = original; }, 1400)); } function buildReportHistoryTabsMarkup(reports) { return [ '
', reports.map(function (report, index) { return '"; }).join(""), "
" ].join(""); } function buildReportHistoryPanelMarkup(report, index) { var normalized = normalizePeriodSnapshot(report) || createEmptyPeriodSnapshot(Date.now()); var metaParts = [ "\u041f\u0435\u0440\u0438\u043e\u0434: " + escapeHtml(getReportPeriodLabel(normalized)) + "", "\u041f\u043e \u043c\u0435\u0441\u044f\u0447\u043d\u043e\u0439 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438: " + escapeHtml(String(normalized.cleanupCount)) + "" ]; return [ '
", '

' + metaParts.join(". ") + ".

", '
' + buildReportPreviewMarkup(normalized) + "
", "
" ].join(""); } function activateReportHistoryTab(overlay, index) { Array.prototype.slice.call(overlay.querySelectorAll("[data-activity-history-tab]")).forEach(function (tab) { var active = toInteger(tab.getAttribute("data-activity-history-tab")) === index; tab.className = active ? "espresso-activity-modal__tab espresso-activity-modal__tab--active" : "espresso-activity-modal__tab"; }); Array.prototype.slice.call(overlay.querySelectorAll("[data-activity-history-panel]")).forEach(function (panel) { var active = toInteger(panel.getAttribute("data-activity-history-panel")) === index; panel.hidden = !active; }); } function getReportPeriodLabel(report) { var normalized = normalizePeriodSnapshot(report); if (!normalized || !normalized.periodStartedAt || !normalized.periodEndedAt) { return "\u043d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445"; } return formatDate(normalized.periodStartedAt) + " - " + formatDate(normalized.periodEndedAt); } function buildReportPreviewMarkup(report) { var cleanupUsers = Array.isArray(report && report.cleanupUsers) ? report.cleanupUsers : []; var appearanceUsers = Array.isArray(report && report.appearanceUsers) ? report.appearanceUsers : []; if (appearanceUsers.length && !cleanupUsers.length) { return [ "

2) \u041d\u0435 \u043e\u0441\u0442\u0430\u0432\u043b\u044f\u043b\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 7 \u0434\u043d\u0435\u0439

", buildMessageRiskPreviewTable(appearanceUsers) ].join(""); } if (cleanupUsers.length) { return [ "

1) \u041d\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u044b \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438: 1 \u043f\u043e\u0441\u0442 / 25 SNS / 100 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0432 \u043c\u0435\u0441\u044f\u0446

", buildActivityPreviewTable(cleanupUsers) ].join(""); } return report && report.cleanupHtml ? report.cleanupHtml : ""; } function buildMessageRiskPreviewTable(users) { return [ '', " ", " ", users.map(function (user) { return [ "", ' ", " ", "" ].join(""); }).join(""), " ", "
\u0418\u0433\u0440\u043e\u043a\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435
' + escapeHtml(user.username || ("id " + user.userId)) + "" + escapeHtml(user.lastForumPostAt ? formatDateTime(user.lastForumPostAt) : "\u043d\u0435\u0442 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439") + "
" ].join(""); } function buildActivityPreviewTable(users) { return [ '', " ", " ", users.map(function (user) { return [ "", ' ", " ", " ", " ", " ", "" ].join(""); }).join(""), " ", "
\u0418\u0433\u0440\u043e\u043a\u0421\u043e\u043e\u0431\u0449.SNS\u0418\u0433\u0440\u043e\u0432\u043e\u0439\u041f\u043e\u0441\u043b. \u0441\u043e\u043e\u0431\u0449.
' + escapeHtml(user.username || ("id " + user.userId)) + "" + escapeHtml(String(Math.max(0, user.recentCount30d))) + "" + escapeHtml(String(Math.max(0, user.snsCount))) + "" + (user.lastGamePostAt ? escapeHtml(formatDate(user.lastGamePostAt)) : "-") + "" + (user.lastForumPostAt ? escapeHtml(formatDateTime(user.lastForumPostAt)) : "-") + "
" ].join(""); } function getDisplayOrder(registry) { var normalized = normalizeRegistry(registry); var baseOrder = normalized.order.filter(function (value, index, list) { return toInteger(value) > 0 && list.indexOf(value) === index; }); return sortDisplayOrder(baseOrder, normalized.users); } function sortDisplayOrder(order, users) { var key = normalizeSortKey(state.sort.key); var direction = normalizeSortDirection(state.sort.direction); var sorted; if (!key) { return order.slice(); } sorted = order.slice(); sorted.sort(function (leftId, rightId) { return compareUsersBySort(users[leftId], users[rightId], key, direction); }); return sorted; } function compareUsersBySort(leftUser, rightUser, key, direction) { var leftValue; var rightValue; var leftMissing; var rightMissing; var diff; if (key === "cleanupRisk") { leftValue = getCleanupRiskScore(leftUser); rightValue = getCleanupRiskScore(rightUser); if (leftValue !== rightValue) { return direction === "asc" ? leftValue - rightValue : rightValue - leftValue; } leftValue = getActivityScore(leftUser); rightValue = getActivityScore(rightUser); if (leftValue !== rightValue) { return leftValue - rightValue; } return compareUsersByName(leftUser, rightUser); } if (key === "recentCount30d" || key === "snsCount") { leftValue = Math.max(0, toInteger(leftUser && leftUser[key])); rightValue = Math.max(0, toInteger(rightUser && rightUser[key])); if (leftValue !== rightValue) { return direction === "asc" ? leftValue - rightValue : rightValue - leftValue; } return compareUsersByName(leftUser, rightUser); } leftValue = toInteger(leftUser && leftUser[key]); rightValue = toInteger(rightUser && rightUser[key]); leftMissing = leftValue <= 0; rightMissing = rightValue <= 0; if (leftMissing || rightMissing) { if (leftMissing && rightMissing) { return compareUsersByName(leftUser, rightUser); } return leftMissing ? 1 : -1; } diff = direction === "asc" ? leftValue - rightValue : rightValue - leftValue; if (diff !== 0) { return diff; } return compareUsersByName(leftUser, rightUser); } function compareUsersByName(leftUser, rightUser) { var leftName = normalizeWhitespace(leftUser && leftUser.username).toLowerCase(); var rightName = normalizeWhitespace(rightUser && rightUser.username).toLowerCase(); var leftId = toInteger(leftUser && leftUser.userId); var rightId = toInteger(rightUser && rightUser.userId); if (leftName < rightName) { return -1; } if (leftName > rightName) { return 1; } return leftId - rightId; } function getCleanupRiskScore(user) { if (!user || normalizeUserStatus(user.status) === "missing") { return -1; } return isCleanupSafe(normalizeUserRecord(user, user && user.userId)) ? 0 : 1; } function getActivityScore(user) { var normalized = normalizeUserRecord(user, user && user.userId); var gameScore = normalized.lastGamePostAt && Date.now() - normalized.lastGamePostAt <= GAME_ACTIVITY_WINDOW_MS ? 1 : 0; var snsScore = Math.min(1, Math.max(0, normalized.snsCount) / CLEANUP_SNS_REQUIRED); return Math.max(gameScore, snsScore); } function toggleSort(key) { var normalizedKey = normalizeSortKey(key); if (!normalizedKey) { return; } if (state.sort.key === normalizedKey) { state.sort.direction = state.sort.direction === "asc" ? "desc" : "asc"; } else { state.sort.key = normalizedKey; state.sort.direction = getInitialSortDirection(normalizedKey); } render(); } function normalizeSortKey(key) { var value = normalizeWhitespace(key); return SORTABLE_KEYS[value] ? value : ""; } function normalizeSortDirection(direction) { return direction === "asc" ? "asc" : "desc"; } function getInitialSortDirection(key) { if (key === "cleanupRisk") { return "desc"; } if (key === "recentCount30d" || key === "snsCount") { return "desc"; } return "desc"; } function getSortTitle(key, direction, isActive) { var normalizedDirection = normalizeSortDirection(direction); if (!isActive) { return "\u041d\u0430\u0436\u043c\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u043a\u0443."; } if (key === "cleanupRisk") { return normalizedDirection === "asc" ? "\u0421\u0435\u0439\u0447\u0430\u0441 \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0435. \u041a\u043b\u0438\u043a \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442 \u043d\u0430 \u0440\u0438\u0441\u043a \u0447\u0438\u0441\u0442\u043a\u0438." : "\u0421\u0435\u0439\u0447\u0430\u0441 \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u0442\u0435, \u043a\u0442\u043e \u043f\u043e\u043f\u0430\u0434\u0430\u0435\u0442 \u0432 \u0447\u0438\u0441\u0442\u043a\u0443. \u041a\u043b\u0438\u043a \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442 \u043d\u0430 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445."; } if (key === "recentCount30d" || key === "snsCount") { return normalizedDirection === "asc" ? "\u0421\u0435\u0439\u0447\u0430\u0441 \u043e\u0442 \u043c\u0435\u043d\u044c\u0448\u0435\u0433\u043e \u043a \u0431\u043e\u043b\u044c\u0448\u0435\u043c\u0443. \u041a\u043b\u0438\u043a \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442 \u043d\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043f\u043e\u0440\u044f\u0434\u043e\u043a." : "\u0421\u0435\u0439\u0447\u0430\u0441 \u043e\u0442 \u0431\u043e\u043b\u044c\u0448\u0435\u0433\u043e \u043a \u043c\u0435\u043d\u044c\u0448\u0435\u043c\u0443. \u041a\u043b\u0438\u043a \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442 \u043d\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043f\u043e\u0440\u044f\u0434\u043e\u043a."; } return normalizedDirection === "asc" ? "\u0421\u0435\u0439\u0447\u0430\u0441 \u0431\u043e\u043b\u0435\u0435 \u0434\u0430\u0432\u043d\u0438\u0435 \u0434\u0430\u0442\u044b \u0441\u0432\u0435\u0440\u0445\u0443. \u041a\u043b\u0438\u043a \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442 \u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u044b\u0435." : "\u0421\u0435\u0439\u0447\u0430\u0441 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u044b\u0435 \u0434\u0430\u0442\u044b \u0441\u0432\u0435\u0440\u0445\u0443. \u041a\u043b\u0438\u043a \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442 \u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u0434\u0430\u0432\u043d\u0438\u0435."; } function normalizePostItem(item) { var post = item && typeof item === "object" ? item : {}; return { id: toInteger(post.id || post.post_id), userId: toInteger(post.user_id || post.poster_id), username: normalizeWhitespace(post.username || post.user_name), postedAt: normalizeTimestamp(post.posted || post.posted_at || post.created || post.created_at || post.date), topicId: toInteger(post.topic_id || post.topic || post.topicId), forumId: toInteger(post.forum_id || post.forum || post.forumId), topicFirstPostId: toInteger(post.first_post_id || post.firstPostId || post.topic_first_post_id), subject: normalizeWhitespace(post.subject || post.topic_name || post.topic_subject) }; } function normalizeTopicItem(item) { var topic = item && typeof item === "object" ? item : {}; return { id: toInteger(topic.id || topic.topic_id), forumId: toInteger(topic.forum_id || topic.forum || topic.forumId), firstPostId: toInteger(topic.first_post_id || topic.firstPostId || topic.post_id_start), lastPostAt: normalizeTimestamp(topic.last_post_date || topic.lastPostDate || topic.last_posted || topic.updated_at), subject: normalizeWhitespace(topic.subject || topic.topic_name || topic.topic_subject) }; } async function isEligibleGamePost(post, snsPostCache) { var snsCheck; if (!post || GAME_FORUM_IDS.indexOf(post.forumId) === -1) { return false; } if (SNS_HTML_DETECT_FORUM_IDS.indexOf(post.forumId) !== -1) { snsCheck = await detectSnsPost(post, snsPostCache || {}); if (snsCheck && snsCheck.matched) { return false; } } if (!IGNORE_TOPIC_STARTERS_IN_GAME_FORUMS) { return true; } return !(post.topicFirstPostId > 0 && post.id === post.topicFirstPostId); } function normalizeTimestamp(value) { var raw = String(value == null ? "" : value).trim(); var numeric; var dateValue; if (!raw) { return 0; } if (/^\d+$/.test(raw)) { numeric = parseInt(raw, 10); return numeric > 1000000000000 ? numeric : numeric * 1000; } dateValue = Date.parse(raw); return isFinite(dateValue) ? dateValue : 0; } function getTopicLabel(topicId, subject) { var cleanSubject = normalizeWhitespace(subject); if (cleanSubject) { return cleanSubject; } return topicId ? "\u0422\u0435\u043c\u0430 #" + topicId : "\u0422\u0435\u043c\u0430"; } function buildPostUrl(topicId, postId) { if (postId) { return location.origin + "/viewtopic.php?pid=" + encodeURIComponent(postId) + "#p" + encodeURIComponent(postId); } if (!topicId) { return "#"; } return location.origin + "/viewtopic.php?id=" + encodeURIComponent(topicId); } function buildPostLookupUrls(topicId, postId) { var urls = []; var directPostUrl = postId ? location.origin + "/viewtopic.php?pid=" + encodeURIComponent(postId) + "#p" + encodeURIComponent(postId) : ""; var topicAnchorUrl = topicId && postId ? location.origin + "/viewtopic.php?id=" + encodeURIComponent(topicId) + "#p" + encodeURIComponent(postId) : ""; var topicPlainUrl = topicId ? location.origin + "/viewtopic.php?id=" + encodeURIComponent(topicId) : ""; [directPostUrl, topicAnchorUrl, topicPlainUrl].forEach(function (url) { if (url && urls.indexOf(url) === -1) { urls.push(url); } }); return urls; } function buildTopicUrl(topicId) { if (!topicId) { return "#"; } return location.origin + "/viewtopic.php?id=" + encodeURIComponent(topicId); } function buildProfileUrl(userId) { return location.origin + "/profile.php?id=" + encodeURIComponent(userId); } function createToken() { return String(Date.now()) + ":" + Math.random().toString(36).slice(2); } function formatDateTime(timestamp) { var date; if (!timestamp) { return ""; } date = new Date(timestamp); if (!isFinite(date.getTime())) { return ""; } return [ date.getFullYear(), "-", pad2(date.getMonth() + 1), "-", pad2(date.getDate()), " ", pad2(date.getHours()), ":", pad2(date.getMinutes()) ].join(""); } function formatDate(timestamp) { var date; if (!timestamp) { return ""; } date = new Date(timestamp); if (!isFinite(date.getTime())) { return ""; } return [ date.getFullYear(), "-", pad2(date.getMonth() + 1), "-", pad2(date.getDate()) ].join(""); } function pad2(value) { return value < 10 ? "0" + value : String(value); } function toAbsoluteUrl(href) { try { return new URL(String(href || ""), location.origin).toString(); } catch (error) { return ""; } } function parseProfileIdFromHref(href) { var match = String(href || "").match(/profile\.php\?id=(\d+)/i); return match ? parseInt(match[1], 10) : 0; } function getTopicId() { if (window.location.pathname.indexOf("viewtopic.php") !== -1) { return getQueryId(); } return ( findNumericGlobal([window.TopicID, window.topicID, window.topic_id]) || extractNumberFromScripts("TopicID") ); } function getUserId() { var userId = findNumericGlobal([ window.UserID, window.userID, window.user_id, window.CurrentUserID, window.currentUserId, window.Rusff && window.Rusff.user_id ]); var profileLink; if (userId) { return userId; } userId = extractNumberFromScripts("UserID"); if (userId) { return userId; } profileLink = document.querySelector( "#navprofile a[href*='profile.php?id='], #pun-navlinks a[href*='profile.php?id='], a.mainmenu[href*='profile.php?id=']" ); return parseProfileIdFromHref(profileLink && profileLink.getAttribute("href")); } function getQueryId() { try { return toInteger(new URLSearchParams(window.location.search).get("id")); } catch (error) { return 0; } } function getCurrentGroupId() { return ( findNumericGlobal([ window.GroupID, window.groupID, window.group_id, window.Rusff && window.Rusff.group_id ]) || extractNumberFromScripts("GroupID") ); } function isCurrentUserAdmin() { var groupId = getCurrentGroupId(); if (ADMIN_GROUP_IDS.indexOf(groupId) !== -1) { return true; } return !!document.querySelector( "#navadmin a, #admconsole a, a[href*='admin_index.php'], a[href*='admin.php'], a[href*='admin_pages.php']" ); } function findNumericGlobal(names) { var index; var candidate; for (index = 0; index < names.length; index += 1) { candidate = names[index]; if (typeof candidate === "number" && candidate > 0) { return Math.floor(candidate); } if (typeof candidate === "string" && /^\d+$/.test(candidate)) { return parseInt(candidate, 10); } } return 0; } function extractNumberFromScripts(variableName) { var pattern = new RegExp("\\b" + variableName + "\\b\\s*[:=]\\s*[\"']?(\\d+)", "i"); var scripts = document.scripts; var index; var match; for (index = 0; index < scripts.length; index += 1) { match = (scripts[index].textContent || "").match(pattern); if (match) { return parseInt(match[1], 10); } } return 0; } function cloneJson(value) { return JSON.parse(JSON.stringify(value)); } function normalizeWhitespace(value) { return String(value == null ? "" : value) .replace(/\u00a0/g, " ") .replace(/\s+/g, " ") .trim(); } function normalizeHtmlBlock(value) { return String(value == null ? "" : value) .replace(/\u00a0/g, " ") .replace(/\r\n?/g, "\n") .replace(/[ \t]+\n/g, "\n") .trim(); } function toInteger(value) { var normalized = normalizeWhitespace(value).replace(/[^\d-]/g, ""); if (!normalized) { return 0; } return parseInt(normalized, 10) || 0; } function escapeHtml(value) { return String(value == null ? "" : value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } })();