import React, { Suspense, useEffect, useMemo, useRef, useState } from "react"; const AccountsTab = React.lazy(() => import("./tabs/AccountsTab.jsx")); const LogsTab = React.lazy(() => import("./tabs/LogsTab.jsx")); const EventsTab = React.lazy(() => import("./tabs/EventsTab.jsx")); const SettingsTab = React.lazy(() => import("./tabs/SettingsTab.jsx")); const emptySettings = { competitorGroups: [""], ourGroup: "", minIntervalMinutes: 5, maxIntervalMinutes: 10, dailyLimit: 100, historyLimit: 200, accountMaxGroups: 10, accountDailyLimit: 50, floodCooldownMinutes: 1440 }; const emptyTaskForm = { id: null, name: "", ourGroup: "", minIntervalMinutes: 5, maxIntervalMinutes: 10, dailyLimit: 100, historyLimit: 100, maxCompetitorBots: 1, maxOurBots: 1, randomAccounts: false, multiAccountsPerRun: false, retryOnFail: true, autoJoinCompetitors: true, autoJoinOurGroup: true, separateBotRoles: false, requireSameBotInBoth: true, stopOnBlocked: true, stopBlockedPercent: 25, notes: "", enabled: true, autoAssignAccounts: true }; const normalizeTask = (row) => ({ id: row.id, name: row.name || "", ourGroup: row.our_group || "", minIntervalMinutes: Number(row.min_interval_minutes || 5), maxIntervalMinutes: Number(row.max_interval_minutes || 10), dailyLimit: Number(row.daily_limit || 100), historyLimit: Number(row.history_limit || 200), maxCompetitorBots: Number(row.max_competitor_bots || 1), maxOurBots: Number(row.max_our_bots || 1), randomAccounts: Boolean(row.random_accounts), multiAccountsPerRun: Boolean(row.multi_accounts_per_run), retryOnFail: Boolean(row.retry_on_fail), autoJoinCompetitors: Boolean(row.auto_join_competitors), autoJoinOurGroup: Boolean(row.auto_join_our_group), separateBotRoles: Boolean(row.separate_bot_roles), requireSameBotInBoth: Boolean(row.require_same_bot_in_both), stopOnBlocked: Boolean(row.stop_on_blocked), stopBlockedPercent: Number(row.stop_blocked_percent || 25), notes: row.notes || "", enabled: Boolean(row.enabled), autoAssignAccounts: true }); const normalizeIntervals = (form) => { const min = Math.max(1, Number(form.minIntervalMinutes || 1)); let max = Math.max(1, Number(form.maxIntervalMinutes || 1)); if (max < min) max = min; return { ...form, minIntervalMinutes: min, maxIntervalMinutes: max }; }; const sanitizeTaskForm = (form) => { let normalized = { ...form }; normalized = normalizeIntervals(normalized); if (normalized.requireSameBotInBoth) { normalized.separateBotRoles = false; normalized.maxOurBots = normalized.maxCompetitorBots; } return normalized; }; export default function App() { const [settings, setSettings] = useState(emptySettings); const [accounts, setAccounts] = useState([]); const [accountStats, setAccountStats] = useState([]); const [accountAssignments, setAccountAssignments] = useState([]); const [logs, setLogs] = useState([]); const [invites, setInvites] = useState([]); const [tasks, setTasks] = useState([]); const [selectedTaskId, setSelectedTaskId] = useState(null); const [taskForm, setTaskForm] = useState(emptyTaskForm); const [competitorText, setCompetitorText] = useState(""); const [selectedAccountIds, setSelectedAccountIds] = useState([]); const [taskStatus, setTaskStatus] = useState({ running: false, queueCount: 0, dailyRemaining: 0, dailyUsed: 0, dailyLimit: 0, monitorInfo: { monitoring: false, groups: [], lastMessageAt: "", lastSource: "" }, nextRunAt: "" }); const [taskStatusMap, setTaskStatusMap] = useState({}); const [membershipStatus, setMembershipStatus] = useState({}); const [groupVisibility, setGroupVisibility] = useState([]); const [accessStatus, setAccessStatus] = useState([]); const [accountEvents, setAccountEvents] = useState([]); const [loginForm, setLoginForm] = useState({ apiId: "", apiHash: "", phone: "", code: "", password: "" }); const [tdataForm, setTdataForm] = useState({ apiId: "2040", apiHash: "b18441a1ff607e10a989891a5462e627" }); const [tdataResult, setTdataResult] = useState(null); const [tdataLoading, setTdataLoading] = useState(false); const [loginId, setLoginId] = useState(""); const [loginStatus, setLoginStatus] = useState(""); const [taskNotice, setTaskNotice] = useState(null); const [settingsNotice, setSettingsNotice] = useState(null); const [tdataNotice, setTdataNotice] = useState(null); const [notification, setNotification] = useState(null); const [notifications, setNotifications] = useState([]); const [notificationsOpen, setNotificationsOpen] = useState(false); const [manualLoginOpen, setManualLoginOpen] = useState(false); const [taskSearch, setTaskSearch] = useState(""); const [taskFilter, setTaskFilter] = useState("all"); const [notificationFilter, setNotificationFilter] = useState("all"); const [infoOpen, setInfoOpen] = useState(false); const [activeTab, setActiveTab] = useState("task"); const [logsTab, setLogsTab] = useState("logs"); const [logSearch, setLogSearch] = useState(""); const [inviteSearch, setInviteSearch] = useState(""); const [logPage, setLogPage] = useState(1); const [invitePage, setInvitePage] = useState(1); const [inviteFilter, setInviteFilter] = useState("all"); const [taskSort, setTaskSort] = useState("activity"); const [sidebarExpanded, setSidebarExpanded] = useState(false); const [expandedInviteId, setExpandedInviteId] = useState(null); const [now, setNow] = useState(Date.now()); const bellRef = useRef(null); const settingsAutosaveReady = useRef(false); const competitorGroups = useMemo(() => { return competitorText .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0); }, [competitorText]); const hasSelectedTask = selectedTaskId != null; const selectedTask = tasks.find((task) => task.id === selectedTaskId) || null; const selectedTaskName = selectedTask ? (selectedTask.name || `Задача #${selectedTask.id}`) : "—"; const roleMode = taskForm.requireSameBotInBoth ? "same" : taskForm.separateBotRoles ? "split" : "shared"; const canSaveTask = Boolean( taskForm.name.trim() && taskForm.ourGroup.trim() && competitorGroups.length > 0 ); const accountById = useMemo(() => { const map = new Map(); accounts.forEach((account) => { map.set(account.id, account); }); return map; }, [accounts]); const assignedAccountMap = useMemo(() => { const map = new Map(); accountAssignments.forEach((row) => { const list = map.get(row.account_id) || []; list.push(row.task_id); map.set(row.account_id, list); }); return map; }, [accountAssignments]); const filterFreeAccounts = tasks.length > 1; const accountBuckets = useMemo(() => { const selected = selectedTaskId; const freeOrSelected = []; const busy = []; const taskNameMap = new Map(); tasks.forEach((task) => { taskNameMap.set(task.id, task.name || `Задача #${task.id}`); }); accounts.forEach((account) => { const assignedTasks = assignedAccountMap.get(account.id) || []; const assignedToSelected = selected != null && assignedTasks.includes(selected); const isFree = assignedTasks.length === 0; if (filterFreeAccounts && !isFree && !assignedToSelected) { busy.push(account); } else { freeOrSelected.push(account); } }); return { freeOrSelected, busy, taskNameMap }; }, [accounts, assignedAccountMap, selectedTaskId, filterFreeAccounts, tasks]); const loadTasks = async () => { const tasksData = await window.api.listTasks(); setTasks(tasksData); if (!tasksData.length) { setSelectedTaskId(null); return tasksData; } if (selectedTaskId == null) { setSelectedTaskId(tasksData[0].id); return tasksData; } if (!tasksData.some((task) => task.id === selectedTaskId)) { setSelectedTaskId(tasksData[0].id); } return tasksData; }; const loadAccountAssignments = async () => { if (!window.api) return; const assignments = await window.api.listAccountAssignments(); setAccountAssignments(assignments || []); }; const loadTaskStatuses = async (tasksData) => { const entries = await Promise.all( (tasksData || []).map(async (task) => { const status = await window.api.taskStatus(task.id); return [task.id, status]; }) ); const map = {}; entries.forEach(([id, status]) => { map[id] = status; }); setTaskStatusMap(map); }; const loadSelectedTask = async (taskId) => { if (!taskId) { setTaskForm(emptyTaskForm); setCompetitorText(""); setSelectedAccountIds([]); setLogs([]); setInvites([]); setGroupVisibility([]); setTaskStatus({ running: false, queueCount: 0, dailyRemaining: 0, dailyUsed: 0, dailyLimit: 0, monitorInfo: { monitoring: false, groups: [], lastMessageAt: "", lastSource: "" }, nextRunAt: "" }); return; } const details = await window.api.getTask(taskId); if (!details) return; setTaskForm(sanitizeTaskForm({ ...emptyTaskForm, ...normalizeTask(details.task) })); setCompetitorText((details.competitors || []).join("\n")); setSelectedAccountIds(details.accountIds || []); setLogs(await window.api.listLogs({ limit: 100, taskId })); setInvites(await window.api.listInvites({ limit: 200, taskId })); setGroupVisibility([]); setTaskStatus(await window.api.taskStatus(taskId)); }; const loadBase = async () => { const [settingsData, accountsData, eventsData, statusData] = await Promise.all([ window.api.getSettings(), window.api.listAccounts(), window.api.listAccountEvents(200), window.api.getStatus() ]); setSettings(settingsData); setAccounts(accountsData); setAccountEvents(eventsData); setAccountStats(statusData.accountStats || []); const tasksData = await loadTasks(); await loadAccountAssignments(); await loadTaskStatuses(tasksData); }; useEffect(() => { loadBase(); }, []); useEffect(() => { loadSelectedTask(selectedTaskId); setAccessStatus([]); setMembershipStatus({}); setTaskNotice(null); }, [selectedTaskId]); const taskSummary = useMemo(() => { const totals = { total: tasks.length, running: 0, queue: 0, dailyUsed: 0, dailyLimit: 0 }; tasks.forEach((task) => { const status = taskStatusMap[task.id]; if (status && status.running) totals.running += 1; if (status) { totals.queue += Number(status.queueCount || 0); totals.dailyUsed += Number(status.dailyUsed || 0); totals.dailyLimit += Number(status.dailyLimit || 0); } }); return totals; }, [tasks, taskStatusMap]); const filteredTasks = useMemo(() => { const query = taskSearch.trim().toLowerCase(); const filtered = tasks.filter((task) => { const name = (task.name || "").toLowerCase(); const group = (task.our_group || "").toLowerCase(); const matchesQuery = !query || name.includes(query) || group.includes(query) || String(task.id).includes(query); if (!matchesQuery) return false; const status = taskStatusMap[task.id]; if (taskFilter === "running") return Boolean(status && status.running); if (taskFilter === "stopped") return Boolean(status && !status.running); return true; }); const sorted = [...filtered].sort((a, b) => { const statusA = taskStatusMap[a.id]; const statusB = taskStatusMap[b.id]; if (taskSort === "queue") { return (statusB ? statusB.queueCount : 0) - (statusA ? statusA.queueCount : 0); } if (taskSort === "limit") { return (statusB ? statusB.dailyLimit : 0) - (statusA ? statusA.dailyLimit : 0); } if (taskSort === "lastMessage") { const dateA = statusA && statusA.monitorInfo && statusA.monitorInfo.lastMessageAt ? Date.parse(statusA.monitorInfo.lastMessageAt) || 0 : 0; const dateB = statusB && statusB.monitorInfo && statusB.monitorInfo.lastMessageAt ? Date.parse(statusB.monitorInfo.lastMessageAt) || 0 : 0; return dateB - dateA; } if (taskSort === "activity") { const aActive = statusA && statusA.running ? 1 : 0; const bActive = statusB && statusB.running ? 1 : 0; if (bActive !== aActive) return bActive - aActive; } if (taskSort === "id") { return b.id - a.id; } return b.id - a.id; }); return sorted; }, [tasks, taskSearch, taskFilter, taskSort, taskStatusMap]); useEffect(() => { if (!window.api) return undefined; const interval = setInterval(async () => { const tasksData = await window.api.listTasks(); setTasks(tasksData); await loadTaskStatuses(tasksData); setAccounts(await window.api.listAccounts()); setAccountAssignments(await window.api.listAccountAssignments()); const statusData = await window.api.getStatus(); setAccountStats(statusData.accountStats || []); if (selectedTaskId != null) { setTaskStatus(await window.api.taskStatus(selectedTaskId)); } }, 5000); return () => clearInterval(interval); }, [selectedTaskId]); useEffect(() => { if (!window.api || activeTab !== "logs" || selectedTaskId == null) return undefined; const load = async () => { setLogs(await window.api.listLogs({ limit: 100, taskId: selectedTaskId })); setInvites(await window.api.listInvites({ limit: 200, taskId: selectedTaskId })); }; load(); const interval = setInterval(load, 5000); return () => clearInterval(interval); }, [activeTab, selectedTaskId]); useEffect(() => { if (!window.api || activeTab !== "events") return undefined; const load = async () => { setAccountEvents(await window.api.listAccountEvents(200)); }; load(); const interval = setInterval(load, 10000); return () => clearInterval(interval); }, [activeTab]); useEffect(() => { const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, []); useEffect(() => { if (selectedTaskId == null) return; setTaskStatusMap((prev) => ({ ...prev, [selectedTaskId]: taskStatus })); }, [selectedTaskId, taskStatus]); const formatAccountStatus = (status) => { if (status === "limited") return "В спаме"; if (status === "ok") return "ОК"; return status || "Неизвестно"; }; const formatTimestamp = (value) => { if (!value) return "—"; const date = value instanceof Date ? value : new Date(value); if (Number.isNaN(date.getTime())) return "—"; return date.toLocaleString("ru-RU"); }; const formatCountdown = (target) => { if (!target) return "—"; const targetTime = new Date(target).getTime(); if (!Number.isFinite(targetTime)) return "—"; const diff = Math.max(0, Math.floor((targetTime - now) / 1000)); const minutes = Math.floor(diff / 60); const seconds = diff % 60; return `${minutes}:${String(seconds).padStart(2, "0")}`; }; const explainInviteError = (error) => { if (!error) return ""; if (error === "USER_ID_INVALID") { return "Пользователь удален/скрыт; access_hash невалиден для этой сессии; приглашение в канал/чат без валидной сущности."; } if (error === "CHAT_WRITE_FORBIDDEN") { return "Аккаунт не может приглашать: нет прав или он не участник группы."; } if (error === "AUTH_KEY_DUPLICATED") { return "Сессия используется в другом месте, Telegram отозвал ключ."; } if (error.includes("FLOOD") || error.includes("PEER_FLOOD")) { return "Ограничение Telegram по частоте действий."; } return ""; }; const showNotification = (text, tone) => { if (tone === "success") return; const entry = { text, tone, id: Date.now() }; setNotification(entry); setNotifications((prev) => [entry, ...prev].slice(0, 6)); }; useEffect(() => { if (!notification) return undefined; const timer = setTimeout(() => { setNotification(null); }, 6000); return () => clearTimeout(timer); }, [notification]); useEffect(() => { const handleClickOutside = (event) => { if (!notificationsOpen) return; if (!bellRef.current) return; if (!bellRef.current.contains(event.target)) { setNotificationsOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [notificationsOpen]); const filteredNotifications = useMemo(() => { if (notificationFilter === "all") return notifications; return notifications.filter((item) => item.tone === notificationFilter); }, [notifications, notificationFilter]); const filteredLogs = useMemo(() => { const query = logSearch.trim().toLowerCase(); if (!query) return logs; return logs.filter((log) => { const text = [ log.startedAt, log.finishedAt, String(log.invitedCount), (log.successIds || []).join(","), (log.errors || []).join("|") ] .join(" ") .toLowerCase(); return text.includes(query); }); }, [logs, logSearch]); const filteredInvites = useMemo(() => { const query = inviteSearch.trim().toLowerCase(); return invites.filter((invite) => { if (inviteFilter === "success" && invite.status !== "success") return false; if (inviteFilter === "error" && invite.status === "success") return false; if (inviteFilter === "skipped" && !invite.skippedReason) return false; const text = [ invite.invitedAt, invite.userId, invite.username, invite.sourceChat, invite.accountPhone, invite.watcherPhone, invite.strategy, invite.strategyMeta, invite.error, invite.skippedReason ] .join(" ") .toLowerCase(); if (!query) return true; return text.includes(query); }); }, [invites, inviteSearch, inviteFilter]); const inviteStrategyStats = useMemo(() => { let success = 0; let failed = 0; invites.forEach((invite) => { if (!invite.strategyMeta) return; try { const parsed = JSON.parse(invite.strategyMeta); if (!Array.isArray(parsed) || !parsed.length) return; const hasOk = parsed.some((item) => item.ok); if (hasOk) success += 1; else failed += 1; } catch (error) { // ignore parse errors } }); return { success, failed }; }, [invites]); const logPageSize = 20; const invitePageSize = 20; const logPageCount = Math.max(1, Math.ceil(filteredLogs.length / logPageSize)); const invitePageCount = Math.max(1, Math.ceil(filteredInvites.length / invitePageSize)); const pagedLogs = filteredLogs.slice((logPage - 1) * logPageSize, logPage * logPageSize); const pagedInvites = filteredInvites.slice((invitePage - 1) * invitePageSize, invitePage * invitePageSize); const onSettingsChange = (field, value) => { setSettings((prev) => ({ ...prev, [field]: value })); }; const updateIntervals = (nextMin, nextMax) => { const updated = normalizeIntervals({ ...taskForm, minIntervalMinutes: nextMin, maxIntervalMinutes: nextMax }); setTaskForm(updated); }; const applyRoleMode = (mode) => { if (mode === "same") { setTaskForm(sanitizeTaskForm({ ...taskForm, requireSameBotInBoth: true, separateBotRoles: false })); return; } if (mode === "split") { setTaskForm({ ...taskForm, requireSameBotInBoth: false, separateBotRoles: true }); return; } setTaskForm({ ...taskForm, requireSameBotInBoth: false, separateBotRoles: false }); }; const resetCooldown = async (accountId) => { if (!window.api) { showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); return; } try { await window.api.resetAccountCooldown(accountId); const updated = await window.api.listAccounts(); setAccounts(updated); setTaskNotice({ text: "Аккаунт снова активен.", tone: "success", source: "accounts" }); } catch (error) { setTaskNotice({ text: error.message || String(error), tone: "error", source: "accounts" }); showNotification(error.message || String(error), "error"); } }; const deleteAccount = async (accountId) => { if (!window.api) { showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); return; } try { await window.api.deleteAccount(accountId); setAccounts(await window.api.listAccounts()); setTaskNotice({ text: "Аккаунт удален.", tone: "success", source: "accounts" }); } catch (error) { setTaskNotice({ text: error.message || String(error), tone: "error", source: "accounts" }); showNotification(error.message || String(error), "error"); } }; const refreshIdentity = async () => { if (!window.api) { showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); return; } try { await window.api.refreshAccountIdentity(); setAccounts(await window.api.listAccounts()); setTaskNotice({ text: "ID аккаунтов обновлены.", tone: "success", source: "accounts" }); } catch (error) { setTaskNotice({ text: error.message || String(error), tone: "error", source: "accounts" }); showNotification(error.message || String(error), "error"); } }; const saveSettings = async () => { if (!window.api) { setSettingsNotice({ text: "Electron API недоступен. Откройте приложение в Electron.", tone: "error" }); showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); return; } try { showNotification("Сохраняем настройки...", "info"); const updated = await window.api.saveSettings(settings); setSettings(updated); setSettingsNotice({ text: "Настройки сохранены.", tone: "success" }); } catch (error) { const message = error.message || String(error); setSettingsNotice({ text: message, tone: "error" }); showNotification(message, "error"); } }; useEffect(() => { if (!settingsAutosaveReady.current) { settingsAutosaveReady.current = true; return; } if (!window.api) return; const timer = setTimeout(async () => { try { const updated = await window.api.saveSettings(settings); setSettings(updated); } catch (error) { showNotification(error.message || String(error), "error"); } }, 600); return () => clearTimeout(timer); }, [settings]); const createTask = () => { setSelectedTaskId(null); setTaskForm(emptyTaskForm); setCompetitorText(""); setSelectedAccountIds([]); setAccessStatus([]); setMembershipStatus({}); }; const selectTask = (taskId) => { if (taskId === selectedTaskId) return; setSelectedTaskId(taskId); }; const saveTask = async (source = "editor") => { if (!window.api) { showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); return; } try { showNotification("Сохраняем задачу...", "info"); const nextForm = sanitizeTaskForm(taskForm); setTaskForm(nextForm); let accountIds = selectedAccountIds; if (nextForm.autoAssignAccounts && (!accountIds || accountIds.length === 0)) { accountIds = accounts.map((account) => account.id); setSelectedAccountIds(accountIds); if (accountIds.length) { setTaskNotice({ text: `Автоназначены аккаунты: ${accountIds.length}`, tone: "success", source }); } } if (!accountIds.length) { showNotification("Нет аккаунтов для этой задачи.", "error"); return; } const requiredAccounts = nextForm.requireSameBotInBoth ? Math.max(1, Number(nextForm.maxCompetitorBots || 1)) : nextForm.separateBotRoles ? Math.max(1, Number(nextForm.maxCompetitorBots || 1)) + Math.max(1, Number(nextForm.maxOurBots || 1)) : 1; if (accountIds.length < requiredAccounts) { showNotification(`Нужно минимум ${requiredAccounts} аккаунтов для выбранного режима.`, "error"); return; } const result = await window.api.saveTask({ task: nextForm, competitors: competitorGroups, accountIds }); if (result.ok) { setTaskNotice({ text: "Задача сохранена.", tone: "success", source }); await loadTasks(); await loadAccountAssignments(); setSelectedTaskId(result.taskId); } else { showNotification(result.error || "Не удалось сохранить задачу", "error"); } } catch (error) { showNotification(error.message || String(error), "error"); } }; const deleteTask = async () => { if (!window.api || selectedTaskId == null) { return; } try { await window.api.deleteTask(selectedTaskId); setTaskNotice({ text: "Задача удалена.", tone: "success", source: "tasks" }); await loadTasks(); await loadAccountAssignments(); } catch (error) { showNotification(error.message || String(error), "error"); } }; const startTask = async (source = "sidebar") => { if (!window.api || selectedTaskId == null) { showNotification("Сначала выберите задачу.", "error"); return; } showNotification("Запуск...", "info"); try { const result = await window.api.startTaskById(selectedTaskId); if (result && result.ok) { setTaskNotice({ text: "Запущено.", tone: "success", source }); } else { showNotification(result.error || "Не удалось запустить", "error"); } } catch (error) { const message = error.message || String(error); setTaskNotice({ text: message, tone: "error", source }); showNotification(message, "error"); } }; const startAllTasks = async () => { if (!window.api) { showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); return; } if (!tasks.length) { showNotification("Нет задач для запуска.", "info"); return; } showNotification("Запускаем все задачи...", "info"); try { const result = await window.api.startAllTasks(); if (result && result.errors && result.errors.length) { const errorText = result.errors.map((item) => `${item.id}: ${item.error}`).join(" | "); showNotification(`Ошибки запуска: ${errorText}`, "error"); } const tasksData = await loadTasks(); await loadTaskStatuses(tasksData); } catch (error) { showNotification(error.message || String(error), "error"); } }; const stopTask = async (source = "sidebar") => { if (!window.api || selectedTaskId == null) { showNotification("Сначала выберите задачу.", "error"); return; } if (!window.confirm(`Остановить задачу: ${selectedTaskName}?`)) { return; } showNotification("Остановка...", "info"); try { await window.api.stopTaskById(selectedTaskId); setTaskNotice({ text: "Остановлено.", tone: "success", source }); } catch (error) { const message = error.message || String(error); setTaskNotice({ text: message, tone: "error", source }); showNotification(message, "error"); } }; const stopAllTasks = async () => { if (!window.api) { showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); return; } if (!tasks.length) { showNotification("Нет задач для остановки.", "info"); return; } if (!window.confirm("Остановить все задачи?")) { return; } showNotification("Останавливаем все задачи...", "info"); try { await window.api.stopAllTasks(); const tasksData = await loadTasks(); await loadTaskStatuses(tasksData); } catch (error) { showNotification(error.message || String(error), "error"); } }; const parseHistory = async (source = "editor") => { if (!window.api || selectedTaskId == null) { showNotification("Сначала выберите задачу.", "error"); return; } showNotification("Собираем историю...", "info"); try { const result = await window.api.parseHistoryByTask(selectedTaskId); if (result && result.ok) { setTaskNotice({ text: "История добавлена в очередь.", tone: "success", source }); if (result.errors && result.errors.length) { showNotification(`Ошибки истории: ${result.errors.join(" | ")}`, "error"); } setLogs(await window.api.listLogs({ limit: 100, taskId: selectedTaskId })); setInvites(await window.api.listInvites({ limit: 200, taskId: selectedTaskId })); return; } const message = result.error || "Ошибка при сборе истории"; setTaskNotice({ text: message, tone: "error", source }); showNotification(message, "error"); } catch (error) { const message = error.message || String(error); setTaskNotice({ text: message, tone: "error", source }); showNotification(message, "error"); } }; const refreshMembership = async (source = "editor") => { if (!window.api || selectedTaskId == null) { showNotification("Сначала выберите задачу.", "error"); return; } showNotification("Проверяем участие аккаунтов...", "info"); try { const status = await window.api.membershipStatusByTask(selectedTaskId); const visibility = await window.api.groupVisibilityByTask(selectedTaskId); const map = {}; status.forEach((item) => { map[item.accountId] = item; }); setMembershipStatus(map); setGroupVisibility(visibility && visibility.result ? visibility.result : []); setTaskNotice({ text: "Статус участия обновлен.", tone: "success", source }); } catch (error) { const message = error.message || String(error); setTaskNotice({ text: message, tone: "error", source }); showNotification(message, "error"); } }; const checkAccess = async (source = "editor") => { if (!window.api || selectedTaskId == null) { showNotification("Сначала выберите задачу.", "error"); return; } showNotification("Проверяем доступ к группам...", "info"); try { const result = await window.api.checkAccessByTask(selectedTaskId); if (!result.ok) { showNotification(result.error || "Не удалось проверить доступ", "error"); return; } setAccessStatus(result.result || []); setTaskNotice({ text: "Проверка доступа завершена.", tone: "success", source }); } catch (error) { showNotification(error.message || String(error), "error"); } }; const clearLogs = async (source = "editor") => { if (!window.api || selectedTaskId == null) { showNotification("Сначала выберите задачу.", "error"); return; } try { await window.api.clearLogs(selectedTaskId); setLogs([]); setTaskNotice({ text: "Логи очищены.", tone: "success", source }); } catch (error) { showNotification(error.message || String(error), "error"); } }; const clearInvites = async (source = "editor") => { if (!window.api || selectedTaskId == null) { showNotification("Сначала выберите задачу.", "error"); return; } try { await window.api.clearInvites(selectedTaskId); setInvites([]); setTaskNotice({ text: "История инвайтов очищена.", tone: "success", source }); } catch (error) { showNotification(error.message || String(error), "error"); } }; const exportLogs = async (source = "editor") => { if (!window.api || selectedTaskId == null) { showNotification("Сначала выберите задачу.", "error"); return; } try { const result = await window.api.exportLogs(selectedTaskId); if (result && result.canceled) return; setTaskNotice({ text: `Логи выгружены: ${result.filePath}`, tone: "success", source }); } catch (error) { showNotification(error.message || String(error), "error"); } }; const exportInvites = async (source = "editor") => { if (!window.api || selectedTaskId == null) { showNotification("Сначала выберите задачу.", "error"); return; } try { const result = await window.api.exportInvites(selectedTaskId); if (result && result.canceled) return; setTaskNotice({ text: `История инвайтов выгружена: ${result.filePath}`, tone: "success", source }); } catch (error) { showNotification(error.message || String(error), "error"); } }; const clearQueue = async (source = "editor") => { if (!window.api || selectedTaskId == null) { showNotification("Сначала выберите задачу.", "error"); return; } try { await window.api.clearQueue(selectedTaskId); const data = await window.api.taskStatus(selectedTaskId); setTaskStatus(data); setTaskNotice({ text: "Очередь очищена.", tone: "success", source }); } catch (error) { showNotification(error.message || String(error), "error"); } }; const clearDatabase = async () => { if (!window.api) { showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); return; } if (!window.confirm("Удалить все данные из базы? Это действие нельзя отменить.")) { return; } try { await window.api.clearDatabase(); showNotification("База очищена.", "info"); setSelectedTaskId(null); setTaskForm(emptyTaskForm); setCompetitorText(""); setSelectedAccountIds([]); setLogs([]); setInvites([]); setTaskStatus({ running: false, queueCount: 0, dailyRemaining: 0, dailyUsed: 0, dailyLimit: 0, monitorInfo: { monitoring: false, groups: [], lastMessageAt: "", lastSource: "" } }); await loadBase(); } catch (error) { showNotification(error.message || String(error), "error"); } }; const toggleAccountSelection = (accountId) => { setSelectedAccountIds((prev) => { if (prev.includes(accountId)) { return prev.filter((id) => id !== accountId); } return [...prev, accountId]; }); }; const assignAccountsToTask = async (accountIds) => { if (!window.api || selectedTaskId == null) return; if (!accountIds.length) return; const result = await window.api.appendTaskAccounts({ taskId: selectedTaskId, accountIds }); if (result && result.ok) { setSelectedAccountIds(result.accountIds || []); await loadAccountAssignments(); } }; const moveAccountToTask = async (accountId) => { if (!window.api || selectedTaskId == null) return; await assignAccountsToTask([accountId]); setTaskNotice({ text: "Аккаунт добавлен в задачу.", tone: "success", source: "accounts" }); }; const removeAccountFromTask = async (accountId) => { if (!window.api || selectedTaskId == null) return; const result = await window.api.removeTaskAccount({ taskId: selectedTaskId, accountId }); if (result && result.ok) { setSelectedAccountIds(result.accountIds || []); await loadAccountAssignments(); setTaskNotice({ text: "Аккаунт удален из задачи.", tone: "success", source: "accounts" }); } }; const startLogin = async () => { if (!window.api) { setLoginStatus("Electron API недоступен. Откройте приложение в Electron."); showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); return; } if (selectedTaskId == null) { setLoginStatus("Сначала выберите задачу."); showNotification("Сначала выберите задачу.", "error"); return; } setLoginStatus("Отправляем код..."); showNotification("Отправляем код...", "info"); try { const result = await window.api.startLogin({ apiId: loginForm.apiId, apiHash: loginForm.apiHash, phone: loginForm.phone }); setLoginId(result.loginId); setLoginStatus("Код отправлен. Введите код для входа."); showNotification("Код отправлен. Введите код для входа.", "success"); } catch (error) { const message = error.message || String(error); setLoginStatus(message); showNotification(message, "error"); } }; const completeLogin = async () => { if (!window.api) { setLoginStatus("Electron API недоступен. Откройте приложение в Electron."); showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); return; } if (selectedTaskId == null) { setLoginStatus("Сначала выберите задачу."); showNotification("Сначала выберите задачу.", "error"); return; } setLoginStatus("Завершаем вход..."); showNotification("Завершаем вход...", "info"); const result = await window.api.completeLogin({ loginId, code: loginForm.code, password: loginForm.password }); if (result.ok) { setLoginStatus("Аккаунт добавлен."); setLoginId(""); setLoginForm({ apiId: "", apiHash: "", phone: "", code: "", password: "" }); await assignAccountsToTask([result.accountId].filter(Boolean)); setAccounts(await window.api.listAccounts()); return; } if (result.error === "DUPLICATE_ACCOUNT") { setLoginStatus("Аккаунт уже добавлен. Привязан к задаче."); setLoginId(""); setLoginForm({ apiId: "", apiHash: "", phone: "", code: "", password: "" }); await assignAccountsToTask([result.accountId].filter(Boolean)); setAccounts(await window.api.listAccounts()); return; } if (result.error === "PASSWORD_REQUIRED") { setLoginStatus("Нужен пароль 2FA. Введите пароль."); showNotification("Нужен пароль 2FA. Введите пароль.", "info"); return; } setLoginStatus(result.error || "Ошибка входа"); showNotification(result.error || "Ошибка входа", "error"); }; const importTdata = async () => { if (!window.api) { showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); return; } showNotification("Импортируем tdata...", "info"); setTdataLoading(true); try { const result = await window.api.importTdata({ apiId: tdataForm.apiId, apiHash: tdataForm.apiHash, taskId: selectedTaskId || undefined }); if (result && result.canceled) return; if (!result.ok) { showNotification(result.error || "Ошибка импорта tdata", "error"); return; } setTdataResult(result); const importedCount = (result.imported || []).length; const skippedCount = (result.skipped || []).length; const failedCount = (result.failed || []).length; const importedIds = (result.imported || []).map((item) => item.accountId).filter(Boolean); const skippedIds = (result.skipped || []).map((item) => item.accountId).filter(Boolean); if ((importedIds.length || skippedIds.length) && hasSelectedTask) { await assignAccountsToTask([...importedIds, ...skippedIds]); } if (importedCount > 0) { setTdataNotice({ text: `Импортировано аккаунтов: ${importedCount}`, tone: "success" }); } else if (skippedCount > 0 && failedCount === 0) { setTdataNotice({ text: `Пропущено дубликатов: ${skippedCount}`, tone: "success" }); } if (failedCount > 0) { showNotification(`Не удалось импортировать: ${failedCount}`, "error"); } setAccounts(await window.api.listAccounts()); } catch (error) { showNotification(error.message || String(error), "error"); } finally { setTdataLoading(false); } }; return (
Парсинг сообщений и приглашения в целевые группы.
“Собрать историю” добавляет в очередь пользователей, которые писали ранее. Без этого будут учитываться только новые сообщения.