diff --git a/src/main/index.js b/src/main/index.js index 0f2693f..203d392 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -37,7 +37,11 @@ function createWindow() { async function bootstrap() { store = initStore(app.getPath("userData")); telegram = new TelegramManager(store); - await telegram.init(); + try { + await telegram.init(); + } catch (error) { + console.error("Failed to initialize Telegram clients:", error); + } scheduler = new Scheduler(store, telegram); } @@ -401,6 +405,29 @@ ipcMain.handle("tasks:membershipStatus", async (_event, id) => { return telegram.getMembershipStatus(competitors, task.our_group); }); +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); + 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) + })); + store.setTaskAccountRoles(id, filtered); + } + const accountIds = accountRows + .filter((row) => existingIds.has(row.account_id)) + .map((row) => row.account_id); + return telegram.checkInvitePermissions(task, accountIds); +}); + ipcMain.handle("tasks:groupVisibility", async (_event, id) => { const task = store.getTask(id); if (!task) return { ok: false, error: "Task not found" }; @@ -475,6 +502,11 @@ ipcMain.handle("accounts:events", async (_event, limit) => { return store.listAccountEvents(limit || 200); }); +ipcMain.handle("accounts:events:clear", async () => { + store.clearAccountEvents(); + return { ok: true }; +}); + ipcMain.handle("accounts:refreshIdentity", async () => { const accounts = store.listAccounts(); for (const account of accounts) { diff --git a/src/main/preload.js b/src/main/preload.js index a3b275e..0cedf1e 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -6,6 +6,7 @@ contextBridge.exposeInMainWorld("api", { listAccounts: () => ipcRenderer.invoke("accounts:list"), resetAccountCooldown: (accountId) => ipcRenderer.invoke("accounts:resetCooldown", accountId), listAccountEvents: (limit) => ipcRenderer.invoke("accounts:events", limit), + clearAccountEvents: () => ipcRenderer.invoke("accounts:events:clear"), deleteAccount: (accountId) => ipcRenderer.invoke("accounts:delete", accountId), refreshAccountIdentity: () => ipcRenderer.invoke("accounts:refreshIdentity"), startLogin: (payload) => ipcRenderer.invoke("accounts:startLogin", payload), @@ -39,6 +40,7 @@ contextBridge.exposeInMainWorld("api", { taskStatus: (id) => ipcRenderer.invoke("tasks:status", id), parseHistoryByTask: (id) => ipcRenderer.invoke("tasks:parseHistory", id), checkAccessByTask: (id) => ipcRenderer.invoke("tasks:checkAccess", id), + checkInviteAccessByTask: (id) => ipcRenderer.invoke("tasks:checkInviteAccess", id), membershipStatusByTask: (id) => ipcRenderer.invoke("tasks:membershipStatus", id), groupVisibilityByTask: (id) => ipcRenderer.invoke("tasks:groupVisibility", id) }); diff --git a/src/main/scheduler.js b/src/main/scheduler.js index 3ae002d..e2eeac8 100644 --- a/src/main/scheduler.js +++ b/src/main/scheduler.js @@ -68,6 +68,8 @@ class Scheduler { 0, "", "", + "", + this.settings.ourGroup || "", "" ); continue; @@ -92,6 +94,8 @@ class Scheduler { 0, "", "", + "", + this.settings.ourGroup || "", "" ); } else { @@ -112,6 +116,8 @@ class Scheduler { 0, "", "", + "", + this.settings.ourGroup || "", "" ); } diff --git a/src/main/store.js b/src/main/store.js index af64ea2..7446e97 100644 --- a/src/main/store.js +++ b/src/main/store.js @@ -96,6 +96,8 @@ function initStore(userDataPath) { strategy TEXT DEFAULT '', strategy_meta TEXT DEFAULT '', source_chat TEXT DEFAULT '', + target_chat TEXT DEFAULT '', + target_type TEXT DEFAULT '', action TEXT DEFAULT 'invite', skipped_reason TEXT DEFAULT '', invited_at TEXT NOT NULL, @@ -165,6 +167,8 @@ function initStore(userDataPath) { ensureColumn("invites", "strategy", "TEXT DEFAULT ''"); ensureColumn("invites", "strategy_meta", "TEXT DEFAULT ''"); ensureColumn("invites", "source_chat", "TEXT DEFAULT ''"); + ensureColumn("invites", "target_chat", "TEXT DEFAULT ''"); + ensureColumn("invites", "target_type", "TEXT DEFAULT ''"); ensureColumn("invites", "action", "TEXT DEFAULT 'invite'"); ensureColumn("invites", "skipped_reason", "TEXT DEFAULT ''"); ensureColumn("invites", "archived", "INTEGER NOT NULL DEFAULT 0"); @@ -330,6 +334,10 @@ function initStore(userDataPath) { })); } + function clearAccountEvents() { + db.prepare("DELETE FROM account_events").run(); + } + function enqueueInvite(taskId, userId, username, sourceChat, accessHash, watcherAccountId) { const now = dayjs().toISOString(); try { @@ -542,7 +550,9 @@ function initStore(userDataPath) { watcherAccountId, watcherPhone, strategy, - strategyMeta + strategyMeta, + targetChat, + targetType ) { const now = dayjs().toISOString(); db.prepare(` @@ -558,6 +568,8 @@ function initStore(userDataPath) { strategy, strategy_meta, source_chat, + target_chat, + target_type, action, skipped_reason, invited_at, @@ -565,7 +577,7 @@ function initStore(userDataPath) { error, archived ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0) `).run( taskId || 0, userId, @@ -578,6 +590,8 @@ function initStore(userDataPath) { strategy || "", strategyMeta || "", sourceChat || "", + targetChat || "", + targetType || "", action || "invite", skippedReason || "", now, @@ -680,6 +694,8 @@ function initStore(userDataPath) { strategy: row.strategy || "", strategyMeta: row.strategy_meta || "", sourceChat: row.source_chat || "", + targetChat: row.target_chat || "", + targetType: row.target_type || "", action: row.action || "invite", skippedReason: row.skipped_reason || "", invitedAt: row.invited_at, @@ -720,6 +736,7 @@ function initStore(userDataPath) { clearAccountCooldown, addAccountEvent, listAccountEvents, + clearAccountEvents, deleteAccount, updateAccountIdentity, addAccount, diff --git a/src/main/taskRunner.js b/src/main/taskRunner.js index e28745a..3eab3db 100644 --- a/src/main/taskRunner.js +++ b/src/main/taskRunner.js @@ -167,7 +167,9 @@ class TaskRunner { watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone : "", result.strategy, - result.strategyMeta + result.strategyMeta, + this.task.our_group, + result.targetType ); } else { errors.push(`${item.user_id}: ${result.error}`); @@ -192,16 +194,33 @@ class TaskRunner { watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone : "", result.strategy, - result.strategyMeta + result.strategyMeta, + this.task.our_group, + result.targetType ); + let strategyLine = result.strategy || "—"; + if (result.strategyMeta) { + try { + const parsed = JSON.parse(result.strategyMeta); + if (Array.isArray(parsed) && parsed.length) { + const steps = parsed + .map((step) => `${step.strategy}:${step.ok ? "ok" : "fail"}`) + .join(", "); + strategyLine = `${strategyLine} (${steps})`; + } + } catch (error) { + // ignore parse errors + } + } const detailed = [ - `user=${item.user_id}`, - `error=${result.error || "unknown"}`, - `strategy=${result.strategy || "—"}`, - `meta=${result.strategyMeta || "—"}`, - `source=${item.source_chat || "—"}`, - `account=${result.accountPhone || result.accountId || "—"}` - ].join(" | "); + `Пользователь: ${item.user_id || "—"}`, + `Ошибка: ${result.error || "unknown"}`, + `Стратегия: ${strategyLine}`, + `Источник: ${item.source_chat || "—"}`, + `Цель: ${this.task.our_group || "—"}`, + `Тип цели: ${result.targetType || "—"}`, + `Аккаунт: ${result.accountPhone || result.accountId || "—"}` + ].join("\n"); this.store.addAccountEvent( watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone : "", diff --git a/src/main/telegram.js b/src/main/telegram.js index 3a7b8d8..a17b852 100644 --- a/src/main/telegram.js +++ b/src/main/telegram.js @@ -25,7 +25,13 @@ class TelegramManager { async init() { const accounts = this.store.listAccounts(); for (const account of accounts) { - await this._connectAccount(account); + try { + await this._connectAccount(account); + } catch (error) { + const errorText = error && (error.errorMessage || error.message) ? (error.errorMessage || error.message) : String(error); + this.store.updateAccountStatus(account.id, "error", errorText); + this.store.addAccountEvent(account.id, account.phone || "", "connect_failed", errorText); + } } } @@ -408,11 +414,12 @@ class TelegramManager { } const { client, account } = entry; + let targetEntity = null; + let targetType = ""; const attemptInvite = async (user) => { - const resolved = await this._resolveGroupEntity(client, task.our_group, Boolean(task.auto_join_our_group), account); - if (!resolved.ok) throw new Error(resolved.error); - const targetEntity = resolved.entity; - + if (!targetEntity) { + throw new Error("Target group not resolved"); + } if (targetEntity.className === "Channel") { await client.invoke( new Api.channels.InviteToChannel({ @@ -492,6 +499,16 @@ class TelegramManager { const providedUsername = options.username || ""; const allowJoin = Boolean(task.auto_join_our_group); await this._autoJoinGroups(client, [task.our_group], allowJoin, account); + const resolvedTarget = await this._resolveGroupEntity(client, task.our_group, allowJoin, account); + if (!resolvedTarget.ok) throw new Error(resolvedTarget.error); + targetEntity = resolvedTarget.entity; + if (targetEntity && targetEntity.className === "Channel") { + targetType = targetEntity.megagroup ? "megagroup" : "channel"; + } else if (targetEntity && targetEntity.className === "Chat") { + targetType = "group"; + } else { + targetType = targetEntity && targetEntity.className ? targetEntity.className : ""; + } const resolved = await resolveInputUser(); lastAttempts = resolved.attempts || []; const user = resolved.user; @@ -504,7 +521,8 @@ class TelegramManager { accountId: account.id, accountPhone: account.phone || "", strategy: last ? last.strategy : "", - strategyMeta: JSON.stringify(lastAttempts) + strategyMeta: JSON.stringify(lastAttempts), + targetType }; } catch (error) { const errorText = error.errorMessage || error.message || String(error); @@ -566,7 +584,8 @@ class TelegramManager { accountId: account.id, accountPhone: account.phone || "", strategy: "", - strategyMeta: fallbackMeta + strategyMeta: fallbackMeta, + targetType }; } } @@ -927,6 +946,124 @@ class TelegramManager { return { ok: true, result }; } + async checkInvitePermissions(task, accountIds) { + if (!task || !task.our_group) { + return { ok: false, error: "No target group" }; + } + const ids = Array.isArray(accountIds) ? accountIds.filter(Boolean) : []; + if (!ids.length) { + return { ok: false, error: "No invite accounts" }; + } + const accounts = this.store.listAccounts(); + const accountMap = new Map(accounts.map((account) => [account.id, account])); + const results = []; + for (const accountId of ids) { + const entry = this.clients.get(accountId); + const accountRecord = accountMap.get(accountId); + if (!entry) { + results.push({ + accountId, + accountPhone: accountRecord ? (accountRecord.phone || "") : "", + ok: false, + canInvite: false, + member: false, + reason: "Сессия не подключена", + targetType: "", + title: "", + targetChat: task.our_group + }); + continue; + } + const { client, account } = entry; + const resolved = await this._resolveGroupEntity(client, task.our_group, Boolean(task.auto_join_our_group), account); + if (!resolved.ok) { + results.push({ + accountId, + accountPhone: account.phone || "", + ok: false, + canInvite: false, + member: false, + reason: resolved.error || "Не удалось получить группу", + targetType: "", + title: "", + targetChat: task.our_group + }); + continue; + } + const entity = resolved.entity; + const title = entity && entity.title ? entity.title : ""; + const className = entity && entity.className ? entity.className : ""; + let targetType = className; + if (className === "Channel") { + targetType = entity && entity.megagroup ? "megagroup" : "channel"; + } else if (className === "Chat") { + targetType = "group"; + } + let canInvite = false; + let member = true; + let reason = ""; + + try { + if (className === "Channel") { + const me = await client.getMe(); + const participant = await client.invoke(new Api.channels.GetParticipant({ + channel: entity, + participant: me + })); + const part = participant && participant.participant ? participant.participant : participant; + const className = part && part.className ? part.className : ""; + const isAdmin = className.includes("Admin") || className.includes("Creator"); + const addUsers = part && part.adminRights ? Boolean(part.adminRights.addUsers) : isAdmin; + canInvite = Boolean(isAdmin && addUsers); + if (!canInvite) { + reason = "Нужны права администратора на добавление участников"; + } + } else if (className === "Chat") { + let fullChat = null; + try { + fullChat = await client.invoke(new Api.messages.GetFullChat({ chatId: entity.id })); + } catch (error) { + fullChat = null; + } + const full = fullChat && fullChat.fullChat ? fullChat.fullChat : null; + const restricted = Boolean(full && full.defaultBannedRights && full.defaultBannedRights.inviteUsers); + if (restricted) { + canInvite = false; + reason = "Добавление пользователей запрещено для участников"; + } else { + canInvite = true; + } + } else { + canInvite = false; + reason = "Не удалось распознать тип цели (не группа/канал)"; + } + } catch (error) { + const errorText = error.errorMessage || error.message || String(error); + if (errorText.includes("USER_NOT_PARTICIPANT")) { + member = false; + reason = "Аккаунт не состоит в группе"; + } else if (errorText.includes("CHAT_ADMIN_REQUIRED")) { + reason = "Нужны права администратора"; + } else { + reason = errorText; + } + } + + results.push({ + accountId, + accountPhone: account.phone || "", + ok: true, + canInvite, + member, + reason, + targetType, + title, + targetChat: task.our_group + }); + } + return { ok: true, result: results }; + } + async _autoJoinGroups(client, groups, enabled, account) { if (!enabled) return; const settings = this.store.getSettings(); diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx index c0e5857..3effc14 100644 --- a/src/renderer/App.jsx +++ b/src/renderer/App.jsx @@ -114,6 +114,7 @@ export default function App() { const [membershipStatus, setMembershipStatus] = useState({}); const [groupVisibility, setGroupVisibility] = useState([]); const [accessStatus, setAccessStatus] = useState([]); + const [inviteAccessStatus, setInviteAccessStatus] = useState([]); const [accountEvents, setAccountEvents] = useState([]); const [loginForm, setLoginForm] = useState({ apiId: "", @@ -345,6 +346,7 @@ export default function App() { useEffect(() => { loadSelectedTask(selectedTaskId); setAccessStatus([]); + setInviteAccessStatus([]); setMembershipStatus({}); setTaskNotice(null); }, [selectedTaskId]); @@ -516,6 +518,7 @@ export default function App() { const formatAccountStatus = (status) => { if (status === "limited") return "В спаме"; + if (status === "error") return "Ошибка"; if (status === "ok") return "ОК"; return status || "Неизвестно"; }; @@ -829,6 +832,19 @@ export default function App() { setTaskForm(nextForm); let accountRolesMap = { ...taskAccountRoles }; let accountIds = Object.keys(accountRolesMap).map((id) => Number(id)); + if (nextForm.requireSameBotInBoth) { + const required = Math.max(1, Number(nextForm.maxCompetitorBots || 1)); + const pool = (selectedAccountIds && selectedAccountIds.length ? selectedAccountIds : accounts.map((account) => account.id)) + .filter((id) => Number.isFinite(id)); + const chosen = pool.slice(0, required); + accountRolesMap = {}; + chosen.forEach((accountId) => { + accountRolesMap[accountId] = { monitor: true, invite: true }; + }); + accountIds = chosen; + setTaskAccountRoles(accountRolesMap); + setSelectedAccountIds(chosen); + } if (nextForm.autoAssignAccounts && (!accountIds || accountIds.length === 0)) { accountIds = accounts.map((account) => account.id); accountRolesMap = {}; @@ -899,8 +915,12 @@ export default function App() { try { await window.api.deleteTask(selectedTaskId); setTaskNotice({ text: "Задача удалена.", tone: "success", source: "tasks" }); - await loadTasks(); + const tasksData = await loadTasks(); await loadAccountAssignments(); + if (!tasksData.length) { + createTask(); + setActiveTab("task"); + } } catch (error) { showNotification(error.message || String(error), "error"); } @@ -1059,6 +1079,26 @@ export default function App() { } }; + const checkInviteAccess = async (source = "editor") => { + if (!window.api || selectedTaskId == null) { + showNotification("Сначала выберите задачу.", "error"); + return; + } + setInviteAccessStatus([]); + showNotification("Проверяем права инвайта...", "info"); + try { + const result = await window.api.checkInviteAccessByTask(selectedTaskId); + if (!result.ok) { + showNotification(result.error || "Не удалось проверить права", "error"); + return; + } + setInviteAccessStatus(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"); @@ -1087,6 +1127,20 @@ export default function App() { } }; + const clearAccountEvents = async () => { + if (!window.api) { + showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); + return; + } + try { + await window.api.clearAccountEvents(); + setAccountEvents([]); + showNotification("События очищены.", "success"); + } catch (error) { + showNotification(error.message || String(error), "error"); + } + }; + const exportLogs = async (source = "editor") => { if (!window.api || selectedTaskId == null) { showNotification("Сначала выберите задачу.", "error"); @@ -1883,21 +1937,13 @@ export default function App() {
{taskStatus.monitorInfo && taskStatus.monitorInfo.groups ? taskStatus.monitorInfo.groups.length : 0}
-
Мониторит
+
Наблюдает
{(() => { const monitorIds = taskStatus.monitorInfo && taskStatus.monitorInfo.accountIds ? taskStatus.monitorInfo.accountIds : (taskStatus.monitorInfo && taskStatus.monitorInfo.accountId ? [taskStatus.monitorInfo.accountId] : []); - if (!monitorIds.length) return "—"; - const labels = monitorIds - .map((id) => { - const account = accountById.get(id); - return account ? (account.phone || account.user_id || String(id)) : String(id); - }) - .filter(Boolean); - if (!labels.length) return "—"; - return labels.length > 2 ? `${labels.length} аккаунта` : labels.join(", "); + return monitorIds.length; })()}
@@ -1913,18 +1959,6 @@ export default function App() {
Очередь инвайтов
{taskStatus.queueCount}
-
-
Очередь: username
-
{taskStatus.pendingStats ? taskStatus.pendingStats.withUsername : 0}
-
-
-
Очередь: access_hash
-
{taskStatus.pendingStats ? taskStatus.pendingStats.withAccessHash : 0}
-
-
-
Очередь: без данных
-
{taskStatus.pendingStats ? taskStatus.pendingStats.withoutData : 0}
-
Лимит в день
{taskStatus.dailyUsed}/{taskStatus.dailyLimit}
@@ -1937,36 +1971,6 @@ export default function App() {
Следующий цикл
{formatCountdown(taskStatus.nextRunAt)}
-
-
Следующий инвайт
-
- {(() => { - const account = accountById.get(taskStatus.nextInviteAccountId); - return account ? (account.phone || account.user_id || taskStatus.nextInviteAccountId) : "—"; - })()} -
-
-
-
Последний инвайт
-
- {(() => { - const account = accountById.get(taskStatus.lastInviteAccountId); - return account ? (account.phone || account.user_id || taskStatus.lastInviteAccountId) : "—"; - })()} -
-
-
-
Стратегии OK/Fail
-
{inviteStrategyStats.success}/{inviteStrategyStats.failed}
-
-
-
Боты мониторят
-
{roleSummary.monitor.length}
-
-
-
Боты инвайтят
-
{roleSummary.invite.length}
-
{taskStatus.running ? ( @@ -1992,40 +1996,6 @@ export default function App() { )}
)} - {hasSelectedTask && ( -
-
-
Мониторинг
-
- {roleSummary.monitor.length - ? roleSummary.monitor.map((id) => { - const account = accountById.get(id); - return ( - - {account ? (account.phone || account.user_id || id) : id} - - ); - }) - : Нет} -
-
-
-
Инвайт
-
- {roleSummary.invite.length - ? roleSummary.invite.map((id) => { - const account = accountById.get(id); - return ( - - {account ? (account.phone || account.user_id || id) : id} - - ); - }) - : Нет} -
-
-
- )} {groupVisibility.length > 0 && (
{groupVisibility.some((item) => item.hidden) && ( @@ -2055,6 +2025,7 @@ export default function App() { +
{taskNotice && taskNotice.source === "editor" && ( @@ -2371,7 +2342,7 @@ export default function App() { {activeTab === "events" && ( Загрузка...}> - + )} @@ -2423,6 +2394,7 @@ export default function App() { + {accessStatus.length > 0 && ( @@ -2443,6 +2415,25 @@ export default function App() { )} + {inviteAccessStatus.length > 0 && ( +
+
Права инвайта
+
+ {inviteAccessStatus.map((item, index) => ( +
+
+ {item.accountPhone || item.accountId}: {item.title || item.targetChat} + {item.targetType ? ` (${item.targetType === "channel" ? "канал" : item.targetType === "megagroup" ? "супергруппа" : item.targetType === "group" ? "группа" : item.targetType})` : ""} +
+
+ {item.canInvite ? "Можно инвайтить" : "Нет прав"} +
+ {!item.canInvite &&
{item.reason || "—"}
} +
+ ))} +
+
+ )} )} {taskNotice && taskNotice.source === "sidebar" && ( diff --git a/src/renderer/styles/app.css b/src/renderer/styles/app.css index 93649d5..5021710 100644 --- a/src/renderer/styles/app.css +++ b/src/renderer/styles/app.css @@ -1122,6 +1122,25 @@ label .hint { white-space: pre-line; } +.log-result-list { + display: grid; + gap: 6px; + margin-top: 6px; +} + +.log-result { + font-size: 13px; + color: #1f2937; +} + +.log-result.success { + color: #0f766e; +} + +.log-result.error { + color: #b91c1c; +} + .invite-details { margin-top: 10px; padding: 10px 12px; diff --git a/src/renderer/tabs/AccountsTab.jsx b/src/renderer/tabs/AccountsTab.jsx index 608a929..33d9c42 100644 --- a/src/renderer/tabs/AccountsTab.jsx +++ b/src/renderer/tabs/AccountsTab.jsx @@ -36,11 +36,19 @@ function AccountsTab({ const buildAccountLabel = (account) => `${account.username ? `@${account.username}` : "—"} (${account.user_id || "—"})`; const roleStats = React.useMemo(() => { - const roles = Object.values(taskAccountRoles || {}); - const monitor = roles.filter((item) => item.monitor).length; - const invite = roles.filter((item) => item.invite).length; - return { monitor, invite, total: roles.length }; - }, [taskAccountRoles]); + const knownIds = new Set((accounts || []).map((account) => account.id)); + let monitor = 0; + let invite = 0; + let total = 0; + Object.entries(taskAccountRoles || {}).forEach(([id, roles]) => { + const accountId = Number(id); + if (!knownIds.has(accountId)) return; + if (roles.monitor) monitor += 1; + if (roles.invite) invite += 1; + if (roles.monitor || roles.invite) total += 1; + }); + return { monitor, invite, total }; + }, [taskAccountRoles, accounts]); return (
diff --git a/src/renderer/tabs/EventsTab.jsx b/src/renderer/tabs/EventsTab.jsx index 77df6d0..4392244 100644 --- a/src/renderer/tabs/EventsTab.jsx +++ b/src/renderer/tabs/EventsTab.jsx @@ -1,6 +1,6 @@ import React, { memo, useMemo, useState } from "react"; -function EventsTab({ accountEvents, formatTimestamp }) { +function EventsTab({ accountEvents, formatTimestamp, onClearEvents }) { const [typeFilter, setTypeFilter] = useState("all"); const [query, setQuery] = useState(""); @@ -23,7 +23,10 @@ function EventsTab({ accountEvents, formatTimestamp }) { return (
-

События аккаунтов

+
+

События аккаунтов

+ +
{ + if (!value) return "—"; + if (value === "channel") return "канал"; + if (value === "megagroup") return "супергруппа"; + if (value === "group") return "группа"; + return value; + }; + const getDurationMs = (start, finish) => { const startMs = new Date(start).getTime(); const finishMs = new Date(finish).getTime(); @@ -149,6 +157,27 @@ function LogsTab({ {pagedLogs.map((log) => { const successIds = Array.isArray(log.successIds) ? log.successIds : []; const errors = Array.isArray(log.errors) ? log.errors : []; + const errorMap = new Map(); + errors.forEach((err) => { + const parts = String(err).split(":"); + if (parts.length < 2) return; + const id = parts[0].trim(); + const code = parts.slice(1).join(":").trim(); + if (!id) return; + errorMap.set(id, code); + }); + const resultRows = [ + ...successIds.map((id) => ({ + id: String(id), + status: "success", + message: "успех" + })), + ...Array.from(errorMap.entries()).map(([id, code]) => ({ + id, + status: "error", + message: `${code} (${explainInviteError(code) || "Причина не определена"})` + })) + ]; return (
@@ -160,6 +189,18 @@ function LogsTab({
Пользователи: {successIds.length ? successIds.join(", ") : "—"}
+ {resultRows.length > 0 && ( +
+
Результаты:
+
+ {resultRows.map((row) => ( +
+ {row.id} — {row.message} +
+ ))} +
+
+ )} {log.invitedCount === 0 && errors.length === 0 && (
Причина: цикл завершён сразу — очередь пуста
)} @@ -274,6 +315,9 @@ function LogsTab({
Источник: {invite.sourceChat || "—"}
+
+ Цель: {invite.targetChat || "—"}{invite.targetType ? ` (${formatTargetType(invite.targetType)})` : ""} +
Инвайт: {invite.accountPhone || "—"} {invite.watcherPhone && invite.accountPhone && ( @@ -318,6 +362,8 @@ function LogsTab({
Аккаунт ID: {invite.accountId || "—"}
Наблюдатель ID: {invite.watcherAccountId || "—"}
Наблюдатель: {invite.watcherPhone || "—"}
+
Цель: {invite.targetChat || "—"}
+
Тип цели: {formatTargetType(invite.targetType)}
Действие: {invite.action || "invite"}
Статус: {invite.status}
Пропуск: {invite.skippedReason || "—"}