From a3aac40a73cdc5345fe76c5c060e683987cb0fc4 Mon Sep 17 00:00:00 2001 From: Ivan Neplokhov Date: Fri, 6 Feb 2026 15:00:37 +0400 Subject: [PATCH] some --- package.json | 2 +- src/main/index.js | 29 ++ src/main/preload.js | 1 + src/main/store.js | 26 ++ src/main/taskRunner.js | 11 +- src/main/telegram.js | 326 ++++++++++++++++++-- src/renderer/App.jsx | 38 ++- src/renderer/components/HelpTip.jsx | 10 + src/renderer/components/InfoModal.jsx | 113 ++++++- src/renderer/components/NowStatusCard.jsx | 12 +- src/renderer/components/QuickActionsBar.jsx | 4 +- src/renderer/components/TaskSettingsTab.jsx | 283 ++++++++++------- src/renderer/hooks/useAccessChecks.js | 26 +- src/renderer/hooks/useAccountManagement.js | 36 ++- src/renderer/hooks/useAppDataState.js | 6 + src/renderer/hooks/useAppTabGroups.js | 9 + src/renderer/hooks/useMainUiProps.js | 26 +- src/renderer/hooks/useTabProps.js | 10 + src/renderer/hooks/useTaskActions.js | 15 + src/renderer/hooks/useTaskLoaders.js | 11 +- src/renderer/hooks/useTaskPresets.js | 4 +- src/renderer/styles/app.css | 67 ++++ src/renderer/tabs/AccountsTab.jsx | 72 ++++- src/renderer/tabs/LogsTab.jsx | 15 +- 24 files changed, 958 insertions(+), 194 deletions(-) create mode 100644 src/renderer/components/HelpTip.jsx diff --git a/package.json b/package.json index 195f254..cfe88ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "telegram-invite-automation", - "version": "1.6.0", + "version": "1.8.0", "private": true, "description": "Automated user parsing and invites for Telegram groups", "main": "src/main/index.js", diff --git a/src/main/index.js b/src/main/index.js index 6e3deec..0005bc5 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1045,6 +1045,35 @@ ipcMain.handle("tasks:checkInviteAccess", async (_event, id) => { 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" }; diff --git a/src/main/preload.js b/src/main/preload.js index 544df7b..f6cc700 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -56,6 +56,7 @@ contextBridge.exposeInMainWorld("api", { parseHistoryByTask: (id) => ipcRenderer.invoke("tasks:parseHistory", id), checkAccessByTask: (id) => ipcRenderer.invoke("tasks:checkAccess", id), checkInviteAccessByTask: (id) => ipcRenderer.invoke("tasks:checkInviteAccess", id), + checkConfirmAccessByTask: (id) => ipcRenderer.invoke("tasks:checkConfirmAccess", id), membershipStatusByTask: (id) => ipcRenderer.invoke("tasks:membershipStatus", id), groupVisibilityByTask: (id) => ipcRenderer.invoke("tasks:groupVisibility", id), joinGroupsByTask: (id) => ipcRenderer.invoke("tasks:joinGroups", id) diff --git a/src/main/store.js b/src/main/store.js index 2c7e425..44b0089 100644 --- a/src/main/store.js +++ b/src/main/store.js @@ -485,6 +485,31 @@ function initStore(userDataPath) { })); } + function getAutoJoinStatus(accountId, group) { + if (!accountId || !group) return null; + const row = db.prepare(` + SELECT event_type, message, created_at + FROM account_events + WHERE account_id = ? + AND event_type IN ('auto_join_ok','auto_join_request','auto_join_already','auto_join_failed') + AND message LIKE ? + ORDER BY id DESC + LIMIT 1 + `).get(accountId, `%${group}%`); + if (!row) return null; + const eventType = row.event_type || ""; + if (eventType === "auto_join_request") { + return { status: "pending", createdAt: row.created_at }; + } + if (eventType === "auto_join_ok" || eventType === "auto_join_already") { + return { status: "ok", createdAt: row.created_at }; + } + if (eventType === "auto_join_failed") { + return { status: "failed", createdAt: row.created_at, message: row.message || "" }; + } + return null; + } + function clearAccountEvents() { db.prepare("DELETE FROM account_events").run(); } @@ -1239,6 +1264,7 @@ function initStore(userDataPath) { clearAccountCooldown, addAccountEvent, listAccountEvents, + getAutoJoinStatus, clearAccountEvents, addTaskAudit, listTaskAudit, diff --git a/src/main/taskRunner.js b/src/main/taskRunner.js index 68b2a5a..95d3368 100644 --- a/src/main/taskRunner.js +++ b/src/main/taskRunner.js @@ -60,7 +60,10 @@ class TaskRunner { const accounts = accountRows.map((row) => row.account_id); const monitorIds = accountRows.filter((row) => row.role_monitor).map((row) => row.account_id); const inviteIds = accountRows.filter((row) => row.role_invite).map((row) => row.account_id); - const confirmIds = accountRows.filter((row) => row.role_confirm).map((row) => row.account_id); + const confirmIdsRaw = accountRows.filter((row) => row.role_confirm).map((row) => row.account_id); + const confirmIds = this.task.separate_confirm_roles + ? confirmIdsRaw.filter((id) => !inviteIds.includes(id)) + : confirmIdsRaw; await this.telegram.joinGroupsForTask(this.task, competitors, accounts, { monitorIds, inviteIds, @@ -524,7 +527,7 @@ class TaskRunner { item.account_id || 0, "", "confirm_retry_ok", - `Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""}` + `задача ${this.task.id}: Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""}` ); continue; } @@ -535,7 +538,7 @@ class TaskRunner { item.account_id || 0, "", "confirm_retry_failed", - `Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""} • лимит попыток` + `задача ${this.task.id}: Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""} • лимит попыток` ); continue; } @@ -551,7 +554,7 @@ class TaskRunner { "", "confirm_retry_scheduled", [ - `Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""}`, + `задача ${this.task.id}: Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""}`, "Повторная проверка через 5 минут", `Попыток: ${attempts}/${item.max_attempts || 2}` ].join("\n") diff --git a/src/main/telegram.js b/src/main/telegram.js index 82c6180..2654706 100644 --- a/src/main/telegram.js +++ b/src/main/telegram.js @@ -175,7 +175,29 @@ class TelegramManager { async _grantTempInviteAdmin(masterClient, targetEntity, account, allowAnonymous = false) { const rights = this._buildInviteAdminRights(allowAnonymous); - const user = await this._resolveAccountEntityForMaster(masterClient, targetEntity, account); + let user = null; + const entry = account ? this.clients.get(account.id) : null; + if (entry) { + try { + const me = await entry.client.getMe(); + if (me && me.id != null && me.accessHash != null) { + user = new Api.InputUser({ userId: BigInt(me.id), accessHash: BigInt(me.accessHash) }); + } + } catch { + // fallback below + } + if (!user) { + try { + const inputMe = await entry.client.getInputEntity("me"); + user = this._toInputUser(inputMe); + } catch { + // fallback below + } + } + } + if (!user) { + user = await this._resolveAccountEntityForMaster(masterClient, targetEntity, account); + } if (!user) { throw new Error("INVITER_ENTITY_NOT_RESOLVED_BY_MASTER"); } @@ -712,24 +734,31 @@ class TelegramManager { return { ok: true, entity: resolvedTarget.entity }; }; const confirmMembership = async (user, confirmClient = client, sourceLabel = "", confirmEntry = null) => { + const confirmAccount = confirmEntry && confirmEntry.account ? confirmEntry.account : account; + const confirmAccountId = confirmAccount && confirmAccount.id ? confirmAccount.id : 0; + const confirmAccountPhone = confirmAccount && confirmAccount.phone ? confirmAccount.phone : ""; const targetForClient = await getTargetEntityForClient(confirmClient, confirmEntry); if (!targetForClient.ok) { return { confirmed: null, error: targetForClient.error, - detail: buildConfirmDetail(targetForClient.error, "ошибка подтверждения участия", sourceLabel) + detail: buildConfirmDetail(targetForClient.error, "ошибка подтверждения участия", sourceLabel), + checkedByAccountId: confirmAccountId, + checkedByAccountPhone: confirmAccountPhone }; } const confirmTargetEntity = targetForClient.entity; if (!confirmTargetEntity || confirmTargetEntity.className !== "Channel") { - return { confirmed: true, error: "", detail: "" }; + return { confirmed: true, error: "", detail: "", checkedByAccountId: confirmAccountId, checkedByAccountPhone: confirmAccountPhone }; } const participantForClient = await resolveUserForClient(confirmClient, user); if (!participantForClient) { return { confirmed: null, error: "INVITED_USER_NOT_RESOLVED_FOR_CONFIRM", - detail: buildConfirmDetail("INVITED_USER_NOT_RESOLVED_FOR_CONFIRM", "не удалось резолвить пользователя для проверки участия", sourceLabel) + detail: buildConfirmDetail("INVITED_USER_NOT_RESOLVED_FOR_CONFIRM", "не удалось резолвить пользователя для проверки участия", sourceLabel), + checkedByAccountId: confirmAccountId, + checkedByAccountPhone: confirmAccountPhone }; } try { @@ -737,27 +766,33 @@ class TelegramManager { channel: confirmTargetEntity, participant: participantForClient })); - return { confirmed: true, error: "", detail: sourceLabel ? `OK (${sourceLabel})` : "OK" }; + return { confirmed: true, error: "", detail: sourceLabel ? `OK (${sourceLabel})` : "OK", checkedByAccountId: confirmAccountId, checkedByAccountPhone: confirmAccountPhone }; } catch (error) { const errorText = error.errorMessage || error.message || String(error); if (errorText.includes("USER_NOT_PARTICIPANT")) { return { confirmed: false, error: "USER_NOT_PARTICIPANT", - detail: buildConfirmDetail("USER_NOT_PARTICIPANT", "пользователь не в группе", sourceLabel) + detail: buildConfirmDetail("USER_NOT_PARTICIPANT", "пользователь не в группе", sourceLabel), + checkedByAccountId: confirmAccountId, + checkedByAccountPhone: confirmAccountPhone }; } if (errorText.includes("CHAT_ADMIN_REQUIRED")) { return { confirmed: null, error: "CHAT_ADMIN_REQUIRED", - detail: buildConfirmDetail("CHAT_ADMIN_REQUIRED", "нет прав для подтверждения участия", sourceLabel) + detail: buildConfirmDetail("CHAT_ADMIN_REQUIRED", "нет прав для подтверждения участия", sourceLabel), + checkedByAccountId: confirmAccountId, + checkedByAccountPhone: confirmAccountPhone }; } return { confirmed: null, error: errorText, - detail: buildConfirmDetail(errorText, "ошибка подтверждения участия", sourceLabel) + detail: buildConfirmDetail(errorText, "ошибка подтверждения участия", sourceLabel), + checkedByAccountId: confirmAccountId, + checkedByAccountPhone: confirmAccountPhone }; } }; @@ -765,17 +800,21 @@ class TelegramManager { const attempts = []; const triedClients = new Set(); const directLabel = formatAccountSource("", inviterEntry); - const direct = await confirmMembership(user, client, directLabel || "проверка этим аккаунтом", entry); - if (direct.detail) { - attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail }); - } - triedClients.add(client); - if (direct.confirmed !== null) { - return { ...direct, attempts }; - } - let finalResult = direct; const roleAssignments = this.taskRoleAssignments.get(task.id) || {}; const confirmIds = Array.isArray(roleAssignments.confirmIds) ? roleAssignments.confirmIds : []; + const preferConfirmRoles = Boolean(task.separate_confirm_roles) && confirmIds.length > 0; + let finalResult = { confirmed: null, detail: "", checkedByAccountId: 0 }; + if (!preferConfirmRoles) { + const direct = await confirmMembership(user, client, directLabel || "проверка этим аккаунтом", entry); + if (direct.detail) { + attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail }); + } + triedClients.add(client); + if (direct.confirmed !== null) { + return { ...direct, attempts }; + } + finalResult = direct; + } for (const confirmId of confirmIds) { const entry = this.clients.get(confirmId); if (!entry || !entry.client || triedClients.has(entry.client)) continue; @@ -790,6 +829,9 @@ class TelegramManager { break; } } + if (preferConfirmRoles && finalResult.confirmed !== null) { + return { ...finalResult, attempts }; + } if (finalResult.confirmed === null && !finalResult.detail) { const masterId = Number(task.invite_admin_master_id || 0); const masterEntry = masterId ? this.clients.get(masterId) : null; @@ -802,7 +844,7 @@ class TelegramManager { finalResult = adminConfirm; } } - if (finalResult.confirmed === null && !finalResult.detail) { + if (finalResult.confirmed === null && !finalResult.detail && !preferConfirmRoles) { await new Promise((resolve) => setTimeout(resolve, 10000)); const retryLabel = directLabel ? `${directLabel}, повтор через 10с` : "проверка этим аккаунтом, повтор через 10с"; const retry = await confirmMembership(user, client, retryLabel, entry); @@ -1194,21 +1236,23 @@ class TelegramManager { ].filter(Boolean).join("\n") ); - if (confirm.error === "USER_NOT_PARTICIPANT") { + if (confirm.confirmed === false) { const nextCheckAt = dayjs().add(5, "minute").toISOString(); const username = resolvedUser && resolvedUser.username ? resolvedUser.username : (user && user.username ? user.username : ""); + const confirmAccountId = confirm.checkedByAccountId || account.id; + const confirmAccountPhone = confirm.checkedByAccountPhone || account.phone || ""; this.store.addConfirmQueue( task.id, userId, username, - account.id, + confirmAccountId, options.watcherAccountId || 0, nextCheckAt, 2 ); this.store.addAccountEvent( - account.id, - account.phone || "", + confirmAccountId, + confirmAccountPhone, "confirm_retry_scheduled", [ `Пользователь: ${userId}${username ? ` (@${username})` : ""}`, @@ -1456,17 +1500,37 @@ class TelegramManager { for (const group of groups) { const isMember = await this._isParticipant(client, group); if (isMember) competitorCount += 1; + let pending = false; + let pendingAt = ""; + if (!isMember) { + const joinStatus = this.store.getAutoJoinStatus(account.id, group); + if (joinStatus && joinStatus.status === "pending") { + pending = true; + pendingAt = joinStatus.createdAt || ""; + } + } competitorGroupsInfo.push({ link: group, title: titleMap.get(group) || "", - isMember + isMember, + pending, + pendingAt }); } let ourGroupMember = false; + let ourGroupPending = false; + let ourGroupPendingAt = ""; let ourGroupInfo = null; if (ourGroup) { ourGroupMember = await this._isParticipant(client, ourGroup); + if (!ourGroupMember) { + const joinStatus = this.store.getAutoJoinStatus(account.id, ourGroup); + if (joinStatus && joinStatus.status === "pending") { + ourGroupPending = true; + ourGroupPendingAt = joinStatus.createdAt || ""; + } + } ourGroupInfo = { link: ourGroup, title: titleMap.get(ourGroup) || "", @@ -1479,6 +1543,8 @@ class TelegramManager { competitorCount, competitorTotal: groups.length, ourGroupMember, + ourGroupPending, + ourGroupPendingAt, competitorGroups: competitorGroupsInfo, ourGroup: ourGroupInfo }); @@ -1894,6 +1960,111 @@ class TelegramManager { return { ok: true, result: results }; } + async checkConfirmAccess(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 confirm 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, + 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, + 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"; + } + if (className !== "Channel") { + results.push({ + accountId, + accountPhone: account.phone || "", + ok: true, + member: true, + reason: "Проверка участия не требуется для обычной группы", + targetType, + title, + targetChat: task.our_group + }); + continue; + } + try { + const me = await client.getMe(); + await client.invoke(new Api.channels.GetParticipant({ + channel: entity, + participant: me + })); + results.push({ + accountId, + accountPhone: account.phone || "", + ok: true, + member: true, + reason: "", + targetType, + title, + targetChat: task.our_group + }); + } catch (error) { + const errorText = error.errorMessage || error.message || String(error); + let reason = errorText; + let member = false; + if (errorText.includes("USER_NOT_PARTICIPANT")) { + reason = "Аккаунт не состоит в нашей группе"; + member = false; + } else if (errorText.includes("CHAT_ADMIN_REQUIRED")) { + reason = "Нет прав для проверки участия"; + } + results.push({ + accountId, + accountPhone: account.phone || "", + ok: false, + member, + reason, + targetType, + title, + targetChat: task.our_group + }); + } + } + return { ok: true, result: results }; + } + async confirmUserInGroup(task, userId, accountId) { if (!task || !task.our_group) { return { ok: false, error: "No target group" }; @@ -1956,6 +2127,20 @@ class TelegramManager { return { ok: false, error: "Admin invite поддерживается только для супергрупп" }; } + try { + const diag = await this._collectInviteDiagnostics(client, targetEntity); + if (diag) { + this.store.addAccountEvent( + masterAccountId, + masterAccount.phone || "", + "admin_grant_master_diag", + diag + ); + } + } catch { + // ignore diagnostics failures + } + const rights = this._buildInviteAdminRights(Boolean(task.invite_admin_anonymous)); const accounts = this.store.listAccounts(); const accountMap = new Map(accounts.map((acc) => [acc.id, acc])); @@ -1963,13 +2148,17 @@ class TelegramManager { const results = []; for (const accountId of targetIds) { const record = accountMap.get(accountId); + const label = record ? (record.phone || record.username || record.id) : accountId; + const diagParts = [`inviter=${label}`, `group=${task.our_group}`]; if (!record) { results.push({ accountId, ok: false, reason: "Аккаунт не найден" }); + this.store.addAccountEvent(masterAccountId, masterAccount.phone || "", "admin_grant_detail", `${diagParts.join(" | ")} | error=account_not_found`); continue; } const targetEntry = this.clients.get(accountId); if (!targetEntry) { results.push({ accountId, ok: false, reason: "Сессия инвайтера не подключена" }); + this.store.addAccountEvent(masterAccountId, masterAccount.phone || "", "admin_grant_detail", `${diagParts.join(" | ")} | error=session_not_connected`); continue; } const targetAccess = await this._resolveGroupEntity( @@ -1984,20 +2173,84 @@ class TelegramManager { ok: false, reason: `Инвайтер не имеет доступа к целевой группе: ${targetAccess.error || "неизвестно"}` }); + this.store.addAccountEvent( + masterAccountId, + masterAccount.phone || "", + "admin_grant_detail", + `${diagParts.join(" | ")} | access=fail (${targetAccess.error || "unknown"})` + ); + continue; + } + let memberStatus = "ok"; + let memberError = ""; + try { + const selfUser = await targetEntry.client.getMe(); + await targetEntry.client.invoke(new Api.channels.GetParticipant({ + channel: targetAccess.entity, + participant: selfUser + })); + } catch (error) { + const errorText = error.errorMessage || error.message || String(error); + memberError = errorText; + if (errorText.includes("USER_NOT_PARTICIPANT")) { + memberStatus = "not_member"; + } else { + memberStatus = "error"; + } + const reason = memberStatus === "not_member" + ? "Инвайтер не состоит в нашей группе или ожидает одобрения" + : `Не удалось проверить участие инвайтера: ${errorText}`; + results.push({ accountId, ok: false, reason }); + this.store.addAccountEvent( + masterAccountId, + masterAccount.phone || "", + "admin_grant_detail", + `${diagParts.join(" | ")} | member=${memberStatus}${memberError ? ` (${memberError})` : ""}` + ); continue; } if (!record.user_id && !record.username && !targetEntry.account.username && !targetEntry.account.user_id) { results.push({ accountId, ok: false, reason: "Нет user_id/username" }); + this.store.addAccountEvent(masterAccountId, masterAccount.phone || "", "admin_grant_detail", `${diagParts.join(" | ")} | error=no_identity`); continue; } try { - const user = await this._resolveAccountEntityForMaster(client, targetEntity, { - ...record, - username: record.username || targetEntry.account.username || "", - user_id: record.user_id || targetEntry.account.user_id || "" - }); + let user = null; + let resolveMethod = ""; + try { + const me = await targetEntry.client.getMe(); + if (me && me.id != null && me.accessHash != null) { + user = new Api.InputUser({ userId: BigInt(me.id), accessHash: BigInt(me.accessHash) }); + resolveMethod = "self:getMe"; + } + } catch { + // fallback to resolver below + } + if (!user) { + try { + const inputMe = await targetEntry.client.getInputEntity("me"); + user = this._toInputUser(inputMe); + if (user) resolveMethod = "self:input"; + } catch { + // fallback to resolver below + } + } + if (!user) { + user = await this._resolveAccountEntityForMaster(client, targetEntity, { + ...record, + username: record.username || targetEntry.account.username || "", + user_id: record.user_id || targetEntry.account.user_id || "" + }); + if (user) resolveMethod = "master:resolve"; + } if (!user) { results.push({ accountId, ok: false, reason: "Мастер-админ не смог резолвить аккаунт инвайтера" }); + this.store.addAccountEvent( + masterAccountId, + masterAccount.phone || "", + "admin_grant_detail", + `${diagParts.join(" | ")} | member=${memberStatus} | resolve=failed` + ); continue; } await client.invoke(new Api.channels.EditAdmin({ @@ -2012,6 +2265,12 @@ class TelegramManager { "admin_grant", `${record.phone || record.username || record.id} -> ${task.our_group}` ); + this.store.addAccountEvent( + masterAccountId, + masterAccount.phone || "", + "admin_grant_detail", + `${diagParts.join(" | ")} | member=${memberStatus} | resolve=${resolveMethod} | result=ok` + ); results.push({ accountId, ok: true }); } catch (error) { const errorText = error.errorMessage || error.message || String(error); @@ -2021,6 +2280,12 @@ class TelegramManager { "admin_grant_failed", `${record.phone || record.username || record.id} -> ${task.our_group} | ${errorText}` ); + this.store.addAccountEvent( + masterAccountId, + masterAccount.phone || "", + "admin_grant_detail", + `${diagParts.join(" | ")} | member=ok | error=${errorText}` + ); results.push({ accountId, ok: false, reason: errorText }); } } @@ -2280,7 +2545,10 @@ class TelegramManager { const explicitMonitorIds = Array.isArray(roleIds.monitorIds) ? roleIds.monitorIds : []; const explicitInviteIds = Array.isArray(roleIds.inviteIds) ? roleIds.inviteIds : []; - const explicitConfirmIds = Array.isArray(roleIds.confirmIds) ? roleIds.confirmIds : []; + const explicitConfirmIdsRaw = Array.isArray(roleIds.confirmIds) ? roleIds.confirmIds : []; + const explicitConfirmIds = task.separate_confirm_roles + ? explicitConfirmIdsRaw.filter((id) => !explicitInviteIds.includes(id)) + : explicitConfirmIdsRaw; const hasExplicitRoles = explicitMonitorIds.length || explicitInviteIds.length || explicitConfirmIds.length; const competitors = competitorGroups || []; diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx index a69ae14..5db57bb 100644 --- a/src/renderer/App.jsx +++ b/src/renderer/App.jsx @@ -1,4 +1,4 @@ -import React, { useDeferredValue, useRef } from "react"; +import React, { useDeferredValue, useMemo, useRef } from "react"; import { emptyTaskForm, normalizeIntervals, sanitizeTaskForm } from "./appDefaults.js"; import { formatAccountLabel, formatAccountStatus, formatTimestamp, formatCountdown } from "./utils/formatters.js"; import { copyToClipboard } from "./utils/clipboard.js"; @@ -83,6 +83,10 @@ export default function App() { setInviteAccessStatus, inviteAccessCheckedAt, setInviteAccessCheckedAt, + confirmAccessStatus, + setConfirmAccessStatus, + confirmAccessCheckedAt, + setConfirmAccessCheckedAt, accountEvents, setAccountEvents, taskAudit, @@ -377,11 +381,29 @@ export default function App() { confirmQueue, queueItems }); - const { checkAccess, checkInviteAccess } = useAccessChecks({ + const confirmStats = useMemo(() => { + const stats = { + total: confirmQueue.length, + confirmed: 0, + failed: 0 + }; + if (!selectedTaskId) return stats; + const prefix = `задача ${selectedTaskId}:`; + (accountEvents || []).forEach((event) => { + if (!event || !event.message || typeof event.message !== "string") return; + if (!event.message.startsWith(prefix)) return; + if (event.eventType === "confirm_retry_ok") stats.confirmed += 1; + if (event.eventType === "confirm_retry_failed") stats.failed += 1; + }); + return stats; + }, [accountEvents, confirmQueue.length, selectedTaskId]); + const { checkAccess, checkInviteAccess, checkConfirmAccess } = useAccessChecks({ selectedTaskId, setAccessStatus, setInviteAccessStatus, setInviteAccessCheckedAt, + setConfirmAccessStatus, + setConfirmAccessCheckedAt, setTaskNotice, showNotification }); @@ -510,7 +532,8 @@ export default function App() { taskActionLoading, loadBase, createTask, - setActiveTab + setActiveTab, + checkConfirmAccess }); const { accountById, @@ -772,6 +795,8 @@ export default function App() { tasksLength: tasks.length, runTestSafe: () => runTest("safe"), exportTaskBundle, + setInfoOpen, + setInfoTab, nowLine, nowExpanded, setNowExpanded, @@ -789,7 +814,8 @@ export default function App() { checklistItems, activeTab, logsTab, - setLogsTab + setLogsTab, + confirmStats }); const { taskSettings, accountsTab, logsTab: logsTabGroup, queueTab: queueTabGroup, eventsTab, settingsTab } = useAppTabGroups({ selectedTaskId, @@ -811,8 +837,11 @@ export default function App() { hasSelectedTask, inviteAccessStatus, inviteAccessCheckedAt, + confirmAccessStatus, + confirmAccessCheckedAt, formatTimestamp, checkInviteAccess, + checkConfirmAccess, accounts, showNotification, copyToClipboard, @@ -881,6 +910,7 @@ export default function App() { setConfirmPage, confirmPageCount, pagedConfirmQueue, + confirmStats, queueItems, queueStats, queueSearch, diff --git a/src/renderer/components/HelpTip.jsx b/src/renderer/components/HelpTip.jsx new file mode 100644 index 0000000..3fe932e --- /dev/null +++ b/src/renderer/components/HelpTip.jsx @@ -0,0 +1,10 @@ +import React from "react"; + +export default function HelpTip({ text }) { + if (!text) return null; + return ( + + ? + + ); +} diff --git a/src/renderer/components/InfoModal.jsx b/src/renderer/components/InfoModal.jsx index ceb7fd3..f3dd7ea 100644 --- a/src/renderer/components/InfoModal.jsx +++ b/src/renderer/components/InfoModal.jsx @@ -25,6 +25,27 @@ export default function InfoModal({ open, onClose, infoTab, setInfoTab }) { > Функции + + + + {infoTab === "usage" && ( @@ -63,12 +91,55 @@ export default function InfoModal({ open, onClose, infoTab, setInfoTab }) { )} {infoTab === "features" && (
- Функции и режимы: -
1) Мониторинг: отслеживает новые сообщения в группах конкурентов и ставит авторов в очередь.
-
2) Инвайт по расписанию: приглашает с интервалом и дневным лимитом.
-
3) Инвайт через админов: временно выдает право “Приглашать”, затем снимает.
-
4) Прогрев лимита: плавно увеличивает нагрузку, чтобы снизить риск ограничений.
-
5) История/Очередь/События: показывает, что произошло и почему.
+ Функции и кнопки: +
    +
  1. Сохранить — сохраняет настройки задачи.
  2. +
  3. Экспорт логов — выгружает логи/очередь/ошибки по задаче.
  4. +
  5. Собрать историю — добавляет авторов последних сообщений конкурентов в очередь.
  6. +
  7. Добавить ботов в Telegram группы — вводит аккаунты в конкурентов/нашу группу.
  8. +
  9. Проверить всё — проверяет доступы и права инвайта у аккаунтов.
  10. +
  11. Тестовый прогон — один реальный инвайт из очереди для проверки логики.
  12. +
  13. Проверить участие — обновляет статусы участия аккаунтов в группах.
  14. +
  15. Обновить ID — подтягивает актуальные user_id/username аккаунтов.
  16. +
  17. Очистить очередь — удаляет пользователей в ожидании.
  18. +
  19. Сбросить сессии — переподключает аккаунты Telegram.
  20. +
