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", "name": "telegram-invite-automation",
"version": "1.1.0", "version": "1.2.0",
"private": true, "private": true,
"description": "Automated user parsing and invites for Telegram groups", "description": "Automated user parsing and invites for Telegram groups",
"main": "src/main/index.js", "main": "src/main/index.js",

View File

@ -38,7 +38,8 @@ const filterTaskRolesByAccounts = (taskId, roles, accounts) => {
filtered.push({ filtered.push({
accountId: row.account_id, accountId: row.account_id,
roleMonitor: Boolean(row.role_monitor), 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) { if (removedMissing || removedError) {
@ -429,7 +430,8 @@ ipcMain.handle("tasks:get", (_event, id) => {
accountRoles: store.listTaskAccounts(id).map((row) => ({ accountRoles: store.listTaskAccounts(id).map((row) => ({
accountId: row.account_id, accountId: row.account_id,
roleMonitor: Boolean(row.role_monitor), 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)) { if (existing.invite_admin_allow_flood !== (payload.task.inviteAdminAllowFlood ? 1 : 0)) {
changes.inviteAdminAllowFlood = [Boolean(existing.invite_admin_allow_flood), Boolean(payload.task.inviteAdminAllowFlood)]; 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)) { if (existing.warmup_enabled !== (payload.task.warmupEnabled ? 1 : 0)) {
changes.warmupEnabled = [Boolean(existing.warmup_enabled), Boolean(payload.task.warmupEnabled)]; 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 existingRows = store.listTaskAccounts(payload.taskId);
const existing = new Map(existingRows.map((row) => [ const existing = new Map(existingRows.map((row) => [
row.account_id, 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) => { (payload.accountIds || []).forEach((accountId) => {
if (!existing.has(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) => { (payload.accountRoles || []).forEach((item) => {
const roleConfirm = item.roleConfirm != null ? item.roleConfirm : item.roleInvite;
existing.set(item.accountId, { existing.set(item.accountId, {
accountId: item.accountId, accountId: item.accountId,
roleMonitor: Boolean(item.roleMonitor), roleMonitor: Boolean(item.roleMonitor),
roleInvite: Boolean(item.roleInvite) roleInvite: Boolean(item.roleInvite),
roleConfirm: Boolean(roleConfirm)
}); });
}); });
const merged = Array.from(existing.values()); const merged = Array.from(existing.values());
@ -557,7 +575,8 @@ ipcMain.handle("tasks:removeAccount", (_event, payload) => {
.map((row) => ({ .map((row) => ({
accountId: row.account_id, accountId: row.account_id,
roleMonitor: Boolean(row.role_monitor), 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); store.setTaskAccountRoles(payload.taskId, existing);
return { ok: true, accountIds: existing.map((item) => item.accountId) }; return { ok: true, accountIds: existing.map((item) => item.accountId) };
@ -765,7 +784,8 @@ ipcMain.handle("tasks:checkInviteAccess", async (_event, id) => {
.map((row) => ({ .map((row) => ({
accountId: row.account_id, accountId: row.account_id,
roleMonitor: Boolean(row.role_monitor), 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); store.setTaskAccountRoles(id, filtered);
} }
@ -831,11 +851,14 @@ const explainInviteError = (error) => {
if (error === "CHAT_ADMIN_REQUIRED") { if (error === "CHAT_ADMIN_REQUIRED") {
return "Для добавления участников нужны права администратора."; return "Для добавления участников нужны права администратора.";
} }
if (error === "CHANNEL_INVALID") {
return "Цель не распознана как группа/канал для этого аккаунта (ссылка недоступна или сущность устарела).";
}
if (error === "USER_ALREADY_PARTICIPANT") { if (error === "USER_ALREADY_PARTICIPANT") {
return "Пользователь уже состоит в целевой группе."; return "Пользователь уже состоит в целевой группе.";
} }
if (error === "CHAT_MEMBER_ADD_FAILED") { if (error === "CHAT_MEMBER_ADD_FAILED") {
return "Telegram отклонил добавление участника (ограничения приватности, антиспам или запрет инвайтов)."; return "Telegram отказал в добавлении пользователя. Возможные причины: пользователь недоступен для инвайта, неверные данные, ограничения чата или лимиты аккаунта.";
} }
if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") { if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") {
return "Инвайт-ссылка недействительна или истекла."; return "Инвайт-ссылка недействительна или истекла.";

View File

@ -160,6 +160,9 @@ function initStore(userDataPath) {
invite_via_admins INTEGER NOT NULL DEFAULT 0, invite_via_admins INTEGER NOT NULL DEFAULT 0,
invite_admin_master_id 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_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_enabled INTEGER NOT NULL DEFAULT 1,
warmup_start_limit INTEGER NOT NULL DEFAULT 3, warmup_start_limit INTEGER NOT NULL DEFAULT 3,
warmup_daily_increase INTEGER NOT NULL DEFAULT 2, warmup_daily_increase INTEGER NOT NULL DEFAULT 2,
@ -183,7 +186,8 @@ function initStore(userDataPath) {
task_id INTEGER NOT NULL, task_id INTEGER NOT NULL,
account_id INTEGER NOT NULL, account_id INTEGER NOT NULL,
role_monitor INTEGER NOT NULL DEFAULT 1, 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 ( 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_via_admins", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "invite_admin_master_id", "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_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_enabled", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "warmup_start_limit", "INTEGER NOT NULL DEFAULT 3"); ensureColumn("tasks", "warmup_start_limit", "INTEGER NOT NULL DEFAULT 3");
ensureColumn("tasks", "warmup_daily_increase", "INTEGER NOT NULL DEFAULT 2"); 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 = ?, 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 = ?, 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 = ?, 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 = ? cycle_competitors = ?, competitor_cursor = ?, invite_link_on_fail = ?, updated_at = ?
WHERE id = ? WHERE id = ?
`).run( `).run(
@ -611,6 +620,9 @@ function initStore(userDataPath) {
task.inviteViaAdmins ? 1 : 0, task.inviteViaAdmins ? 1 : 0,
task.inviteAdminMasterId || 0, task.inviteAdminMasterId || 0,
task.inviteAdminAllowFlood ? 1 : 0, task.inviteAdminAllowFlood ? 1 : 0,
task.inviteAdminAnonymous ? 1 : 0,
task.separateConfirmRoles ? 1 : 0,
task.maxConfirmBots || 1,
task.warmupEnabled ? 1 : 0, task.warmupEnabled ? 1 : 0,
task.warmupStartLimit || 3, task.warmupStartLimit || 3,
task.warmupDailyIncrease || 2, 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, 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, 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, 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) competitor_cursor, invite_link_on_fail, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run( `).run(
task.name, task.name,
task.ourGroup, task.ourGroup,
@ -657,6 +670,9 @@ function initStore(userDataPath) {
task.inviteViaAdmins ? 1 : 0, task.inviteViaAdmins ? 1 : 0,
task.inviteAdminMasterId || 0, task.inviteAdminMasterId || 0,
task.inviteAdminAllowFlood ? 1 : 0, task.inviteAdminAllowFlood ? 1 : 0,
task.inviteAdminAnonymous ? 1 : 0,
task.separateConfirmRoles ? 1 : 0,
task.maxConfirmBots || 1,
task.warmupEnabled ? 1 : 0, task.warmupEnabled ? 1 : 0,
task.warmupStartLimit || 3, task.warmupStartLimit || 3,
task.warmupDailyIncrease || 2, task.warmupDailyIncrease || 2,
@ -709,24 +725,26 @@ function initStore(userDataPath) {
function setTaskAccounts(taskId, accountIds) { function setTaskAccounts(taskId, accountIds) {
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId); db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId);
const stmt = db.prepare(` const stmt = db.prepare(`
INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite) INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite, role_confirm)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`); `);
(accountIds || []).forEach((accountId) => stmt.run(taskId, accountId, 1, 1)); (accountIds || []).forEach((accountId) => stmt.run(taskId, accountId, 1, 1, 1));
} }
function setTaskAccountRoles(taskId, roles) { function setTaskAccountRoles(taskId, roles) {
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId); db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId);
const stmt = db.prepare(` const stmt = db.prepare(`
INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite) INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite, role_confirm)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`); `);
(roles || []).forEach((item) => { (roles || []).forEach((item) => {
const roleConfirm = item.roleConfirm != null ? item.roleConfirm : item.roleInvite;
stmt.run( stmt.run(
taskId, taskId,
item.accountId, item.accountId,
item.roleMonitor ? 1 : 0, 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 accounts = accountRows.map((row) => row.account_id);
const monitorIds = accountRows.filter((row) => row.role_monitor).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 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, { await this.telegram.joinGroupsForTask(this.task, competitors, accounts, {
monitorIds, monitorIds,
inviteIds inviteIds,
confirmIds
}); });
await this.telegram.startTaskMonitor(this.task, competitors, accounts, monitorIds); await this.telegram.startTaskMonitor(this.task, competitors, accounts, monitorIds);
} }
@ -96,12 +98,16 @@ class TaskRunner {
if (ttlHours > 0) { if (ttlHours > 0) {
this.store.clearQueueOlderThan(this.task.id, ttlHours); 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; let inviteAccounts = accounts;
const roles = this.telegram.getTaskRoleAssignments(this.task.id); const roles = this.telegram.getTaskRoleAssignments(this.task.id);
const hasExplicitRoles = roles && ((roles.ourIds || []).length || (roles.competitorIds || []).length); const hasExplicitRoles = roles && ((roles.ourIds || []).length || (roles.competitorIds || []).length);
if (hasExplicitRoles) { if (explicitInviteIds.length) {
inviteAccounts = (roles.ourIds || []).length ? roles.ourIds : (roles.competitorIds || []); inviteAccounts = explicitInviteIds.slice();
} else if (hasExplicitRoles) {
inviteAccounts = (roles.ourIds || []).length ? roles.ourIds : [];
if (!inviteAccounts.length) { if (!inviteAccounts.length) {
errors.push("No invite accounts (role-based)"); errors.push("No invite accounts (role-based)");
} }
@ -193,10 +199,7 @@ class TaskRunner {
this.store.markInviteStatus(item.id, "failed"); this.store.markInviteStatus(item.id, "failed");
continue; continue;
} }
let accountsForInvite = inviteAccounts; const accountsForInvite = inviteAccounts;
if (item.watcher_account_id && !inviteAccounts.includes(item.watcher_account_id)) {
accountsForInvite = [item.watcher_account_id];
}
const watcherAccount = accountMap.get(item.watcher_account_id || 0); const watcherAccount = accountMap.get(item.watcher_account_id || 0);
const result = await this.telegram.inviteUserForTask(this.task, item.user_id, accountsForInvite, { const result = await this.telegram.inviteUserForTask(this.task, item.user_id, accountsForInvite, {
randomize: Boolean(this.task.random_accounts), randomize: Boolean(this.task.random_accounts),

View File

@ -23,7 +23,7 @@ class TelegramManager {
this.authKeyResetDone = false; this.authKeyResetDone = false;
} }
_buildInviteAdminRights() { _buildInviteAdminRights(allowAnonymous = false) {
return new Api.ChatAdminRights({ return new Api.ChatAdminRights({
inviteUsers: true, inviteUsers: true,
addUsers: true, addUsers: true,
@ -36,7 +36,7 @@ class TelegramManager {
manageTopics: false, manageTopics: false,
postMessages: false, postMessages: false,
editMessages: false, editMessages: false,
anonymous: false anonymous: Boolean(allowAnonymous)
}); });
} }
@ -86,8 +86,8 @@ class TelegramManager {
return lines.join("\n"); return lines.join("\n");
} }
async _grantTempInviteAdmin(masterClient, targetEntity, account) { async _grantTempInviteAdmin(masterClient, targetEntity, account, allowAnonymous = false) {
const rights = this._buildInviteAdminRights(); const rights = this._buildInviteAdminRights(allowAnonymous);
const identifier = account.user_id const identifier = account.user_id
? BigInt(account.user_id) ? BigInt(account.user_id)
: (account.username ? `@${account.username}` : ""); : (account.username ? `@${account.username}` : "");
@ -555,6 +555,16 @@ class TelegramManager {
const base = message ? `${code}: ${message}` : code; const base = message ? `${code}: ${message}` : code;
return sourceLabel ? `${base} (${sourceLabel})` : base; 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 = "") => { const confirmMembership = async (user, confirmClient = client, sourceLabel = "") => {
if (!targetEntity || targetEntity.className !== "Channel") { if (!targetEntity || targetEntity.className !== "Channel") {
return { confirmed: true, error: "", detail: "" }; return { confirmed: true, error: "", detail: "" };
@ -564,7 +574,7 @@ class TelegramManager {
channel: targetEntity, channel: targetEntity,
participant: user participant: user
})); }));
return { confirmed: true, error: "", detail: "" }; return { confirmed: true, error: "", detail: sourceLabel ? `OK (${sourceLabel})` : "OK" };
} catch (error) { } catch (error) {
const errorText = error.errorMessage || error.message || String(error); const errorText = error.errorMessage || error.message || String(error);
if (errorText.includes("USER_NOT_PARTICIPANT")) { 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 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) { if (direct.detail) {
attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail }); attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail });
} }
triedClients.add(client);
if (direct.confirmed !== null) { if (direct.confirmed !== null) {
return { ...direct, attempts }; return { ...direct, attempts };
} }
const masterId = Number(task.invite_admin_master_id || 0); let finalResult = direct;
const masterEntry = masterId ? this.clients.get(masterId) : null; const roleAssignments = this.taskRoleAssignments.get(task.id) || {};
if (masterEntry && masterEntry.client && masterEntry.client !== client) { const confirmIds = Array.isArray(roleAssignments.confirmIds) ? roleAssignments.confirmIds : [];
const adminConfirm = await confirmMembership(user, masterEntry.client, "проверка админом"); for (const confirmId of confirmIds) {
if (adminConfirm.detail) { const entry = this.clients.get(confirmId);
attempts.push({ strategy: "confirm_admin", ok: adminConfirm.confirmed === true, detail: adminConfirm.detail }); 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) => { const attemptInvite = async (user) => {
if (!targetEntity) { if (!targetEntity) {
@ -631,14 +673,50 @@ class TelegramManager {
throw new Error("Unsupported target chat type"); 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) { if (!targetEntity) {
throw new Error("Target group not resolved"); throw new Error("Target group not resolved");
} }
if (targetEntity.className !== "Channel") { if (targetEntity.className !== "Channel") {
throw new Error("ADMIN_INVITE_UNSUPPORTED_TARGET"); throw new Error("ADMIN_INVITE_UNSUPPORTED_TARGET");
} }
const rights = this._buildInviteAdminRights(); const rights = this._buildInviteAdminRights(allowAnonymous);
await adminClient.invoke(new Api.channels.EditAdmin({ await adminClient.invoke(new Api.channels.EditAdmin({
channel: targetEntity, channel: targetEntity,
userId: user, userId: user,
@ -737,10 +815,14 @@ class TelegramManager {
const masterEntry = masterId ? this.clients.get(masterId) : null; const masterEntry = masterId ? this.clients.get(masterId) : null;
if (masterEntry && masterId !== account.id) { if (masterEntry && masterId !== account.id) {
try { 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" }); lastAttempts.push({ strategy: "temp_admin", ok: true, detail: "granted" });
await attemptInvite(user); 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) { if (confirm.attempts && confirm.attempts.length) {
lastAttempts.push(...confirm.attempts); lastAttempts.push(...confirm.attempts);
} }
@ -775,8 +857,12 @@ class TelegramManager {
const masterId = Number(task.invite_admin_master_id || 0); const masterId = Number(task.invite_admin_master_id || 0);
const masterEntry = masterId ? this.clients.get(masterId) : null; const masterEntry = masterId ? this.clients.get(masterId) : null;
const adminClient = masterEntry ? masterEntry.client : client; const adminClient = masterEntry ? masterEntry.client : client;
await attemptAdminInvite(user, adminClient); await attemptAdminInvite(user, adminClient, Boolean(task.invite_admin_anonymous));
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) { if (confirm.attempts && confirm.attempts.length) {
lastAttempts.push(...confirm.attempts); lastAttempts.push(...confirm.attempts);
} }
@ -795,6 +881,13 @@ class TelegramManager {
} catch (adminError) { } catch (adminError) {
const adminText = adminError.errorMessage || adminError.message || String(adminError); const adminText = adminError.errorMessage || adminError.message || String(adminError);
if (adminText.includes("CHANNEL_INVALID")) { 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 { try {
const retryResolved = await this._resolveGroupEntity(client, task.our_group, Boolean(task.auto_join_our_group), account); const retryResolved = await this._resolveGroupEntity(client, task.our_group, Boolean(task.auto_join_our_group), account);
if (retryResolved.ok) { if (retryResolved.ok) {
@ -813,7 +906,11 @@ class TelegramManager {
} }
} }
await attemptInvite(user); 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) { if (confirm.attempts && confirm.attempts.length) {
lastAttempts.push(...confirm.attempts); lastAttempts.push(...confirm.attempts);
} }
@ -832,6 +929,14 @@ class TelegramManager {
}; };
} catch (error) { } catch (error) {
const errorText = error.errorMessage || error.message || String(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); this._handleAuthKeyDuplicated(errorText);
let fallbackMeta = lastAttempts.length ? JSON.stringify(lastAttempts) : ""; let fallbackMeta = lastAttempts.length ? JSON.stringify(lastAttempts) : "";
if (errorText === "USER_NOT_MUTUAL_CONTACT") { if (errorText === "USER_NOT_MUTUAL_CONTACT") {
@ -1453,7 +1558,7 @@ class TelegramManager {
return { ok: false, error: "Admin invite поддерживается только для супергрупп" }; 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 accounts = this.store.listAccounts();
const accountMap = new Map(accounts.map((acc) => [acc.id, acc])); const accountMap = new Map(accounts.map((acc) => [acc.id, acc]));
const results = []; const results = [];
@ -1683,7 +1788,8 @@ class TelegramManager {
const explicitMonitorIds = Array.isArray(roleIds.monitorIds) ? roleIds.monitorIds : []; const explicitMonitorIds = Array.isArray(roleIds.monitorIds) ? roleIds.monitorIds : [];
const explicitInviteIds = Array.isArray(roleIds.inviteIds) ? roleIds.inviteIds : []; 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 || []; const competitors = competitorGroups || [];
let cursor = 0; let cursor = 0;
@ -1715,7 +1821,9 @@ class TelegramManager {
const usedForOur = new Set(); const usedForOur = new Set();
if (task.our_group) { if (task.our_group) {
if (hasExplicitRoles) { 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) { for (const entry of pool) {
usedForOur.add(entry.account.id); usedForOur.add(entry.account.id);
if (task.auto_join_our_group) { if (task.auto_join_our_group) {
@ -1747,7 +1855,8 @@ class TelegramManager {
} }
this.taskRoleAssignments.set(task.id, { this.taskRoleAssignments.set(task.id, {
competitorIds: hasExplicitRoles ? explicitMonitorIds : Array.from(usedForCompetitors), 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, accessHash,
monitorAccount.account.id 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) { if (enqueued) {
const now = Date.now(); const now = Date.now();
const lastEvent = monitorEntry.lastMessageEventAt.get(chatId) || 0; const lastEvent = monitorEntry.lastMessageEventAt.get(chatId) || 0;
@ -1954,7 +2069,7 @@ class TelegramManager {
monitorAccount.account.id, monitorAccount.account.id,
monitorAccount.account.phone, monitorAccount.account.phone,
"new_message", "new_message",
`${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}` `${formatGroupLabel(st)}: ${senderLabel}${username ? `@${username}` : senderId}${messageSuffix}`
); );
} }
} else if (shouldLogEvent(`${chatId}:dup`, 30000)) { } else if (shouldLogEvent(`${chatId}:dup`, 30000)) {
@ -1964,7 +2079,7 @@ class TelegramManager {
monitorAccount.account.id, monitorAccount.account.id,
monitorAccount.account.phone, monitorAccount.account.phone,
"new_message_duplicate", "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; monitorEntry.lastSource = st.source;
if (this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id)) { if (this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id)) {
enqueued += 1; 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 now = Date.now();
const lastEvent = monitorEntry.lastMessageEventAt.get(key) || 0; const lastEvent = monitorEntry.lastMessageEventAt.get(key) || 0;
if (now - lastEvent > 15000) { if (now - lastEvent > 15000) {
@ -2107,17 +2228,23 @@ class TelegramManager {
monitorAccount.account.id, monitorAccount.account.id,
monitorAccount.account.phone, monitorAccount.account.phone,
"new_message", "new_message",
`${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}` `${formatGroupLabel(st)}: ${senderLabel}${username ? `@${username}` : senderId}${messageSuffix}`
); );
} }
} else if (shouldLogEvent(`${key}:dup`, 30000)) { } else if (shouldLogEvent(`${key}:dup`, 30000)) {
const status = this.store.getInviteStatus(task.id, senderId, st.source); const status = this.store.getInviteStatus(task.id, senderId, st.source);
const suffix = status && status !== "pending" ? ` (уже обработан: ${status})` : " (уже в очереди)"; 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( this.store.addAccountEvent(
monitorAccount.account.id, monitorAccount.account.id,
monitorAccount.account.phone, monitorAccount.account.phone,
"new_message_duplicate", "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) { getTaskRoleAssignments(taskId) {
return this.taskRoleAssignments.get(taskId) || { competitorIds: [], ourIds: [] }; return this.taskRoleAssignments.get(taskId) || { competitorIds: [], ourIds: [], confirmIds: [] };
} }
getTaskMonitorInfo(taskId) { getTaskMonitorInfo(taskId) {

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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