From ff66cd7bf03a34dab8a11a7ec5479973f79691f6 Mon Sep 17 00:00:00 2001 From: Ivan Neplokhov Date: Thu, 5 Feb 2026 00:16:39 +0400 Subject: [PATCH] some --- package.json | 2 +- src/main/index.js | 15 +++ src/main/telegram.js | 213 +++++++++++++++++++++++++------ src/renderer/utils/errorHints.js | 15 +++ 4 files changed, 206 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index ab10ad6..6fd4907 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "telegram-invite-automation", - "version": "1.4.0", + "version": "1.5.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 d682d3b..6cade75 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1092,6 +1092,21 @@ const explainInviteError = (error) => { if (error === "CHAT_MEMBER_ADD_FAILED") { return "Telegram отказал в добавлении пользователя. Возможные причины: пользователь недоступен для инвайта, неверные данные, ограничения чата или лимиты аккаунта."; } + if (error === "INVITER_ENTITY_NOT_RESOLVED_BY_MASTER") { + return "Мастер-админ не смог получить сущность инвайтера в своей сессии. Обычно это кэш сущностей/доступность аккаунта."; + } + if (error === "MASTER_TARGET_RESOLVE_FAILED" || error === "TARGET_RESOLVE_FAILED") { + return "Не удалось корректно резолвить целевую группу для текущего аккаунта."; + } + if (error === "TARGET_CLIENT_NOT_SET") { + return "Внутренняя ошибка: не задан клиент для проверки цели."; + } + if (error === "INVITED_USER_NOT_RESOLVED_FOR_ADMIN") { + return "Не удалось резолвить приглашаемого пользователя в сессии админ-аккаунта."; + } + if (error === "INVITED_USER_NOT_RESOLVED_FOR_CONFIRM") { + return "Не удалось резолвить пользователя в сессии аккаунта, который проверяет участие."; + } if (error === "SOURCE_ADMIN_SKIPPED") { return "Пользователь является администратором в группе конкурента и пропущен по фильтру."; } diff --git a/src/main/telegram.js b/src/main/telegram.js index 8ff10a3..a2faa79 100644 --- a/src/main/telegram.js +++ b/src/main/telegram.js @@ -41,6 +41,48 @@ class TelegramManager { }); } + async _resolveAccountEntityForMaster(masterClient, targetEntity, account) { + if (!masterClient || !targetEntity || !account) return null; + const username = account.username ? String(account.username).trim() : ""; + const userId = account.user_id ? String(account.user_id).trim() : ""; + + if (username) { + try { + const byUsername = await masterClient.getEntity(username.startsWith("@") ? username : `@${username}`); + if (byUsername && byUsername.className === "User") return byUsername; + } catch (error) { + // continue fallback chain + } + } + + if (userId) { + try { + const byId = await masterClient.getEntity(BigInt(userId)); + if (byId && byId.className === "User") return byId; + } catch (error) { + // continue fallback chain + } + } + + try { + const participants = await masterClient.getParticipants(targetEntity, { + limit: 400, + search: username || "" + }); + const found = (participants || []).find((item) => { + if (!item || item.className !== "User") return false; + const sameId = userId && item.id != null && item.id.toString() === userId; + const sameUsername = username && item.username && item.username.toLowerCase() === username.toLowerCase(); + return Boolean(sameId || sameUsername); + }); + if (found) return found; + } catch (error) { + // no-op + } + + return null; + } + async _collectInviteDiagnostics(client, targetEntity) { const lines = []; if (!targetEntity) return "Диагностика: цель не определена"; @@ -90,13 +132,10 @@ class TelegramManager { 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}` : ""); - if (!identifier) { - throw new Error("NO_ACCOUNT_IDENTITY"); + const user = await this._resolveAccountEntityForMaster(masterClient, targetEntity, account); + if (!user) { + throw new Error("INVITER_ENTITY_NOT_RESOLVED_BY_MASTER"); } - const user = await masterClient.getEntity(identifier); await masterClient.invoke(new Api.channels.EditAdmin({ channel: targetEntity, userId: user, @@ -114,13 +153,8 @@ class TelegramManager { } async _revokeTempInviteAdmin(masterClient, targetEntity, account) { - const identifier = account.user_id - ? BigInt(account.user_id) - : (account.username ? `@${account.username}` : ""); - if (!identifier) { - return; - } - const user = await masterClient.getEntity(identifier); + const user = await this._resolveAccountEntityForMaster(masterClient, targetEntity, account); + if (!user) return; await masterClient.invoke(new Api.channels.EditAdmin({ channel: targetEntity, userId: user, @@ -565,6 +599,7 @@ class TelegramManager { let targetEntity = null; let targetType = ""; let resolvedUser = null; + const targetEntityCache = new Map(); const buildConfirmDetail = (code, message, sourceLabel) => { if (!code) return message || ""; const base = message ? `${code}: ${message}` : code; @@ -580,14 +615,100 @@ class TelegramManager { if (username) return `проверка аккаунтом ${username}`; return "проверка аккаунтом"; }; - const confirmMembership = async (user, confirmClient = client, sourceLabel = "") => { - if (!targetEntity || targetEntity.className !== "Channel") { + const resolveUserForClient = async (targetClient, preferredUser = null) => { + if (!targetClient) return null; + const providedUsername = options.username || ""; + const normalizedUsername = providedUsername + ? (providedUsername.startsWith("@") ? providedUsername : `@${providedUsername}`) + : ""; + if (normalizedUsername) { + try { + const byUsername = await targetClient.getEntity(normalizedUsername); + if (byUsername && byUsername.className === "User") return byUsername; + } catch (error) { + // continue fallback chain + } + } + if (userId != null && userId !== "") { + try { + const byId = await targetClient.getEntity(BigInt(String(userId))); + if (byId && byId.className === "User") return byId; + } catch (error) { + // continue fallback chain + } + } + if (preferredUser && preferredUser.className === "User") { + return preferredUser; + } + if (preferredUser && preferredUser.userId != null && preferredUser.accessHash != null) { + try { + return new Api.InputUser({ + userId: BigInt(String(preferredUser.userId)), + accessHash: BigInt(String(preferredUser.accessHash)) + }); + } catch (error) { + // ignore malformed input user + } + } + if (preferredUser && preferredUser.id != null && preferredUser.accessHash != null) { + try { + return new Api.InputUser({ + userId: BigInt(String(preferredUser.id)), + accessHash: BigInt(String(preferredUser.accessHash)) + }); + } catch (error) { + // ignore malformed user entity + } + } + return null; + }; + const getTargetEntityForClient = async (clientForTarget, accountEntry = null) => { + if (!clientForTarget) { + return { ok: false, error: "TARGET_CLIENT_NOT_SET" }; + } + if (targetEntityCache.has(clientForTarget)) { + return { ok: true, entity: targetEntityCache.get(clientForTarget) }; + } + const accountForTarget = accountEntry && accountEntry.account + ? accountEntry.account + : (accountEntry || account); + const resolvedTarget = await this._resolveGroupEntity( + clientForTarget, + task.our_group, + Boolean(task.auto_join_our_group), + accountForTarget || null + ); + if (!resolvedTarget.ok) { + return { ok: false, error: resolvedTarget.error || "TARGET_RESOLVE_FAILED" }; + } + targetEntityCache.set(clientForTarget, resolvedTarget.entity); + return { ok: true, entity: resolvedTarget.entity }; + }; + const confirmMembership = async (user, confirmClient = client, sourceLabel = "", confirmEntry = null) => { + const targetForClient = await getTargetEntityForClient(confirmClient, confirmEntry); + if (!targetForClient.ok) { + return { + confirmed: null, + error: targetForClient.error, + detail: buildConfirmDetail(targetForClient.error, "ошибка подтверждения участия", sourceLabel) + }; + } + const confirmTargetEntity = targetForClient.entity; + if (!confirmTargetEntity || confirmTargetEntity.className !== "Channel") { return { confirmed: true, error: "", detail: "" }; } + 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) + }; + } try { await confirmClient.invoke(new Api.channels.GetParticipant({ - channel: targetEntity, - participant: user + channel: confirmTargetEntity, + participant: participantForClient })); return { confirmed: true, error: "", detail: sourceLabel ? `OK (${sourceLabel})` : "OK" }; } catch (error) { @@ -617,7 +738,7 @@ class TelegramManager { const attempts = []; const triedClients = new Set(); const directLabel = formatAccountSource("", inviterEntry); - const direct = await confirmMembership(user, client, directLabel || "проверка этим аккаунтом"); + const direct = await confirmMembership(user, client, directLabel || "проверка этим аккаунтом", entry); if (direct.detail) { attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail }); } @@ -633,7 +754,7 @@ class TelegramManager { 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); + const confirmResult = await confirmMembership(user, entry.client, label, entry); if (confirmResult.detail) { attempts.push({ strategy: "confirm_role", ok: confirmResult.confirmed === true, detail: confirmResult.detail }); } @@ -647,7 +768,7 @@ class TelegramManager { 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); + const adminConfirm = await confirmMembership(user, masterEntry.client, adminLabel, masterEntry); if (adminConfirm.detail) { attempts.push({ strategy: "confirm_admin", ok: adminConfirm.confirmed === true, detail: adminConfirm.detail }); } @@ -657,7 +778,7 @@ class TelegramManager { 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); + const retry = await confirmMembership(user, client, retryLabel, entry); if (retry.detail) { attempts.push({ strategy: "confirm_retry", ok: retry.confirmed === true, detail: retry.detail }); } @@ -670,20 +791,22 @@ class TelegramManager { return { ...finalResult, attempts }; }; const attemptInvite = async (user) => { - if (!targetEntity) { - throw new Error("Target group not resolved"); + const targetForClient = await getTargetEntityForClient(client, entry); + if (!targetForClient.ok || !targetForClient.entity) { + throw new Error(targetForClient.error || "Target group not resolved"); } - if (targetEntity.className === "Channel") { + const inviteTargetEntity = targetForClient.entity; + if (inviteTargetEntity.className === "Channel") { await client.invoke( new Api.channels.InviteToChannel({ - channel: targetEntity, + channel: inviteTargetEntity, users: [user] }) ); - } else if (targetEntity.className === "Chat") { + } else if (inviteTargetEntity.className === "Chat") { await client.invoke( new Api.messages.AddChatUser({ - chatId: targetEntity.id, + chatId: inviteTargetEntity.id, userId: user, fwdLimit: 0 }) @@ -736,23 +859,29 @@ class TelegramManager { } return "Недостаточно прав для инвайта."; }; - const attemptAdminInvite = async (user, adminClient = client, allowAnonymous = false) => { - if (!targetEntity) { - throw new Error("Target group not resolved"); + const attemptAdminInvite = async (user, adminClient = client, adminEntry = entry, allowAnonymous = false) => { + const targetForAdmin = await getTargetEntityForClient(adminClient, adminEntry); + if (!targetForAdmin.ok || !targetForAdmin.entity) { + throw new Error(targetForAdmin.error || "Target group not resolved"); } - if (targetEntity.className !== "Channel") { + const adminTargetEntity = targetForAdmin.entity; + if (adminTargetEntity.className !== "Channel") { throw new Error("ADMIN_INVITE_UNSUPPORTED_TARGET"); } + const userForAdminClient = await resolveUserForClient(adminClient, user); + if (!userForAdminClient) { + throw new Error("INVITED_USER_NOT_RESOLVED_FOR_ADMIN"); + } const rights = this._buildInviteAdminRights(allowAnonymous); await adminClient.invoke(new Api.channels.EditAdmin({ - channel: targetEntity, - userId: user, + channel: adminTargetEntity, + userId: userForAdminClient, adminRights: rights, rank: "invite" })); await adminClient.invoke(new Api.channels.EditAdmin({ - channel: targetEntity, - userId: user, + channel: adminTargetEntity, + userId: userForAdminClient, adminRights: new Api.ChatAdminRights({}), rank: "" })); @@ -904,8 +1033,14 @@ class TelegramManager { const masterId = Number(task.invite_admin_master_id || 0); const masterEntry = masterId ? this.clients.get(masterId) : null; if (masterEntry && masterId !== account.id) { + let masterTargetEntity = null; try { - await this._grantTempInviteAdmin(masterEntry.client, targetEntity, account, Boolean(task.invite_admin_anonymous)); + const masterTarget = await getTargetEntityForClient(masterEntry.client, masterEntry); + if (!masterTarget.ok || !masterTarget.entity) { + throw new Error(masterTarget.error || "MASTER_TARGET_RESOLVE_FAILED"); + } + masterTargetEntity = masterTarget.entity; + await this._grantTempInviteAdmin(masterEntry.client, masterTargetEntity, account, Boolean(task.invite_admin_anonymous)); lastAttempts.push({ strategy: "temp_admin", ok: true, detail: "granted" }); await attemptInvite(user); const confirm = await confirmMembershipWithFallback(user, entry); @@ -933,7 +1068,9 @@ class TelegramManager { lastAttempts.push({ strategy: "temp_admin_invite", ok: false, detail: adminText }); } finally { try { - await this._revokeTempInviteAdmin(masterEntry.client, targetEntity, account); + if (masterTargetEntity) { + await this._revokeTempInviteAdmin(masterEntry.client, masterTargetEntity, account); + } } catch (revokeError) { // ignore revoke errors } @@ -947,7 +1084,7 @@ 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, Boolean(task.invite_admin_anonymous)); + await attemptAdminInvite(user, adminClient, masterEntry || entry, Boolean(task.invite_admin_anonymous)); const confirm = await confirmMembershipWithFallback(user, entry); if (confirm.confirmed !== true && !confirm.detail) { const label = formatAccountSource("", entry) || "проверка этим аккаунтом"; diff --git a/src/renderer/utils/errorHints.js b/src/renderer/utils/errorHints.js index 0f017e0..04a0ec9 100644 --- a/src/renderer/utils/errorHints.js +++ b/src/renderer/utils/errorHints.js @@ -30,6 +30,21 @@ export const explainInviteError = (error) => { if (error === "CHAT_MEMBER_ADD_FAILED") { return "Telegram отклонил добавление. Обычно это антиспам-ограничение или недостаток прав."; } + if (error === "INVITER_ENTITY_NOT_RESOLVED_BY_MASTER") { + return "Мастер-админ не смог получить сущность инвайтера в своей сессии. Обычно это кэш сущностей/доступность аккаунта."; + } + if (error === "MASTER_TARGET_RESOLVE_FAILED" || error === "TARGET_RESOLVE_FAILED") { + return "Не удалось корректно резолвить целевую группу для текущего аккаунта."; + } + if (error === "TARGET_CLIENT_NOT_SET") { + return "Внутренняя ошибка: не задан клиент для проверки цели."; + } + if (error === "INVITED_USER_NOT_RESOLVED_FOR_ADMIN") { + return "Не удалось резолвить приглашаемого пользователя в сессии админ-аккаунта."; + } + if (error === "INVITED_USER_NOT_RESOLVED_FOR_CONFIRM") { + return "Не удалось резолвить пользователя в сессии аккаунта, который проверяет участие."; + } if (error === "SOURCE_ADMIN_SKIPPED") { return "Пользователь является администратором в группе конкурента и пропущен по фильтру."; }