This commit is contained in:
Ivan Neplokhov 2026-02-05 00:16:39 +04:00
parent 6af9c974be
commit ff66cd7bf0
4 changed files with 206 additions and 39 deletions

View File

@ -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",

View File

@ -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 "Пользователь является администратором в группе конкурента и пропущен по фильтру.";
}

View File

@ -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) || "проверка этим аккаунтом";

View File

@ -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 "Пользователь является администратором в группе конкурента и пропущен по фильтру.";
}