diff --git a/package.json b/package.json index dba4b25..0b1e035 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "telegram-invite-automation", - "version": "0.2.0", + "version": "0.3.0", "private": true, "description": "Automated user parsing and invites for Telegram groups", "main": "src/main/index.js", @@ -35,6 +35,7 @@ "build": { "appId": "com.profi.telegram-invite-automation", "productName": "Telegram Invite Automation", + "icon": "resources/icon.png", "directories": { "output": "dist/release" }, diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000..bfe1b8f Binary files /dev/null and b/resources/icon.png differ diff --git a/src/main/index.js b/src/main/index.js index 2add08f..daa786c 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -14,9 +14,11 @@ let scheduler; const taskRunners = new Map(); function createWindow() { + const iconPath = path.join(__dirname, "..", "..", "resources", "icon.png"); mainWindow = new BrowserWindow({ width: 1200, height: 800, + icon: iconPath, webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, @@ -67,6 +69,18 @@ ipcMain.handle("accounts:delete", async (_event, accountId) => { store.addAccountEvent(accountId, "", "delete", "Account deleted by user"); return { ok: true }; }); +ipcMain.handle("db:clear", async () => { + for (const runner of taskRunners.values()) { + runner.stop(); + } + taskRunners.clear(); + const accounts = store.listAccounts(); + for (const account of accounts) { + await telegram.removeAccount(account.id); + } + store.clearAllData(); + return { ok: true }; +}); ipcMain.handle("accounts:startLogin", async (_event, payload) => { const result = await telegram.startLogin(payload); return result; diff --git a/src/main/preload.js b/src/main/preload.js index 5d736bc..116bf40 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -11,6 +11,7 @@ contextBridge.exposeInMainWorld("api", { startLogin: (payload) => ipcRenderer.invoke("accounts:startLogin", payload), completeLogin: (payload) => ipcRenderer.invoke("accounts:completeLogin", payload), importTdata: (payload) => ipcRenderer.invoke("accounts:importTdata", payload), + clearDatabase: () => ipcRenderer.invoke("db:clear"), listLogs: (payload) => ipcRenderer.invoke("logs:list", payload), listInvites: (payload) => ipcRenderer.invoke("invites:list", payload), clearLogs: (taskId) => ipcRenderer.invoke("logs:clear", taskId), diff --git a/src/main/store.js b/src/main/store.js index 8bd6ed4..84cb9e1 100644 --- a/src/main/store.js +++ b/src/main/store.js @@ -203,6 +203,20 @@ function initStore(userDataPath) { return db.prepare("SELECT * FROM accounts ORDER BY id DESC").all(); } + function clearAllData() { + db.prepare("DELETE FROM task_accounts").run(); + db.prepare("DELETE FROM task_competitors").run(); + db.prepare("DELETE FROM tasks").run(); + db.prepare("DELETE FROM invite_queue").run(); + db.prepare("DELETE FROM invites").run(); + db.prepare("DELETE FROM logs").run(); + db.prepare("DELETE FROM account_events").run(); + db.prepare("DELETE FROM accounts").run(); + db.prepare("DELETE FROM settings").run(); + db.prepare("INSERT INTO settings (key, value) VALUES (?, ?)") + .run("settings", JSON.stringify(DEFAULT_SETTINGS)); + } + function findAccountByIdentity({ userId, phone, session }) { return db.prepare(` SELECT * FROM accounts @@ -576,6 +590,7 @@ function initStore(userDataPath) { saveSettings, listAccounts, findAccountByIdentity, + clearAllData, listTasks, getTask, saveTask, diff --git a/src/main/telegram.js b/src/main/telegram.js index 0610500..b96e2c1 100644 --- a/src/main/telegram.js +++ b/src/main/telegram.js @@ -873,29 +873,33 @@ class TelegramManager { groups, lastMessageAt: "", lastSource: "", - lastErrorAt: new Map() + lastErrorAt: new Map(), + lastSkipAt: new Map() }; + this.store.addAccountEvent( + monitorAccount.account.id, + monitorAccount.account.phone, + "monitor_started", + `Групп: ${resolved.length}` + ); const timer = setInterval(async () => { for (const [key, st] of state.entries()) { try { const messages = await monitorAccount.client.getMessages(st.entity, { limit: 10 }); + let totalMessages = 0; + let enqueued = 0; + let skipped = 0; for (const message of messages.reverse()) { + totalMessages += 1; if (st.lastId && message.id <= st.lastId) continue; st.lastId = Math.max(st.lastId || 0, message.id || 0); - if (!message.senderId) continue; - const senderId = message.senderId.toString(); - if (this._isOwnAccount(senderId)) continue; - let username = ""; - let accessHash = ""; - try { - const sender = await message.getSender(); - if (sender && sender.bot) continue; - username = sender && sender.username ? sender.username : ""; - accessHash = sender && sender.accessHash ? sender.accessHash.toString() : ""; - } catch (error) { - username = ""; - accessHash = ""; + const senderInfo = await this._getUserInfoFromMessage(message); + if (!senderInfo) { + skipped += 1; + continue; } + const { userId: senderId, username, accessHash } = senderInfo; + if (this._isOwnAccount(senderId)) continue; let messageDate = new Date(); if (message.date instanceof Date) { messageDate = message.date; @@ -904,7 +908,22 @@ class TelegramManager { } monitorEntry.lastMessageAt = messageDate.toISOString(); monitorEntry.lastSource = st.source; - this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash); + if (this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash)) { + enqueued += 1; + } + } + if (totalMessages > 0 && enqueued === 0 && skipped > 0) { + const now = Date.now(); + const lastSkip = monitorEntry.lastSkipAt.get(key) || 0; + if (now - lastSkip > 60000) { + monitorEntry.lastSkipAt.set(key, now); + this.store.addAccountEvent( + monitorAccount.account.id, + monitorAccount.account.phone, + "monitor_skip", + `${st.source}: сообщения есть, но пользователей нет (пропущено: ${skipped})` + ); + } } } catch (error) { const now = Date.now(); @@ -937,6 +956,8 @@ class TelegramManager { if (task.auto_join_competitors) { await this._autoJoinGroups(entry.client, groups, true, entry.account); } + const summaryLines = []; + let totalEnqueued = 0; const errors = []; for (const group of groups) { const resolved = await this._resolveGroupEntity(entry.client, group, Boolean(task.auto_join_competitors), entry.account); @@ -945,28 +966,59 @@ class TelegramManager { continue; } const messages = await entry.client.getMessages(resolved.entity, { limit: perGroupLimit }); + let total = 0; + let enqueued = 0; + let skipped = 0; for (const message of messages) { - const senderId = message.senderId; - if (!senderId) continue; - const senderStr = senderId.toString(); - if (this._isOwnAccount(senderStr)) continue; - let username = ""; - let accessHash = ""; - try { - const sender = await message.getSender(); - if (sender && sender.bot) continue; - username = sender && sender.username ? sender.username : ""; - accessHash = sender && sender.accessHash ? sender.accessHash.toString() : ""; - } catch (error) { - username = ""; - accessHash = ""; + total += 1; + const senderInfo = await this._getUserInfoFromMessage(message); + if (!senderInfo) { + skipped += 1; + continue; + } + const { userId: senderId, username, accessHash } = senderInfo; + if (this._isOwnAccount(senderId)) continue; + if (this.store.enqueueInvite(task.id, senderId, username, group, accessHash)) { + enqueued += 1; + totalEnqueued += 1; } - this.store.enqueueInvite(task.id, senderStr, username, group, accessHash); } + summaryLines.push(`${group}: сообщений ${total}, добавлено ${enqueued}, пропущено ${skipped}`); + } + if (summaryLines.length) { + this.store.addAccountEvent( + entry.account.id, + entry.account.phone, + "history_summary", + summaryLines.join(" | ") + ); + } + if (totalEnqueued === 0 && errors.length === 0) { + this.store.addAccountEvent( + entry.account.id, + entry.account.phone, + "history_empty", + "История собрана, но пользователей для очереди нет" + ); } return { ok: true, errors }; } + async _getUserInfoFromMessage(message) { + try { + const sender = await message.getSender(); + if (!sender || sender.className !== "User") return null; + if (sender.bot) return null; + const userId = sender.id != null ? sender.id.toString() : ""; + if (!userId) return null; + const username = sender.username ? sender.username : ""; + const accessHash = sender.accessHash ? sender.accessHash.toString() : ""; + return { userId, username, accessHash }; + } catch (error) { + return null; + } + } + stopTaskMonitor(taskId) { const entry = this.taskMonitors.get(taskId); if (!entry) return; diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx index 9500424..722204a 100644 --- a/src/renderer/App.jsx +++ b/src/renderer/App.jsx @@ -794,6 +794,37 @@ export default function App() { } }; + const clearDatabase = async () => { + if (!window.api) { + showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); + return; + } + if (!window.confirm("Удалить все данные из базы? Это действие нельзя отменить.")) { + return; + } + try { + await window.api.clearDatabase(); + showNotification("База очищена.", "info"); + setSelectedTaskId(null); + setTaskForm(emptyTaskForm); + setCompetitorText(""); + setSelectedAccountIds([]); + setLogs([]); + setInvites([]); + setTaskStatus({ + running: false, + queueCount: 0, + dailyRemaining: 0, + dailyUsed: 0, + dailyLimit: 0, + monitorInfo: { monitoring: false, groups: [], lastMessageAt: "", lastSource: "" } + }); + await loadBase(); + } catch (error) { + showNotification(error.message || String(error), "error"); + } + }; + const toggleAccountSelection = (accountId) => { setSelectedAccountIds((prev) => { if (prev.includes(accountId)) { @@ -998,6 +1029,9 @@ export default function App() { Остановить все +
+

Аккаунты задачи

- {manualLoginOpen && ( -
- {!hasSelectedTask && ( -
Выберите задачу, чтобы добавить аккаунт.
- )} -
+
+
+

Добавить аккаунт по коду

+ +
+ {manualLoginOpen && ( +
+ {!hasSelectedTask && ( +
Выберите задачу, чтобы добавить аккаунт.
+ )} +
+ + +
- +
+ + +
+
+ + +
+ {loginStatus &&
{loginStatus}
}
+ )} +
+ +
+

Импорт из tdata

+
+ Можно выбрать сразу несколько папок. Значения по умолчанию — API Telegram Desktop. +
+
+ -
- - -
-
- - -
- {loginStatus &&
{loginStatus}
}
- )} -
+ + {tdataLoading &&
Идет импорт, это может занять несколько секунд.
} + {tdataNotice && ( +
{tdataNotice.text}
+ )} + {tdataResult && ( +
+
Импортировано: {(tdataResult.imported || []).length}
+
Пропущено: {(tdataResult.skipped || []).length}
+
Ошибок: {(tdataResult.failed || []).length}
+ {(tdataResult.failed || []).length > 0 && ( +
+ {tdataResult.failed.map((item, index) => ( +
+
{item.path}
+
{item.error}
+
+ ))} +
+ )} +
+ )} +
+ -
-

Импорт из tdata

-
- Можно выбрать сразу несколько папок. Значения по умолчанию — API Telegram Desktop. - {hasSelectedTask ? ` Импорт в задачу: ${selectedTaskName}.` : " Выберите задачу для импорта."} +
+
+

Общий обзор

+
Все задачи
-
- - -
- - {tdataLoading &&
Идет импорт, это может занять несколько секунд.
} - {tdataNotice && ( -
{tdataNotice.text}
- )} - {tdataResult && ( -
-
Импортировано: {(tdataResult.imported || []).length}
-
Пропущено: {(tdataResult.skipped || []).length}
-
Ошибок: {(tdataResult.failed || []).length}
- {(tdataResult.failed || []).length > 0 && ( -
- {tdataResult.failed.map((item, index) => ( -
-
{item.path}
-
{item.error}
-
- ))} -
- )} +
+
+
Всего задач
+
{taskSummary.total}
+
+
+
Запущено
+
{taskSummary.running}
+
+
+
Очередь
+
{taskSummary.queue}
+
+
+
Лимит в день
+
{taskSummary.dailyUsed}/{taskSummary.dailyLimit}
- )} -
-
- -
-
-

Общий обзор

-
Все задачи
-
-
-
-
Всего задач
-
{taskSummary.total}
-
-
Запущено
-
{taskSummary.running}
-
-
-
Очередь
-
{taskSummary.queue}
-
-
-
Лимит в день
-
{taskSummary.dailyUsed}/{taskSummary.dailyLimit}
-
-
-
+ +
{notification && (
diff --git a/src/renderer/styles/app.css b/src/renderer/styles/app.css index a3bb368..5e0825a 100644 --- a/src/renderer/styles/app.css +++ b/src/renderer/styles/app.css @@ -368,6 +368,13 @@ body { align-items: start; } +.top-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 20px; + align-items: start; +} + .main { display: flex; flex-direction: column; @@ -898,6 +905,10 @@ button:disabled { grid-template-columns: 1fr; } + .top-row { + grid-template-columns: 1fr; + } + .sticky { position: static; height: auto;