This commit is contained in:
Ivan Neplokhov 2026-01-15 12:40:53 +04:00
parent 59d46f4e00
commit 712d11d8d8
8 changed files with 286 additions and 157 deletions

View File

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

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

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

View File

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

View File

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

View File

@ -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;
}
this.store.enqueueInvite(task.id, senderStr, username, group, accessHash);
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;
}
}
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;

View File

@ -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() {
Остановить все
</button>
</div>
<button type="button" className="danger" onClick={clearDatabase}>
Очистить БД
</button>
<div className="notification-bell" ref={bellRef}>
<button
type="button"
@ -1079,6 +1113,7 @@ export default function App() {
</div>
)}
<div className="top-row">
<section className="card task-accounts">
<div className="row-header">
<h2>Аккаунты задачи</h2>
@ -1152,7 +1187,6 @@ export default function App() {
<h3>Импорт из tdata</h3>
<div className="hint">
Можно выбрать сразу несколько папок. Значения по умолчанию API Telegram Desktop.
{hasSelectedTask ? ` Импорт в задачу: ${selectedTaskName}.` : " Выберите задачу для импорта."}
</div>
<div className="row">
<label>
@ -1223,6 +1257,7 @@ export default function App() {
</div>
</div>
</section>
</div>
{notification && (
<div className={`notice ${notification.tone}`}>

View File

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