This commit is contained in:
Ivan Neplokhov 2026-02-14 18:12:17 +04:00
parent f09f412fd9
commit 7ed57048da
10 changed files with 173 additions and 21 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "telegram-invite-automation", "name": "telegram-invite-automation",
"version": "1.9.4", "version": "1.9.5",
"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

@ -715,6 +715,7 @@ ipcMain.handle("test:inviteOnce", async (_event, payload) => {
userAccessHash: item.user_access_hash, userAccessHash: item.user_access_hash,
username: item.username, username: item.username,
sourceChat: item.source_chat, sourceChat: item.source_chat,
sourceMessageId: Number(item.source_message_id || 0),
watcherAccountId: watcherAccount ? watcherAccount.id : 0, watcherAccountId: watcherAccount ? watcherAccount.id : 0,
watcherPhone: watcherAccount ? watcherAccount.phone : "" watcherPhone: watcherAccount ? watcherAccount.phone : ""
}); });

View File

@ -60,11 +60,12 @@ function initStore(userDataPath) {
user_access_hash TEXT DEFAULT '', user_access_hash TEXT DEFAULT '',
watcher_account_id INTEGER DEFAULT 0, watcher_account_id INTEGER DEFAULT 0,
source_chat TEXT NOT NULL, source_chat TEXT NOT NULL,
source_message_id INTEGER NOT NULL DEFAULT 0,
attempts INTEGER NOT NULL DEFAULT 0, attempts INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending', status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
updated_at TEXT NOT NULL, updated_at TEXT NOT NULL,
UNIQUE(user_id, source_chat) UNIQUE(task_id, user_id, source_chat)
); );
CREATE TABLE IF NOT EXISTS logs ( CREATE TABLE IF NOT EXISTS logs (
@ -255,6 +256,7 @@ function initStore(userDataPath) {
ensureColumn("invite_queue", "username", "TEXT DEFAULT ''"); ensureColumn("invite_queue", "username", "TEXT DEFAULT ''");
ensureColumn("invite_queue", "user_access_hash", "TEXT DEFAULT ''"); ensureColumn("invite_queue", "user_access_hash", "TEXT DEFAULT ''");
ensureColumn("invite_queue", "watcher_account_id", "INTEGER DEFAULT 0"); ensureColumn("invite_queue", "watcher_account_id", "INTEGER DEFAULT 0");
ensureColumn("invite_queue", "source_message_id", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("invites", "username", "TEXT DEFAULT ''"); ensureColumn("invites", "username", "TEXT DEFAULT ''");
ensureColumn("invites", "user_access_hash", "TEXT DEFAULT ''"); ensureColumn("invites", "user_access_hash", "TEXT DEFAULT ''");
ensureColumn("invites", "confirmed", "INTEGER NOT NULL DEFAULT 1"); ensureColumn("invites", "confirmed", "INTEGER NOT NULL DEFAULT 1");
@ -342,6 +344,71 @@ function initStore(userDataPath) {
ensureColumn("task_accounts", "role_invite", "INTEGER NOT NULL DEFAULT 1"); ensureColumn("task_accounts", "role_invite", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("task_accounts", "invite_limit", "INTEGER NOT NULL DEFAULT 0"); ensureColumn("task_accounts", "invite_limit", "INTEGER NOT NULL DEFAULT 0");
const hasInviteQueueScopedUnique = () => {
const indexes = db.prepare("PRAGMA index_list(invite_queue)").all();
for (const idx of indexes) {
if (!idx || !idx.unique) continue;
const cols = db.prepare(`PRAGMA index_info(${idx.name})`).all().map((row) => row.name);
if (cols.length === 3 && cols[0] === "task_id" && cols[1] === "user_id" && cols[2] === "source_chat") {
return true;
}
}
return false;
};
const migrateInviteQueueUniqueness = () => {
if (hasInviteQueueScopedUnique()) return;
const migrate = db.transaction(() => {
db.exec(`
CREATE TABLE IF NOT EXISTS invite_queue_migrated (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER DEFAULT 0,
user_id TEXT NOT NULL,
username TEXT DEFAULT '',
user_access_hash TEXT DEFAULT '',
watcher_account_id INTEGER DEFAULT 0,
source_chat TEXT NOT NULL,
source_message_id INTEGER NOT NULL DEFAULT 0,
attempts INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(task_id, user_id, source_chat)
);
`);
db.exec(`
INSERT INTO invite_queue_migrated (
id, task_id, user_id, username, user_access_hash, watcher_account_id, source_chat,
source_message_id, attempts, status, created_at, updated_at
)
SELECT
iq.id,
COALESCE(iq.task_id, 0),
iq.user_id,
iq.username,
iq.user_access_hash,
COALESCE(iq.watcher_account_id, 0),
iq.source_chat,
COALESCE(iq.source_message_id, 0),
COALESCE(iq.attempts, 0),
iq.status,
iq.created_at,
iq.updated_at
FROM invite_queue iq
INNER JOIN (
SELECT MAX(id) AS id
FROM invite_queue
GROUP BY COALESCE(task_id, 0), user_id, source_chat
) latest ON latest.id = iq.id;
`);
db.exec("DROP TABLE invite_queue;");
db.exec("ALTER TABLE invite_queue_migrated RENAME TO invite_queue;");
});
migrate();
};
migrateInviteQueueUniqueness();
const settingsRow = db.prepare("SELECT value FROM settings WHERE key = ?").get("settings"); const settingsRow = db.prepare("SELECT value FROM settings WHERE key = ?").get("settings");
if (!settingsRow) { if (!settingsRow) {
db.prepare("INSERT INTO settings (key, value) VALUES (?, ?)") db.prepare("INSERT INTO settings (key, value) VALUES (?, ?)")
@ -646,7 +713,7 @@ function initStore(userDataPath) {
db.prepare("DELETE FROM task_audit WHERE task_id = ?").run(taskId || 0); db.prepare("DELETE FROM task_audit WHERE task_id = ?").run(taskId || 0);
} }
function enqueueInvite(taskId, userId, username, sourceChat, accessHash, watcherAccountId) { function enqueueInvite(taskId, userId, username, sourceChat, accessHash, watcherAccountId, sourceMessageId = 0) {
const now = dayjs().toISOString(); const now = dayjs().toISOString();
try { try {
if (taskId) { if (taskId) {
@ -658,9 +725,11 @@ function initStore(userDataPath) {
} }
} }
const result = db.prepare(` const result = db.prepare(`
INSERT OR IGNORE INTO invite_queue (task_id, user_id, username, user_access_hash, watcher_account_id, source_chat, status, created_at, updated_at) INSERT OR IGNORE INTO invite_queue (
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?) task_id, user_id, username, user_access_hash, watcher_account_id, source_chat, source_message_id, status, created_at, updated_at
`).run(taskId || 0, userId, username || "", accessHash || "", watcherAccountId || 0, sourceChat, now, now); )
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)
`).run(taskId || 0, userId, username || "", accessHash || "", watcherAccountId || 0, sourceChat, Number(sourceMessageId || 0), now, now);
return result.changes > 0; return result.changes > 0;
} catch (error) { } catch (error) {
return false; return false;

View File

@ -353,6 +353,7 @@ class TaskRunner {
userAccessHash: item.user_access_hash, userAccessHash: item.user_access_hash,
username: item.username, username: item.username,
sourceChat: item.source_chat, sourceChat: item.source_chat,
sourceMessageId: Number(item.source_message_id || 0),
watcherAccountId: watcherAccount ? watcherAccount.id : 0, watcherAccountId: watcherAccount ? watcherAccount.id : 0,
watcherPhone: watcherAccount ? watcherAccount.phone : "" watcherPhone: watcherAccount ? watcherAccount.phone : ""
}); });

View File

@ -870,7 +870,7 @@ class TelegramManager {
const accessHash = senderEntity && senderEntity.accessHash ? senderEntity.accessHash.toString() : ""; const accessHash = senderEntity && senderEntity.accessHash ? senderEntity.accessHash.toString() : "";
const sourceChat = event.message.chatId ? event.message.chatId.toString() : "unknown"; const sourceChat = event.message.chatId ? event.message.chatId.toString() : "unknown";
if (!this.monitorState.has(sourceChat)) return; if (!this.monitorState.has(sourceChat)) return;
this.store.enqueueInvite(0, userId, username, sourceChat, accessHash, this.monitorClientId || 0); this.store.enqueueInvite(0, userId, username, sourceChat, accessHash, this.monitorClientId || 0, 0);
this.lastMonitorMessageAt = new Date().toISOString(); this.lastMonitorMessageAt = new Date().toISOString();
this.lastMonitorSource = sourceChat; this.lastMonitorSource = sourceChat;
}; };
@ -1479,6 +1479,7 @@ class TelegramManager {
const accessHash = options.userAccessHash || ""; const accessHash = options.userAccessHash || "";
const providedUsername = options.username || ""; const providedUsername = options.username || "";
const sourceChat = options.sourceChat || ""; const sourceChat = options.sourceChat || "";
const sourceMessageId = Number(options.sourceMessageId || 0);
const attempts = []; const attempts = [];
let user = null; let user = null;
const resolveEvents = []; const resolveEvents = [];
@ -1539,6 +1540,32 @@ class TelegramManager {
attempts.push({ strategy: "participants", ok: false, detail: "source chat not provided" }); attempts.push({ strategy: "participants", ok: false, detail: "source chat not provided" });
resolveEvents.push("participants: skip (no source chat)"); resolveEvents.push("participants: skip (no source chat)");
} }
if (!user && sourceChat && sourceMessageId > 0) {
try {
const resolvedSource = await this._resolveGroupEntity(client, sourceChat, false, account);
if (resolvedSource && resolvedSource.ok && resolvedSource.entity) {
const peer = await client.getInputEntity(resolvedSource.entity);
user = new Api.InputUserFromMessage({
peer,
msgId: sourceMessageId,
userId: BigInt(userId)
});
attempts.push({ strategy: "from_message", ok: true, detail: `msgId=${sourceMessageId}` });
resolveEvents.push(`from_message: ok (msgId=${sourceMessageId})`);
} else {
const detail = resolvedSource ? resolvedSource.error : "resolve failed";
attempts.push({ strategy: "from_message", ok: false, detail });
resolveEvents.push(`from_message: fail (${detail})`);
}
} catch (error) {
const detail = error.errorMessage || error.message || String(error);
attempts.push({ strategy: "from_message", ok: false, detail });
resolveEvents.push(`from_message: fail (${detail})`);
}
} else if (!user && sourceChat && sourceMessageId <= 0) {
attempts.push({ strategy: "from_message", ok: false, detail: "source message not provided" });
resolveEvents.push("from_message: skip (no source message)");
}
// username already attempted above // username already attempted above
if (!user) { if (!user) {
const resolvedUser = await client.getEntity(userId); const resolvedUser = await client.getEntity(userId);
@ -2190,7 +2217,7 @@ class TelegramManager {
username = ""; username = "";
accessHash = ""; accessHash = "";
} }
this.store.enqueueInvite(0, senderId.toString(), username, group, accessHash, this.monitorClientId || 0); this.store.enqueueInvite(0, senderId.toString(), username, group, accessHash, this.monitorClientId || 0, Number(message && message.id ? message.id : 0));
} }
} }
@ -2423,7 +2450,9 @@ class TelegramManager {
if (!sourceChat) { if (!sourceChat) {
return { accessHash: "", detail: "no source chat" }; return { accessHash: "", detail: "no source chat" };
} }
const cacheEntry = this.participantCache.get(sourceChat); const accountId = Number(client && client.__traceContext && client.__traceContext.accountId ? client.__traceContext.accountId : 0);
const cacheKey = `${accountId}:${sourceChat}`;
const cacheEntry = this.participantCache.get(cacheKey);
const now = Date.now(); const now = Date.now();
if (cacheEntry && now - cacheEntry.at < 5 * 60 * 1000) { if (cacheEntry && now - cacheEntry.at < 5 * 60 * 1000) {
const cached = cacheEntry.map.get(userId.toString()); const cached = cacheEntry.map.get(userId.toString());
@ -2440,7 +2469,7 @@ class TelegramManager {
return { accessHash: "", detail: `resolve failed (${resolved ? resolved.error : "unknown"})` }; return { accessHash: "", detail: `resolve failed (${resolved ? resolved.error : "unknown"})` };
} }
const map = await this._loadParticipantCache(client, resolved.entity, 400); const map = await this._loadParticipantCache(client, resolved.entity, 400);
this.participantCache.set(sourceChat, { at: now, map }); this.participantCache.set(cacheKey, { at: now, map });
if (!map.size) { if (!map.size) {
return { accessHash: "", detail: "participants hidden" }; return { accessHash: "", detail: "participants hidden" };
} }
@ -3539,7 +3568,7 @@ class TelegramManager {
username = ""; username = "";
accessHash = ""; accessHash = "";
} }
this.store.enqueueInvite(0, senderId, username, state.source, accessHash, this.monitorClientId || 0); this.store.enqueueInvite(0, senderId, username, state.source, accessHash, this.monitorClientId || 0, Number(message && message.id ? message.id : 0));
this.lastMonitorMessageAt = new Date().toISOString(); this.lastMonitorMessageAt = new Date().toISOString();
this.lastMonitorSource = state.source; this.lastMonitorSource = state.source;
} }
@ -3594,6 +3623,25 @@ class TelegramManager {
: explicitConfirmIdsRaw; : explicitConfirmIdsRaw;
const hasExplicitRoles = explicitMonitorIds.length || explicitInviteIds.length || explicitConfirmIds.length; const hasExplicitRoles = explicitMonitorIds.length || explicitInviteIds.length || explicitConfirmIds.length;
if (forceJoin) {
const competitors = competitorGroups || [];
for (const entry of accounts) {
this._instrumentClientInvoke(entry.client, entry.account.id, entry.account.phone || "", Number(task && task.id ? task.id : 0));
if (competitors.length) {
await this._autoJoinGroups(entry.client, competitors, true, entry.account);
}
if (task.our_group) {
await this._autoJoinGroups(entry.client, [task.our_group], true, entry.account);
}
}
this.taskRoleAssignments.set(task.id, {
competitorIds: hasExplicitRoles ? explicitMonitorIds : accounts.map((entry) => entry.account.id),
ourIds: hasExplicitRoles ? explicitInviteIds : accounts.map((entry) => entry.account.id),
confirmIds: hasExplicitRoles ? explicitConfirmIds : accounts.map((entry) => entry.account.id)
});
return;
}
const competitors = competitorGroups || []; const competitors = competitorGroups || [];
let cursor = 0; let cursor = 0;
const usedForCompetitors = new Set(); const usedForCompetitors = new Set();
@ -3808,7 +3856,7 @@ class TelegramManager {
} }
monitorEntry.lastMessageAt = new Date().toISOString(); monitorEntry.lastMessageAt = new Date().toISOString();
monitorEntry.lastSource = st.source; monitorEntry.lastSource = st.source;
this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id); this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id, Number(message && message.id ? message.id : 0));
return; return;
} }
const senderPayload = { ...senderInfo.info }; const senderPayload = { ...senderInfo.info };
@ -3866,7 +3914,8 @@ class TelegramManager {
username, username,
st.source, st.source,
accessHash, accessHash,
monitorAccount.account.id monitorAccount.account.id,
Number(message && message.id ? message.id : 0)
); );
const sender = message && message.sender ? message.sender : null; const sender = message && message.sender ? message.sender : null;
const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : ""; const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : "";
@ -3974,7 +4023,7 @@ class TelegramManager {
} }
monitorEntry.lastMessageAt = new Date().toISOString(); monitorEntry.lastMessageAt = new Date().toISOString();
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, Number(message && message.id ? message.id : 0))) {
enqueued += 1; enqueued += 1;
} }
continue; continue;
@ -4031,7 +4080,7 @@ class TelegramManager {
} }
monitorEntry.lastMessageAt = messageDate.toISOString(); monitorEntry.lastMessageAt = messageDate.toISOString();
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, Number(message && message.id ? message.id : 0))) {
enqueued += 1; enqueued += 1;
const sender = message && message.sender ? message.sender : null; const sender = message && message.sender ? message.sender : null;
const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : ""; const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : "";
@ -4154,7 +4203,17 @@ class TelegramManager {
const targetGroups = task.cycle_competitors const targetGroups = task.cycle_competitors
? [groups[Math.max(0, Number(task.competitor_cursor || 0)) % groups.length]] ? [groups[Math.max(0, Number(task.competitor_cursor || 0)) % groups.length]]
: groups; : groups;
const entry = this._pickClientFromAllowed(accountIds); const monitorIds = this.store.listTaskAccounts(task.id)
.filter((row) => row.role_monitor)
.map((row) => Number(row.account_id))
.filter(Boolean);
const preferredMonitorIds = monitorIds.length
? monitorIds.filter((id) => (accountIds || []).includes(id))
: [];
let entry = this._pickClientFromAllowed(preferredMonitorIds);
if (!entry) {
entry = this._pickClientFromAllowed(accountIds);
}
if (!entry) { if (!entry) {
const ids = Array.isArray(accountIds) ? accountIds.filter(Boolean) : []; const ids = Array.isArray(accountIds) ? accountIds.filter(Boolean) : [];
const allAccounts = this.store.listAccounts(); const allAccounts = this.store.listAccounts();
@ -4243,7 +4302,7 @@ class TelegramManager {
} }
continue; continue;
} }
if (this.store.enqueueInvite(task.id, rawSenderId, "", group, resolved.accessHash, entry.account.id)) { if (this.store.enqueueInvite(task.id, rawSenderId, "", group, resolved.accessHash, entry.account.id, Number(message && message.id ? message.id : 0))) {
enqueued += 1; enqueued += 1;
totalEnqueued += 1; totalEnqueued += 1;
} }
@ -4277,7 +4336,7 @@ class TelegramManager {
} }
const { userId: senderId, username, accessHash } = senderPayload; const { userId: senderId, username, accessHash } = senderPayload;
if (this._isOwnAccount(senderId)) continue; if (this._isOwnAccount(senderId)) continue;
if (this.store.enqueueInvite(task.id, senderId, username, group, accessHash, entry.account.id)) { if (this.store.enqueueInvite(task.id, senderId, username, group, accessHash, entry.account.id, Number(message && message.id ? message.id : 0))) {
enqueued += 1; enqueued += 1;
totalEnqueued += 1; totalEnqueued += 1;
} }
@ -4437,6 +4496,8 @@ class TelegramManager {
return "поиск по username"; return "поиск по username";
case "entity": case "entity":
return "getEntity по userId"; return "getEntity по userId";
case "from_message":
return "InputUserFromMessage";
case "retry": case "retry":
return "повторная попытка"; return "повторная попытка";
default: default:
@ -4455,6 +4516,7 @@ class TelegramManager {
if (normalized === "no result") return "нет результата"; if (normalized === "no result") return "нет результата";
if (normalized === "resolve failed") return "не удалось найти пользователя по username"; if (normalized === "resolve failed") return "не удалось найти пользователя по username";
if (normalized === "getentity(userid)") return "получен через getEntity"; if (normalized === "getentity(userid)") return "получен через getEntity";
if (normalized.startsWith("msgid=")) return `из сообщения (${raw})`;
if (normalized === "no access_hash") return "нет access_hash"; if (normalized === "no access_hash") return "нет access_hash";
if (normalized === "no sender_id") return "в сообщении нет sender_id"; if (normalized === "no sender_id") return "в сообщении нет sender_id";
if (normalized.includes("could not find the input entity")) { if (normalized.includes("could not find the input entity")) {

View File

@ -96,7 +96,7 @@ export default function InfoModal({ open, onClose, infoTab, setInfoTab }) {
<li>Сохранить сохраняет настройки задачи.</li> <li>Сохранить сохраняет настройки задачи.</li>
<li>Экспорт логов выгружает логи/очередь/ошибки по задаче.</li> <li>Экспорт логов выгружает логи/очередь/ошибки по задаче.</li>
<li>Собрать историю добавляет авторов последних сообщений конкурентов в очередь.</li> <li>Собрать историю добавляет авторов последних сообщений конкурентов в очередь.</li>
<li>Добавить ботов в Telegram группы вводит аккаунты в конкурентов/нашу группу.</li> <li>Отправить заявки/вступить в группы отправляет join (в приватных группах будет INVITE_REQUEST_SENT до ручного одобрения).</li>
<li>Проверить всё проверяет доступы и права инвайта у аккаунтов.</li> <li>Проверить всё проверяет доступы и права инвайта у аккаунтов.</li>
<li>Тестовый прогон один реальный инвайт из очереди для проверки логики.</li> <li>Тестовый прогон один реальный инвайт из очереди для проверки логики.</li>
<li>Проверить участие обновляет статусы участия аккаунтов в группах.</li> <li>Проверить участие обновляет статусы участия аккаунтов в группах.</li>

View File

@ -54,7 +54,7 @@ export default function QuickActionsBar({
{taskActionLoading ? "Собираем..." : "Собрать историю"} {taskActionLoading ? "Собираем..." : "Собрать историю"}
</button> </button>
<button className="secondary" onClick={() => joinGroupsForTask("bar")} disabled={!hasSelectedTask}> <button className="secondary" onClick={() => joinGroupsForTask("bar")} disabled={!hasSelectedTask}>
Добавить ботов в Telegram группы Отправить заявки/вступить в группы
</button> </button>
<button className="secondary" onClick={() => refreshMembership("bar")} disabled={!hasSelectedTask}> <button className="secondary" onClick={() => refreshMembership("bar")} disabled={!hasSelectedTask}>
Проверить участие Проверить участие

View File

@ -128,6 +128,7 @@ export default function useLogsView({
item.user_id, item.user_id,
item.username, item.username,
item.source_chat, item.source_chat,
item.source_message_id,
item.watcher_account_id, item.watcher_account_id,
item.attempts, item.attempts,
item.created_at item.created_at

View File

@ -112,12 +112,25 @@ function LogsTab({
return "username (никнейм)"; return "username (никнейм)";
case "entity": case "entity":
return "entity по ID"; return "entity по ID";
case "from_message":
return "InputUserFromMessage";
case "retry": case "retry":
return "повторная попытка"; return "повторная попытка";
default: default:
return strategy || "—"; return strategy || "—";
} }
}; };
const usedInputUserFromMessage = (invite) => {
if (!invite) return false;
if (invite.strategy === "from_message") return true;
if (!invite.strategyMeta) return false;
try {
const parsed = JSON.parse(invite.strategyMeta);
return Array.isArray(parsed) && parsed.some((item) => item && item.strategy === "from_message" && item.ok);
} catch (error) {
return false;
}
};
const formatStrategies = (meta) => { const formatStrategies = (meta) => {
if (!meta) return ""; if (!meta) return "";
@ -618,6 +631,9 @@ function LogsTab({
{isMissingInvitee(invite) && ( {isMissingInvitee(invite) && (
<span className="inline-flag warn">Не доставлен Telegram (INVITE_MISSING_INVITEE)</span> <span className="inline-flag warn">Не доставлен Telegram (INVITE_MISSING_INVITEE)</span>
)} )}
{usedInputUserFromMessage(invite) && (
<span className="inline-flag">Через InputUserFromMessage</span>
)}
</div> </div>
</div> </div>
<div className="log-details"> <div className="log-details">
@ -708,7 +724,7 @@ function LogsTab({
<div>Примечание: у наблюдателя стоят обе роли, но включен случайный выбор инвайт выполнен другим аккаунтом.</div> <div>Примечание: у наблюдателя стоят обе роли, но включен случайный выбор инвайт выполнен другим аккаунтом.</div>
)} )}
<div>Пояснение: {buildDetailedExplanation(invite)}</div> <div>Пояснение: {buildDetailedExplanation(invite)}</div>
<div>Стратегия: {invite.strategy || "—"}</div> <div>Стратегия: {strategyLabel(invite.strategy)}</div>
{invite.strategyMeta && !hasStrategySuccess(invite.strategyMeta) && ( {invite.strategyMeta && !hasStrategySuccess(invite.strategyMeta) && (
<div>Результат: все стратегии не сработали</div> <div>Результат: все стратегии не сработали</div>
)} )}

View File

@ -167,6 +167,7 @@ export default function QueueTab({
<div className="log-head"> <div className="log-head">
<span>Пользователь</span> <span>Пользователь</span>
<span>Источник</span> <span>Источник</span>
<span>msg_id</span>
<span>Наблюдатель</span> <span>Наблюдатель</span>
<span>Попытки</span> <span>Попытки</span>
<span>Добавлен</span> <span>Добавлен</span>
@ -182,6 +183,7 @@ export default function QueueTab({
<div className={`log-row ${issue ? "queue-row-warn" : ""}`} key={item.id}> <div className={`log-row ${issue ? "queue-row-warn" : ""}`} key={item.id}>
<div>{formatUserWithUsername(item)}</div> <div>{formatUserWithUsername(item)}</div>
<div>{item.source_chat || "—"}</div> <div>{item.source_chat || "—"}</div>
<div>{item.source_message_id ? String(item.source_message_id) : "—"}</div>
<div>{watcherLabel}</div> <div>{watcherLabel}</div>
<div>{item.attempts ?? 0}</div> <div>{item.attempts ?? 0}</div>
<div> <div>