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 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; } if (account.status && account.status !== "ok") { removedError += 1; return; } filtered.push({ 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) }); }); if (removedMissing || removedError) { store.setTaskAccountRoles(taskId, filtered); } return { filtered, removedMissing, removedError }; }; const startTaskWithChecks = async (id) => { const task = store.getTask(id); if (!task) return { ok: false, error: "Task not found" }; const competitors = store.listTaskCompetitors(id).map((row) => row.link); const taskAccounts = store.listTaskAccounts(id); const existingAccounts = store.listAccounts(); const filteredResult = filterTaskRolesByAccounts(id, taskAccounts, existingAccounts); const filteredRoles = filteredResult.filtered; let adminPrepPartialWarning = ""; 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) { return { ok: false, error: "Нет аккаунтов с ролью инвайта." }; } if (!monitorIds.length) { return { ok: false, error: "Нет аккаунтов с ролью мониторинга." }; } 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); if (inviteAccess && inviteAccess.ok) { store.setTaskInviteAccess(id, inviteAccess.result || []); const canInvite = (inviteAccess.result || []).filter((row) => row.canInvite); 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}` : ""}.`; } 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) { if (!task.invite_admin_master_id) { return { ok: false, error: "Не выбран главный аккаунт для инвайта через админов." }; } const adminPrep = await telegram.prepareInviteAdmins(task, task.invite_admin_master_id, inviteIds); if (adminPrep && !adminPrep.ok) { return { ok: false, error: adminPrep.error || "Не удалось подготовить права админов." }; } if (adminPrep && Array.isArray(adminPrep.result)) { const failed = adminPrep.result.filter((item) => !item.ok); if (failed.length) { adminPrepPartialWarning = `Часть прав админа не выдана: ${failed.length} аккаунт(ов).`; } } } 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}.`); } 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) => store.saveSettings(settings)); ipcMain.handle("accounts:list", () => store.listAccounts()); 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: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(); 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); } } if (authKeyDuplicatedCount > 0) { telegram.resetAllSessions(); } 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 : "", "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, 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 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 || "" ); 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") { 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 || "" ); } else { if (task.retry_on_fail) { store.incrementInviteAttempt(item.id); store.markInviteStatus(item.id, "pending"); } else { store.markInviteStatus(item.id, "failed"); } 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 || "" ); } 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: store.listTaskCompetitors(id).map((row) => row.link), 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_allow_flood !== (payload.task.inviteAdminAllowFlood ? 1 : 0)) { changes.inviteAdminAllowFlood = [Boolean(existing.invite_admin_allow_flood), Boolean(payload.task.inviteAdminAllowFlood)]; } 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 unconfirmedCount = store.countInvitesByStatus(id, "unconfirmed"); const task = store.getTask(id); const monitorInfo = telegram.getTaskMonitorInfo(id); const warnings = []; const readiness = { ok: true, reasons: [] }; if (task) { const accountRows = store.listTaskAccounts(id); const accounts = store.listAccounts(); const accountsById = new Map(accounts.map((acc) => [acc.id, acc])); 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) { warnings.push("Задача остановлена: нет доступных аккаунтов."); store.setTaskStopReason(id, "Нет доступных аккаунтов"); runner.stop(); } } const inviteRows = accountRows.filter((row) => row.role_invite); const monitorRows = accountRows.filter((row) => row.role_monitor); if (!inviteRows.length) { 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} задачах. Лимиты действий/групп общие.`); } }); 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, unconfirmedCount, dailyLimit: effectiveLimit, 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, 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 = store.listTaskCompetitors(id).map((row) => row.link); 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 = store.listTaskCompetitors(id).map((row) => row.link); 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 = store.listTaskCompetitors(id).map((row) => row.link); 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 = store.listTaskCompetitors(id).map((row) => row.link); 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 accountRows = store.listTaskAccounts(id).filter((row) => row.role_confirm); 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 inviteIdSet = task.separate_confirm_roles ? new Set(store.listTaskAccounts(id).filter((row) => row.role_invite).map((row) => row.account_id)) : null; const accountIds = accountRows .filter((row) => existingIds.has(row.account_id)) .map((row) => row.account_id) .filter((accountId) => !inviteIdSet || !inviteIdSet.has(accountId)); 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 = store.listTaskCompetitors(id).map((row) => row.link); 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 === "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 : "", 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", "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 = store.listTaskCompetitors(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 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 accountEvents = allAccountEvents.filter((item) => { if (taskAccountIds.has(Number(item.accountId))) return true; const message = String(item.message || "").toLowerCase(); return taskHints.some((hint) => message.includes(hint)); }); const exportPayload = { exportedAt: new Date().toISOString(), formatVersion: 1, task, competitors, taskAccounts, accounts, logs, invites, queue, fallback, confirmQueue, taskAudit, accountEvents, counts: { competitors: competitors.length, taskAccounts: taskAccounts.length, accounts: accounts.length, logs: logs.length, invites: invites.length, queue: queue.length, fallback: fallback.length, confirmQueue: confirmQueue.length, taskAudit: taskAudit.length, accountEvents: accountEvents.length } }; 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) }; }); const csv = toCsv(enriched, [ "taskId", "invitedAt", "userId", "username", "status", "error", "errorHuman", "confirmed", "confirmError", "confirmErrorHuman", "accountId", "accountPhone", "watcherAccountId", "watcherPhone", "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 })); const csv = toCsv(filtered, [ "userId", "username", "status", "error", "errorHuman", "skippedReason", "skippedReasonHuman", "confirmed", "confirmError", "confirmErrorHuman", "invitedAt", "sourceChat", "targetChat" ]); 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("tasks:audit", (_event, id) => { return store.listTaskAudit(id, 200); }); 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; });