(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("\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 [ "\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) + "\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) + "' + 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 ? '' + escapeHtml(state.anketa.error) + "
" : "", '| \u0418\u0433\u0440\u043e\u043a | ', 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"), "
\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0441\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u0447\u0435\u0442? \u041f\u0440\u0438 \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u043e\u0442\u0447\u0435\u0442\u0430 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u043f\u0435\u0440\u0438\u043e\u0434 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d \u0438 \u043d\u0430\u0447\u0430\u0442 \u043d\u043e\u0432\u044b\u0439, \u0430 \u043f\u0440\u043e\u0433\u0440\u0435\u0441\u0441 \u0438\u0433\u0440\u043e\u043a\u043e\u0432 \u0441\u0431\u0440\u043e\u0441\u0438\u0442\u0441\u044f.
', '| \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") + " | ", "
| \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)) : "-") + " | ", "