+
+ )} + {infoTab === "admin" && ( +
+ Инвайт через админов: +
    +
  1. Мастер‑админ должен быть участником целевой группы и иметь право “Добавлять админов”.
  2. +
  3. Перед инвайтом мастер‑админ временно выдает права “Приглашать” инвайтеру.
  4. +
  5. Инвайтер выполняет приглашение пользователя в группу.
  6. +
  7. После попытки права у инвайтера снимаются.
  8. +
+

+ Если master‑admin не может резолвить инвайтера или не имеет прав — инвайт через админов не сработает. +

+
+ )} + {infoTab === "confirm" && ( +
+ Подтверждение участия: +
    +
  1. Успех — пользователь найден в группе (OK).
  2. +
  3. Не подтверждено — инвайт отправлен, но вступление не найдено.
  4. +
  5. Ошибка — подтвердить участие нельзя (нет прав, приватность, цель недоступна).
  6. +
  7. При USER_NOT_PARTICIPANT автоматически ставится повторная проверка через 5 минут (до 2 попыток).
  8. +
+
+ )} + {infoTab === "queue" && ( +
+ Очередь: +
    +
  1. Pending — пользователь ожидает инвайт.
  2. +
  3. Unconfirmed — инвайт отправлен, участие не подтверждено.
  4. +
  5. Failed — ошибка инвайта (с кодом причины).
  6. +
  7. Skipped — пользователь пропущен (например, админ/бот конкурента).
  8. +
)} {infoTab === "terms" && ( @@ -95,16 +166,28 @@ export default function InfoModal({ open, onClose, infoTab, setInfoTab }) {
После успешного инвайта выполняется проверка фактического вступления.
)} + {infoTab === "faq" && ( +
+ Частые вопросы: +
• Почему “Не подтверждено”? — Инвайт отправлен, но Telegram ещё не видит пользователя в группе.
+
• Почему нет прав инвайта? — Аккаунт не в группе или нет права “Приглашать”.
+
• Почему USER_NOT_MUTUAL_CONTACT? — У пользователя закрыт приём инвайтов или в группе “только контакты”.
+
• Почему CHANNEL_INVALID? — Цель недоступна в этой сессии или ссылка некорректна.
+
• Что делать при FLOOD? — Уменьшить лимиты и увеличить интервалы.
+
• Почему мониторинг не даёт username? — Пользователь скрывает username, нужен access_hash.
+
+ )} {infoTab === "limits" && (
- Особенности Telegram и ошибки: -
1) AUTH_KEY_DUPLICATED: tdata уже используется — выйдите из аккаунта на других устройствах и пересоберите tdata.
-
2) CHAT_ADMIN_REQUIRED: аккаунт должен быть админом с правом “добавлять участников”.
-
3) USER_ID_INVALID: скрытые/анонимные авторы, удаленные аккаунты — инвайт возможен только по username.
-
4) USER_NOT_MUTUAL_CONTACT: ограничения Telegram/приватность пользователя — помогает инвайт‑ссылка или другой аккаунт.
-
5) USER_PRIVACY_RESTRICTED: пользователь запретил инвайты в чаты.
-
6) FLOOD/PEER_FLOOD: снизить лимиты, увеличить интервалы, распределить нагрузку.
-
7) CHANNEL_PRIVATE/INVITE_HASH_INVALID: ссылка недействительна или чат приватный.
+ Ошибки и ограничения Telegram: +
1) AUTH_KEY_DUPLICATED — tdata уже используется. Выйдите из аккаунта на других устройствах.
+
2) CHAT_ADMIN_REQUIRED — у аккаунта нет прав админа для инвайта.
+
3) USER_ID_INVALID — скрытый/удалённый автор, нужен username.
+
4) USER_NOT_MUTUAL_CONTACT — приватность или «только контакты».
+
5) USER_PRIVACY_RESTRICTED — пользователь запретил инвайты.
+
6) CHAT_WRITE_FORBIDDEN — аккаунт не в группе или не имеет прав.
+
7) CHANNEL_INVALID / CHANNEL_PRIVATE — цель не резолвится в сессии.
+
8) FLOOD / PEER_FLOOD — снизить лимиты и увеличить интервалы.
)} diff --git a/src/renderer/components/NowStatusCard.jsx b/src/renderer/components/NowStatusCard.jsx index 57150bc..80e4bdf 100644 --- a/src/renderer/components/NowStatusCard.jsx +++ b/src/renderer/components/NowStatusCard.jsx @@ -12,7 +12,9 @@ export default function NowStatusCard({ taskStatus, groupVisibility, lastEvents, - formatTimestamp + formatTimestamp, + confirmStats, + openConfirmTab }) { return (
@@ -55,6 +57,14 @@ export default function NowStatusCard({ {taskStatus.lastStopAt ? ` (${formatTimestamp(taskStatus.lastStopAt)})` : ""} )} + {confirmStats && ( +
+ Неподтвержденные: очередь {confirmStats.total} · подтвердилось {confirmStats.confirmed} · не подтвердилось {confirmStats.failed} + +
+ )} {taskStatus.warnings && taskStatus.warnings.length > 0 && (
{taskStatus.warnings.map((warning, index) => ( diff --git a/src/renderer/components/QuickActionsBar.jsx b/src/renderer/components/QuickActionsBar.jsx index 904d086..8391e94 100644 --- a/src/renderer/components/QuickActionsBar.jsx +++ b/src/renderer/components/QuickActionsBar.jsx @@ -25,7 +25,8 @@ export default function QuickActionsBar({ setActiveTab, tasksLength, runTestSafe, - exportTaskBundle + exportTaskBundle, + openHelp }) { return (
@@ -50,6 +51,7 @@ export default function QuickActionsBar({ + {taskStatus.running ? ( +
+ )}