This commit is contained in:
Ivan Neplokhov 2026-01-27 11:30:04 +04:00
parent d5d2a84a2e
commit 77baea862f
9 changed files with 881 additions and 435 deletions

View File

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

View File

@ -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 "Инвайт-ссылка недействительна или истекла.";

View File

@ -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
);
});
}

View File

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

View File

@ -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) {

File diff suppressed because it is too large Load Diff

View File

@ -1040,6 +1040,11 @@ button.secondary {
color: #1f2937;
}
button.secondary.active {
background: #2563eb;
color: #fff;
}
button.danger {
background: #ef4444;
color: #fff;

View File

@ -11,6 +11,7 @@ function AccountsTab({
selectedAccountIds,
taskAccountRoles,
hasSelectedTask,
inviteAdminMasterId,
refreshMembership,
refreshIdentity,
formatAccountStatus,
@ -39,15 +40,17 @@ function AccountsTab({
const knownIds = new Set((accounts || []).map((account) => account.id));
let monitor = 0;
let invite = 0;
let confirm = 0;
let total = 0;
Object.entries(taskAccountRoles || {}).forEach(([id, roles]) => {
const accountId = Number(id);
if (!knownIds.has(accountId)) return;
if (roles.monitor) monitor += 1;
if (roles.invite) invite += 1;
if (roles.monitor || roles.invite) total += 1;
if (roles.confirm) confirm += 1;
if (roles.monitor || roles.invite || roles.confirm) total += 1;
});
return { monitor, invite, total };
return { monitor, invite, confirm, total };
}, [taskAccountRoles, accounts]);
return (
<section className="card">
@ -66,7 +69,12 @@ function AccountsTab({
)}
{hasSelectedTask && (
<div className="account-summary">
Мониторят: {roleStats.monitor} · Инвайтят: {roleStats.invite} · Всего: {roleStats.total}
Мониторят: {roleStats.monitor} · Инвайтят: {roleStats.invite} · Подтверждают: {roleStats.confirm} · Всего: {roleStats.total}
</div>
)}
{hasSelectedTask && (
<div className="hint">
Ручные чекбоксы ролей имеют приоритет над автораспределением в настройках.
</div>
)}
{hasSelectedTask && (
@ -104,13 +112,15 @@ function AccountsTab({
? `В нашей: ${membership.ourGroupMember ? "да" : "нет"}`
: "В нашей: —";
const selected = selectedAccountIds.includes(account.id);
const roles = taskAccountRoles[account.id] || { monitor: false, invite: false };
const roles = taskAccountRoles[account.id] || { monitor: false, invite: false, confirm: false };
const isMasterAdmin = hasSelectedTask && inviteAdminMasterId && Number(inviteAdminMasterId) === account.id;
const taskNames = assignedTasks
.map((item) => {
const name = accountBuckets.taskNameMap.get(item.taskId) || `Задача #${item.taskId}`;
const roles = [
item.roleMonitor ? "М" : null,
item.roleInvite ? "И" : null
item.roleInvite ? "И" : null,
item.roleConfirm ? "П" : null
].filter(Boolean).join("/");
return roles ? `${name} (${roles})` : name;
})
@ -124,6 +134,8 @@ function AccountsTab({
<div className="role-badges">
{roles.monitor && <span className="role-pill">Мониторинг</span>}
{roles.invite && <span className="role-pill">Инвайт</span>}
{roles.confirm && <span className="role-pill">Подтверждение</span>}
{isMasterAdmin && <span className="role-pill accent">Мастер-админ</span>}
</div>
<div className="account-meta">User ID: {account.user_id || "—"}</div>
<div className="account-meta membership-row">
@ -187,12 +199,20 @@ function AccountsTab({
/>
Инвайт
</label>
<label className="checkbox">
<input
type="checkbox"
checked={Boolean(roles.confirm)}
onChange={(event) => updateAccountRole(account.id, "confirm", event.target.checked)}
/>
Подтверждение
</label>
<button
className="ghost tiny"
type="button"
onClick={() => setAccountRolesAll(account.id, !selected)}
>
{selected ? "Снять роли" : "Оба"}
{selected ? "Снять роли" : "Все роли"}
</button>
</div>
)}
@ -226,14 +246,16 @@ function AccountsTab({
const assignedTasks = assignedAccountMap.get(account.id) || [];
const roles = {
monitor: assignedTasks.some((item) => item.roleMonitor),
invite: assignedTasks.some((item) => item.roleInvite)
invite: assignedTasks.some((item) => item.roleInvite),
confirm: assignedTasks.some((item) => item.roleConfirm)
};
const taskNames = assignedTasks
.map((item) => {
const name = accountBuckets.taskNameMap.get(item.taskId) || `Задача #${item.taskId}`;
const roles = [
item.roleMonitor ? "М" : null,
item.roleInvite ? "И" : null
item.roleInvite ? "И" : null,
item.roleConfirm ? "П" : null
].filter(Boolean).join("/");
return roles ? `${name} (${roles})` : name;
})
@ -270,6 +292,7 @@ function AccountsTab({
<div className="role-badges">
{roles.monitor && <span className="role-pill">Мониторинг</span>}
{roles.invite && <span className="role-pill">Инвайт</span>}
{roles.confirm && <span className="role-pill">Подтверждение</span>}
</div>
<div className="account-meta">User ID: {account.user_id || "—"}</div>
<div className="account-meta membership-row">

View File

@ -432,7 +432,7 @@ function LogsTab({
<span className={`match-badge ${invite.watcherAccountId === invite.accountId ? "ok" : "warn"}`}>
{invite.watcherAccountId === invite.accountId
? "Инвайт тем же аккаунтом, что наблюдал"
: "Инвайт другим аккаунтом (наблюдатель отличается)"}
: ""}
</span>
)}
</div>
@ -452,9 +452,14 @@ function LogsTab({
{invite.error && invite.error !== "" && (
<div className="log-errors">Ошибка: {formatErrorWithExplain(invite.error)}</div>
)}
{invite.confirmError && (
<div className="log-errors">
Проверка участия: {formatErrorWithExplain(invite.confirmError)}
<div className="log-errors">
Проверка участия: {invite.confirmError
? formatErrorWithExplain(invite.confirmError)
: (invite.confirmed ? "OK" : "Не подтверждено")}
</div>
{invite.confirmError && invite.confirmError.includes("(") && (
<div className="log-users">
Проверял: {invite.confirmError.slice(invite.confirmError.indexOf("(") + 1, invite.confirmError.lastIndexOf(")"))}
</div>
)}
{invite.strategy && (
@ -488,7 +493,12 @@ function LogsTab({
<div>Статус: {formatInviteStatus(invite.status)}</div>
<div>Результат: {formatErrorWithExplain(invite.skippedReason)}</div>
<div>Ошибка: {formatErrorWithExplain(invite.error)}</div>
<div>Проверка участия: {formatErrorWithExplain(invite.confirmError)}</div>
<div>Проверка участия: {invite.confirmError
? formatErrorWithExplain(invite.confirmError)
: (invite.confirmed ? "OK" : "Не подтверждено")}</div>
{invite.confirmError && invite.confirmError.includes("(") && (
<div>Проверял: {invite.confirmError.slice(invite.confirmError.indexOf("(") + 1, invite.confirmError.lastIndexOf(")"))}</div>
)}
{invite.watcherAccountId && invite.accountId && invite.watcherAccountId !== invite.accountId
&& selectedTask && selectedTask.randomAccounts && hasBothRoles(invite.watcherAccountId) && (
<div>Примечание: у наблюдателя стоят обе роли, но включен случайный выбор инвайт выполнен другим аккаунтом.</div>