From 7ed57048daa0ff7ee31cffa0543adb54da3975cb Mon Sep 17 00:00:00 2001 From: Ivan Neplokhov Date: Sat, 14 Feb 2026 18:12:17 +0400 Subject: [PATCH] some --- package.json | 2 +- src/main/index.js | 1 + src/main/store.js | 79 +++++++++++++++++-- src/main/taskRunner.js | 1 + src/main/telegram.js | 86 ++++++++++++++++++--- src/renderer/components/InfoModal.jsx | 2 +- src/renderer/components/QuickActionsBar.jsx | 2 +- src/renderer/hooks/useLogsView.js | 1 + src/renderer/tabs/LogsTab.jsx | 18 ++++- src/renderer/tabs/QueueTab.jsx | 2 + 10 files changed, 173 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 93d3606..11835aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "telegram-invite-automation", - "version": "1.9.4", + "version": "1.9.5", "private": true, "description": "Automated user parsing and invites for Telegram groups", "main": "src/main/index.js", diff --git a/src/main/index.js b/src/main/index.js index e94cdb0..17f5e32 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -715,6 +715,7 @@ ipcMain.handle("test:inviteOnce", async (_event, payload) => { userAccessHash: item.user_access_hash, username: item.username, sourceChat: item.source_chat, + sourceMessageId: Number(item.source_message_id || 0), watcherAccountId: watcherAccount ? watcherAccount.id : 0, watcherPhone: watcherAccount ? watcherAccount.phone : "" }); diff --git a/src/main/store.js b/src/main/store.js index 2818cae..808eca7 100644 --- a/src/main/store.js +++ b/src/main/store.js @@ -60,11 +60,12 @@ function initStore(userDataPath) { 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(user_id, source_chat) + UNIQUE(task_id, user_id, source_chat) ); CREATE TABLE IF NOT EXISTS logs ( @@ -255,6 +256,7 @@ function initStore(userDataPath) { ensureColumn("invite_queue", "username", "TEXT DEFAULT ''"); ensureColumn("invite_queue", "user_access_hash", "TEXT DEFAULT ''"); 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", "user_access_hash", "TEXT DEFAULT ''"); 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", "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"); if (!settingsRow) { 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); } - function enqueueInvite(taskId, userId, username, sourceChat, accessHash, watcherAccountId) { + function enqueueInvite(taskId, userId, username, sourceChat, accessHash, watcherAccountId, sourceMessageId = 0) { const now = dayjs().toISOString(); try { if (taskId) { @@ -658,9 +725,11 @@ function initStore(userDataPath) { } } 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) - VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?) - `).run(taskId || 0, userId, username || "", accessHash || "", watcherAccountId || 0, sourceChat, now, now); + INSERT OR IGNORE INTO invite_queue ( + task_id, user_id, username, user_access_hash, watcher_account_id, source_chat, source_message_id, status, created_at, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?) + `).run(taskId || 0, userId, username || "", accessHash || "", watcherAccountId || 0, sourceChat, Number(sourceMessageId || 0), now, now); return result.changes > 0; } catch (error) { return false; diff --git a/src/main/taskRunner.js b/src/main/taskRunner.js index 667aeee..2cccb71 100644 --- a/src/main/taskRunner.js +++ b/src/main/taskRunner.js @@ -353,6 +353,7 @@ class TaskRunner { userAccessHash: item.user_access_hash, username: item.username, sourceChat: item.source_chat, + sourceMessageId: Number(item.source_message_id || 0), watcherAccountId: watcherAccount ? watcherAccount.id : 0, watcherPhone: watcherAccount ? watcherAccount.phone : "" }); diff --git a/src/main/telegram.js b/src/main/telegram.js index 899a4ab..64e0c4d 100644 --- a/src/main/telegram.js +++ b/src/main/telegram.js @@ -870,7 +870,7 @@ class TelegramManager { const accessHash = senderEntity && senderEntity.accessHash ? senderEntity.accessHash.toString() : ""; const sourceChat = event.message.chatId ? event.message.chatId.toString() : "unknown"; 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.lastMonitorSource = sourceChat; }; @@ -1479,6 +1479,7 @@ class TelegramManager { const accessHash = options.userAccessHash || ""; const providedUsername = options.username || ""; const sourceChat = options.sourceChat || ""; + const sourceMessageId = Number(options.sourceMessageId || 0); const attempts = []; let user = null; const resolveEvents = []; @@ -1539,6 +1540,32 @@ class TelegramManager { attempts.push({ strategy: "participants", ok: false, detail: "source chat not provided" }); 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 if (!user) { const resolvedUser = await client.getEntity(userId); @@ -2190,7 +2217,7 @@ class TelegramManager { username = ""; 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) { 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(); if (cacheEntry && now - cacheEntry.at < 5 * 60 * 1000) { const cached = cacheEntry.map.get(userId.toString()); @@ -2440,7 +2469,7 @@ class TelegramManager { return { accessHash: "", detail: `resolve failed (${resolved ? resolved.error : "unknown"})` }; } 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) { return { accessHash: "", detail: "participants hidden" }; } @@ -3539,7 +3568,7 @@ class TelegramManager { username = ""; 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.lastMonitorSource = state.source; } @@ -3594,6 +3623,25 @@ class TelegramManager { : explicitConfirmIdsRaw; 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 || []; let cursor = 0; const usedForCompetitors = new Set(); @@ -3808,7 +3856,7 @@ class TelegramManager { } monitorEntry.lastMessageAt = new Date().toISOString(); 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; } const senderPayload = { ...senderInfo.info }; @@ -3866,7 +3914,8 @@ class TelegramManager { username, st.source, accessHash, - monitorAccount.account.id + monitorAccount.account.id, + Number(message && message.id ? message.id : 0) ); const sender = message && message.sender ? message.sender : null; const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : ""; @@ -3974,7 +4023,7 @@ class TelegramManager { } monitorEntry.lastMessageAt = new Date().toISOString(); 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; } continue; @@ -4031,7 +4080,7 @@ class TelegramManager { } monitorEntry.lastMessageAt = messageDate.toISOString(); 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; const sender = message && message.sender ? message.sender : null; const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : ""; @@ -4154,7 +4203,17 @@ class TelegramManager { const targetGroups = task.cycle_competitors ? [groups[Math.max(0, Number(task.competitor_cursor || 0)) % groups.length]] : 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) { const ids = Array.isArray(accountIds) ? accountIds.filter(Boolean) : []; const allAccounts = this.store.listAccounts(); @@ -4243,7 +4302,7 @@ class TelegramManager { } 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; totalEnqueued += 1; } @@ -4277,7 +4336,7 @@ class TelegramManager { } const { userId: senderId, username, accessHash } = senderPayload; 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; totalEnqueued += 1; } @@ -4437,6 +4496,8 @@ class TelegramManager { return "поиск по username"; case "entity": return "getEntity по userId"; + case "from_message": + return "InputUserFromMessage"; case "retry": return "повторная попытка"; default: @@ -4455,6 +4516,7 @@ class TelegramManager { if (normalized === "no result") return "нет результата"; if (normalized === "resolve failed") return "не удалось найти пользователя по username"; if (normalized === "getentity(userid)") return "получен через getEntity"; + if (normalized.startsWith("msgid=")) return `из сообщения (${raw})`; if (normalized === "no access_hash") return "нет access_hash"; if (normalized === "no sender_id") return "в сообщении нет sender_id"; if (normalized.includes("could not find the input entity")) { diff --git a/src/renderer/components/InfoModal.jsx b/src/renderer/components/InfoModal.jsx index 9826981..d0efd37 100644 --- a/src/renderer/components/InfoModal.jsx +++ b/src/renderer/components/InfoModal.jsx @@ -96,7 +96,7 @@ export default function InfoModal({ open, onClose, infoTab, setInfoTab }) {
  • Сохранить — сохраняет настройки задачи.
  • Экспорт логов — выгружает логи/очередь/ошибки по задаче.
  • Собрать историю — добавляет авторов последних сообщений конкурентов в очередь.
  • -
  • Добавить ботов в Telegram группы — вводит аккаунты в конкурентов/нашу группу.
  • +
  • Отправить заявки/вступить в группы — отправляет join (в приватных группах будет INVITE_REQUEST_SENT до ручного одобрения).
  • Проверить всё — проверяет доступы и права инвайта у аккаунтов.
  • Тестовый прогон — один реальный инвайт из очереди для проверки логики.
  • Проверить участие — обновляет статусы участия аккаунтов в группах.
  • diff --git a/src/renderer/components/QuickActionsBar.jsx b/src/renderer/components/QuickActionsBar.jsx index a86449f..aaa7a41 100644 --- a/src/renderer/components/QuickActionsBar.jsx +++ b/src/renderer/components/QuickActionsBar.jsx @@ -54,7 +54,7 @@ export default function QuickActionsBar({ {taskActionLoading ? "Собираем..." : "Собрать историю"}