diff --git a/package.json b/package.json index 677ec84..8e3c82e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "telegram-invite-automation", - "version": "1.1.0", + "version": "1.2.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 6ce193b..496e119 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -38,7 +38,8 @@ const filterTaskRolesByAccounts = (taskId, roles, accounts) => { filtered.push({ accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), - roleInvite: Boolean(row.role_invite) + roleInvite: Boolean(row.role_invite), + roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite) }); }); if (removedMissing || removedError) { @@ -429,7 +430,8 @@ ipcMain.handle("tasks:get", (_event, id) => { accountRoles: store.listTaskAccounts(id).map((row) => ({ accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), - roleInvite: Boolean(row.role_invite) + roleInvite: Boolean(row.role_invite), + roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite) })) }; }); @@ -474,6 +476,15 @@ ipcMain.handle("tasks:save", (_event, payload) => { if (existing.invite_admin_allow_flood !== (payload.task.inviteAdminAllowFlood ? 1 : 0)) { changes.inviteAdminAllowFlood = [Boolean(existing.invite_admin_allow_flood), Boolean(payload.task.inviteAdminAllowFlood)]; } + if (existing.invite_admin_anonymous !== (payload.task.inviteAdminAnonymous ? 1 : 0)) { + changes.inviteAdminAnonymous = [Boolean(existing.invite_admin_anonymous), Boolean(payload.task.inviteAdminAnonymous)]; + } + if (existing.separate_confirm_roles !== (payload.task.separateConfirmRoles ? 1 : 0)) { + changes.separateConfirmRoles = [Boolean(existing.separate_confirm_roles), Boolean(payload.task.separateConfirmRoles)]; + } + if (existing.max_confirm_bots !== Number(payload.task.maxConfirmBots || 0)) { + changes.maxConfirmBots = [existing.max_confirm_bots, Number(payload.task.maxConfirmBots || 0)]; + } if (existing.warmup_enabled !== (payload.task.warmupEnabled ? 1 : 0)) { changes.warmupEnabled = [Boolean(existing.warmup_enabled), Boolean(payload.task.warmupEnabled)]; } @@ -528,18 +539,25 @@ ipcMain.handle("tasks:appendAccounts", (_event, payload) => { const existingRows = store.listTaskAccounts(payload.taskId); const existing = new Map(existingRows.map((row) => [ row.account_id, - { accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), roleInvite: Boolean(row.role_invite) } + { + 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) + } ])); (payload.accountIds || []).forEach((accountId) => { if (!existing.has(accountId)) { - existing.set(accountId, { accountId, roleMonitor: true, roleInvite: true }); + existing.set(accountId, { accountId, roleMonitor: true, roleInvite: true, roleConfirm: true }); } }); (payload.accountRoles || []).forEach((item) => { + const roleConfirm = item.roleConfirm != null ? item.roleConfirm : item.roleInvite; existing.set(item.accountId, { accountId: item.accountId, roleMonitor: Boolean(item.roleMonitor), - roleInvite: Boolean(item.roleInvite) + roleInvite: Boolean(item.roleInvite), + roleConfirm: Boolean(roleConfirm) }); }); const merged = Array.from(existing.values()); @@ -557,7 +575,8 @@ ipcMain.handle("tasks:removeAccount", (_event, payload) => { .map((row) => ({ accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), - roleInvite: Boolean(row.role_invite) + roleInvite: Boolean(row.role_invite), + roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite) })); store.setTaskAccountRoles(payload.taskId, existing); return { ok: true, accountIds: existing.map((item) => item.accountId) }; @@ -765,7 +784,8 @@ ipcMain.handle("tasks:checkInviteAccess", async (_event, id) => { .map((row) => ({ accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), - roleInvite: Boolean(row.role_invite) + roleInvite: Boolean(row.role_invite), + roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite) })); store.setTaskAccountRoles(id, filtered); } @@ -831,11 +851,14 @@ const explainInviteError = (error) => { if (error === "CHAT_ADMIN_REQUIRED") { return "Для добавления участников нужны права администратора."; } + if (error === "CHANNEL_INVALID") { + return "Цель не распознана как группа/канал для этого аккаунта (ссылка недоступна или сущность устарела)."; + } if (error === "USER_ALREADY_PARTICIPANT") { return "Пользователь уже состоит в целевой группе."; } if (error === "CHAT_MEMBER_ADD_FAILED") { - return "Telegram отклонил добавление участника (ограничения приватности, антиспам или запрет инвайтов)."; + return "Telegram отказал в добавлении пользователя. Возможные причины: пользователь недоступен для инвайта, неверные данные, ограничения чата или лимиты аккаунта."; } if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") { return "Инвайт-ссылка недействительна или истекла."; diff --git a/src/main/store.js b/src/main/store.js index ca78704..65396f5 100644 --- a/src/main/store.js +++ b/src/main/store.js @@ -160,6 +160,9 @@ function initStore(userDataPath) { invite_via_admins INTEGER NOT NULL DEFAULT 0, invite_admin_master_id INTEGER NOT NULL DEFAULT 0, invite_admin_allow_flood INTEGER NOT NULL DEFAULT 0, + invite_admin_anonymous INTEGER NOT NULL DEFAULT 1, + separate_confirm_roles INTEGER NOT NULL DEFAULT 0, + max_confirm_bots INTEGER NOT NULL DEFAULT 1, warmup_enabled INTEGER NOT NULL DEFAULT 1, warmup_start_limit INTEGER NOT NULL DEFAULT 3, warmup_daily_increase INTEGER NOT NULL DEFAULT 2, @@ -183,7 +186,8 @@ function initStore(userDataPath) { task_id INTEGER NOT NULL, account_id INTEGER NOT NULL, role_monitor INTEGER NOT NULL DEFAULT 1, - role_invite INTEGER NOT NULL DEFAULT 1 + role_invite INTEGER NOT NULL DEFAULT 1, + role_confirm INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS task_audit ( @@ -251,6 +255,10 @@ function initStore(userDataPath) { ensureColumn("tasks", "invite_via_admins", "INTEGER NOT NULL DEFAULT 0"); ensureColumn("tasks", "invite_admin_master_id", "INTEGER NOT NULL DEFAULT 0"); ensureColumn("tasks", "invite_admin_allow_flood", "INTEGER NOT NULL DEFAULT 0"); + ensureColumn("tasks", "invite_admin_anonymous", "INTEGER NOT NULL DEFAULT 1"); + ensureColumn("tasks", "separate_confirm_roles", "INTEGER NOT NULL DEFAULT 0"); + ensureColumn("tasks", "max_confirm_bots", "INTEGER NOT NULL DEFAULT 1"); + ensureColumn("task_accounts", "role_confirm", "INTEGER NOT NULL DEFAULT 1"); ensureColumn("tasks", "warmup_enabled", "INTEGER NOT NULL DEFAULT 0"); ensureColumn("tasks", "warmup_start_limit", "INTEGER NOT NULL DEFAULT 3"); ensureColumn("tasks", "warmup_daily_increase", "INTEGER NOT NULL DEFAULT 2"); @@ -582,7 +590,8 @@ function initStore(userDataPath) { retry_on_fail = ?, auto_join_competitors = ?, auto_join_our_group = ?, separate_bot_roles = ?, require_same_bot_in_both = ?, stop_on_blocked = ?, stop_blocked_percent = ?, notes = ?, enabled = ?, allow_start_without_invite_rights = ?, parse_participants = ?, invite_via_admins = ?, invite_admin_master_id = ?, - invite_admin_allow_flood = ?, warmup_enabled = ?, warmup_start_limit = ?, warmup_daily_increase = ?, + invite_admin_allow_flood = ?, invite_admin_anonymous = ?, separate_confirm_roles = ?, max_confirm_bots = ?, + warmup_enabled = ?, warmup_start_limit = ?, warmup_daily_increase = ?, cycle_competitors = ?, competitor_cursor = ?, invite_link_on_fail = ?, updated_at = ? WHERE id = ? `).run( @@ -611,6 +620,9 @@ function initStore(userDataPath) { task.inviteViaAdmins ? 1 : 0, task.inviteAdminMasterId || 0, task.inviteAdminAllowFlood ? 1 : 0, + task.inviteAdminAnonymous ? 1 : 0, + task.separateConfirmRoles ? 1 : 0, + task.maxConfirmBots || 1, task.warmupEnabled ? 1 : 0, task.warmupStartLimit || 3, task.warmupDailyIncrease || 2, @@ -628,9 +640,10 @@ function initStore(userDataPath) { max_invites_per_cycle, max_competitor_bots, max_our_bots, random_accounts, multi_accounts_per_run, retry_on_fail, auto_join_competitors, auto_join_our_group, separate_bot_roles, require_same_bot_in_both, stop_on_blocked, stop_blocked_percent, notes, enabled, allow_start_without_invite_rights, parse_participants, invite_via_admins, invite_admin_master_id, - invite_admin_allow_flood, warmup_enabled, warmup_start_limit, warmup_daily_increase, cycle_competitors, + invite_admin_allow_flood, invite_admin_anonymous, separate_confirm_roles, max_confirm_bots, + warmup_enabled, warmup_start_limit, warmup_daily_increase, cycle_competitors, competitor_cursor, invite_link_on_fail, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( task.name, task.ourGroup, @@ -657,6 +670,9 @@ function initStore(userDataPath) { task.inviteViaAdmins ? 1 : 0, task.inviteAdminMasterId || 0, task.inviteAdminAllowFlood ? 1 : 0, + task.inviteAdminAnonymous ? 1 : 0, + task.separateConfirmRoles ? 1 : 0, + task.maxConfirmBots || 1, task.warmupEnabled ? 1 : 0, task.warmupStartLimit || 3, task.warmupDailyIncrease || 2, @@ -709,24 +725,26 @@ function initStore(userDataPath) { function setTaskAccounts(taskId, accountIds) { db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId); const stmt = db.prepare(` - INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite) - VALUES (?, ?, ?, ?) + INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite, role_confirm) + VALUES (?, ?, ?, ?, ?) `); - (accountIds || []).forEach((accountId) => stmt.run(taskId, accountId, 1, 1)); + (accountIds || []).forEach((accountId) => stmt.run(taskId, accountId, 1, 1, 1)); } function setTaskAccountRoles(taskId, roles) { db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId); const stmt = db.prepare(` - INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite) - VALUES (?, ?, ?, ?) + INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite, role_confirm) + VALUES (?, ?, ?, ?, ?) `); (roles || []).forEach((item) => { + const roleConfirm = item.roleConfirm != null ? item.roleConfirm : item.roleInvite; stmt.run( taskId, item.accountId, item.roleMonitor ? 1 : 0, - item.roleInvite ? 1 : 0 + item.roleInvite ? 1 : 0, + roleConfirm ? 1 : 0 ); }); } diff --git a/src/main/taskRunner.js b/src/main/taskRunner.js index 8d9e894..257f463 100644 --- a/src/main/taskRunner.js +++ b/src/main/taskRunner.js @@ -60,9 +60,11 @@ 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); await this.telegram.joinGroupsForTask(this.task, competitors, accounts, { monitorIds, - inviteIds + inviteIds, + confirmIds }); await this.telegram.startTaskMonitor(this.task, competitors, accounts, monitorIds); } @@ -96,12 +98,16 @@ class TaskRunner { if (ttlHours > 0) { this.store.clearQueueOlderThan(this.task.id, ttlHours); } - const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id); + const accountRows = this.store.listTaskAccounts(this.task.id); + const accounts = accountRows.map((row) => row.account_id); + const explicitInviteIds = accountRows.filter((row) => row.role_invite).map((row) => row.account_id); let inviteAccounts = accounts; const roles = this.telegram.getTaskRoleAssignments(this.task.id); const hasExplicitRoles = roles && ((roles.ourIds || []).length || (roles.competitorIds || []).length); - if (hasExplicitRoles) { - inviteAccounts = (roles.ourIds || []).length ? roles.ourIds : (roles.competitorIds || []); + if (explicitInviteIds.length) { + inviteAccounts = explicitInviteIds.slice(); + } else if (hasExplicitRoles) { + inviteAccounts = (roles.ourIds || []).length ? roles.ourIds : []; if (!inviteAccounts.length) { errors.push("No invite accounts (role-based)"); } @@ -193,10 +199,7 @@ class TaskRunner { this.store.markInviteStatus(item.id, "failed"); continue; } - let accountsForInvite = inviteAccounts; - if (item.watcher_account_id && !inviteAccounts.includes(item.watcher_account_id)) { - accountsForInvite = [item.watcher_account_id]; - } + const accountsForInvite = inviteAccounts; const watcherAccount = accountMap.get(item.watcher_account_id || 0); const result = await this.telegram.inviteUserForTask(this.task, item.user_id, accountsForInvite, { randomize: Boolean(this.task.random_accounts), diff --git a/src/main/telegram.js b/src/main/telegram.js index 9d328a5..da8f360 100644 --- a/src/main/telegram.js +++ b/src/main/telegram.js @@ -23,7 +23,7 @@ class TelegramManager { this.authKeyResetDone = false; } - _buildInviteAdminRights() { + _buildInviteAdminRights(allowAnonymous = false) { return new Api.ChatAdminRights({ inviteUsers: true, addUsers: true, @@ -36,7 +36,7 @@ class TelegramManager { manageTopics: false, postMessages: false, editMessages: false, - anonymous: false + anonymous: Boolean(allowAnonymous) }); } @@ -86,8 +86,8 @@ class TelegramManager { return lines.join("\n"); } - async _grantTempInviteAdmin(masterClient, targetEntity, account) { - const rights = this._buildInviteAdminRights(); + async _grantTempInviteAdmin(masterClient, targetEntity, account, allowAnonymous = false) { + const rights = this._buildInviteAdminRights(allowAnonymous); const identifier = account.user_id ? BigInt(account.user_id) : (account.username ? `@${account.username}` : ""); @@ -555,6 +555,16 @@ class TelegramManager { const base = message ? `${code}: ${message}` : code; return sourceLabel ? `${base} (${sourceLabel})` : base; }; + const formatAccountSource = (label, accountEntry) => { + if (label) return label; + if (!accountEntry || !accountEntry.account) return ""; + const phone = accountEntry.account.phone || ""; + const username = accountEntry.account.username ? `@${accountEntry.account.username}` : ""; + if (phone && username) return `проверка аккаунтом ${phone} (${username})`; + if (phone) return `проверка аккаунтом ${phone}`; + if (username) return `проверка аккаунтом ${username}`; + return "проверка аккаунтом"; + }; const confirmMembership = async (user, confirmClient = client, sourceLabel = "") => { if (!targetEntity || targetEntity.className !== "Channel") { return { confirmed: true, error: "", detail: "" }; @@ -564,7 +574,7 @@ class TelegramManager { channel: targetEntity, participant: user })); - return { confirmed: true, error: "", detail: "" }; + return { confirmed: true, error: "", detail: sourceLabel ? `OK (${sourceLabel})` : "OK" }; } catch (error) { const errorText = error.errorMessage || error.message || String(error); if (errorText.includes("USER_NOT_PARTICIPANT")) { @@ -588,25 +598,57 @@ class TelegramManager { }; } }; - const confirmMembershipWithFallback = async (user) => { + const confirmMembershipWithFallback = async (user, inviterEntry = null) => { const attempts = []; - const direct = await confirmMembership(user, client, "проверка этим аккаунтом"); + const triedClients = new Set(); + const directLabel = formatAccountSource("", inviterEntry); + const direct = await confirmMembership(user, client, directLabel || "проверка этим аккаунтом"); if (direct.detail) { attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail }); } + triedClients.add(client); if (direct.confirmed !== null) { return { ...direct, attempts }; } - const masterId = Number(task.invite_admin_master_id || 0); - const masterEntry = masterId ? this.clients.get(masterId) : null; - if (masterEntry && masterEntry.client && masterEntry.client !== client) { - const adminConfirm = await confirmMembership(user, masterEntry.client, "проверка админом"); - if (adminConfirm.detail) { - attempts.push({ strategy: "confirm_admin", ok: adminConfirm.confirmed === true, detail: adminConfirm.detail }); + let finalResult = direct; + const roleAssignments = this.taskRoleAssignments.get(task.id) || {}; + const confirmIds = Array.isArray(roleAssignments.confirmIds) ? roleAssignments.confirmIds : []; + for (const confirmId of confirmIds) { + const entry = this.clients.get(confirmId); + if (!entry || !entry.client || triedClients.has(entry.client)) continue; + triedClients.add(entry.client); + const label = formatAccountSource("проверка подтверждающим аккаунтом", entry); + const confirmResult = await confirmMembership(user, entry.client, label); + if (confirmResult.detail) { + attempts.push({ strategy: "confirm_role", ok: confirmResult.confirmed === true, detail: confirmResult.detail }); + } + finalResult = confirmResult; + if (confirmResult.confirmed !== null || confirmResult.detail) { + break; } - return { ...adminConfirm, attempts }; } - return { ...direct, attempts }; + if (finalResult.confirmed === null && !finalResult.detail) { + const masterId = Number(task.invite_admin_master_id || 0); + const masterEntry = masterId ? this.clients.get(masterId) : null; + if (masterEntry && masterEntry.client && !triedClients.has(masterEntry.client)) { + const adminLabel = formatAccountSource("проверка админом", masterEntry); + const adminConfirm = await confirmMembership(user, masterEntry.client, adminLabel); + if (adminConfirm.detail) { + attempts.push({ strategy: "confirm_admin", ok: adminConfirm.confirmed === true, detail: adminConfirm.detail }); + } + finalResult = adminConfirm; + } + } + if (finalResult.confirmed === null && !finalResult.detail) { + await new Promise((resolve) => setTimeout(resolve, 10000)); + const retryLabel = directLabel ? `${directLabel}, повтор через 10с` : "проверка этим аккаунтом, повтор через 10с"; + const retry = await confirmMembership(user, client, retryLabel); + if (retry.detail) { + attempts.push({ strategy: "confirm_retry", ok: retry.confirmed === true, detail: retry.detail }); + } + finalResult = retry; + } + return { ...finalResult, attempts }; }; const attemptInvite = async (user) => { if (!targetEntity) { @@ -631,14 +673,50 @@ class TelegramManager { throw new Error("Unsupported target chat type"); } }; - const attemptAdminInvite = async (user, adminClient = client) => { + const explainWriteForbidden = async () => { + if (!targetEntity) return "Цель не определена"; + try { + if (targetEntity.className === "Channel") { + const me = await client.getMe(); + const participant = await client.invoke(new Api.channels.GetParticipant({ + channel: targetEntity, + participant: me + })); + const part = participant && participant.participant ? participant.participant : participant; + const className = part && part.className ? part.className : ""; + const isCreator = className.includes("Creator"); + const isAdmin = className.includes("Admin") || isCreator; + const rights = part && part.adminRights ? part.adminRights : null; + const canInvite = rights ? Boolean(rights.inviteUsers || rights.addUsers) : false; + if (!part) return "Аккаунт не состоит в группе назначения."; + if (!isAdmin) return "Аккаунт не админ в группе назначения."; + if (!canInvite) return "У администратора нет права приглашать."; + return "Недостаточно прав для инвайта (проверьте настройки группы)."; + } + if (targetEntity.className === "Chat") { + const fullChat = await client.invoke(new Api.messages.GetFullChat({ chatId: targetEntity.id })); + const full = fullChat && fullChat.fullChat ? fullChat.fullChat : null; + const restricted = Boolean(full && full.defaultBannedRights && full.defaultBannedRights.inviteUsers); + if (restricted) return "В группе запрещено приглашать участников."; + return "Недостаточно прав для инвайта в группе."; + } + } catch (error) { + const text = error.errorMessage || error.message || String(error); + if (String(text).includes("USER_NOT_PARTICIPANT")) { + return "Аккаунт не состоит в группе назначения."; + } + return `Не удалось уточнить причину: ${text}`; + } + return "Недостаточно прав для инвайта."; + }; + const attemptAdminInvite = async (user, adminClient = client, allowAnonymous = false) => { if (!targetEntity) { throw new Error("Target group not resolved"); } if (targetEntity.className !== "Channel") { throw new Error("ADMIN_INVITE_UNSUPPORTED_TARGET"); } - const rights = this._buildInviteAdminRights(); + const rights = this._buildInviteAdminRights(allowAnonymous); await adminClient.invoke(new Api.channels.EditAdmin({ channel: targetEntity, userId: user, @@ -737,10 +815,14 @@ class TelegramManager { const masterEntry = masterId ? this.clients.get(masterId) : null; if (masterEntry && masterId !== account.id) { try { - await this._grantTempInviteAdmin(masterEntry.client, targetEntity, account); + await this._grantTempInviteAdmin(masterEntry.client, targetEntity, account, Boolean(task.invite_admin_anonymous)); lastAttempts.push({ strategy: "temp_admin", ok: true, detail: "granted" }); await attemptInvite(user); - const confirm = await confirmMembershipWithFallback(user); + const confirm = await confirmMembershipWithFallback(user, entry); + if (confirm.confirmed !== true && !confirm.detail) { + const label = formatAccountSource("", entry) || "проверка этим аккаунтом"; + confirm.detail = buildConfirmDetail("CONFIRM_UNKNOWN", "результат проверки не определен", label); + } if (confirm.attempts && confirm.attempts.length) { lastAttempts.push(...confirm.attempts); } @@ -775,8 +857,12 @@ class TelegramManager { const masterId = Number(task.invite_admin_master_id || 0); const masterEntry = masterId ? this.clients.get(masterId) : null; const adminClient = masterEntry ? masterEntry.client : client; - await attemptAdminInvite(user, adminClient); - const confirm = await confirmMembershipWithFallback(user); + await attemptAdminInvite(user, adminClient, Boolean(task.invite_admin_anonymous)); + const confirm = await confirmMembershipWithFallback(user, entry); + if (confirm.confirmed !== true && !confirm.detail) { + const label = formatAccountSource("", entry) || "проверка этим аккаунтом"; + confirm.detail = buildConfirmDetail("CONFIRM_UNKNOWN", "результат проверки не определен", label); + } if (confirm.attempts && confirm.attempts.length) { lastAttempts.push(...confirm.attempts); } @@ -795,6 +881,13 @@ class TelegramManager { } catch (adminError) { const adminText = adminError.errorMessage || adminError.message || String(adminError); if (adminText.includes("CHANNEL_INVALID")) { + const targetLabel = task.our_group || ""; + const entityType = targetEntity && targetEntity.className ? targetEntity.className : "unknown"; + lastAttempts.push({ + strategy: "admin_invite", + ok: false, + detail: `CHANNEL_INVALID; target=${targetLabel}; entity=${entityType}` + }); try { const retryResolved = await this._resolveGroupEntity(client, task.our_group, Boolean(task.auto_join_our_group), account); if (retryResolved.ok) { @@ -813,7 +906,11 @@ class TelegramManager { } } await attemptInvite(user); - const confirm = await confirmMembershipWithFallback(user); + const confirm = await confirmMembershipWithFallback(user, entry); + if (confirm.confirmed !== true && !confirm.detail) { + const label = formatAccountSource("", entry) || "проверка этим аккаунтом"; + confirm.detail = buildConfirmDetail("CONFIRM_UNKNOWN", "результат проверки не определен", label); + } if (confirm.attempts && confirm.attempts.length) { lastAttempts.push(...confirm.attempts); } @@ -832,6 +929,14 @@ class TelegramManager { }; } catch (error) { const errorText = error.errorMessage || error.message || String(error); + if (errorText.includes("CHAT_WRITE_FORBIDDEN")) { + try { + const reason = await explainWriteForbidden(); + lastAttempts.push({ strategy: "invite_access", ok: false, detail: `CHAT_WRITE_FORBIDDEN: ${reason}` }); + } catch (diagError) { + // ignore diagnostics errors + } + } this._handleAuthKeyDuplicated(errorText); let fallbackMeta = lastAttempts.length ? JSON.stringify(lastAttempts) : ""; if (errorText === "USER_NOT_MUTUAL_CONTACT") { @@ -1453,7 +1558,7 @@ class TelegramManager { return { ok: false, error: "Admin invite поддерживается только для супергрупп" }; } - const rights = this._buildInviteAdminRights(); + const rights = this._buildInviteAdminRights(Boolean(task.invite_admin_anonymous)); const accounts = this.store.listAccounts(); const accountMap = new Map(accounts.map((acc) => [acc.id, acc])); const results = []; @@ -1683,7 +1788,8 @@ class TelegramManager { const explicitMonitorIds = Array.isArray(roleIds.monitorIds) ? roleIds.monitorIds : []; const explicitInviteIds = Array.isArray(roleIds.inviteIds) ? roleIds.inviteIds : []; - const hasExplicitRoles = explicitMonitorIds.length || explicitInviteIds.length; + const explicitConfirmIds = Array.isArray(roleIds.confirmIds) ? roleIds.confirmIds : []; + const hasExplicitRoles = explicitMonitorIds.length || explicitInviteIds.length || explicitConfirmIds.length; const competitors = competitorGroups || []; let cursor = 0; @@ -1715,7 +1821,9 @@ class TelegramManager { const usedForOur = new Set(); if (task.our_group) { if (hasExplicitRoles) { - const pool = accounts.filter((entry) => explicitInviteIds.includes(entry.account.id)); + const pool = accounts.filter((entry) => + explicitInviteIds.includes(entry.account.id) || explicitConfirmIds.includes(entry.account.id) + ); for (const entry of pool) { usedForOur.add(entry.account.id); if (task.auto_join_our_group) { @@ -1747,7 +1855,8 @@ class TelegramManager { } this.taskRoleAssignments.set(task.id, { competitorIds: hasExplicitRoles ? explicitMonitorIds : Array.from(usedForCompetitors), - ourIds: hasExplicitRoles ? explicitInviteIds : Array.from(usedForOur) + ourIds: hasExplicitRoles ? explicitInviteIds : Array.from(usedForOur), + confirmIds: hasExplicitRoles ? explicitConfirmIds : Array.from(usedForOur) }); } @@ -1945,6 +2054,12 @@ class TelegramManager { accessHash, monitorAccount.account.id ); + const sender = message && message.sender ? message.sender : null; + const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : ""; + const messageText = message && message.message ? String(message.message).trim() : ""; + const messagePreview = messageText.length > 160 ? `${messageText.slice(0, 157)}...` : messageText; + const messageSuffix = messagePreview ? `\nСообщение: ${messagePreview}` : ""; + const senderLabel = senderName ? `${senderName} ` : ""; if (enqueued) { const now = Date.now(); const lastEvent = monitorEntry.lastMessageEventAt.get(chatId) || 0; @@ -1954,7 +2069,7 @@ class TelegramManager { monitorAccount.account.id, monitorAccount.account.phone, "new_message", - `${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}` + `${formatGroupLabel(st)}: ${senderLabel}${username ? `@${username}` : senderId}${messageSuffix}` ); } } else if (shouldLogEvent(`${chatId}:dup`, 30000)) { @@ -1964,7 +2079,7 @@ class TelegramManager { monitorAccount.account.id, monitorAccount.account.phone, "new_message_duplicate", - `${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}${suffix}` + `${formatGroupLabel(st)}: ${senderLabel}${username ? `@${username}` : senderId}${suffix}${messageSuffix}` ); } }; @@ -2099,6 +2214,12 @@ class TelegramManager { monitorEntry.lastSource = st.source; if (this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id)) { enqueued += 1; + const sender = message && message.sender ? message.sender : null; + const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : ""; + const messageText = message && message.message ? String(message.message).trim() : ""; + const messagePreview = messageText.length > 160 ? `${messageText.slice(0, 157)}...` : messageText; + const messageSuffix = messagePreview ? `\nСообщение: ${messagePreview}` : ""; + const senderLabel = senderName ? `${senderName} ` : ""; const now = Date.now(); const lastEvent = monitorEntry.lastMessageEventAt.get(key) || 0; if (now - lastEvent > 15000) { @@ -2107,17 +2228,23 @@ class TelegramManager { monitorAccount.account.id, monitorAccount.account.phone, "new_message", - `${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}` + `${formatGroupLabel(st)}: ${senderLabel}${username ? `@${username}` : senderId}${messageSuffix}` ); } } else if (shouldLogEvent(`${key}:dup`, 30000)) { const status = this.store.getInviteStatus(task.id, senderId, st.source); const suffix = status && status !== "pending" ? ` (уже обработан: ${status})` : " (уже в очереди)"; + const sender = message && message.sender ? message.sender : null; + const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : ""; + const messageText = message && message.message ? String(message.message).trim() : ""; + const messagePreview = messageText.length > 160 ? `${messageText.slice(0, 157)}...` : messageText; + const messageSuffix = messagePreview ? `\nСообщение: ${messagePreview}` : ""; + const senderLabel = senderName ? `${senderName} ` : ""; this.store.addAccountEvent( monitorAccount.account.id, monitorAccount.account.phone, "new_message_duplicate", - `${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}${suffix}` + `${formatGroupLabel(st)}: ${senderLabel}${username ? `@${username}` : senderId}${suffix}${messageSuffix}` ); } } @@ -2521,7 +2648,7 @@ class TelegramManager { } getTaskRoleAssignments(taskId) { - return this.taskRoleAssignments.get(taskId) || { competitorIds: [], ourIds: [] }; + return this.taskRoleAssignments.get(taskId) || { competitorIds: [], ourIds: [], confirmIds: [] }; } getTaskMonitorInfo(taskId) { diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx index 108c647..4d39d68 100644 --- a/src/renderer/App.jsx +++ b/src/renderer/App.jsx @@ -40,6 +40,9 @@ const emptySettings = { inviteViaAdmins: false, inviteAdminMasterId: 0, inviteAdminAllowFlood: false, + inviteAdminAnonymous: true, + separateConfirmRoles: false, + maxConfirmBots: 1, warmupEnabled: true, warmupStartLimit: 3, warmupDailyIncrease: 2, @@ -76,6 +79,9 @@ const emptySettings = { inviteViaAdmins: Boolean(row.invite_via_admins), inviteAdminMasterId: Number(row.invite_admin_master_id || 0), inviteAdminAllowFlood: Boolean(row.invite_admin_allow_flood), + inviteAdminAnonymous: row.invite_admin_anonymous == null ? true : Boolean(row.invite_admin_anonymous), + separateConfirmRoles: Boolean(row.separate_confirm_roles), + maxConfirmBots: Number(row.max_confirm_bots || 1), warmupEnabled: row.warmup_enabled == null ? true : Boolean(row.warmup_enabled), warmupStartLimit: Number(row.warmup_start_limit || 3), warmupDailyIncrease: Number(row.warmup_daily_increase || 2), @@ -106,6 +112,9 @@ const sanitizeTaskForm = (form) => { normalized.separateBotRoles = false; normalized.maxOurBots = normalized.maxCompetitorBots; } + if (!normalized.separateBotRoles) { + normalized.separateConfirmRoles = false; + } return normalized; }; @@ -124,6 +133,8 @@ export default function App() { const [competitorText, setCompetitorText] = useState(""); const [selectedAccountIds, setSelectedAccountIds] = useState([]); const [taskAccountRoles, setTaskAccountRoles] = useState({}); + const [activePreset, setActivePreset] = useState(""); + const presetSignatureRef = useRef(""); const [taskStatus, setTaskStatus] = useState({ running: false, queueCount: 0, @@ -275,12 +286,14 @@ export default function App() { const roleSummary = useMemo(() => { const monitor = []; const invite = []; + const confirm = []; Object.entries(taskAccountRoles).forEach(([id, roles]) => { const accountId = Number(id); if (roles.monitor) monitor.push(accountId); if (roles.invite) invite.push(accountId); + if (roles.confirm) confirm.push(accountId); }); - return { monitor, invite }; + return { monitor, invite, confirm }; }, [taskAccountRoles]); const roleIntersectionCount = useMemo(() => { let count = 0; @@ -290,7 +303,7 @@ export default function App() { return count; }, [taskAccountRoles]); const assignedAccountCount = useMemo(() => { - const ids = new Set([...roleSummary.monitor, ...roleSummary.invite]); + const ids = new Set([...roleSummary.monitor, ...roleSummary.invite, ...roleSummary.confirm]); return ids.size; }, [roleSummary]); const assignedAccountMap = useMemo(() => { @@ -300,7 +313,8 @@ export default function App() { list.push({ taskId: row.task_id, roleMonitor: Boolean(row.role_monitor), - roleInvite: Boolean(row.role_invite) + roleInvite: Boolean(row.role_invite), + roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite) }); map.set(row.account_id, list); }); @@ -400,14 +414,16 @@ export default function App() { const roleMap = {}; if (details.accountRoles && details.accountRoles.length) { details.accountRoles.forEach((item) => { + const roleConfirm = item.roleConfirm != null ? item.roleConfirm : item.roleInvite; roleMap[item.accountId] = { monitor: Boolean(item.roleMonitor), - invite: Boolean(item.roleInvite) + invite: Boolean(item.roleInvite), + confirm: Boolean(roleConfirm) }; }); } else { (details.accountIds || []).forEach((accountId) => { - roleMap[accountId] = { monitor: true, invite: true }; + roleMap[accountId] = { monitor: true, invite: true, confirm: true }; }); } setTaskAccountRoles(roleMap); @@ -450,12 +466,49 @@ export default function App() { setInviteAccessStatus([]); setMembershipStatus({}); setTaskNotice(null); + setActivePreset(""); if (selectedTaskId != null) { checkAccess("auto", true); checkInviteAccess("auto", true); } }, [selectedTaskId]); + const buildPresetSignature = (form, roles) => { + const roleEntries = Object.entries(roles || {}) + .map(([id, value]) => ({ + id: Number(id), + monitor: Boolean(value && value.monitor), + invite: Boolean(value && value.invite), + confirm: Boolean(value && value.confirm) + })) + .sort((a, b) => a.id - b.id); + const snapshot = { + form: { + warmupEnabled: Boolean(form.warmupEnabled), + historyLimit: Number(form.historyLimit || 0), + separateBotRoles: Boolean(form.separateBotRoles), + requireSameBotInBoth: Boolean(form.requireSameBotInBoth), + maxCompetitorBots: Number(form.maxCompetitorBots || 0), + maxOurBots: Number(form.maxOurBots || 0), + separateConfirmRoles: Boolean(form.separateConfirmRoles), + maxConfirmBots: Number(form.maxConfirmBots || 0), + inviteViaAdmins: Boolean(form.inviteViaAdmins), + inviteAdminAnonymous: Boolean(form.inviteAdminAnonymous), + inviteAdminMasterId: Number(form.inviteAdminMasterId || 0) + }, + roles: roleEntries + }; + return JSON.stringify(snapshot); + }; + + useEffect(() => { + if (!activePreset) return; + const currentSignature = buildPresetSignature(taskForm, taskAccountRoles); + if (currentSignature !== presetSignatureRef.current) { + setActivePreset(""); + } + }, [taskForm, taskAccountRoles, activePreset]); + const taskSummary = useMemo(() => { const totals = { total: tasks.length, @@ -643,11 +696,16 @@ export default function App() { if (taskForm.separateBotRoles) { const nextCompetitors = Math.max(1, roleSummary.monitor.length || 0); const nextOur = Math.max(1, roleSummary.invite.length || 0); - if (taskForm.maxCompetitorBots !== nextCompetitors || taskForm.maxOurBots !== nextOur) { + const hasConfirmRoles = roleSummary.confirm.length > 0; + const nextConfirm = taskForm.separateConfirmRoles && hasConfirmRoles + ? Math.max(1, roleSummary.confirm.length) + : taskForm.maxConfirmBots; + if (taskForm.maxCompetitorBots !== nextCompetitors || taskForm.maxOurBots !== nextOur || (taskForm.separateConfirmRoles && hasConfirmRoles && taskForm.maxConfirmBots !== nextConfirm)) { setTaskForm((prev) => ({ ...prev, maxCompetitorBots: nextCompetitors, - maxOurBots: nextOur + maxOurBots: nextOur, + maxConfirmBots: nextConfirm })); } } @@ -656,8 +714,11 @@ export default function App() { roleIntersectionCount, roleSummary.monitor.length, roleSummary.invite.length, + roleSummary.confirm.length, taskForm.requireSameBotInBoth, - taskForm.separateBotRoles + taskForm.separateBotRoles, + taskForm.separateConfirmRoles, + taskForm.maxConfirmBots ]); const formatAccountStatus = (status) => { @@ -720,6 +781,9 @@ export default function App() { if (error === "CHAT_ADMIN_REQUIRED") { return "Для добавления участников нужны права администратора."; } + if (error === "CHANNEL_INVALID") { + return "Цель не распознана как группа/канал для этого аккаунта (ссылка недоступна или сущность устарела)."; + } if (error === "USER_ALREADY_PARTICIPANT") { return "Пользователь уже состоит в целевой группе."; } @@ -1159,7 +1223,7 @@ export default function App() { const chosen = pool.slice(0, required); accountRolesMap = {}; chosen.forEach((accountId) => { - accountRolesMap[accountId] = { monitor: true, invite: true }; + accountRolesMap[accountId] = { monitor: true, invite: true, confirm: true }; }); accountIds = chosen; setTaskAccountRoles(accountRolesMap); @@ -1169,7 +1233,7 @@ export default function App() { accountIds = accounts.map((account) => account.id); accountRolesMap = {}; accountIds.forEach((accountId) => { - accountRolesMap[accountId] = { monitor: true, invite: true }; + accountRolesMap[accountId] = { monitor: true, invite: true, confirm: true }; }); setTaskAccountRoles(accountRolesMap); setSelectedAccountIds(accountIds); @@ -1187,6 +1251,7 @@ export default function App() { if (roleEntries.length) { const hasMonitor = roleEntries.some((item) => item.monitor); const hasInvite = roleEntries.some((item) => item.invite); + const hasConfirm = roleEntries.some((item) => item.confirm); if (!hasMonitor) { if (!silent) { showNotification("Нужен хотя бы один аккаунт с ролью мониторинга.", "error"); @@ -1199,11 +1264,19 @@ export default function App() { } return; } + if (nextForm.separateConfirmRoles && !hasConfirm) { + if (!silent) { + showNotification("Нужен хотя бы один аккаунт с ролью подтверждения.", "error"); + } + return; + } } else { 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)) + ? Math.max(1, Number(nextForm.maxCompetitorBots || 1)) + + Math.max(1, Number(nextForm.maxOurBots || 1)) + + (nextForm.separateConfirmRoles ? Math.max(1, Number(nextForm.maxConfirmBots || 1)) : 0) : 1; if (accountIds.length < requiredAccounts) { if (!silent) { @@ -1215,7 +1288,8 @@ export default function App() { const accountRoles = Object.entries(accountRolesMap).map(([id, roles]) => ({ accountId: Number(id), roleMonitor: Boolean(roles.monitor), - roleInvite: Boolean(roles.invite) + roleInvite: Boolean(roles.invite), + roleConfirm: Boolean(roles.confirm != null ? roles.confirm : roles.invite) })); const result = await window.api.saveTask({ task: nextForm, @@ -1641,7 +1715,8 @@ export default function App() { const rolePayload = Object.entries(next).map(([id, roles]) => ({ accountId: Number(id), roleMonitor: Boolean(roles.monitor), - roleInvite: Boolean(roles.invite) + roleInvite: Boolean(roles.invite), + roleConfirm: Boolean(roles.confirm != null ? roles.confirm : roles.invite) })); await window.api.appendTaskAccounts({ taskId: selectedTaskId, @@ -1652,9 +1727,9 @@ export default function App() { const updateAccountRole = (accountId, role, value) => { const next = { ...taskAccountRoles }; - const existing = next[accountId] || { monitor: false, invite: false }; + const existing = next[accountId] || { monitor: false, invite: false, confirm: false }; next[accountId] = { ...existing, [role]: value }; - if (!next[accountId].monitor && !next[accountId].invite) { + if (!next[accountId].monitor && !next[accountId].invite && !next[accountId].confirm) { delete next[accountId]; } const ids = Object.keys(next).map((id) => Number(id)); @@ -1666,7 +1741,7 @@ export default function App() { const setAccountRolesAll = (accountId, value) => { const next = { ...taskAccountRoles }; if (value) { - next[accountId] = { monitor: true, invite: true }; + next[accountId] = { monitor: true, invite: true, confirm: true }; } else { delete next[accountId]; } @@ -1688,25 +1763,35 @@ export default function App() { const next = {}; if (type === "all") { availableIds.forEach((id) => { - next[id] = { monitor: true, invite: true }; + next[id] = { monitor: true, invite: true, confirm: true }; }); } else if (type === "one") { const id = availableIds[0]; - next[id] = { monitor: true, invite: true }; + next[id] = { monitor: true, invite: true, confirm: true }; } else if (type === "split") { const monitorCount = Math.max(1, Number(taskForm.maxCompetitorBots || 1)); const inviteCount = Math.max(1, Number(taskForm.maxOurBots || 1)); const monitorIds = availableIds.slice(0, monitorCount); const inviteIds = availableIds.slice(monitorCount, monitorCount + inviteCount); + const confirmCount = taskForm.separateConfirmRoles ? Math.max(1, Number(taskForm.maxConfirmBots || 1)) : 0; + const confirmIds = taskForm.separateConfirmRoles + ? availableIds.slice(monitorCount + inviteCount, monitorCount + inviteCount + confirmCount) + : []; monitorIds.forEach((id) => { - next[id] = { monitor: true, invite: false }; + next[id] = { monitor: true, invite: false, confirm: false }; }); inviteIds.forEach((id) => { - next[id] = { monitor: false, invite: true }; + next[id] = { monitor: false, invite: true, confirm: true }; + }); + confirmIds.forEach((id) => { + next[id] = { monitor: false, invite: false, confirm: true }; }); if (inviteIds.length < inviteCount) { showNotification("Не хватает аккаунтов для роли инвайта.", "error"); } + if (taskForm.separateConfirmRoles && confirmIds.length < confirmCount) { + showNotification("Не хватает аккаунтов для роли подтверждения.", "error"); + } } const ids = Object.keys(next).map((id) => Number(id)); setTaskAccountRoles(next); @@ -1716,19 +1801,95 @@ export default function App() { setTaskNotice({ text: `Пресет: ${label}`, tone: "success", source: "accounts" }); }; + const applyTaskPreset = (type) => { + if (!hasSelectedTask) { + showNotification("Сначала выберите задачу.", "error"); + return; + } + if (!accounts.length) { + showNotification("Нет доступных аккаунтов.", "error"); + return; + } + const masterId = accounts[0].id; + const requiredCount = 3; + const baseIds = selectedAccountIds.length >= requiredCount + ? selectedAccountIds.slice() + : accounts.map((account) => account.id); + if (baseIds.length < 3) { + showNotification("Для раздельных ролей желательно минимум 3 аккаунта (мониторинг/инвайт/подтверждение).", "info"); + } + if (!baseIds.includes(masterId)) { + baseIds.unshift(masterId); + } + const pool = baseIds.filter((id) => id !== masterId); + const roleMap = {}; + const addRole = (id, role) => { + if (!id) return; + if (!roleMap[id]) roleMap[id] = { monitor: false, invite: false, confirm: false }; + roleMap[id][role] = true; + }; + const takeFromPool = (count, used) => { + const result = []; + for (const id of pool) { + if (result.length >= count) break; + if (used.has(id)) continue; + used.add(id); + result.push(id); + } + return result; + }; + const used = new Set(); + const monitorCount = 1; + const inviteCount = 1; + const confirmCount = 1; + const monitorIds = takeFromPool(monitorCount, used); + const confirmIds = takeFromPool(confirmCount, used); + const inviteIds = masterId ? [masterId] : takeFromPool(inviteCount, used); + if (monitorIds.length < monitorCount) addRole(masterId, "monitor"); + if (confirmIds.length < confirmCount) addRole(masterId, "confirm"); + monitorIds.forEach((id) => addRole(id, "monitor")); + confirmIds.forEach((id) => addRole(id, "confirm")); + inviteIds.forEach((id) => addRole(id, "invite")); + + const nextForm = sanitizeTaskForm({ + ...taskForm, + warmupEnabled: true, + historyLimit: 35, + separateBotRoles: true, + requireSameBotInBoth: false, + maxCompetitorBots: 1, + maxOurBots: 1, + separateConfirmRoles: true, + maxConfirmBots: 1, + inviteViaAdmins: type === "admin", + inviteAdminAnonymous: true, + inviteAdminMasterId: masterId + }); + const signature = buildPresetSignature(nextForm, roleMap); + presetSignatureRef.current = signature; + setTaskForm(nextForm); + setTaskAccountRoles(roleMap); + setSelectedAccountIds(Object.keys(roleMap).map((id) => Number(id))); + persistAccountRoles(roleMap); + const label = type === "admin" ? "Автораспределение + Инвайт через админа" : "Автораспределение + Без админки"; + setTaskNotice({ text: `Пресет: ${label}`, tone: "success", source: "task" }); + setActivePreset(type); + }; + const assignAccountsToTask = async (accountIds) => { if (!window.api || selectedTaskId == null) return; if (!accountIds.length) return; const nextRoles = { ...taskAccountRoles }; accountIds.forEach((accountId) => { if (!nextRoles[accountId]) { - nextRoles[accountId] = { monitor: true, invite: true }; + nextRoles[accountId] = { monitor: true, invite: true, confirm: true }; } }); const rolePayload = Object.entries(nextRoles).map(([accountId, roles]) => ({ accountId: Number(accountId), roleMonitor: Boolean(roles.monitor), - roleInvite: Boolean(roles.invite) + roleInvite: Boolean(roles.invite), + roleConfirm: Boolean(roles.confirm != null ? roles.confirm : roles.invite) })); const result = await window.api.appendTaskAccounts({ taskId: selectedTaskId, @@ -2524,6 +2685,26 @@ export default function App() {
Основное
+
+
Пресеты:
+ + + + Мастер-админ: {taskForm.inviteAdminMasterId ? formatAccountLabel(accountById.get(taskForm.inviteAdminMasterId)) : "не выбран"} + +
- Роли ботов и вступление -
- - -
-
- - - -
-
- Режим “один и тот же бот” нужен, когда аккаунт должен быть в конкурентах и в нашей группе для инвайта. -
-
-
- Инвайт через админов -
- - - - -
-
-
- Интервалы и лимиты -
+ Базовые настройки +
+ Роли ботов и вступление +
+ + +
+
+ + + +
+
+ Режим “один и тот же бот” нужен, когда аккаунт должен быть в конкурентах и в нашей группе для инвайта. +
+
+
+ Интервалы и лимиты +
+
- Импорт аудитории -
- - - - -
-
- - {fileImportResult && ( -
- Импортировано: {fileImportResult.importedCount} · Пропущено: {fileImportResult.skippedCount} · Ошибок: {fileImportResult.failed.length} -
- )} -
- {fileImportResult && fileImportResult.failed.length > 0 && ( -
- {fileImportResult.failed.map((item, index) => ( -
-
{item.path}
-
{item.error}
-
- ))} -
- )} -
-
- Распределение ботов -
- {roleMode === "same" ? ( -
- Безопасность -
- - - - - -
-
- -
+ Экспертные настройки +
+ Безопасность +
+ + + + + +
+
+ +
+
+
+ Ошибки аккаунтов + {criticalErrorAccounts.length === 0 && ( +
Ошибок нет.
+ )} + {criticalErrorAccounts.length > 0 && ( +
+ {criticalErrorAccounts.map((account) => ( +
+
{formatAccountLabel(account)}
+
{account.last_error || "Ошибка сессии"}
+
+ ))} +
+ )} +