const { app, BrowserWindow, ipcMain, dialog } = require("electron"); const path = require("path"); const fs = require("fs"); const { execFileSync } = require("child_process"); const { initStore } = require("./store"); const { TelegramManager } = require("./telegram"); const { Scheduler } = require("./scheduler"); const { TaskRunner } = require("./taskRunner"); let mainWindow; let store; let telegram; let scheduler; const taskRunners = new Map(); const formatTimestamp = (value) => { if (!value) return "—"; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); return date.toLocaleString("ru-RU"); }; const isRetryableInviteError = (errorText) => { const error = String(errorText || ""); if (!error) return true; const nonRetryable = [ "USER_ID_INVALID", "USER_NOT_MUTUAL_CONTACT", "INVITE_MISSING_INVITEE", "FROZEN_METHOD_INVALID", "CHAT_MEMBER_ADD_FAILED", "USER_BANNED_IN_CHANNEL", "USER_KICKED", "SOURCE_ADMIN_SKIPPED", "SOURCE_BOT_SKIPPED" ]; if (nonRetryable.some((code) => error.includes(code))) return false; if (error.includes("FLOOD") || error.includes("PEER_FLOOD")) return true; if (error.includes("TIMEOUT") || error.includes("NETWORK")) return true; return true; }; const filterTaskRolesByAccounts = (taskId, roles, accounts) => { const accountMap = new Map(accounts.map((acc) => [acc.id, acc])); const filtered = []; let removedMissing = 0; let removedError = 0; roles.forEach((row) => { const account = accountMap.get(row.account_id); if (!account) { removedMissing += 1; return; } const roleMonitorRaw = Boolean(row.role_monitor); const roleInviteRaw = Boolean(row.role_invite); const roleConfirmRaw = row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite); const monitorAvailable = roleMonitorRaw ? (telegram && typeof telegram.isAccountConnected === "function" && typeof telegram.isAccountGeneralBlocked === "function" ? (telegram.isAccountConnected(account.id) && !telegram.isAccountGeneralBlocked(account.id)) : ((account.status || "ok") === "ok" && !isCooldownActive(account))) : false; const inviteAvailable = roleInviteRaw ? (telegram && typeof telegram.isInviteAccountAvailable === "function" ? telegram.isInviteAccountAvailable(account.id) : ((account.status || "ok") === "ok" && !isCooldownActive(account))) : false; const confirmAvailable = roleConfirmRaw ? (telegram && typeof telegram.isAccountConnected === "function" && typeof telegram.isAccountGeneralBlocked === "function" ? (telegram.isAccountConnected(account.id) && !telegram.isAccountGeneralBlocked(account.id)) : ((account.status || "ok") === "ok" && !isCooldownActive(account))) : false; if (!monitorAvailable && !inviteAvailable && !confirmAvailable) { removedError += 1; return; } filtered.push({ accountId: row.account_id, roleMonitor: monitorAvailable, roleInvite: inviteAvailable, roleConfirm: confirmAvailable, inviteLimit: Number(row.invite_limit || 0) }); }); if (removedMissing || removedError) { store.setTaskAccountRoles(taskId, filtered); } return { filtered, removedMissing, removedError }; }; const isCooldownActive = (account) => { if (!account || !account.cooldown_until) return false; try { return new Date(account.cooldown_until).getTime() > Date.now(); } catch { return false; } }; const formatAccountLabel = (account, fallbackId = 0) => { if (!account) return String(fallbackId || "—"); const base = account.phone || account.user_id || account.id || fallbackId || "—"; const username = account.username ? ` (@${account.username})` : ""; return `${base}${username}`; }; const normalizeGroupLinks = (links = []) => Array.from(new Set( (links || []) .flatMap((value) => String(value || "").split(/\r?\n/)) .map((value) => value.trim()) .filter(Boolean) )); const getTaskCompetitorLinks = (taskId) => normalizeGroupLinks(store.listTaskCompetitors(taskId).map((row) => row.link)); const refreshTaskAccountIdentities = async (taskId, taskAccounts = []) => { if (!telegram || !Array.isArray(taskAccounts) || !taskAccounts.length) { return { refreshed: 0, failed: 0 }; } const accountIds = [...new Set( taskAccounts .map((row) => Number(row.account_id || 0)) .filter((id) => id > 0) )]; let refreshed = 0; let failed = 0; for (const accountId of accountIds) { try { await telegram.refreshAccountIdentity(accountId); refreshed += 1; } catch { failed += 1; } } store.addAccountEvent( 0, "", "identity_refresh_auto", `задача ${taskId}: авто-обновление ID перед запуском (${refreshed}/${accountIds.length})` ); return { refreshed, failed }; }; const describeAccountRestriction = (account) => { if (!account) return "аккаунт не найден"; if (telegram && typeof telegram.isAccountConnected === "function" && !telegram.isAccountConnected(account.id)) { return "сессия не подключена"; } if (account.status && account.status !== "ok") { const err = account.last_error ? `: ${account.last_error}` : ""; return `статус ${account.status}${err}`; } if (isCooldownActive(account)) { const until = formatTimestamp(account.cooldown_until); const reason = account.cooldown_reason ? ` (${account.cooldown_reason})` : ""; return `в cooldown до ${until}${reason}`; } return ""; }; const startTaskWithChecks = async (id) => { const task = store.getTask(id); if (!task) return { ok: false, error: "Task not found" }; const competitors = getTaskCompetitorLinks(id); const taskAccounts = store.listTaskAccounts(id); await refreshTaskAccountIdentities(id, taskAccounts); const existingAccounts = store.listAccounts(); const filteredResult = filterTaskRolesByAccounts(id, taskAccounts, existingAccounts); const filteredRoles = filteredResult.filtered; let adminPrepPartialWarning = ""; const accountsById = new Map(existingAccounts.map((acc) => [acc.id, acc])); const assignedRestrictions = taskAccounts .map((row) => ({ row, account: accountsById.get(row.account_id) })) .filter(({ account }) => !account || describeAccountRestriction(account)) .map(({ row, account }) => { const roleParts = []; if (row.role_monitor) roleParts.push("мониторинг"); if (row.role_invite) roleParts.push("инвайт"); if (row.role_confirm) roleParts.push("подтверждение"); const roles = roleParts.length ? ` (${roleParts.join("/")})` : ""; const label = formatAccountLabel(account, row.account_id); const reason = describeAccountRestriction(account); return `${label}${roles}: ${reason}`; }); const isGeneralAvailable = (account) => { if (!account) return false; if (telegram && typeof telegram.isAccountConnected === "function" && typeof telegram.isAccountGeneralBlocked === "function") { return telegram.isAccountConnected(account.id) && !telegram.isAccountGeneralBlocked(account.id); } if (account.status !== "ok") return false; return !isCooldownActive(account); }; const isInviteAvailable = (account) => { if (!account) return false; if (telegram && typeof telegram.isInviteAccountAvailable === "function") { return telegram.isInviteAccountAvailable(account.id); } return isGeneralAvailable(account); }; if (!filteredRoles.length) { const details = assignedRestrictions.length ? ` Недоступные аккаунты: ${assignedRestrictions.join("; ")}` : ""; const reason = `Нет доступных аккаунтов для запуска.${details}`; store.addAccountEvent(0, "", "invite_skipped", `задача ${id}: ${reason}`); return { ok: false, error: reason }; } const inviteIds = filteredRoles .filter((row) => row.roleInvite && Number(row.inviteLimit || 0) > 0) .map((row) => row.accountId); const monitorIds = filteredRoles.filter((row) => row.roleMonitor).map((row) => row.accountId); if (!inviteIds.length) { const inviteRoleRows = taskAccounts.filter((row) => Boolean(row.role_invite)); const blockedInvite = inviteRoleRows .map((row) => accountsById.get(row.account_id)) .filter((acc) => !isInviteAvailable(acc)) .map((acc) => `${formatAccountLabel(acc)}: ${describeAccountRestriction(acc)}`) .filter(Boolean); const reason = blockedInvite.length ? `Нет доступных аккаунтов с ролью инвайта. Причины: ${blockedInvite.join("; ")}` : "Нет доступных аккаунтов с ролью инвайта (все в ограничении/спаме)."; store.addAccountEvent(0, "", "invite_skipped", `задача ${id}: ${reason}`); return { ok: false, error: reason }; } if (!monitorIds.length) { return { ok: false, error: "Нет аккаунтов с ролью мониторинга." }; } const confirmCheckIds = task.separate_confirm_roles ? filteredRoles .filter((row) => row.roleConfirm && !row.roleInvite) .map((row) => row.accountId) .filter((accountId) => isGeneralAvailable(accountsById.get(accountId))) : [...inviteIds]; if (task.separate_confirm_roles && !confirmCheckIds.length) { const reason = "Не хватает доступных аккаунтов для роли подтверждения (отдельные роли)."; store.addAccountEvent(0, "", "invite_skipped", `задача ${id}: ${reason}`); return { ok: false, error: reason }; } if (confirmCheckIds.length) { const confirmAccess = await telegram.checkConfirmAccess(task, confirmCheckIds); if (!confirmAccess || !confirmAccess.ok) { return { ok: false, error: (confirmAccess && confirmAccess.error) || "Не удалось проверить аккаунты подтверждения." }; } const checkRows = Array.isArray(confirmAccess.result) ? confirmAccess.result : []; const confirmReady = checkRows.filter((item) => item && item.ok && item.canConfirm !== false); const failedConfirm = checkRows.filter((item) => !item || !item.ok || item.canConfirm === false); const runtimeTempConfirmMode = Boolean(task.invite_via_admins) && !Boolean(task.separate_confirm_roles); if (!confirmReady.length) { const details = failedConfirm .map((item) => { const username = item && item.accountUsername ? `(@${item.accountUsername})` : ""; const label = item && item.accountPhone ? `${item.accountPhone}${username ? ` ${username}` : ""}` : `${item && item.accountId ? item.accountId : "—"}${username ? ` ${username}` : ""}`; return `${label}: ${item && item.reason ? item.reason : "нет доступа"}`; }) .join("; "); const reason = `Проверка подтверждения перед запуском: нет аккаунтов с правом подтверждения участия.${details ? ` ${details}` : ""}`; if (runtimeTempConfirmMode) { store.addAccountEvent( 0, "", "confirm_preflight_warn", `задача ${id}: ${reason} Режим инвайта через админов включен — запуск продолжается, проверка будет через runtime-выдачу прав.` ); } else { store.addAccountEvent(0, "", "invite_skipped", `задача ${id}: ${reason}`); return { ok: false, error: reason }; } } if (failedConfirm.length) { const details = failedConfirm .slice(0, 5) .map((item) => { const username = item && item.accountUsername ? `(@${item.accountUsername})` : ""; const label = item && item.accountPhone ? `${item.accountPhone}${username ? ` ${username}` : ""}` : `${item && item.accountId ? item.accountId : "—"}${username ? ` ${username}` : ""}`; return `${label}: ${item && item.reason ? item.reason : "нет доступа"}`; }) .join("; "); store.addAccountEvent( 0, "", "confirm_preflight_warn", `задача ${id}: часть аккаунтов не может подтверждать участие. ${details}` ); } } const accessCheck = await telegram.checkGroupAccess(competitors, task.our_group); if (accessCheck && accessCheck.ok) { const ourAccess = accessCheck.result.find((item) => item.type === "our"); if (ourAccess && !ourAccess.ok) { return { ok: false, error: `Нет доступа к нашей группе: ${ourAccess.details || ourAccess.value}` }; } } const inviteAccess = await telegram.checkInvitePermissions(task, inviteIds); let adminGrantIds = [...inviteIds]; if (inviteAccess && inviteAccess.ok) { store.setTaskInviteAccess(id, inviteAccess.result || []); const canInvite = (inviteAccess.result || []).filter((row) => row.canInvite); const byId = new Map((inviteAccess.result || []).map((row) => [Number(row.accountId), row])); adminGrantIds = inviteIds.filter((accountId) => { const row = byId.get(Number(accountId)); // If permissions were not checked for this account, keep it in grant list. if (!row) return true; // Grant when account cannot invite yet. if (!row.canInvite) return true; // If anonymous mode is enabled, sync inviters to anonymous admins as well. if (task.invite_via_admins && task.invite_admin_anonymous) { const isAnonymousAdmin = Boolean(row.isAdmin && row.adminRights && row.adminRights.anonymous); if (!isAnonymousAdmin) return true; } return false; }); if (!canInvite.length && !task.allow_start_without_invite_rights) { const rows = inviteAccess.result || []; const notMembers = rows.filter((row) => row.member === false); const noRights = rows.filter((row) => row.member !== false && row.ok && !row.canInvite); const noSession = rows.filter((row) => row.ok === false && row.reason === "Сессия не подключена"); const buildList = (list) => list .map((row) => { const label = row.accountPhone || row.accountId || "—"; const reason = row.reason ? ` (${row.reason})` : ""; return `${label}${reason}`; }) .join(", "); let reason = "Нет аккаунтов с правами инвайта в нашей группе."; if (notMembers.length) { const list = buildList(notMembers); reason = `Инвайт невозможен: инвайтеры не состоят в нашей группе${list ? `: ${list}` : ""}.`; } else if (noRights.length) { const list = buildList(noRights); reason = `Инвайт невозможен: в нашей группе у инвайтеров нет права «Приглашать»${list ? `: ${list}` : ""}.`; } else if (noSession.length) { const list = buildList(noSession); reason = `Инвайт невозможен: сессии инвайтеров не подключены${list ? `: ${list}` : ""}.`; } if (task.invite_via_admins) { store.addAccountEvent( 0, "", "invite_preflight_warn", `задача ${id}: ${reason} Режим «Инвайт через админов» включен — запуск продолжается, права будут выданы в runtime.` ); } else { store.addAccountEvent(0, "", "invite_skipped", `задача ${id}: ${reason}`); return { ok: false, error: reason }; } } } else if (inviteAccess && inviteAccess.error) { return { ok: false, error: inviteAccess.error }; } if (task.invite_via_admins) { const currentMasterId = Number(task.invite_admin_master_id || 0); const currentMasterGeneralAvailable = currentMasterId ? isGeneralAvailable(accountsById.get(currentMasterId)) : false; let currentMasterEligible = false; if (currentMasterId && currentMasterGeneralAvailable) { try { const currentMasterCheck = await telegram.checkInvitePermissions(task, [currentMasterId]); if (currentMasterCheck && currentMasterCheck.ok && Array.isArray(currentMasterCheck.result) && currentMasterCheck.result.length) { const row = currentMasterCheck.result[0]; currentMasterEligible = Boolean( row && row.ok && row.member !== false && row.isAdmin && row.adminRights && row.adminRights.addAdmins && (!task.invite_admin_anonymous || row.adminRights.anonymous) ); } } catch { currentMasterEligible = false; } } if (!currentMasterId || !currentMasterEligible) { const candidateIds = filteredRoles .map((row) => Number(row.accountId || 0)) .filter(Boolean) .filter((id) => id !== currentMasterId) .filter((id) => isGeneralAvailable(accountsById.get(id))); if (candidateIds.length) { try { const access = await telegram.checkInvitePermissions(task, candidateIds); if (access && access.ok && Array.isArray(access.result)) { const eligible = access.result.filter((row) => { if (!row || !row.ok) return false; if (row.member === false) return false; if (!row.isAdmin) return false; const canAddAdmins = Boolean(row.adminRights && row.adminRights.addAdmins); if (!canAddAdmins) return false; if (task.invite_admin_anonymous && !(row.adminRights && row.adminRights.anonymous)) return false; return true; }); const picked = eligible.length ? Number(eligible[0].accountId || 0) : 0; if (picked > 0) { store.setTaskInviteAdminMaster(id, picked); task.invite_admin_master_id = picked; store.addTaskAudit( id, "master_admin_auto_switch", JSON.stringify({ from: currentMasterId || 0, to: picked, reason: "preflight autoselect" }) ); store.addAccountEvent( 0, "", "master_admin_auto_switch", `задача ${id}: мастер-админ переключен ${currentMasterId || "не выбран"} -> ${picked} (preflight)` ); } } } catch { // keep fallback to validation below } } } if (!task.invite_admin_master_id) { return { ok: false, error: "Не выбран главный аккаунт для инвайта через админов (и не найден резерв с addAdmins)." }; } if (!adminGrantIds.length) { store.addAccountEvent(0, "", "admin_grant_summary", `задача ${id}: выдача прав не требуется (все инвайтеры уже могут приглашать)`); } else { const adminPrep = await telegram.prepareInviteAdmins(task, task.invite_admin_master_id, adminGrantIds); if (adminPrep && !adminPrep.ok) { return { ok: false, error: adminPrep.error || "Не удалось подготовить права админов." }; } if (adminPrep && adminPrep.warning) { adminPrepPartialWarning = adminPrep.warning; } if (adminPrep && Array.isArray(adminPrep.result)) { const failed = adminPrep.result.filter((item) => !item.ok); if (failed.length) { const fallbackNote = adminPrep && adminPrep.warning ? " (используется runtime-фолбэк)" : ""; adminPrepPartialWarning = `Часть прав админа не выдана: ${failed.length} аккаунт(ов)${fallbackNote}.`; } } } } let runner = taskRunners.get(id); if (!runner) { runner = new TaskRunner(store, telegram, task); taskRunners.set(id, runner); } else { runner.task = task; } store.setTaskStopReason(id, ""); store.addAccountEvent(0, "", "task_start", `задача ${id}: запуск`); await runner.start(); const warnings = []; if (accessCheck && accessCheck.ok) { const competitorIssues = accessCheck.result.filter((item) => item.type === "competitor" && !item.ok); if (competitorIssues.length) { const list = competitorIssues.map((item) => item.title || item.value).join(", "); warnings.push(`Нет доступа к конкурентам: ${list}.`); } } if (inviteAccess && inviteAccess.ok) { const missingSessions = (inviteAccess.result || []).filter((row) => !row.ok || row.reason === "Сессия не подключена"); if (missingSessions.length) { warnings.push(`Сессии не подключены: ${missingSessions.length} аккаунт(ов).`); } if (task.invite_via_admins) { const noRights = (inviteAccess.result || []).filter((row) => !row.canInvite); if (noRights.length) { warnings.push(`Нет прав инвайта у ${noRights.length} аккаунт(ов).`); } } } if (task.invite_via_admins) { warnings.push("Режим инвайта через админов включен."); if (adminPrepPartialWarning) { warnings.push(adminPrepPartialWarning); } } if (filteredResult.removedError) { warnings.push(`Отключены аккаунты с ошибкой: ${filteredResult.removedError}.`); } if (filteredResult.removedMissing) { warnings.push(`Удалены отсутствующие аккаунты: ${filteredResult.removedMissing}.`); } if (assignedRestrictions.length) { warnings.push(`Есть аккаунты с ограничениями: ${assignedRestrictions.length}.`); } store.addTaskAudit(id, "start", warnings.length ? JSON.stringify({ warnings }) : ""); return { ok: true, warnings }; }; function createWindow() { const iconPath = path.join(__dirname, "..", "..", "resources", "icon.png"); mainWindow = new BrowserWindow({ width: 1200, height: 800, icon: iconPath, webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false } }); const devUrl = process.env.VITE_DEV_SERVER_URL || "http://127.0.0.1:5173"; if (app.isPackaged) { mainWindow.loadFile(path.join(__dirname, "..", "..", "dist", "index.html")); } else { mainWindow.loadURL(devUrl); } } async function bootstrap() { store = initStore(app.getPath("userData")); telegram = new TelegramManager(store); try { await telegram.init(); } catch (error) { console.error("Failed to initialize Telegram clients:", error); } scheduler = new Scheduler(store, telegram); } app.whenReady().then(async () => { await bootstrap(); createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); }); ipcMain.handle("settings:get", () => store.getSettings()); ipcMain.handle("settings:save", (_event, settings) => { const updated = store.saveSettings(settings); if (telegram && typeof telegram.setApiTraceEnabled === "function") { telegram.setApiTraceEnabled(Boolean(updated && updated.apiTraceEnabled)); } return updated; }); ipcMain.handle("accounts:list", () => store.listAccounts()); ipcMain.handle("proxies:list", () => store.listProxies()); ipcMain.handle("proxies:save", async (_event, payload) => { const proxyId = store.saveProxy(payload || {}); const result = await telegram.testProxy({ ...(payload || {}), id: proxyId }); const affectedAccountIds = store.listAccounts() .filter((account) => Number(account.proxy_id || 0) === Number(proxyId)) .map((account) => Number(account.id || 0)) .filter(Boolean); for (const accountId of affectedAccountIds) { await telegram.reconnectAccount(accountId); } return { ok: true, proxyId, test: result }; }); ipcMain.handle("proxies:test", async (_event, payload) => { if (!payload || (payload.id == null && !payload.host)) { return { ok: false, error: "Proxy payload is required" }; } const proxy = payload.id ? store.getProxyById(payload.id) : payload; if (!proxy) return { ok: false, error: "Proxy not found" }; return telegram.testProxy(proxy); }); ipcMain.handle("proxies:delete", async (_event, proxyId) => { const parsedId = Number(proxyId || 0); if (!parsedId) return { ok: false, error: "Invalid proxy id" }; const affectedAccountIds = store.listAccounts() .filter((account) => Number(account.proxy_id || 0) === parsedId) .map((account) => Number(account.id || 0)) .filter(Boolean); store.deleteProxy(parsedId); for (const accountId of affectedAccountIds) { await telegram.reconnectAccount(accountId); } return { ok: true }; }); ipcMain.handle("accounts:resetCooldown", async (_event, accountId) => { store.clearAccountCooldown(accountId); store.addAccountEvent(accountId, "", "manual_reset", "Cooldown reset by user"); return { ok: true }; }); ipcMain.handle("accounts:setProxy", async (_event, payload) => { const accountId = Number(payload && payload.accountId ? payload.accountId : 0); const proxyId = Number(payload && payload.proxyId ? payload.proxyId : 0); if (!accountId) return { ok: false, error: "Invalid account id" }; const proxy = proxyId > 0 ? store.getProxyById(proxyId) : null; if (proxyId > 0 && !proxy) { return { ok: false, error: "Proxy not found" }; } store.assignAccountProxy(accountId, proxyId); const reconnectResult = await telegram.reconnectAccount(accountId); store.addAccountEvent( accountId, "", "proxy_changed", proxyId ? `Назначен proxy #${proxyId}${proxy && proxy.name ? ` (${proxy.name})` : ""}${proxy && proxy.host ? ` ${proxy.host}:${proxy.port}` : ""}` : "Прокси снят" ); return reconnectResult && reconnectResult.ok ? { ok: true } : { ok: false, error: reconnectResult && reconnectResult.error ? reconnectResult.error : "Reconnect failed" }; }); ipcMain.handle("accounts:setProxyBulk", async (_event, payload) => { const proxyId = Number(payload && payload.proxyId ? payload.proxyId : 0); const accountIds = Array.isArray(payload && payload.accountIds) ? payload.accountIds : []; const normalizedIds = accountIds.map((id) => Number(id || 0)).filter((id) => id > 0); if (!normalizedIds.length) return { ok: false, error: "No accounts selected" }; const proxy = proxyId > 0 ? store.getProxyById(proxyId) : null; if (proxyId > 0 && !proxy) { return { ok: false, error: "Proxy not found" }; } const changed = store.assignAccountProxyBulk(normalizedIds, proxyId); let reconnected = 0; let failed = 0; for (const accountId of normalizedIds) { const result = await telegram.reconnectAccount(accountId); if (result && result.ok) reconnected += 1; else failed += 1; } store.addAccountEvent( 0, "", "proxy_bulk_changed", `${proxyId ? `Назначен proxy #${proxyId}${proxy && proxy.name ? ` (${proxy.name})` : ""}${proxy && proxy.host ? ` ${proxy.host}:${proxy.port}` : ""}` : "Прокси снят"}: аккаунтов=${normalizedIds.length}, обновлено=${changed}, reconnect_ok=${reconnected}, reconnect_failed=${failed}` ); return { ok: true, changed, reconnected, failed }; }); ipcMain.handle("accounts:setProxyMap", async (_event, payload) => { const rows = Array.isArray(payload && payload.assignments) ? payload.assignments : []; if (!rows.length) return { ok: false, error: "No assignments" }; const validRows = rows .map((row) => ({ accountId: Number(row && row.accountId ? row.accountId : 0), proxyId: Number(row && row.proxyId ? row.proxyId : 0) })) .filter((row) => row.accountId > 0); if (!validRows.length) return { ok: false, error: "No valid assignments" }; const proxyIds = Array.from(new Set(validRows.map((row) => row.proxyId).filter((id) => id > 0))); for (const proxyId of proxyIds) { if (!store.getProxyById(proxyId)) { return { ok: false, error: `Proxy not found: ${proxyId}` }; } } const changed = store.assignAccountProxyMap(validRows); let reconnected = 0; let failed = 0; for (const row of validRows) { const result = await telegram.reconnectAccount(row.accountId); if (result && result.ok) reconnected += 1; else failed += 1; } store.addAccountEvent( 0, "", "proxy_map_changed", `Карта прокси применена: записей=${validRows.length}, обновлено=${changed}, reconnect_ok=${reconnected}, reconnect_failed=${failed}` ); return { ok: true, changed, reconnected, failed }; }); ipcMain.handle("accounts:delete", async (_event, accountId) => { await telegram.removeAccount(accountId); store.deleteAccount(accountId); store.addAccountEvent(accountId, "", "delete", "Account deleted by user"); return { ok: true }; }); ipcMain.handle("db:clear", async () => { for (const runner of taskRunners.values()) { runner.stop(); } taskRunners.clear(); const accounts = store.listAccounts(); for (const account of accounts) { await telegram.removeAccount(account.id); } store.clearAllData(); return { ok: true }; }); ipcMain.handle("sessions:reset", async () => { for (const runner of taskRunners.values()) { runner.stop(); } taskRunners.clear(); telegram.resetAllSessions("manual_user", { source: "ipc:sessions:reset" }); return { ok: true }; }); ipcMain.handle("accounts:startLogin", async (_event, payload) => { const result = await telegram.startLogin(payload); return result; }); ipcMain.handle("accounts:completeLogin", async (_event, payload) => { const result = await telegram.completeLogin(payload); return result; }); ipcMain.handle("accounts:importTdata", async (_event, payload) => { const { canceled, filePaths } = await dialog.showOpenDialog({ title: "Выберите папку tdata", properties: ["openDirectory", "multiSelections"] }); if (canceled || !filePaths || !filePaths.length) return { ok: false, canceled: true }; const platformDir = process.platform === "win32" ? "win" : "mac"; const binaryName = process.platform === "win32" ? "tgconvertor.exe" : "tgconvertor"; const devBinary = path.join(__dirname, "..", "..", "resources", "converter", platformDir, binaryName); const packagedBinary = path.join(process.resourcesPath, "converter", platformDir, binaryName); const binaryPath = app.isPackaged ? packagedBinary : devBinary; if (!fs.existsSync(binaryPath)) { return { ok: false, error: "Встроенный конвертер не найден. Соберите его через scripts/build-converter.*" }; } const imported = []; const failed = []; const skipped = []; const assignedIds = []; let authKeyDuplicatedCount = 0; for (const chosenPath of filePaths) { let tdataPath = chosenPath; const tdataCandidate = path.join(tdataPath, "tdata"); if (fs.existsSync(tdataCandidate) && fs.lstatSync(tdataCandidate).isDirectory()) { tdataPath = tdataCandidate; } if (path.basename(tdataPath) !== "tdata") { failed.push({ path: chosenPath, error: "Нужна папка tdata или папка с вложенной tdata." }); continue; } let output = ""; try { output = execFileSync(binaryPath, [tdataPath], { encoding: "utf8" }); } catch (error) { failed.push({ path: chosenPath, error: "Не удалось запустить встроенный конвертер tdata." }); continue; } const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); const candidate = lines.find((line) => line.length > 50) || lines[lines.length - 1]; if (!candidate) { failed.push({ path: chosenPath, error: "Не удалось получить строку сессии из tdata." }); continue; } try { const result = await telegram.importTdataSession({ sessionString: candidate, apiId: payload && payload.apiId, apiHash: payload && payload.apiHash }); if (!result.ok) { if (result.error === "DUPLICATE_ACCOUNT") { skipped.push({ path: chosenPath, reason: "Дубликат", accountId: result.accountId }); if (result.accountId) assignedIds.push(result.accountId); continue; } if (result.error && String(result.error).includes("AUTH_KEY_DUPLICATED")) { authKeyDuplicatedCount += 1; failed.push({ path: chosenPath, error: result.error }); continue; } failed.push({ path: chosenPath, error: result.error || "Ошибка импорта" }); continue; } imported.push({ path: chosenPath, accountId: result.accountId }); if (result.accountId) assignedIds.push(result.accountId); } catch (error) { const errorText = error.message || String(error); if (String(errorText).includes("AUTH_KEY_DUPLICATED")) { authKeyDuplicatedCount += 1; failed.push({ path: chosenPath, error: errorText }); continue; } failed.push({ path: chosenPath, error: errorText }); } } if (payload && payload.taskId && assignedIds.length) { const task = store.getTask(payload.taskId); if (task) { const existing = store.listTaskAccounts(payload.taskId).map((row) => row.account_id); const merged = Array.from(new Set([...(existing || []), ...assignedIds])); store.setTaskAccounts(payload.taskId, merged); } } return { ok: true, imported, skipped, failed, authKeyDuplicatedCount }; }); ipcMain.handle("logs:list", (_event, payload) => { if (payload && typeof payload === "object") { return store.listLogs(payload.limit || 100, payload.taskId); } return store.listLogs(payload || 100); }); ipcMain.handle("invites:importFile", async (_event, payload) => { const taskId = payload && payload.taskId ? Number(payload.taskId) : 0; if (!taskId) return { ok: false, error: "Task not selected" }; const onlyIds = Boolean(payload && payload.onlyIds); const sourceChat = payload && payload.sourceChat ? String(payload.sourceChat).trim() : ""; if (onlyIds && !sourceChat) { return { ok: false, error: "Источник обязателен для файла только с ID" }; } const { canceled, filePaths } = await dialog.showOpenDialog({ title: "Выберите txt файл с пользователями", properties: ["openFile", "multiSelections"], filters: [{ name: "Text", extensions: ["txt"] }] }); if (canceled || !filePaths || !filePaths.length) return { ok: false, canceled: true }; const imported = []; const skipped = []; const failed = []; const targetSource = sourceChat || `file:${taskId}`; const parseLine = (line) => { const trimmed = line.trim(); if (!trimmed) return null; if (onlyIds) { const id = trimmed.replace(/[^\d]/g, ""); return id ? { userId: id, username: "" } : null; } if (trimmed.startsWith("@")) { const name = trimmed.replace(/^@+/, "").trim(); return name ? { userId: name, username: name } : null; } if (/^\d+$/.test(trimmed)) { return { userId: trimmed, username: "" }; } const urlMatch = trimmed.match(/t\.me\/([A-Za-z0-9_]+)/i); if (urlMatch) { const name = urlMatch[1]; return name ? { userId: name, username: name } : null; } return { userId: trimmed, username: trimmed }; }; for (const filePath of filePaths) { try { const content = fs.readFileSync(filePath, "utf8"); const lines = content.split(/\r?\n/); for (const line of lines) { const parsed = parseLine(line); if (!parsed) continue; const ok = store.enqueueInvite( taskId, parsed.userId, parsed.username, targetSource, "", 0 ); if (ok) { imported.push(parsed.userId); } else { skipped.push(parsed.userId); } } } catch (error) { failed.push({ path: filePath, error: error.message || String(error) }); } } if (imported.length) { store.addTaskAudit(taskId, "import_list", `Импорт из файла: ${imported.length}`); } return { ok: true, importedCount: imported.length, skippedCount: skipped.length, failed }; }); ipcMain.handle("invites:list", (_event, payload) => { if (payload && typeof payload === "object") { return store.listInvites(payload.limit || 200, payload.taskId); } return store.listInvites(payload || 200); }); ipcMain.handle("logs:clear", (_event, taskId) => { store.clearLogs(taskId); return { ok: true }; }); ipcMain.handle("invites:clear", (_event, taskId) => { store.clearInvites(taskId); return { ok: true }; }); ipcMain.handle("queue:clear", (_event, taskId) => { store.clearQueue(taskId); return { ok: true }; }); ipcMain.handle("queue:clearItems", (_event, payload) => { const taskId = payload && payload.taskId != null ? Number(payload.taskId) : null; const ids = payload && Array.isArray(payload.ids) ? payload.ids : []; const removed = store.clearQueueItems(taskId, ids); return { ok: true, removed }; }); ipcMain.handle("queue:list", (_event, payload) => { const taskId = payload && payload.taskId != null ? payload.taskId : 0; const limit = payload && payload.limit != null ? payload.limit : 200; const offset = payload && payload.offset != null ? payload.offset : 0; const items = store.getPendingInvites(taskId, limit, offset); const stats = store.getPendingStats(taskId); return { items, stats }; }); ipcMain.handle("test:inviteOnce", async (_event, payload) => { const taskId = payload && payload.taskId != null ? payload.taskId : 0; const task = store.getTask(taskId); if (!task) return { ok: false, error: "Task not found" }; const pending = store.getPendingInvites(taskId, 1, 0); if (!pending.length) return { ok: false, error: "Queue empty" }; const item = pending[0]; const accountRows = store.listTaskAccounts(taskId).filter((row) => row.role_invite && Number(row.invite_limit || 0) > 0); if (!accountRows.length) return { ok: false, error: "No invite accounts" }; const accounts = store.listAccounts(); const accountMap = new Map(); accounts.forEach((account) => accountMap.set(account.id, account)); let accountsForInvite = accountRows.map((row) => row.account_id); if (!item.username && task.use_watcher_invite_no_username && item.watcher_account_id) { const watcherCanInvite = accountRows.some((row) => Number(row.account_id) === Number(item.watcher_account_id)); if (watcherCanInvite) { accountsForInvite = [item.watcher_account_id]; const watcherAccount = accountMap.get(item.watcher_account_id || 0); store.addAccountEvent( watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone || "" : "", "invite_watcher_fallback_used", `задача ${taskId}: тестовый инвайт без username -> инвайт через наблюдателя` ); } } const watcherAccount = accountMap.get(item.watcher_account_id || 0); const getProxySnapshot = (account) => { if (!account) return { proxyId: 0, proxyLabel: "" }; const proxyId = Number(account.proxy_id || 0); const proxyLabel = account.proxy_name ? String(account.proxy_name) : (account.proxy_host && account.proxy_port ? `${account.proxy_host}:${account.proxy_port}` : ""); return { proxyId, proxyLabel }; }; store.addAccountEvent( watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone : "", "test_invite_attempt", `задача ${taskId}: тестовый инвайт для ${item.user_id}${item.username ? ` (@${item.username})` : ""}` ); const result = await telegram.inviteUserForTask(task, item.user_id, accountsForInvite, { randomize: Boolean(task.random_accounts), userAccessHash: item.user_access_hash, username: item.username, sourceChat: item.source_chat, sourceMessageId: Number(item.source_message_id || 0), watcherAccountId: watcherAccount ? watcherAccount.id : 0, watcherPhone: watcherAccount ? watcherAccount.phone : "" }); const fallbackRoute = (error, confirmed) => { if (confirmed === false) return "link"; switch (error) { case "USER_NOT_MUTUAL_CONTACT": return "link"; case "USER_PRIVACY_RESTRICTED": return "stories"; case "USER_ID_INVALID": return "exclude"; case "USER_NOT_PARTICIPANT": return "retry"; case "USER_BANNED_IN_CHANNEL": case "USER_KICKED": return "exclude"; case "CHAT_ADMIN_REQUIRED": case "PEER_FLOOD": case "FLOOD": return "retry"; default: return "retry"; } }; if (result.ok) { const inviteAccount = accountMap.get(result.accountId || 0); const inviteProxy = getProxySnapshot(inviteAccount); const watcherProxy = getProxySnapshot(watcherAccount); const isConfirmed = result.confirmed === true; store.markInviteStatus(item.id, isConfirmed ? "invited" : "unconfirmed"); store.recordInvite( taskId, item.user_id, item.username, result.accountId, result.accountPhone, item.source_chat, isConfirmed ? "success" : "unconfirmed", "", "", "invite", item.user_access_hash, watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone : "", result.strategy, result.strategyMeta, task.our_group, result.targetType, result.confirmed === true, result.confirmError || "", Number(item.source_message_id || 0), inviteProxy.proxyId, inviteProxy.proxyLabel, watcherProxy.proxyId, watcherProxy.proxyLabel ); if (result.confirmed === false) { store.addFallback( taskId, item.user_id, item.username, item.source_chat, task.our_group, "NOT_CONFIRMED", fallbackRoute("", false) ); } } else if (result.error === "USER_ALREADY_PARTICIPANT") { const inviteAccount = accountMap.get(result.accountId || 0); const inviteProxy = getProxySnapshot(inviteAccount); const watcherProxy = getProxySnapshot(watcherAccount); store.markInviteStatus(item.id, "skipped"); store.recordInvite( taskId, item.user_id, item.username, result.accountId, result.accountPhone, item.source_chat, "skipped", "", "USER_ALREADY_PARTICIPANT", "invite", item.user_access_hash, watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone : "", result.strategy, result.strategyMeta, task.our_group, result.targetType, false, result.error || "", Number(item.source_message_id || 0), inviteProxy.proxyId, inviteProxy.proxyLabel, watcherProxy.proxyId, watcherProxy.proxyLabel ); } else { const inviteAccount = accountMap.get(result.accountId || 0); const inviteProxy = getProxySnapshot(inviteAccount); const watcherProxy = getProxySnapshot(watcherAccount); const canRetryError = isRetryableInviteError(result.error); if (task.retry_on_fail && canRetryError) { store.incrementInviteAttempt(item.id); store.markInviteStatus(item.id, "pending"); } else { store.markInviteStatus(item.id, "failed"); if (task.retry_on_fail && !canRetryError) { store.addAccountEvent( watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone || "" : "", "invite_retry_skipped", `задача ${taskId}: отключен ретрай для ${item.user_id}${item.username ? ` (@${item.username})` : ""} из-за ошибки ${result.error || "unknown"}` ); } } store.addFallback( taskId, item.user_id, item.username, item.source_chat, task.our_group, result.error || "unknown", fallbackRoute(result.error, true) ); store.recordInvite( taskId, item.user_id, item.username, result.accountId, result.accountPhone, item.source_chat, "failed", result.error || "", result.error || "", "invite", item.user_access_hash, watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone : "", result.strategy, result.strategyMeta, task.our_group, result.targetType, false, result.error || "", Number(item.source_message_id || 0), inviteProxy.proxyId, inviteProxy.proxyLabel, watcherProxy.proxyId, watcherProxy.proxyLabel ); } store.addAccountEvent( watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone : "", "test_invite_result", `задача ${taskId}: ${result.ok ? "ok" : "fail"} · ${result.error || ""}`.trim() ); return result; }); ipcMain.handle("confirm:list", (_event, payload) => { if (payload && typeof payload === "object") { return store.listConfirmQueue(payload.taskId, payload.limit || 200); } return store.listConfirmQueue(payload || 200); }); ipcMain.handle("confirm:clear", (_event, taskId) => { store.clearConfirmQueue(taskId); return { ok: true }; }); ipcMain.handle("tasks:list", () => store.listTasks()); ipcMain.handle("tasks:get", (_event, id) => { const task = store.getTask(id); if (!task) return null; return { task, competitors: getTaskCompetitorLinks(id), accountIds: store.listTaskAccounts(id).map((row) => row.account_id), accountRoles: store.listTaskAccounts(id).map((row) => ({ accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), roleInvite: Boolean(row.role_invite), roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite), inviteLimit: Number(row.invite_limit || 0) })) }; }); ipcMain.handle("tasks:save", (_event, payload) => { const existing = payload.task.id ? store.getTask(payload.task.id) : null; const taskId = store.saveTask(payload.task); store.setTaskCompetitors(taskId, payload.competitors || []); if (payload.accountRoles && payload.accountRoles.length) { store.setTaskAccountRoles(taskId, payload.accountRoles); } else { store.setTaskAccounts(taskId, payload.accountIds || []); } if (!existing) { store.addTaskAudit(taskId, "create", JSON.stringify({ name: payload.task.name, ourGroup: payload.task.ourGroup })); } else { const changes = {}; if (existing.name !== payload.task.name) changes.name = [existing.name, payload.task.name]; if (existing.our_group !== payload.task.ourGroup) changes.ourGroup = [existing.our_group, payload.task.ourGroup]; if (existing.daily_limit !== payload.task.dailyLimit) changes.dailyLimit = [existing.daily_limit, payload.task.dailyLimit]; if (existing.min_interval_minutes !== payload.task.minIntervalMinutes || existing.max_interval_minutes !== payload.task.maxIntervalMinutes) { changes.intervals = [ `${existing.min_interval_minutes}-${existing.max_interval_minutes}`, `${payload.task.minIntervalMinutes}-${payload.task.maxIntervalMinutes}` ]; } if (existing.history_limit !== payload.task.historyLimit) changes.historyLimit = [existing.history_limit, payload.task.historyLimit]; if (existing.max_invites_per_cycle !== Number(payload.task.maxInvitesPerCycle || 0)) { changes.maxInvitesPerCycle = [existing.max_invites_per_cycle, Number(payload.task.maxInvitesPerCycle || 0)]; } if (existing.allow_start_without_invite_rights !== (payload.task.allowStartWithoutInviteRights ? 1 : 0)) { changes.allowStartWithoutInviteRights = [Boolean(existing.allow_start_without_invite_rights), Boolean(payload.task.allowStartWithoutInviteRights)]; } if (existing.parse_participants !== (payload.task.parseParticipants ? 1 : 0)) { changes.parseParticipants = [Boolean(existing.parse_participants), Boolean(payload.task.parseParticipants)]; } if (existing.invite_via_admins !== (payload.task.inviteViaAdmins ? 1 : 0)) { changes.inviteViaAdmins = [Boolean(existing.invite_via_admins), Boolean(payload.task.inviteViaAdmins)]; } if ((existing.invite_admin_master_id || 0) !== Number(payload.task.inviteAdminMasterId || 0)) { changes.inviteAdminMasterId = [existing.invite_admin_master_id || 0, Number(payload.task.inviteAdminMasterId || 0)]; } if (existing.invite_admin_anonymous !== (payload.task.inviteAdminAnonymous ? 1 : 0)) { changes.inviteAdminAnonymous = [Boolean(existing.invite_admin_anonymous), Boolean(payload.task.inviteAdminAnonymous)]; } if (existing.separate_confirm_roles !== (payload.task.separateConfirmRoles ? 1 : 0)) { changes.separateConfirmRoles = [Boolean(existing.separate_confirm_roles), Boolean(payload.task.separateConfirmRoles)]; } if (existing.max_confirm_bots !== Number(payload.task.maxConfirmBots || 0)) { changes.maxConfirmBots = [existing.max_confirm_bots, Number(payload.task.maxConfirmBots || 0)]; } if (existing.warmup_enabled !== (payload.task.warmupEnabled ? 1 : 0)) { changes.warmupEnabled = [Boolean(existing.warmup_enabled), Boolean(payload.task.warmupEnabled)]; } if (existing.warmup_start_limit !== Number(payload.task.warmupStartLimit || 0)) { changes.warmupStartLimit = [existing.warmup_start_limit, Number(payload.task.warmupStartLimit || 0)]; } if (existing.warmup_daily_increase !== Number(payload.task.warmupDailyIncrease || 0)) { changes.warmupDailyIncrease = [existing.warmup_daily_increase, Number(payload.task.warmupDailyIncrease || 0)]; } if (existing.cycle_competitors !== (payload.task.cycleCompetitors ? 1 : 0)) { changes.cycleCompetitors = [Boolean(existing.cycle_competitors), Boolean(payload.task.cycleCompetitors)]; } if (existing.invite_link_on_fail !== (payload.task.inviteLinkOnFail ? 1 : 0)) { changes.inviteLinkOnFail = [Boolean(existing.invite_link_on_fail), Boolean(payload.task.inviteLinkOnFail)]; } if (Object.keys(changes).length) { store.addTaskAudit(taskId, "update", JSON.stringify(changes)); } } return { ok: true, taskId }; }); ipcMain.handle("tasks:delete", (_event, id) => { const runner = taskRunners.get(id); if (runner) { runner.stop(); taskRunners.delete(id); } store.addTaskAudit(id, "delete", ""); store.deleteTask(id); return { ok: true }; }); ipcMain.handle("tasks:start", async (_event, id) => { return startTaskWithChecks(id); }); ipcMain.handle("tasks:stop", (_event, id) => { const runner = taskRunners.get(id); if (runner) { runner.stop(); taskRunners.delete(id); } store.setTaskStopReason(id, "Остановлено пользователем"); store.addTaskAudit(id, "stop", "Остановлено пользователем"); store.addAccountEvent(0, "", "task_stop", `задача ${id}: остановлена пользователем`); return { ok: true }; }); ipcMain.handle("tasks:accountAssignments", () => { return store.listAllTaskAccounts(); }); ipcMain.handle("tasks:appendAccounts", (_event, payload) => { if (!payload || !payload.taskId) return { ok: false, error: "Task not found" }; const task = store.getTask(payload.taskId); if (!task) return { ok: false, error: "Task not found" }; const existingRows = store.listTaskAccounts(payload.taskId); const existing = new Map(existingRows.map((row) => [ row.account_id, { accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), roleInvite: Boolean(row.role_invite), roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite), inviteLimit: Number(row.invite_limit || 0) } ])); (payload.accountIds || []).forEach((accountId) => { if (!existing.has(accountId)) { existing.set(accountId, { accountId, roleMonitor: true, roleInvite: true, roleConfirm: true, inviteLimit: 0 }); } }); (payload.accountRoles || []).forEach((item) => { const roleConfirm = item.roleConfirm != null ? item.roleConfirm : item.roleInvite; existing.set(item.accountId, { accountId: item.accountId, roleMonitor: Boolean(item.roleMonitor), roleInvite: Boolean(item.roleInvite), roleConfirm: Boolean(roleConfirm), inviteLimit: Number(item.inviteLimit || 0) }); }); const merged = Array.from(existing.values()); store.setTaskAccountRoles(payload.taskId, merged); return { ok: true, accountIds: merged.map((item) => item.accountId) }; }); ipcMain.handle("tasks:removeAccount", (_event, payload) => { if (!payload || !payload.taskId || !payload.accountId) { return { ok: false, error: "Task not found" }; } const task = store.getTask(payload.taskId); if (!task) return { ok: false, error: "Task not found" }; const existing = store.listTaskAccounts(payload.taskId) .filter((row) => row.account_id !== payload.accountId) .map((row) => ({ accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), roleInvite: Boolean(row.role_invite), roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite), inviteLimit: Number(row.invite_limit || 0) })); store.setTaskAccountRoles(payload.taskId, existing); return { ok: true, accountIds: existing.map((item) => item.accountId) }; }); ipcMain.handle("tasks:startAll", async () => { const tasks = store.listTasks(); let started = 0; let skipped = 0; const errors = []; for (const task of tasks) { if (!task.enabled) { skipped += 1; continue; } try { const result = await startTaskWithChecks(task.id); if (!result.ok) { errors.push({ id: task.id, error: result.error || "start failed" }); continue; } started += 1; } catch (error) { errors.push({ id: task.id, error: error.message || String(error) }); } } return { ok: errors.length === 0, started, skipped, errors }; }); ipcMain.handle("tasks:stopAll", () => { let stopped = 0; for (const [id, runner] of taskRunners.entries()) { runner.stop(); taskRunners.delete(id); stopped += 1; } return { ok: true, stopped }; }); ipcMain.handle("tasks:status", (_event, id) => { const runner = taskRunners.get(id); const queueCount = store.getPendingCount(id); const dailyUsed = store.countInvitesToday(id); const confirmQueueStats = store.getConfirmQueueStats(id); const unconfirmedCount = Number(confirmQueueStats.pending || 0) + Number(confirmQueueStats.failed || 0); const task = store.getTask(id); const monitorInfo = telegram.getTaskMonitorInfo(id); const warnings = []; const readiness = { ok: true, reasons: [] }; let restrictedAccounts = []; let totalInvites = 0; let totalInvitesSuccess = 0; let totalInvitesAttempts = 0; let taskInviteLimitTotal = 0; let accountDailyLimitTotal = 0; let inviteAccountsCount = 0; if (task) { const accountRows = store.listTaskAccounts(id); const accounts = store.listAccounts(); const accountsById = new Map(accounts.map((acc) => [acc.id, acc])); const isInviteAvailableNow = (account) => { if (!account) return false; if (telegram && typeof telegram.isInviteAccountAvailable === "function") { return telegram.isInviteAccountAvailable(account.id); } return (account.status || "ok") === "ok" && !isCooldownActive(account); }; if (runner && runner.isRunning()) { const sanitized = filterTaskRolesByAccounts(id, accountRows, accounts); if (sanitized.removedError || sanitized.removedMissing) { warnings.push(`Авто-синхронизация ролей: удалено ${sanitized.removedMissing + sanitized.removedError} аккаунт(ов).`); } if (!sanitized.filtered.length) { const blockedDetails = accountRows .map((row) => { const account = accountsById.get(row.account_id); const roleParts = []; if (row.role_monitor) roleParts.push("мониторинг"); if (row.role_invite) roleParts.push("инвайт"); if (row.role_confirm) roleParts.push("подтверждение"); const roles = roleParts.length ? ` (${roleParts.join("/")})` : ""; return `${formatAccountLabel(account, row.account_id)}${roles}: ${describeAccountRestriction(account) || "недоступен"}`; }) .join("; "); const stopReason = blockedDetails ? `Нет доступных аккаунтов. Причины: ${blockedDetails}` : "Нет доступных аккаунтов"; warnings.push(`Задача остановлена: ${stopReason}`); store.setTaskStopReason(id, stopReason); store.addAccountEvent(0, "", "task_stop_auto", `задача ${id}: ${stopReason}`); runner.stop(); } } const inviteRows = accountRows.filter((row) => row.role_invite); inviteAccountsCount = inviteRows.length; taskInviteLimitTotal = inviteRows.reduce((sum, row) => sum + Math.max(0, Number(row.invite_limit || 0)), 0); accountDailyLimitTotal = inviteRows.reduce((sum, row) => { const account = accountsById.get(row.account_id); return sum + Math.max(0, Number(account && account.daily_limit ? account.daily_limit : 0)); }, 0); totalInvitesSuccess = Number(store.countInvitesByStatus(id, "success") || 0); totalInvitesAttempts = totalInvitesSuccess + Number(store.countInvitesByStatus(id, "failed") || 0) + Number(store.countInvitesByStatus(id, "skipped") || 0) + Number(store.countInvitesByStatus(id, "unconfirmed") || 0); totalInvites = totalInvitesAttempts; const monitorRows = accountRows.filter((row) => row.role_monitor); if (!inviteRows.length) { const fallbackAvailable = accountRows .map((row) => accountsById.get(row.account_id)) .filter((acc) => isInviteAvailableNow(acc)).length; if (fallbackAvailable > 0) { warnings.push(`Нет роли инвайта: используется авто-failover (${fallbackAvailable} доступных аккаунт(ов)).`); } else { readiness.ok = false; readiness.reasons.push("Нет аккаунтов с ролью инвайта."); } } if (!monitorRows.length) { readiness.ok = false; readiness.reasons.push("Нет аккаунтов с ролью мониторинга."); } if (task.require_same_bot_in_both) { const hasSame = accountRows.some((row) => row.role_monitor && row.role_invite); if (!hasSame) { warnings.push("Режим “один бот в обоих группах” включен, но нет аккаунта с обеими ролями."); readiness.ok = false; readiness.reasons.push("Нет аккаунта с двумя ролями в режиме “один бот”."); } } const allAssignments = store.listAllTaskAccounts(); const accountTaskMap = new Map(); allAssignments.forEach((row) => { if (!accountTaskMap.has(row.account_id)) accountTaskMap.set(row.account_id, new Set()); accountTaskMap.get(row.account_id).add(row.task_id); }); const seen = new Set(); accountRows.forEach((row) => { const tasksForAccount = accountTaskMap.get(row.account_id); if (tasksForAccount && tasksForAccount.size > 1 && !seen.has(row.account_id)) { seen.add(row.account_id); const account = accountsById.get(row.account_id); const label = account ? `${account.phone || account.user_id || row.account_id}${account.username ? ` (@${account.username})` : ""}` : row.account_id; warnings.push(`Аккаунт ${label} используется в ${tasksForAccount.size} задачах. Лимиты действий/групп общие.`); } }); restrictedAccounts = accountRows .map((row) => { const account = accountsById.get(row.account_id); const reason = describeAccountRestriction(account); if (!reason) return null; return { accountId: Number(row.account_id), label: formatAccountLabel(account, row.account_id), reason, roleMonitor: Boolean(row.role_monitor), roleInvite: Boolean(row.role_invite), roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite) }; }) .filter(Boolean); if (restrictedAccounts.length) { warnings.push(`Есть аккаунты с ограничениями: ${restrictedAccounts.length}.`); } if (inviteRows.length) { const inviteAccounts = inviteRows.map((row) => accountsById.get(row.account_id)).filter(Boolean); const badSessions = inviteAccounts.filter((acc) => acc.status && acc.status !== "ok"); if (badSessions.length) { readiness.ok = false; readiness.reasons.push(`Есть аккаунты с ошибкой сессии: ${badSessions.length}.`); } } if (runner && runner.isRunning() && queueCount === 0) { if (!monitorInfo || !monitorInfo.monitoring) { warnings.push("Очередь пуста: мониторинг не активен."); } else if (!monitorInfo.groups || monitorInfo.groups.length === 0) { warnings.push("Очередь пуста: нет групп в мониторинге."); } else if (!monitorInfo.lastMessageAt) { warnings.push("Очередь пуста: новых сообщений пока нет."); } else { warnings.push(`Очередь пуста: последнее сообщение ${formatTimestamp(monitorInfo.lastMessageAt)}.`); } } if (task.our_group && (task.our_group.includes("joinchat/") || task.our_group.includes("t.me/+"))) { warnings.push("Целевая группа указана по инвайт-ссылке — доступ может быть ограничен."); } if (task.task_invite_access) { try { const parsed = JSON.parse(task.task_invite_access); if (Array.isArray(parsed) && parsed.length) { const total = parsed.length; const canInvite = parsed.filter((row) => row.canInvite).length; const disconnected = parsed.filter((row) => row.reason === "Сессия не подключена").length; const isChannel = parsed.some((row) => row.targetType === "channel"); const checkedAt = task.task_invite_access_at || ""; if (task.invite_via_admins) { warnings.push(`Права инвайта: ${canInvite}/${total} аккаунтов могут добавлять.${checkedAt ? ` Проверка: ${formatTimestamp(checkedAt)}.` : ""}`); } if (disconnected) { warnings.push(`Сессия не подключена: ${disconnected} аккаунт(ов).`); } if (isChannel && task.invite_via_admins) { warnings.push("Цель — канал: добавлять участников могут только админы."); } if (canInvite === 0 && task.invite_via_admins) { readiness.ok = false; readiness.reasons.push("Нет аккаунтов с правами инвайта."); } } } catch (error) { // ignore parsing errors } } } const effectiveLimit = task ? store.getEffectiveDailyLimit(task) : 0; return { running: runner ? runner.isRunning() : false, queueCount, dailyUsed, totalInvites, totalInvitesSuccess, totalInvitesAttempts, unconfirmedCount, dailyLimit: effectiveLimit, taskDailyLimitBase: task ? Number(task.daily_limit || 0) : 0, taskInviteLimitTotal, accountDailyLimitTotal, inviteAccountsCount, dailyRemaining: task ? Math.max(0, Number(effectiveLimit || 0) - dailyUsed) : 0, cycleCompetitors: task ? Boolean(task.cycle_competitors) : false, competitorCursor: task ? Number(task.competitor_cursor || 0) : 0, monitorInfo, nextRunAt: runner ? runner.getNextRunAt() : "", nextInviteAccountId: runner ? runner.getNextInviteAccountId() : 0, lastInviteAccountId: runner ? runner.getLastInviteAccountId() : 0, pendingStats: store.getPendingStats(id), warnings, readiness, restrictedAccounts, lastStopReason: task ? task.last_stop_reason || "" : "", lastStopAt: task ? task.last_stop_at || "" : "" }; }); ipcMain.handle("tasks:parseHistory", async (_event, id) => { const task = store.getTask(id); if (!task) return { ok: false, error: "Task not found" }; const competitors = getTaskCompetitorLinks(id); const accounts = store.listTaskAccounts(id).map((row) => row.account_id); return telegram.parseHistoryForTask(task, competitors, accounts); }); ipcMain.handle("tasks:checkAccess", async (_event, id) => { const task = store.getTask(id); if (!task) return { ok: false, error: "Task not found" }; const competitors = getTaskCompetitorLinks(id); return telegram.checkGroupAccess(competitors, task.our_group); }); ipcMain.handle("tasks:membershipStatus", async (_event, id) => { const task = store.getTask(id); if (!task) return { ok: false, error: "Task not found" }; const competitors = getTaskCompetitorLinks(id); return telegram.getMembershipStatus(competitors, task.our_group); }); ipcMain.handle("tasks:joinGroups", async (_event, id) => { const task = store.getTask(id); if (!task) return { ok: false, error: "Task not found" }; const competitors = getTaskCompetitorLinks(id); const accountRows = store.listTaskAccounts(id); const accountIds = accountRows.map((row) => row.account_id); const roleIds = { monitorIds: accountRows.filter((row) => row.role_monitor).map((row) => row.account_id), inviteIds: accountRows.filter((row) => row.role_invite).map((row) => row.account_id), confirmIds: accountRows.filter((row) => row.role_confirm).map((row) => row.account_id) }; await telegram.joinGroupsForTask(task, competitors, accountIds, roleIds, { forceJoin: true }); store.addAccountEvent(0, "", "auto_join_request", `задача ${id}: запрос на вступление в группы отправлен`); return { ok: true }; }); ipcMain.handle("tasks:checkInviteAccess", async (_event, id) => { const task = store.getTask(id); if (!task) return { ok: false, error: "Task not found" }; const accountRows = store.listTaskAccounts(id).filter((row) => row.role_invite && Number(row.invite_limit || 0) > 0); const existingAccounts = store.listAccounts(); const existingIds = new Set(existingAccounts.map((account) => account.id)); const missing = accountRows.filter((row) => !existingIds.has(row.account_id)); if (missing.length) { const filtered = accountRows .filter((row) => existingIds.has(row.account_id)) .map((row) => ({ accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), roleInvite: Boolean(row.role_invite), roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite), inviteLimit: Number(row.invite_limit || 0) })); store.setTaskAccountRoles(id, filtered); } const accountIds = accountRows .filter((row) => existingIds.has(row.account_id)) .map((row) => row.account_id); if (task.invite_via_admins && task.invite_admin_master_id && existingIds.has(Number(task.invite_admin_master_id))) { accountIds.push(Number(task.invite_admin_master_id)); } const dedupedAccountIds = Array.from(new Set(accountIds)); const result = await telegram.checkInvitePermissions(task, dedupedAccountIds); if (result && result.ok) { store.setTaskInviteAccess(id, result.result || []); } return result; }); ipcMain.handle("tasks:checkConfirmAccess", async (_event, id) => { const task = store.getTask(id); if (!task) return { ok: false, error: "Task not found" }; const allRows = store.listTaskAccounts(id); const accountRows = task.separate_confirm_roles ? allRows.filter((row) => row.role_confirm && !row.role_invite) : allRows.filter((row) => row.role_invite && Number(row.invite_limit || 0) > 0); const existingAccounts = store.listAccounts(); const existingIds = new Set(existingAccounts.map((account) => account.id)); const missing = accountRows.filter((row) => !existingIds.has(row.account_id)); if (missing.length) { const filtered = accountRows .filter((row) => existingIds.has(row.account_id)) .map((row) => ({ accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), roleInvite: Boolean(row.role_invite), roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite), inviteLimit: Number(row.invite_limit || 0) })); store.setTaskAccountRoles(id, filtered); } const accountIds = accountRows .filter((row) => existingIds.has(row.account_id)) .map((row) => row.account_id); return telegram.checkConfirmAccess(task, accountIds); }); ipcMain.handle("tasks:groupVisibility", async (_event, id) => { const task = store.getTask(id); if (!task) return { ok: false, error: "Task not found" }; const competitors = getTaskCompetitorLinks(id); const result = await telegram.getGroupVisibility(task, competitors); return { ok: true, result }; }); const toCsv = (rows, headers) => { const escape = (value) => { const text = value == null ? "" : String(value); if (text.includes("\"") || text.includes(",") || text.includes("\n")) { return `"${text.replace(/\"/g, "\"\"")}"`; } return text; }; const lines = [headers.join(",")]; rows.forEach((row) => { lines.push(headers.map((key) => escape(row[key])).join(",")); }); return lines.join("\n"); }; const sanitizeFileName = (value) => { return String(value || "") .replace(/[<>:"/\\|?*\x00-\x1F]/g, "_") .replace(/\s+/g, "_") .replace(/_+/g, "_") .replace(/^_+|_+$/g, "") .slice(0, 80) || "task"; }; const explainInviteError = (error) => { if (!error) return ""; if (error === "USER_ID_INVALID") { return "Пользователь удален/скрыт; access_hash невалиден для этой сессии; приглашение в канал/чат без валидной сущности."; } if (error === "CHAT_WRITE_FORBIDDEN") { return "Аккаунт не может приглашать: нет прав или он не участник группы."; } if (error === "USER_NOT_MUTUAL_CONTACT") { return "Пользователь не взаимный контакт для добавляющего аккаунта. Обычно это происходит, когда в группе/канале включена опция «добавлять могут только контакты» или у пользователя закрыт приём инвайтов."; } if (error === "USER_PRIVACY_RESTRICTED") { return "Приглашение запрещено пользователем: приватность не позволяет добавлять в группы."; } if (error === "USER_NOT_PARTICIPANT") { return "Аккаунт не состоит в целевой группе или канал приватный."; } if (error === "USER_BANNED_IN_CHANNEL") { return "Пользователь заблокирован в группе или канале назначения."; } if (error === "USER_BOT") { return "Бота нельзя приглашать как обычного пользователя."; } if (error === "USER_KICKED") { return "Пользователь был удален из группы ранее."; } if (error === "CHAT_ADMIN_REQUIRED") { return "Для добавления участников нужны права администратора."; } if (error === "CHANNEL_INVALID") { return "Цель не распознана как группа/канал для этого аккаунта (ссылка недоступна или сущность устарела)."; } if (error === "USER_ALREADY_PARTICIPANT") { return "Пользователь уже состоит в целевой группе."; } if (error === "CHAT_MEMBER_ADD_FAILED") { return "Telegram отказал в добавлении пользователя. Возможные причины: пользователь недоступен для инвайта, неверные данные, ограничения чата или лимиты аккаунта."; } if (error === "INVITE_MISSING_INVITEE") { return "Telegram принял запрос инвайта, но вернул missing_invitees: пользователь не был фактически добавлен (детали и флаги причины в «Подробнее»)."; } if (error === "INVITER_ENTITY_NOT_RESOLVED_BY_MASTER") { return "Мастер-админ не смог получить сущность инвайтера в своей сессии. Обычно это кэш сущностей/доступность аккаунта."; } if (error === "MASTER_TARGET_RESOLVE_FAILED" || error === "TARGET_RESOLVE_FAILED") { return "Не удалось корректно резолвить целевую группу для текущего аккаунта."; } if (error === "TARGET_CLIENT_NOT_SET") { return "Внутренняя ошибка: не задан клиент для проверки цели."; } if (error === "INVITED_USER_NOT_RESOLVED_FOR_ADMIN") { return "Не удалось резолвить приглашаемого пользователя в сессии админ-аккаунта."; } if (error === "INVITED_USER_NOT_RESOLVED_FOR_CONFIRM") { return "Не удалось резолвить пользователя в сессии аккаунта, который проверяет участие."; } if (error === "SOURCE_ADMIN_SKIPPED") { return "Пользователь является администратором в группе конкурента и пропущен по фильтру."; } if (error === "SOURCE_BOT_SKIPPED") { return "Пользователь является ботом в группе конкурента и пропущен по фильтру."; } if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") { return "Инвайт-ссылка недействительна или истекла."; } if (error === "CHANNEL_PRIVATE") { return "Целевая группа/канал приватные и недоступны по ссылке."; } if (error === "AUTH_KEY_DUPLICATED") { return "Сессия используется в другом месте, Telegram отозвал ключ."; } if (error.includes("FLOOD") || error.includes("PEER_FLOOD")) { return "Ограничение Telegram по частоте действий."; } return ""; }; const extractErrorCode = (value) => { if (!value) return ""; const text = String(value).trim(); const split = text.split(/[:(]/, 1); return split && split[0] ? split[0].trim() : text; }; ipcMain.handle("logs:export", async (_event, taskId) => { const { canceled, filePath } = await dialog.showSaveDialog({ title: "Выгрузить логи", defaultPath: "logs.csv" }); if (canceled || !filePath) return { ok: false, canceled: true }; const logs = store.listLogs(1000, taskId).map((log) => ({ taskId: log.taskId, startedAt: log.startedAt, finishedAt: log.finishedAt, invitedCount: log.invitedCount, unconfirmedCount: log.meta && log.meta.unconfirmedCount != null ? log.meta.unconfirmedCount : "", cycleLimit: log.meta && log.meta.cycleLimit ? log.meta.cycleLimit : "", queueCount: log.meta && log.meta.queueCount != null ? log.meta.queueCount : "", batchSize: log.meta && log.meta.batchSize != null ? log.meta.batchSize : "", proxyUsage: log.meta && log.meta.proxyUsage ? JSON.stringify(log.meta.proxyUsage) : "[]", successIds: JSON.stringify(log.successIds || []), errors: JSON.stringify(log.errors || []), errorsHuman: JSON.stringify((log.errors || []).map((value) => { const code = extractErrorCode(value); const explanation = explainInviteError(code); return explanation ? `${value} (${explanation})` : value; })) })); const csv = toCsv(logs, ["taskId", "startedAt", "finishedAt", "invitedCount", "unconfirmedCount", "cycleLimit", "queueCount", "batchSize", "proxyUsage", "successIds", "errors", "errorsHuman"]); fs.writeFileSync(filePath, csv, "utf8"); return { ok: true, filePath }; }); ipcMain.handle("tasks:exportBundle", async (_event, taskId) => { const id = Number(taskId || 0); if (!id) return { ok: false, error: "Task not found" }; const task = store.getTask(id); if (!task) return { ok: false, error: "Task not found" }; const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const taskLabel = sanitizeFileName(task.name || `task-${id}`); const { canceled, filePath } = await dialog.showSaveDialog({ title: "Выгрузить логи задачи", defaultPath: `${taskLabel}_${id}_${stamp}.json` }); if (canceled || !filePath) return { ok: false, canceled: true }; const competitors = getTaskCompetitorLinks(id); const taskAccounts = store.listTaskAccounts(id); const taskAccountIds = new Set(taskAccounts.map((row) => Number(row.account_id))); const accounts = store.listAccounts().filter((account) => taskAccountIds.has(Number(account.id))); const proxies = store.listProxies(); const taskProxies = proxies.filter((proxy) => accounts.some((account) => Number(account.proxy_id || 0) === Number(proxy.id || 0)) ); const logs = store.listLogs(10000, id); const invites = store.listInvites(50000, id); const queue = store.getPendingInvites(id, 10000, 0); const fallback = store.listFallback(10000, id); const confirmQueue = store.listConfirmQueue(id, 10000); const taskAudit = store.listTaskAudit(id, 10000); const allAccountEvents = store.listAccountEvents(20000); const taskHints = [`задача ${id}`, `задача:${id}`, `task ${id}`, `task:${id}`, `id: ${id}`]; const normalizeLink = (value) => String(value || "") .trim() .toLowerCase() .replace(/^https?:\/\//, "") .replace(/\/+$/, ""); const taskTargets = Array.from(new Set( [task.our_group, ...(competitors || [])] .map((link) => normalizeLink(link)) .filter(Boolean) )); const accountEvents = allAccountEvents.filter((item) => { const message = String(item.message || "").toLowerCase(); const hasTaskHint = taskHints.some((hint) => message.includes(hint)); if (hasTaskHint) return true; if (!taskTargets.length) return false; const hasTargetContext = taskTargets.some((target) => message.includes(target)); if (!hasTargetContext) return false; // Keep task-context events only, even if account is shared across tasks. return true; }); const settings = store.getSettings(); let taskInviteAccessParsed = []; try { taskInviteAccessParsed = task.task_invite_access ? JSON.parse(task.task_invite_access) : []; } catch { taskInviteAccessParsed = []; } const queueStats = store.getPendingStats(id); const dailyUsed = store.countInvitesToday(id); const confirmQueueStats = store.getConfirmQueueStats(id); const unconfirmedCount = Number(confirmQueueStats.pending || 0) + Number(confirmQueueStats.failed || 0); const invitesByStatus = { success: store.countInvitesByStatus(id, "success"), error: store.countInvitesByStatus(id, "failed") + store.countInvitesByStatus(id, "error"), unconfirmed: unconfirmedCount }; const effectiveDailyLimit = store.getEffectiveDailyLimit(task); const runner = taskRunners.get(id); const monitorInfo = telegram.getTaskMonitorInfo(id); const taskAccountsDetailed = taskAccounts.map((row) => { const account = accounts.find((acc) => Number(acc.id) === Number(row.account_id)); return { ...row, accountPhone: account ? account.phone : "", accountUsername: account ? account.username : "", accountStatus: account ? account.status : "", accountLastError: account ? account.last_error : "", cooldownUntil: account ? account.cooldown_until : "", cooldownReason: account ? account.cooldown_reason : "" }; }); const roleSummary = { monitor: taskAccounts.filter((row) => row.role_monitor).length, invite: taskAccounts.filter((row) => row.role_invite).length, confirm: taskAccounts.filter((row) => row.role_confirm).length }; const membershipStatus = {}; for (const row of taskAccounts) { const accountId = Number(row.account_id); const our = task.our_group ? store.getAutoJoinStatus(accountId, task.our_group) : null; const competitorsStatus = competitors.map((group) => ({ group, status: store.getAutoJoinStatus(accountId, group) })); membershipStatus[accountId] = { our, competitors: competitorsStatus }; } const inviteAccessSummary = taskInviteAccessParsed.map((row) => { const account = accounts.find((acc) => Number(acc.id) === Number(row.accountId)); return { accountId: row.accountId, accountPhone: row.accountPhone || (account ? account.phone : ""), accountUsername: account ? account.username : "", ok: row.ok, canInvite: row.canInvite, member: row.member, reason: row.reason || "" }; }); const recentEvents = accountEvents.slice(0, 200); const statusSnapshot = (() => { const queueCount = store.getPendingCount(id); const dailyUsed = store.countInvitesToday(id); const confirmStats = store.getConfirmQueueStats(id); const unconfirmed = Number(confirmStats.pending || 0) + Number(confirmStats.failed || 0); const taskRow = task; const accountRows = store.listTaskAccounts(id); const accountsAll = store.listAccounts(); const accountsById = new Map(accountsAll.map((acc) => [acc.id, acc])); const inviteRows = accountRows.filter((row) => row.role_invite); const monitorRows = accountRows.filter((row) => row.role_monitor); const warnings = []; const readiness = { ok: true, reasons: [] }; if (!inviteRows.length) { readiness.ok = false; readiness.reasons.push("Нет аккаунтов с ролью инвайта."); } if (!monitorRows.length) { readiness.ok = false; readiness.reasons.push("Нет аккаунтов с ролью мониторинга."); } if (taskRow && taskRow.require_same_bot_in_both) { const hasSame = accountRows.some((row) => row.role_monitor && row.role_invite); if (!hasSame) { warnings.push("Режим “один бот в обоих группах” включен, но нет аккаунта с обеими ролями."); readiness.ok = false; readiness.reasons.push("Нет аккаунта с двумя ролями в режиме “один бот”."); } } const inviteAccess = taskInviteAccessParsed || []; const canInvite = inviteAccess.filter((row) => row.canInvite); if (!canInvite.length) { if (taskRow && taskRow.invite_via_admins) { warnings.push("Нет аккаунтов с прямыми правами инвайта (режим «через админов»: права будут выданы в runtime)."); } else { warnings.push("Нет аккаунтов с правами инвайта в нашей группе."); } } const readinessDetail = readiness.ok ? "ok" : "blocked"; return { running: Boolean(runner && runner.isRunning()), queueCount, dailyUsed, unconfirmed, warnings, readiness, readinessDetail }; })(); const exportPayload = { exportedAt: new Date().toISOString(), formatVersion: 1, settings, task, taskInviteAccessParsed, inviteAccessSummary, taskStatus: { running: Boolean(runner && runner.isRunning()), queueCount: store.getPendingCount(id), queueStats, dailyUsed, effectiveDailyLimit, unconfirmedCount, invitesByStatus, monitorInfo, lastStopReason: task.last_stop_reason || "", lastStopAt: task.last_stop_at || "" }, statusSnapshot, competitors, taskAccounts, taskAccountsDetailed, roleSummary, membershipStatus, accounts, proxies, taskProxies, logs, invites, queue, fallback, confirmQueue, taskAudit, accountEvents, recentEvents, counts: { competitors: competitors.length, taskAccounts: taskAccounts.length, accounts: accounts.length, proxies: proxies.length, taskProxies: taskProxies.length, logs: logs.length, invites: invites.length, queue: queue.length, fallback: fallback.length, confirmQueue: confirmQueue.length, taskAudit: taskAudit.length, accountEvents: accountEvents.length, recentEvents: recentEvents.length, roleSummary } }; fs.writeFileSync(filePath, JSON.stringify(exportPayload, null, 2), "utf8"); return { ok: true, filePath, counts: exportPayload.counts }; }); ipcMain.handle("invites:export", async (_event, taskId) => { const { canceled, filePath } = await dialog.showSaveDialog({ title: "Выгрузить историю инвайтов", defaultPath: "invites.csv" }); if (canceled || !filePath) return { ok: false, canceled: true }; const invites = store.listInvites(2000, taskId); const enriched = invites.map((invite) => { const errorCode = extractErrorCode(invite.error); const skippedCode = extractErrorCode(invite.skippedReason); const confirmCode = extractErrorCode(invite.confirmError); return { ...invite, errorHuman: explainInviteError(errorCode), skippedReasonHuman: explainInviteError(skippedCode), confirmErrorHuman: explainInviteError(confirmCode), accountProxyId: invite.accountProxyId || 0, accountProxyLabel: invite.accountProxyLabel || "", watcherProxyId: invite.watcherProxyId || 0, watcherProxyLabel: invite.watcherProxyLabel || "" }; }); const csv = toCsv(enriched, [ "taskId", "invitedAt", "userId", "username", "status", "error", "errorHuman", "confirmed", "confirmError", "confirmErrorHuman", "accountId", "accountPhone", "accountProxyId", "accountProxyLabel", "watcherAccountId", "watcherPhone", "watcherProxyId", "watcherProxyLabel", "strategy", "strategyMeta", "sourceChat", "skippedReason", "skippedReasonHuman" ]); fs.writeFileSync(filePath, csv, "utf8"); return { ok: true, filePath }; }); ipcMain.handle("invites:exportProblems", async (_event, taskId) => { const { canceled, filePath } = await dialog.showSaveDialog({ title: "Выгрузить проблемные инвайты", defaultPath: "problem-invites.csv" }); if (canceled || !filePath) return { ok: false, canceled: true }; const all = store.listInvites(5000, taskId); const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; const filtered = all.filter((invite) => { const time = invite.invitedAt ? new Date(invite.invitedAt).getTime() : 0; if (!Number.isFinite(time) || time < cutoff) return false; if (invite.status === "success") return false; if (invite.status === "unconfirmed") return true; if (!invite.error && !invite.skippedReason) return false; return true; }).map((invite) => ({ userId: invite.userId, username: invite.username ? `@${invite.username}` : "", status: invite.status, error: invite.error || "", errorHuman: explainInviteError(extractErrorCode(invite.error)), skippedReason: invite.skippedReason || "", skippedReasonHuman: explainInviteError(extractErrorCode(invite.skippedReason)), confirmed: invite.confirmed, confirmError: invite.confirmError || "", confirmErrorHuman: explainInviteError(extractErrorCode(invite.confirmError)), invitedAt: invite.invitedAt, sourceChat: invite.sourceChat, targetChat: invite.targetChat, accountProxyId: invite.accountProxyId || 0, accountProxyLabel: invite.accountProxyLabel || "", watcherProxyId: invite.watcherProxyId || 0, watcherProxyLabel: invite.watcherProxyLabel || "" })); const csv = toCsv(filtered, [ "userId", "username", "status", "error", "errorHuman", "skippedReason", "skippedReasonHuman", "confirmed", "confirmError", "confirmErrorHuman", "invitedAt", "sourceChat", "targetChat", "accountProxyId", "accountProxyLabel", "watcherProxyId", "watcherProxyLabel" ]); fs.writeFileSync(filePath, csv, "utf8"); return { ok: true, filePath }; }); ipcMain.handle("fallback:export", async (_event, taskId) => { const { canceled, filePath } = await dialog.showSaveDialog({ title: "Выгрузить fallback список", defaultPath: "fallback.csv" }); if (canceled || !filePath) return { ok: false, canceled: true }; const rows = store.listFallback(5000, taskId); const csv = toCsv(rows, [ "taskId", "createdAt", "userId", "username", "reason", "route", "status", "sourceChat", "targetChat" ]); fs.writeFileSync(filePath, csv, "utf8"); return { ok: true, filePath }; }); ipcMain.handle("fallback:list", (_event, payload) => { if (payload && typeof payload === "object") { return store.listFallback(payload.limit || 500, payload.taskId); } return store.listFallback(payload || 500); }); ipcMain.handle("fallback:update", (_event, payload) => { if (!payload || !payload.id) return { ok: false, error: "Missing id" }; store.updateFallbackStatus(payload.id, payload.status || "done"); return { ok: true }; }); ipcMain.handle("fallback:clear", (_event, taskId) => { store.clearFallback(taskId); return { ok: true }; }); ipcMain.handle("accounts:events", async (_event, limit) => { return store.listAccountEvents(limit || 200); }); ipcMain.handle("accounts:eventAdd", (_event, payload) => { if (!payload) return { ok: false }; store.addAccountEvent(payload.accountId || 0, payload.phone || "", payload.action || "custom", payload.details || ""); return { ok: true }; }); ipcMain.handle("accounts:events:clear", async () => { store.clearAccountEvents(); return { ok: true }; }); ipcMain.handle("apiTrace:list", async (_event, payload) => { return store.listApiTraceLogs(payload || {}); }); ipcMain.handle("apiTrace:clear", async (_event, taskId) => { store.clearApiTraceLogs(taskId); return { ok: true }; }); ipcMain.handle("apiTrace:exportJson", async (_event, taskId) => { const parsedTaskId = Number(taskId || 0); const task = parsedTaskId > 0 ? store.getTask(parsedTaskId) : null; const baseName = sanitizeFileName(task && task.name ? task.name : `api-trace-${parsedTaskId || "all"}`); const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const { canceled, filePath } = await dialog.showSaveDialog({ title: "Экспорт API трассировки (JSON)", defaultPath: `${baseName}_${stamp}.json` }); if (canceled || !filePath) return { ok: false, canceled: true }; const rows = store.listApiTraceLogs({ limit: 100000, taskId: parsedTaskId || 0 }); const payload = { exportedAt: new Date().toISOString(), taskId: parsedTaskId || 0, count: rows.length, rows }; fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8"); return { ok: true, filePath, count: rows.length }; }); ipcMain.handle("apiTrace:exportCsv", async (_event, taskId) => { const parsedTaskId = Number(taskId || 0); const task = parsedTaskId > 0 ? store.getTask(parsedTaskId) : null; const baseName = sanitizeFileName(task && task.name ? task.name : `api-trace-${parsedTaskId || "all"}`); const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const { canceled, filePath } = await dialog.showSaveDialog({ title: "Экспорт API трассировки (CSV)", defaultPath: `${baseName}_${stamp}.csv` }); if (canceled || !filePath) return { ok: false, canceled: true }; const rows = store.listApiTraceLogs({ limit: 100000, taskId: parsedTaskId || 0 }); const csv = toCsv(rows, [ "id", "taskId", "accountId", "phone", "method", "ok", "durationMs", "errorText", "requestJson", "headersJson", "responseJson", "createdAt" ]); fs.writeFileSync(filePath, csv, "utf8"); return { ok: true, filePath, count: rows.length }; }); ipcMain.handle("tasks:audit", (_event, id) => { return store.listTaskAudit(id, 200); }); ipcMain.handle("tasks:audit:clear", (_event, id) => { store.clearTaskAudit(id); return { ok: true }; }); ipcMain.handle("accounts:refreshIdentity", async () => { const accounts = store.listAccounts(); for (const account of accounts) { await telegram.refreshAccountIdentity(account.id); } return { ok: true }; }); ipcMain.handle("task:start", async () => { const settings = store.getSettings(); if (settings.autoJoinOurGroup) { await telegram.ensureJoinOurGroup(settings.ourGroup); } await telegram.joinGroupsForAllAccounts(settings.competitorGroups, settings.ourGroup, settings); const monitorResult = await telegram.startMonitoring(settings.competitorGroups); scheduler.start(settings); return { running: true, monitorErrors: monitorResult && monitorResult.errors ? monitorResult.errors : [] }; }); ipcMain.handle("task:stop", async () => { scheduler.stop(); await telegram.stopMonitoring(); return { running: false }; }); ipcMain.handle("status:get", () => { const settings = store.getSettings(); const dailyUsed = store.countInvitesToday(); const dailyRemaining = Math.max(0, Number(settings.dailyLimit || 0) - dailyUsed); const queueCount = store.getPendingCount(); const accounts = store.listAccounts(); const connectedSessions = telegram.getConnectedCount(); const accountStats = accounts.map((account) => { const used = store.countInvitesTodayByAccount(account.id); const limit = Number(account.daily_limit || settings.accountDailyLimit || 0); const remaining = limit > 0 ? Math.max(0, limit - used) : null; return { id: account.id, usedToday: used, remainingToday: remaining, limit }; }); const monitorInfo = telegram.getMonitorInfo(); return { running: scheduler ? scheduler.isRunning() : false, queueCount, dailyRemaining, dailyUsed, dailyLimit: Number(settings.dailyLimit || 0), connectedSessions, totalAccounts: accounts.length, accountStats, monitorInfo }; }); ipcMain.handle("task:parseHistory", async (_event, limit) => { const settings = store.getSettings(); const result = await telegram.parseHistory(settings.competitorGroups, limit || settings.historyLimit); return result; }); ipcMain.handle("accounts:membershipStatus", async () => { const settings = store.getSettings(); const result = await telegram.getMembershipStatus(settings.competitorGroups, settings.ourGroup); return result; }); ipcMain.handle("groups:checkAccess", async () => { const settings = store.getSettings(); const result = await telegram.checkGroupAccess(settings.competitorGroups, settings.ourGroup); return result; });