some
This commit is contained in:
parent
59d46f4e00
commit
712d11d8d8
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "telegram-invite-automation",
|
"name": "telegram-invite-automation",
|
||||||
"version": "0.2.0",
|
"version": "0.3.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",
|
||||||
@ -35,6 +35,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"appId": "com.profi.telegram-invite-automation",
|
"appId": "com.profi.telegram-invite-automation",
|
||||||
"productName": "Telegram Invite Automation",
|
"productName": "Telegram Invite Automation",
|
||||||
|
"icon": "resources/icon.png",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist/release"
|
"output": "dist/release"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
resources/icon.png
Normal file
BIN
resources/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@ -14,9 +14,11 @@ let scheduler;
|
|||||||
const taskRunners = new Map();
|
const taskRunners = new Map();
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
|
const iconPath = path.join(__dirname, "..", "..", "resources", "icon.png");
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
|
icon: iconPath,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "preload.js"),
|
preload: path.join(__dirname, "preload.js"),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
@ -67,6 +69,18 @@ ipcMain.handle("accounts:delete", async (_event, accountId) => {
|
|||||||
store.addAccountEvent(accountId, "", "delete", "Account deleted by user");
|
store.addAccountEvent(accountId, "", "delete", "Account deleted by user");
|
||||||
return { ok: true };
|
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) => {
|
ipcMain.handle("accounts:startLogin", async (_event, payload) => {
|
||||||
const result = await telegram.startLogin(payload);
|
const result = await telegram.startLogin(payload);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ contextBridge.exposeInMainWorld("api", {
|
|||||||
startLogin: (payload) => ipcRenderer.invoke("accounts:startLogin", payload),
|
startLogin: (payload) => ipcRenderer.invoke("accounts:startLogin", payload),
|
||||||
completeLogin: (payload) => ipcRenderer.invoke("accounts:completeLogin", payload),
|
completeLogin: (payload) => ipcRenderer.invoke("accounts:completeLogin", payload),
|
||||||
importTdata: (payload) => ipcRenderer.invoke("accounts:importTdata", payload),
|
importTdata: (payload) => ipcRenderer.invoke("accounts:importTdata", payload),
|
||||||
|
clearDatabase: () => ipcRenderer.invoke("db:clear"),
|
||||||
listLogs: (payload) => ipcRenderer.invoke("logs:list", payload),
|
listLogs: (payload) => ipcRenderer.invoke("logs:list", payload),
|
||||||
listInvites: (payload) => ipcRenderer.invoke("invites:list", payload),
|
listInvites: (payload) => ipcRenderer.invoke("invites:list", payload),
|
||||||
clearLogs: (taskId) => ipcRenderer.invoke("logs:clear", taskId),
|
clearLogs: (taskId) => ipcRenderer.invoke("logs:clear", taskId),
|
||||||
|
|||||||
@ -203,6 +203,20 @@ function initStore(userDataPath) {
|
|||||||
return db.prepare("SELECT * FROM accounts ORDER BY id DESC").all();
|
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 }) {
|
function findAccountByIdentity({ userId, phone, session }) {
|
||||||
return db.prepare(`
|
return db.prepare(`
|
||||||
SELECT * FROM accounts
|
SELECT * FROM accounts
|
||||||
@ -576,6 +590,7 @@ function initStore(userDataPath) {
|
|||||||
saveSettings,
|
saveSettings,
|
||||||
listAccounts,
|
listAccounts,
|
||||||
findAccountByIdentity,
|
findAccountByIdentity,
|
||||||
|
clearAllData,
|
||||||
listTasks,
|
listTasks,
|
||||||
getTask,
|
getTask,
|
||||||
saveTask,
|
saveTask,
|
||||||
|
|||||||
@ -873,29 +873,33 @@ class TelegramManager {
|
|||||||
groups,
|
groups,
|
||||||
lastMessageAt: "",
|
lastMessageAt: "",
|
||||||
lastSource: "",
|
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 () => {
|
const timer = setInterval(async () => {
|
||||||
for (const [key, st] of state.entries()) {
|
for (const [key, st] of state.entries()) {
|
||||||
try {
|
try {
|
||||||
const messages = await monitorAccount.client.getMessages(st.entity, { limit: 10 });
|
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()) {
|
for (const message of messages.reverse()) {
|
||||||
|
totalMessages += 1;
|
||||||
if (st.lastId && message.id <= st.lastId) continue;
|
if (st.lastId && message.id <= st.lastId) continue;
|
||||||
st.lastId = Math.max(st.lastId || 0, message.id || 0);
|
st.lastId = Math.max(st.lastId || 0, message.id || 0);
|
||||||
if (!message.senderId) continue;
|
const senderInfo = await this._getUserInfoFromMessage(message);
|
||||||
const senderId = message.senderId.toString();
|
if (!senderInfo) {
|
||||||
if (this._isOwnAccount(senderId)) continue;
|
skipped += 1;
|
||||||
let username = "";
|
continue;
|
||||||
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 { userId: senderId, username, accessHash } = senderInfo;
|
||||||
|
if (this._isOwnAccount(senderId)) continue;
|
||||||
let messageDate = new Date();
|
let messageDate = new Date();
|
||||||
if (message.date instanceof Date) {
|
if (message.date instanceof Date) {
|
||||||
messageDate = message.date;
|
messageDate = message.date;
|
||||||
@ -904,7 +908,22 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
monitorEntry.lastMessageAt = messageDate.toISOString();
|
monitorEntry.lastMessageAt = messageDate.toISOString();
|
||||||
monitorEntry.lastSource = st.source;
|
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) {
|
} catch (error) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@ -937,6 +956,8 @@ class TelegramManager {
|
|||||||
if (task.auto_join_competitors) {
|
if (task.auto_join_competitors) {
|
||||||
await this._autoJoinGroups(entry.client, groups, true, entry.account);
|
await this._autoJoinGroups(entry.client, groups, true, entry.account);
|
||||||
}
|
}
|
||||||
|
const summaryLines = [];
|
||||||
|
let totalEnqueued = 0;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
const resolved = await this._resolveGroupEntity(entry.client, group, Boolean(task.auto_join_competitors), entry.account);
|
const resolved = await this._resolveGroupEntity(entry.client, group, Boolean(task.auto_join_competitors), entry.account);
|
||||||
@ -945,28 +966,59 @@ class TelegramManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const messages = await entry.client.getMessages(resolved.entity, { limit: perGroupLimit });
|
const messages = await entry.client.getMessages(resolved.entity, { limit: perGroupLimit });
|
||||||
|
let total = 0;
|
||||||
|
let enqueued = 0;
|
||||||
|
let skipped = 0;
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
const senderId = message.senderId;
|
total += 1;
|
||||||
if (!senderId) continue;
|
const senderInfo = await this._getUserInfoFromMessage(message);
|
||||||
const senderStr = senderId.toString();
|
if (!senderInfo) {
|
||||||
if (this._isOwnAccount(senderStr)) continue;
|
skipped += 1;
|
||||||
let username = "";
|
continue;
|
||||||
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 = "";
|
|
||||||
}
|
}
|
||||||
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 };
|
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) {
|
stopTaskMonitor(taskId) {
|
||||||
const entry = this.taskMonitors.get(taskId);
|
const entry = this.taskMonitors.get(taskId);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
|
|||||||
@ -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) => {
|
const toggleAccountSelection = (accountId) => {
|
||||||
setSelectedAccountIds((prev) => {
|
setSelectedAccountIds((prev) => {
|
||||||
if (prev.includes(accountId)) {
|
if (prev.includes(accountId)) {
|
||||||
@ -998,6 +1029,9 @@ export default function App() {
|
|||||||
Остановить все
|
Остановить все
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" className="danger" onClick={clearDatabase}>
|
||||||
|
Очистить БД
|
||||||
|
</button>
|
||||||
<div className="notification-bell" ref={bellRef}>
|
<div className="notification-bell" ref={bellRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -1079,6 +1113,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="top-row">
|
||||||
<section className="card task-accounts">
|
<section className="card task-accounts">
|
||||||
<div className="row-header">
|
<div className="row-header">
|
||||||
<h2>Аккаунты задачи</h2>
|
<h2>Аккаунты задачи</h2>
|
||||||
@ -1152,7 +1187,6 @@ export default function App() {
|
|||||||
<h3>Импорт из tdata</h3>
|
<h3>Импорт из tdata</h3>
|
||||||
<div className="hint">
|
<div className="hint">
|
||||||
Можно выбрать сразу несколько папок. Значения по умолчанию — API Telegram Desktop.
|
Можно выбрать сразу несколько папок. Значения по умолчанию — API Telegram Desktop.
|
||||||
{hasSelectedTask ? ` Импорт в задачу: ${selectedTaskName}.` : " Выберите задачу для импорта."}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<label>
|
<label>
|
||||||
@ -1223,6 +1257,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
{notification && (
|
{notification && (
|
||||||
<div className={`notice ${notification.tone}`}>
|
<div className={`notice ${notification.tone}`}>
|
||||||
|
|||||||
@ -368,6 +368,13 @@ body {
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -898,6 +905,10 @@ button:disabled {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.sticky {
|
.sticky {
|
||||||
position: static;
|
position: static;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user