some
This commit is contained in:
parent
d860acc4f3
commit
59d46f4e00
13
package.json
13
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "telegram-invite-automation",
|
"name": "telegram-invite-automation",
|
||||||
"version": "0.1.0",
|
"version": "0.2.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",
|
||||||
@ -13,7 +13,9 @@
|
|||||||
"build:mac": "vite build && electron-builder --mac",
|
"build:mac": "vite build && electron-builder --mac",
|
||||||
"build:all": "vite build && electron-builder --win --mac",
|
"build:all": "vite build && electron-builder --win --mac",
|
||||||
"build:linux": "vite build && electron-builder --linux",
|
"build:linux": "vite build && electron-builder --linux",
|
||||||
"dist": "vite build && electron-builder"
|
"dist": "vite build && electron-builder",
|
||||||
|
"build:converter:mac": "bash scripts/build-converter.sh",
|
||||||
|
"build:converter:win": "powershell -ExecutionPolicy Bypass -File scripts/build-converter.ps1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^9.4.0",
|
"better-sqlite3": "^9.4.0",
|
||||||
@ -39,8 +41,15 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist/**",
|
"dist/**",
|
||||||
"src/main/**",
|
"src/main/**",
|
||||||
|
"resources/**",
|
||||||
"package.json"
|
"package.json"
|
||||||
],
|
],
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "resources/converter",
|
||||||
|
"to": "converter"
|
||||||
|
}
|
||||||
|
],
|
||||||
"asar": true,
|
"asar": true,
|
||||||
"mac": {
|
"mac": {
|
||||||
"category": "public.app-category.productivity",
|
"category": "public.app-category.productivity",
|
||||||
|
|||||||
0
resources/converter/.gitkeep
Normal file
0
resources/converter/.gitkeep
Normal file
BIN
resources/converter/mac/tgconvertor
Executable file
BIN
resources/converter/mac/tgconvertor
Executable file
Binary file not shown.
31
scripts/build-converter.ps1
Normal file
31
scripts/build-converter.ps1
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$root = Split-Path -Parent $PSScriptRoot
|
||||||
|
$outDir = Join-Path $root "resources/converter/win"
|
||||||
|
$venv = Join-Path $root "scripts/.converter-venv"
|
||||||
|
|
||||||
|
$python = Get-Command py -ErrorAction SilentlyContinue
|
||||||
|
if ($python) {
|
||||||
|
& py -3.11 -c "import sys; print(sys.version)" 2>$null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "Python 3.11 is required (py -3.11). Please install it."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
& py -3.11 -m venv $venv
|
||||||
|
} else {
|
||||||
|
Write-Host "Python launcher (py) not found. Please install Python 3.11 and ensure 'py' is available."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
& "$venv/Scripts/activate.ps1"
|
||||||
|
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install "TGConvertor[tdata]==0.1.4" telethon pyinstaller
|
||||||
|
|
||||||
|
pyinstaller --onefile "$root/scripts/tdata_converter.py" --name tgconvertor --distpath "$root/dist/tdata_converter" --collect-all TGConvertor --collect-all opentele --collect-all telethon
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
||||||
|
Copy-Item -Force "$root/dist/tdata_converter/tgconvertor.exe" (Join-Path $outDir "tgconvertor.exe")
|
||||||
|
|
||||||
|
Remove-Item -Recurse -Force "$root/dist/tdata_converter" "$root/build" "$root/tdata_converter.spec"
|
||||||
|
|
||||||
|
Write-Host "Converter built: $outDir\tgconvertor.exe"
|
||||||
45
scripts/build-converter.sh
Normal file
45
scripts/build-converter.sh
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
OUT_DIR="$ROOT_DIR/resources/converter/mac"
|
||||||
|
WORK_DIR="$ROOT_DIR/scripts/.converter-venv"
|
||||||
|
|
||||||
|
PYTHON_BIN=""
|
||||||
|
for candidate in python3.11 python3.10 python3; do
|
||||||
|
if command -v "$candidate" >/dev/null 2>&1; then
|
||||||
|
PYTHON_BIN="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$PYTHON_BIN" ]; then
|
||||||
|
echo "Python 3.10+ is required. Install python3.11 and retry."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PYTHON_VERSION=$("$PYTHON_BIN" -c "import sys; print(f\"{sys.version_info.major}.{sys.version_info.minor}\")")
|
||||||
|
if [ "$PYTHON_VERSION" = "3.13" ]; then
|
||||||
|
echo "Python 3.13 is not supported by opentele/TGConvertor. Install python3.11 and retry."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$PYTHON_BIN" -m venv "$WORK_DIR"
|
||||||
|
source "$WORK_DIR/bin/activate"
|
||||||
|
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install "TGConvertor[tdata]==0.1.4" telethon pyinstaller
|
||||||
|
|
||||||
|
pyinstaller --onefile "$ROOT_DIR/scripts/tdata_converter.py" \
|
||||||
|
--name tgconvertor \
|
||||||
|
--distpath "$ROOT_DIR/dist/tdata_converter" \
|
||||||
|
--collect-all TGConvertor \
|
||||||
|
--collect-all opentele \
|
||||||
|
--collect-all telethon
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
cp -f "$ROOT_DIR/dist/tdata_converter/tgconvertor" "$OUT_DIR/tgconvertor"
|
||||||
|
|
||||||
|
rm -rf "$ROOT_DIR/dist/tdata_converter" "$ROOT_DIR/build" "$ROOT_DIR/tdata_converter.spec"
|
||||||
|
|
||||||
|
echo "Converter built: $OUT_DIR/tgconvertor"
|
||||||
30
scripts/tdata_converter.py
Normal file
30
scripts/tdata_converter.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from TGConvertor import SessionManager
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"ERROR: TGConvertor not available: {exc}")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("ERROR: tdata path is required")
|
||||||
|
sys.exit(2)
|
||||||
|
tdata_path = Path(sys.argv[1]).expanduser().resolve()
|
||||||
|
if not tdata_path.exists():
|
||||||
|
print("ERROR: tdata path does not exist")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
session = SessionManager.from_tdata_folder(tdata_path)
|
||||||
|
# Convert to Telethon string for GramJS compatibility
|
||||||
|
from TGConvertor.sessions.tele import TeleSession
|
||||||
|
|
||||||
|
tele = TeleSession(dc_id=session.dc_id, auth_key=session.auth_key)
|
||||||
|
session_string = tele.to_string()
|
||||||
|
print(session_string)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -1,14 +1,17 @@
|
|||||||
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
|
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const { execFileSync } = require("child_process");
|
||||||
const { initStore } = require("./store");
|
const { initStore } = require("./store");
|
||||||
const { TelegramManager } = require("./telegram");
|
const { TelegramManager } = require("./telegram");
|
||||||
const { Scheduler } = require("./scheduler");
|
const { Scheduler } = require("./scheduler");
|
||||||
|
const { TaskRunner } = require("./taskRunner");
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
let store;
|
let store;
|
||||||
let telegram;
|
let telegram;
|
||||||
let scheduler;
|
let scheduler;
|
||||||
|
const taskRunners = new Map();
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
@ -53,6 +56,17 @@ ipcMain.handle("settings:get", () => store.getSettings());
|
|||||||
ipcMain.handle("settings:save", (_event, settings) => store.saveSettings(settings));
|
ipcMain.handle("settings:save", (_event, settings) => store.saveSettings(settings));
|
||||||
|
|
||||||
ipcMain.handle("accounts:list", () => store.listAccounts());
|
ipcMain.handle("accounts:list", () => store.listAccounts());
|
||||||
|
ipcMain.handle("accounts:resetCooldown", async (_event, accountId) => {
|
||||||
|
store.clearAccountCooldown(accountId);
|
||||||
|
store.addAccountEvent(accountId, "", "manual_reset", "Cooldown reset by user");
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
ipcMain.handle("accounts:delete", async (_event, accountId) => {
|
||||||
|
await telegram.removeAccount(accountId);
|
||||||
|
store.deleteAccount(accountId);
|
||||||
|
store.addAccountEvent(accountId, "", "delete", "Account deleted by user");
|
||||||
|
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;
|
||||||
@ -62,16 +76,254 @@ ipcMain.handle("accounts:completeLogin", async (_event, payload) => {
|
|||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("logs:list", (_event, limit) => store.listLogs(limit || 100));
|
ipcMain.handle("accounts:importTdata", async (_event, payload) => {
|
||||||
ipcMain.handle("invites:list", (_event, limit) => store.listInvites(limit || 200));
|
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||||
ipcMain.handle("logs:clear", () => {
|
title: "Выберите папку tdata",
|
||||||
store.clearLogs();
|
properties: ["openDirectory", "multiSelections"]
|
||||||
|
});
|
||||||
|
if (canceled || !filePaths || !filePaths.length) return { ok: false, canceled: true };
|
||||||
|
|
||||||
|
const platformDir = process.platform === "win32" ? "win" : "mac";
|
||||||
|
const binaryName = process.platform === "win32" ? "tgconvertor.exe" : "tgconvertor";
|
||||||
|
const devBinary = path.join(__dirname, "..", "..", "resources", "converter", platformDir, binaryName);
|
||||||
|
const packagedBinary = path.join(process.resourcesPath, "converter", platformDir, binaryName);
|
||||||
|
const binaryPath = app.isPackaged ? packagedBinary : devBinary;
|
||||||
|
|
||||||
|
if (!fs.existsSync(binaryPath)) {
|
||||||
|
return { ok: false, error: "Встроенный конвертер не найден. Соберите его через scripts/build-converter.*" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const imported = [];
|
||||||
|
const failed = [];
|
||||||
|
const skipped = [];
|
||||||
|
const assignedIds = [];
|
||||||
|
|
||||||
|
for (const chosenPath of filePaths) {
|
||||||
|
let tdataPath = chosenPath;
|
||||||
|
const tdataCandidate = path.join(tdataPath, "tdata");
|
||||||
|
if (fs.existsSync(tdataCandidate) && fs.lstatSync(tdataCandidate).isDirectory()) {
|
||||||
|
tdataPath = tdataCandidate;
|
||||||
|
}
|
||||||
|
if (path.basename(tdataPath) !== "tdata") {
|
||||||
|
failed.push({ path: chosenPath, error: "Нужна папка tdata или папка с вложенной tdata." });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
try {
|
||||||
|
output = execFileSync(binaryPath, [tdataPath], { encoding: "utf8" });
|
||||||
|
} catch (error) {
|
||||||
|
failed.push({ path: chosenPath, error: "Не удалось запустить встроенный конвертер tdata." });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
||||||
|
const candidate = lines.find((line) => line.length > 50) || lines[lines.length - 1];
|
||||||
|
if (!candidate) {
|
||||||
|
failed.push({ path: chosenPath, error: "Не удалось получить строку сессии из tdata." });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await telegram.importTdataSession({
|
||||||
|
sessionString: candidate,
|
||||||
|
apiId: payload && payload.apiId,
|
||||||
|
apiHash: payload && payload.apiHash
|
||||||
|
});
|
||||||
|
if (!result.ok) {
|
||||||
|
if (result.error === "DUPLICATE_ACCOUNT") {
|
||||||
|
skipped.push({ path: chosenPath, reason: "Дубликат", accountId: result.accountId });
|
||||||
|
if (result.accountId) assignedIds.push(result.accountId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
failed.push({ path: chosenPath, error: result.error || "Ошибка импорта" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
imported.push({ path: chosenPath, accountId: result.accountId });
|
||||||
|
if (result.accountId) assignedIds.push(result.accountId);
|
||||||
|
} catch (error) {
|
||||||
|
failed.push({ path: chosenPath, error: error.message || String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload && payload.taskId && assignedIds.length) {
|
||||||
|
const task = store.getTask(payload.taskId);
|
||||||
|
if (task) {
|
||||||
|
const existing = store.listTaskAccounts(payload.taskId).map((row) => row.account_id);
|
||||||
|
const merged = Array.from(new Set([...(existing || []), ...assignedIds]));
|
||||||
|
store.setTaskAccounts(payload.taskId, merged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, imported, skipped, failed };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("logs:list", (_event, payload) => {
|
||||||
|
if (payload && typeof payload === "object") {
|
||||||
|
return store.listLogs(payload.limit || 100, payload.taskId);
|
||||||
|
}
|
||||||
|
return store.listLogs(payload || 100);
|
||||||
|
});
|
||||||
|
ipcMain.handle("invites:list", (_event, payload) => {
|
||||||
|
if (payload && typeof payload === "object") {
|
||||||
|
return store.listInvites(payload.limit || 200, payload.taskId);
|
||||||
|
}
|
||||||
|
return store.listInvites(payload || 200);
|
||||||
|
});
|
||||||
|
ipcMain.handle("logs:clear", (_event, taskId) => {
|
||||||
|
store.clearLogs(taskId);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
ipcMain.handle("invites:clear", () => {
|
ipcMain.handle("invites:clear", (_event, taskId) => {
|
||||||
store.clearInvites();
|
store.clearInvites(taskId);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
ipcMain.handle("queue:clear", (_event, taskId) => {
|
||||||
|
store.clearQueue(taskId);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("tasks:list", () => store.listTasks());
|
||||||
|
ipcMain.handle("tasks:get", (_event, id) => {
|
||||||
|
const task = store.getTask(id);
|
||||||
|
if (!task) return null;
|
||||||
|
return {
|
||||||
|
task,
|
||||||
|
competitors: store.listTaskCompetitors(id).map((row) => row.link),
|
||||||
|
accountIds: store.listTaskAccounts(id).map((row) => row.account_id)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
ipcMain.handle("tasks:save", (_event, payload) => {
|
||||||
|
const taskId = store.saveTask(payload.task);
|
||||||
|
store.setTaskCompetitors(taskId, payload.competitors || []);
|
||||||
|
store.setTaskAccounts(taskId, payload.accountIds || []);
|
||||||
|
return { ok: true, taskId };
|
||||||
|
});
|
||||||
|
ipcMain.handle("tasks:delete", (_event, id) => {
|
||||||
|
const runner = taskRunners.get(id);
|
||||||
|
if (runner) {
|
||||||
|
runner.stop();
|
||||||
|
taskRunners.delete(id);
|
||||||
|
}
|
||||||
|
store.deleteTask(id);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
ipcMain.handle("tasks:start", async (_event, id) => {
|
||||||
|
const task = store.getTask(id);
|
||||||
|
if (!task) return { ok: false, error: "Task not found" };
|
||||||
|
let runner = taskRunners.get(id);
|
||||||
|
if (!runner) {
|
||||||
|
runner = new TaskRunner(store, telegram, task);
|
||||||
|
taskRunners.set(id, runner);
|
||||||
|
} else {
|
||||||
|
runner.task = task;
|
||||||
|
}
|
||||||
|
await runner.start();
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
ipcMain.handle("tasks:stop", (_event, id) => {
|
||||||
|
const runner = taskRunners.get(id);
|
||||||
|
if (runner) {
|
||||||
|
runner.stop();
|
||||||
|
taskRunners.delete(id);
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
ipcMain.handle("tasks:accountAssignments", () => {
|
||||||
|
return store.listAllTaskAccounts();
|
||||||
|
});
|
||||||
|
ipcMain.handle("tasks:appendAccounts", (_event, payload) => {
|
||||||
|
if (!payload || !payload.taskId) return { ok: false, error: "Task not found" };
|
||||||
|
const task = store.getTask(payload.taskId);
|
||||||
|
if (!task) return { ok: false, error: "Task not found" };
|
||||||
|
const existing = store.listTaskAccounts(payload.taskId).map((row) => row.account_id);
|
||||||
|
const merged = Array.from(new Set([...(existing || []), ...((payload.accountIds || []))]));
|
||||||
|
store.setTaskAccounts(payload.taskId, merged);
|
||||||
|
return { ok: true, accountIds: merged };
|
||||||
|
});
|
||||||
|
ipcMain.handle("tasks:removeAccount", (_event, payload) => {
|
||||||
|
if (!payload || !payload.taskId || !payload.accountId) {
|
||||||
|
return { ok: false, error: "Task not found" };
|
||||||
|
}
|
||||||
|
const task = store.getTask(payload.taskId);
|
||||||
|
if (!task) return { ok: false, error: "Task not found" };
|
||||||
|
const existing = store.listTaskAccounts(payload.taskId).map((row) => row.account_id);
|
||||||
|
const filtered = existing.filter((id) => id !== payload.accountId);
|
||||||
|
store.setTaskAccounts(payload.taskId, filtered);
|
||||||
|
return { ok: true, accountIds: filtered };
|
||||||
|
});
|
||||||
|
ipcMain.handle("tasks:startAll", async () => {
|
||||||
|
const tasks = store.listTasks();
|
||||||
|
let started = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
const errors = [];
|
||||||
|
for (const task of tasks) {
|
||||||
|
if (!task.enabled) {
|
||||||
|
skipped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let runner = taskRunners.get(task.id);
|
||||||
|
if (!runner) {
|
||||||
|
runner = new TaskRunner(store, telegram, task);
|
||||||
|
taskRunners.set(task.id, runner);
|
||||||
|
} else {
|
||||||
|
runner.task = task;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await runner.start();
|
||||||
|
started += 1;
|
||||||
|
} catch (error) {
|
||||||
|
errors.push({ id: task.id, error: error.message || String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: errors.length === 0, started, skipped, errors };
|
||||||
|
});
|
||||||
|
ipcMain.handle("tasks:stopAll", () => {
|
||||||
|
let stopped = 0;
|
||||||
|
for (const [id, runner] of taskRunners.entries()) {
|
||||||
|
runner.stop();
|
||||||
|
taskRunners.delete(id);
|
||||||
|
stopped += 1;
|
||||||
|
}
|
||||||
|
return { ok: true, stopped };
|
||||||
|
});
|
||||||
|
ipcMain.handle("tasks:status", (_event, id) => {
|
||||||
|
const runner = taskRunners.get(id);
|
||||||
|
const queueCount = store.getPendingCount(id);
|
||||||
|
const dailyUsed = store.countInvitesToday(id);
|
||||||
|
const task = store.getTask(id);
|
||||||
|
const monitorInfo = telegram.getTaskMonitorInfo(id);
|
||||||
|
return {
|
||||||
|
running: runner ? runner.isRunning() : false,
|
||||||
|
queueCount,
|
||||||
|
dailyUsed,
|
||||||
|
dailyLimit: task ? task.daily_limit : 0,
|
||||||
|
dailyRemaining: task ? Math.max(0, Number(task.daily_limit || 0) - dailyUsed) : 0,
|
||||||
|
monitorInfo
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("tasks:parseHistory", async (_event, id) => {
|
||||||
|
const task = store.getTask(id);
|
||||||
|
if (!task) return { ok: false, error: "Task not found" };
|
||||||
|
const competitors = store.listTaskCompetitors(id).map((row) => row.link);
|
||||||
|
const accounts = store.listTaskAccounts(id).map((row) => row.account_id);
|
||||||
|
return telegram.parseHistoryForTask(task, competitors, accounts);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("tasks:checkAccess", async (_event, id) => {
|
||||||
|
const task = store.getTask(id);
|
||||||
|
if (!task) return { ok: false, error: "Task not found" };
|
||||||
|
const competitors = store.listTaskCompetitors(id).map((row) => row.link);
|
||||||
|
return telegram.checkGroupAccess(competitors, task.our_group);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("tasks:membershipStatus", async (_event, id) => {
|
||||||
|
const task = store.getTask(id);
|
||||||
|
if (!task) return { ok: false, error: "Task not found" };
|
||||||
|
const competitors = store.listTaskCompetitors(id).map((row) => row.link);
|
||||||
|
return telegram.getMembershipStatus(competitors, task.our_group);
|
||||||
|
});
|
||||||
|
|
||||||
const toCsv = (rows, headers) => {
|
const toCsv = (rows, headers) => {
|
||||||
const escape = (value) => {
|
const escape = (value) => {
|
||||||
@ -88,43 +340,60 @@ const toCsv = (rows, headers) => {
|
|||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
ipcMain.handle("logs:export", async () => {
|
ipcMain.handle("logs:export", async (_event, taskId) => {
|
||||||
const { canceled, filePath } = await dialog.showSaveDialog({
|
const { canceled, filePath } = await dialog.showSaveDialog({
|
||||||
title: "Выгрузить логи",
|
title: "Выгрузить логи",
|
||||||
defaultPath: "logs.csv"
|
defaultPath: "logs.csv"
|
||||||
});
|
});
|
||||||
if (canceled || !filePath) return { ok: false, canceled: true };
|
if (canceled || !filePath) return { ok: false, canceled: true };
|
||||||
|
|
||||||
const logs = store.listLogs(1000).map((log) => ({
|
const logs = store.listLogs(1000, taskId).map((log) => ({
|
||||||
|
taskId: log.taskId,
|
||||||
startedAt: log.startedAt,
|
startedAt: log.startedAt,
|
||||||
finishedAt: log.finishedAt,
|
finishedAt: log.finishedAt,
|
||||||
invitedCount: log.invitedCount,
|
invitedCount: log.invitedCount,
|
||||||
successIds: JSON.stringify(log.successIds || []),
|
successIds: JSON.stringify(log.successIds || []),
|
||||||
errors: JSON.stringify(log.errors || [])
|
errors: JSON.stringify(log.errors || [])
|
||||||
}));
|
}));
|
||||||
const csv = toCsv(logs, ["startedAt", "finishedAt", "invitedCount", "successIds", "errors"]);
|
const csv = toCsv(logs, ["taskId", "startedAt", "finishedAt", "invitedCount", "successIds", "errors"]);
|
||||||
fs.writeFileSync(filePath, csv, "utf8");
|
fs.writeFileSync(filePath, csv, "utf8");
|
||||||
return { ok: true, filePath };
|
return { ok: true, filePath };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("invites:export", async () => {
|
ipcMain.handle("invites:export", async (_event, taskId) => {
|
||||||
const { canceled, filePath } = await dialog.showSaveDialog({
|
const { canceled, filePath } = await dialog.showSaveDialog({
|
||||||
title: "Выгрузить историю инвайтов",
|
title: "Выгрузить историю инвайтов",
|
||||||
defaultPath: "invites.csv"
|
defaultPath: "invites.csv"
|
||||||
});
|
});
|
||||||
if (canceled || !filePath) return { ok: false, canceled: true };
|
if (canceled || !filePath) return { ok: false, canceled: true };
|
||||||
|
|
||||||
const invites = store.listInvites(2000);
|
const invites = store.listInvites(2000, taskId);
|
||||||
const csv = toCsv(invites, ["invitedAt", "userId", "username", "status", "error"]);
|
const csv = toCsv(invites, ["taskId", "invitedAt", "userId", "username", "status", "error"]);
|
||||||
fs.writeFileSync(filePath, csv, "utf8");
|
fs.writeFileSync(filePath, csv, "utf8");
|
||||||
return { ok: true, filePath };
|
return { ok: true, filePath };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("accounts:events", async (_event, limit) => {
|
||||||
|
return store.listAccountEvents(limit || 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("accounts:refreshIdentity", async () => {
|
||||||
|
const accounts = store.listAccounts();
|
||||||
|
for (const account of accounts) {
|
||||||
|
await telegram.refreshAccountIdentity(account.id);
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("task:start", async () => {
|
ipcMain.handle("task:start", async () => {
|
||||||
const settings = store.getSettings();
|
const settings = store.getSettings();
|
||||||
await telegram.startMonitoring(settings.competitorGroups);
|
if (settings.autoJoinOurGroup) {
|
||||||
|
await telegram.ensureJoinOurGroup(settings.ourGroup);
|
||||||
|
}
|
||||||
|
await telegram.joinGroupsForAllAccounts(settings.competitorGroups, settings.ourGroup, settings);
|
||||||
|
const monitorResult = await telegram.startMonitoring(settings.competitorGroups);
|
||||||
scheduler.start(settings);
|
scheduler.start(settings);
|
||||||
return { running: true };
|
return { running: true, monitorErrors: monitorResult && monitorResult.errors ? monitorResult.errors : [] };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("task:stop", async () => {
|
ipcMain.handle("task:stop", async () => {
|
||||||
@ -133,9 +402,34 @@ ipcMain.handle("task:stop", async () => {
|
|||||||
return { running: false };
|
return { running: false };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("status:get", () => ({
|
ipcMain.handle("status:get", () => {
|
||||||
running: scheduler ? scheduler.isRunning() : false
|
const settings = store.getSettings();
|
||||||
}));
|
const dailyUsed = store.countInvitesToday();
|
||||||
|
const dailyRemaining = Math.max(0, Number(settings.dailyLimit || 0) - dailyUsed);
|
||||||
|
const queueCount = store.getPendingCount();
|
||||||
|
const accounts = store.listAccounts();
|
||||||
|
const accountStats = accounts.map((account) => {
|
||||||
|
const used = store.countInvitesTodayByAccount(account.id);
|
||||||
|
const limit = Number(account.daily_limit || settings.accountDailyLimit || 0);
|
||||||
|
const remaining = limit > 0 ? Math.max(0, limit - used) : null;
|
||||||
|
return {
|
||||||
|
id: account.id,
|
||||||
|
usedToday: used,
|
||||||
|
remainingToday: remaining,
|
||||||
|
limit
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const monitorInfo = telegram.getMonitorInfo();
|
||||||
|
return {
|
||||||
|
running: scheduler ? scheduler.isRunning() : false,
|
||||||
|
queueCount,
|
||||||
|
dailyRemaining,
|
||||||
|
dailyUsed,
|
||||||
|
dailyLimit: Number(settings.dailyLimit || 0),
|
||||||
|
accountStats,
|
||||||
|
monitorInfo
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("task:parseHistory", async (_event, limit) => {
|
ipcMain.handle("task:parseHistory", async (_event, limit) => {
|
||||||
const settings = store.getSettings();
|
const settings = store.getSettings();
|
||||||
@ -148,3 +442,9 @@ ipcMain.handle("accounts:membershipStatus", async () => {
|
|||||||
const result = await telegram.getMembershipStatus(settings.competitorGroups, settings.ourGroup);
|
const result = await telegram.getMembershipStatus(settings.competitorGroups, settings.ourGroup);
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("groups:checkAccess", async () => {
|
||||||
|
const settings = store.getSettings();
|
||||||
|
const result = await telegram.checkGroupAccess(settings.competitorGroups, settings.ourGroup);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|||||||
@ -4,17 +4,39 @@ contextBridge.exposeInMainWorld("api", {
|
|||||||
getSettings: () => ipcRenderer.invoke("settings:get"),
|
getSettings: () => ipcRenderer.invoke("settings:get"),
|
||||||
saveSettings: (settings) => ipcRenderer.invoke("settings:save", settings),
|
saveSettings: (settings) => ipcRenderer.invoke("settings:save", settings),
|
||||||
listAccounts: () => ipcRenderer.invoke("accounts:list"),
|
listAccounts: () => ipcRenderer.invoke("accounts:list"),
|
||||||
|
resetAccountCooldown: (accountId) => ipcRenderer.invoke("accounts:resetCooldown", accountId),
|
||||||
|
listAccountEvents: (limit) => ipcRenderer.invoke("accounts:events", limit),
|
||||||
|
deleteAccount: (accountId) => ipcRenderer.invoke("accounts:delete", accountId),
|
||||||
|
refreshAccountIdentity: () => ipcRenderer.invoke("accounts:refreshIdentity"),
|
||||||
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),
|
||||||
listLogs: (limit) => ipcRenderer.invoke("logs:list", limit),
|
importTdata: (payload) => ipcRenderer.invoke("accounts:importTdata", payload),
|
||||||
listInvites: (limit) => ipcRenderer.invoke("invites:list", limit),
|
listLogs: (payload) => ipcRenderer.invoke("logs:list", payload),
|
||||||
clearLogs: () => ipcRenderer.invoke("logs:clear"),
|
listInvites: (payload) => ipcRenderer.invoke("invites:list", payload),
|
||||||
clearInvites: () => ipcRenderer.invoke("invites:clear"),
|
clearLogs: (taskId) => ipcRenderer.invoke("logs:clear", taskId),
|
||||||
exportLogs: () => ipcRenderer.invoke("logs:export"),
|
clearInvites: (taskId) => ipcRenderer.invoke("invites:clear", taskId),
|
||||||
exportInvites: () => ipcRenderer.invoke("invites:export"),
|
exportLogs: (taskId) => ipcRenderer.invoke("logs:export", taskId),
|
||||||
|
exportInvites: (taskId) => ipcRenderer.invoke("invites:export", taskId),
|
||||||
|
clearQueue: (taskId) => ipcRenderer.invoke("queue:clear", taskId),
|
||||||
startTask: () => ipcRenderer.invoke("task:start"),
|
startTask: () => ipcRenderer.invoke("task:start"),
|
||||||
stopTask: () => ipcRenderer.invoke("task:stop"),
|
stopTask: () => ipcRenderer.invoke("task:stop"),
|
||||||
getStatus: () => ipcRenderer.invoke("status:get"),
|
getStatus: () => ipcRenderer.invoke("status:get"),
|
||||||
parseHistory: (limit) => ipcRenderer.invoke("task:parseHistory", limit),
|
parseHistory: (limit) => ipcRenderer.invoke("task:parseHistory", limit),
|
||||||
getMembershipStatus: () => ipcRenderer.invoke("accounts:membershipStatus")
|
getMembershipStatus: () => ipcRenderer.invoke("accounts:membershipStatus"),
|
||||||
|
checkGroupAccess: () => ipcRenderer.invoke("groups:checkAccess"),
|
||||||
|
listTasks: () => ipcRenderer.invoke("tasks:list"),
|
||||||
|
getTask: (id) => ipcRenderer.invoke("tasks:get", id),
|
||||||
|
saveTask: (payload) => ipcRenderer.invoke("tasks:save", payload),
|
||||||
|
deleteTask: (id) => ipcRenderer.invoke("tasks:delete", id),
|
||||||
|
startTaskById: (id) => ipcRenderer.invoke("tasks:start", id),
|
||||||
|
stopTaskById: (id) => ipcRenderer.invoke("tasks:stop", id),
|
||||||
|
listAccountAssignments: () => ipcRenderer.invoke("tasks:accountAssignments"),
|
||||||
|
appendTaskAccounts: (payload) => ipcRenderer.invoke("tasks:appendAccounts", payload),
|
||||||
|
removeTaskAccount: (payload) => ipcRenderer.invoke("tasks:removeAccount", payload),
|
||||||
|
startAllTasks: () => ipcRenderer.invoke("tasks:startAll"),
|
||||||
|
stopAllTasks: () => ipcRenderer.invoke("tasks:stopAll"),
|
||||||
|
taskStatus: (id) => ipcRenderer.invoke("tasks:status", id),
|
||||||
|
parseHistoryByTask: (id) => ipcRenderer.invoke("tasks:parseHistory", id),
|
||||||
|
checkAccessByTask: (id) => ipcRenderer.invoke("tasks:checkAccess", id),
|
||||||
|
membershipStatusByTask: (id) => ipcRenderer.invoke("tasks:membershipStatus", id)
|
||||||
});
|
});
|
||||||
|
|||||||
@ -47,19 +47,58 @@ class Scheduler {
|
|||||||
} else {
|
} else {
|
||||||
const remaining = dailyLimit - alreadyInvited;
|
const remaining = dailyLimit - alreadyInvited;
|
||||||
const batchSize = Math.min(20, remaining);
|
const batchSize = Math.min(20, remaining);
|
||||||
const pending = this.store.getPendingInvites(batchSize);
|
const pending = this.store.getPendingInvites(0, batchSize);
|
||||||
|
|
||||||
|
const accountIds = new Set(this.store.listAccounts().map((account) => account.user_id).filter(Boolean));
|
||||||
for (const item of pending) {
|
for (const item of pending) {
|
||||||
|
if (accountIds.has(item.user_id)) {
|
||||||
|
this.store.markInviteStatus(item.id, "skipped");
|
||||||
|
this.store.recordInvite(
|
||||||
|
0,
|
||||||
|
item.user_id,
|
||||||
|
item.username,
|
||||||
|
0,
|
||||||
|
"",
|
||||||
|
item.source_chat,
|
||||||
|
"skipped",
|
||||||
|
"",
|
||||||
|
"account_own",
|
||||||
|
"skip"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const result = await this.telegram.inviteUser(this.settings.ourGroup, item.user_id);
|
const result = await this.telegram.inviteUser(this.settings.ourGroup, item.user_id);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
invitedCount += 1;
|
invitedCount += 1;
|
||||||
successIds.push(item.user_id);
|
successIds.push(item.user_id);
|
||||||
this.store.markInviteStatus(item.id, "invited");
|
this.store.markInviteStatus(item.id, "invited");
|
||||||
this.store.recordInvite(item.user_id, item.username, "success", "");
|
this.store.recordInvite(
|
||||||
|
0,
|
||||||
|
item.user_id,
|
||||||
|
item.username,
|
||||||
|
result.accountId,
|
||||||
|
result.accountPhone,
|
||||||
|
item.source_chat,
|
||||||
|
"success",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"invite"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
errors.push(`${item.user_id}: ${result.error}`);
|
errors.push(`${item.user_id}: ${result.error}`);
|
||||||
this.store.markInviteStatus(item.id, "failed");
|
this.store.markInviteStatus(item.id, "failed");
|
||||||
this.store.recordInvite(item.user_id, item.username, "failed", result.error || "");
|
this.store.recordInvite(
|
||||||
|
0,
|
||||||
|
item.user_id,
|
||||||
|
item.username,
|
||||||
|
result.accountId,
|
||||||
|
result.accountPhone,
|
||||||
|
item.source_chat,
|
||||||
|
"failed",
|
||||||
|
result.error || "",
|
||||||
|
result.error || "",
|
||||||
|
"invite"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,9 @@ const DEFAULT_SETTINGS = {
|
|||||||
maxIntervalMinutes: 10,
|
maxIntervalMinutes: 10,
|
||||||
dailyLimit: 100,
|
dailyLimit: 100,
|
||||||
historyLimit: 200,
|
historyLimit: 200,
|
||||||
|
accountMaxGroups: 10,
|
||||||
|
accountDailyLimit: 50,
|
||||||
|
floodCooldownMinutes: 1440,
|
||||||
autoJoinCompetitors: false,
|
autoJoinCompetitors: false,
|
||||||
autoJoinOurGroup: false
|
autoJoinOurGroup: false
|
||||||
};
|
};
|
||||||
@ -34,17 +37,25 @@ function initStore(userDataPath) {
|
|||||||
api_id INTEGER NOT NULL,
|
api_id INTEGER NOT NULL,
|
||||||
api_hash TEXT NOT NULL,
|
api_hash TEXT NOT NULL,
|
||||||
session TEXT NOT NULL,
|
session TEXT NOT NULL,
|
||||||
|
user_id TEXT DEFAULT '',
|
||||||
|
max_groups INTEGER DEFAULT 10,
|
||||||
|
daily_limit INTEGER DEFAULT 50,
|
||||||
status TEXT NOT NULL DEFAULT 'ok',
|
status TEXT NOT NULL DEFAULT 'ok',
|
||||||
last_error TEXT DEFAULT '',
|
last_error TEXT DEFAULT '',
|
||||||
|
cooldown_until TEXT DEFAULT '',
|
||||||
|
cooldown_reason TEXT DEFAULT '',
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS invite_queue (
|
CREATE TABLE IF NOT EXISTS invite_queue (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
task_id INTEGER DEFAULT 0,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
username TEXT DEFAULT '',
|
username TEXT DEFAULT '',
|
||||||
|
user_access_hash TEXT DEFAULT '',
|
||||||
source_chat TEXT NOT NULL,
|
source_chat TEXT NOT NULL,
|
||||||
|
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,
|
||||||
@ -53,6 +64,7 @@ function initStore(userDataPath) {
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS logs (
|
CREATE TABLE IF NOT EXISTS logs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
task_id INTEGER DEFAULT 0,
|
||||||
started_at TEXT NOT NULL,
|
started_at TEXT NOT NULL,
|
||||||
finished_at TEXT NOT NULL,
|
finished_at TEXT NOT NULL,
|
||||||
invited_count INTEGER NOT NULL,
|
invited_count INTEGER NOT NULL,
|
||||||
@ -60,14 +72,65 @@ function initStore(userDataPath) {
|
|||||||
error_summary TEXT NOT NULL
|
error_summary TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS account_events (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
account_id INTEGER NOT NULL,
|
||||||
|
phone TEXT NOT NULL,
|
||||||
|
event_type TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS invites (
|
CREATE TABLE IF NOT EXISTS invites (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
task_id INTEGER DEFAULT 0,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
username TEXT DEFAULT '',
|
username TEXT DEFAULT '',
|
||||||
|
user_access_hash TEXT DEFAULT '',
|
||||||
|
account_id INTEGER DEFAULT 0,
|
||||||
|
account_phone TEXT DEFAULT '',
|
||||||
|
source_chat TEXT DEFAULT '',
|
||||||
|
action TEXT DEFAULT 'invite',
|
||||||
|
skipped_reason TEXT DEFAULT '',
|
||||||
invited_at TEXT NOT NULL,
|
invited_at TEXT NOT NULL,
|
||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
error TEXT NOT NULL
|
error TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tasks (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
our_group TEXT NOT NULL,
|
||||||
|
min_interval_minutes INTEGER NOT NULL,
|
||||||
|
max_interval_minutes INTEGER NOT NULL,
|
||||||
|
daily_limit INTEGER NOT NULL,
|
||||||
|
history_limit INTEGER NOT NULL,
|
||||||
|
max_competitor_bots INTEGER NOT NULL,
|
||||||
|
max_our_bots INTEGER NOT NULL,
|
||||||
|
random_accounts INTEGER NOT NULL DEFAULT 0,
|
||||||
|
multi_accounts_per_run INTEGER NOT NULL DEFAULT 0,
|
||||||
|
retry_on_fail INTEGER NOT NULL DEFAULT 0,
|
||||||
|
auto_join_competitors INTEGER NOT NULL DEFAULT 1,
|
||||||
|
auto_join_our_group INTEGER NOT NULL DEFAULT 1,
|
||||||
|
stop_on_blocked INTEGER NOT NULL DEFAULT 0,
|
||||||
|
stop_blocked_percent INTEGER NOT NULL DEFAULT 25,
|
||||||
|
notes TEXT NOT NULL DEFAULT '',
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS task_competitors (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
task_id INTEGER NOT NULL,
|
||||||
|
link TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS task_accounts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
task_id INTEGER NOT NULL,
|
||||||
|
account_id INTEGER NOT NULL
|
||||||
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const ensureColumn = (table, column, definition) => {
|
const ensureColumn = (table, column, definition) => {
|
||||||
@ -79,7 +142,25 @@ function initStore(userDataPath) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ensureColumn("invite_queue", "username", "TEXT DEFAULT ''");
|
ensureColumn("invite_queue", "username", "TEXT DEFAULT ''");
|
||||||
|
ensureColumn("invite_queue", "user_access_hash", "TEXT DEFAULT ''");
|
||||||
ensureColumn("invites", "username", "TEXT DEFAULT ''");
|
ensureColumn("invites", "username", "TEXT DEFAULT ''");
|
||||||
|
ensureColumn("invites", "user_access_hash", "TEXT DEFAULT ''");
|
||||||
|
ensureColumn("invites", "account_id", "INTEGER DEFAULT 0");
|
||||||
|
ensureColumn("accounts", "max_groups", "INTEGER DEFAULT 10");
|
||||||
|
ensureColumn("accounts", "daily_limit", "INTEGER DEFAULT 50");
|
||||||
|
ensureColumn("invites", "account_phone", "TEXT DEFAULT ''");
|
||||||
|
ensureColumn("invites", "source_chat", "TEXT DEFAULT ''");
|
||||||
|
ensureColumn("invites", "action", "TEXT DEFAULT 'invite'");
|
||||||
|
ensureColumn("invites", "skipped_reason", "TEXT DEFAULT ''");
|
||||||
|
ensureColumn("accounts", "cooldown_until", "TEXT DEFAULT ''");
|
||||||
|
ensureColumn("accounts", "cooldown_reason", "TEXT DEFAULT ''");
|
||||||
|
ensureColumn("accounts", "user_id", "TEXT DEFAULT ''");
|
||||||
|
ensureColumn("invite_queue", "task_id", "INTEGER DEFAULT 0");
|
||||||
|
ensureColumn("invite_queue", "attempts", "INTEGER NOT NULL DEFAULT 0");
|
||||||
|
ensureColumn("logs", "task_id", "INTEGER DEFAULT 0");
|
||||||
|
ensureColumn("invites", "task_id", "INTEGER DEFAULT 0");
|
||||||
|
ensureColumn("tasks", "auto_join_competitors", "INTEGER NOT NULL DEFAULT 1");
|
||||||
|
ensureColumn("tasks", "auto_join_our_group", "INTEGER NOT NULL DEFAULT 1");
|
||||||
|
|
||||||
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) {
|
||||||
@ -122,16 +203,29 @@ 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 findAccountByIdentity({ userId, phone, session }) {
|
||||||
|
return db.prepare(`
|
||||||
|
SELECT * FROM accounts
|
||||||
|
WHERE (user_id = ? AND user_id != '')
|
||||||
|
OR (phone = ? AND phone != '')
|
||||||
|
OR (session = ? AND session != '')
|
||||||
|
LIMIT 1
|
||||||
|
`).get(userId || "", phone || "", session || "");
|
||||||
|
}
|
||||||
|
|
||||||
function addAccount(account) {
|
function addAccount(account) {
|
||||||
const now = dayjs().toISOString();
|
const now = dayjs().toISOString();
|
||||||
const result = db.prepare(`
|
const result = db.prepare(`
|
||||||
INSERT INTO accounts (phone, api_id, api_hash, session, status, last_error, created_at, updated_at)
|
INSERT INTO accounts (phone, api_id, api_hash, session, user_id, max_groups, daily_limit, status, last_error, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(
|
`).run(
|
||||||
account.phone,
|
account.phone,
|
||||||
account.apiId,
|
account.apiId,
|
||||||
account.apiHash,
|
account.apiHash,
|
||||||
account.session,
|
account.session,
|
||||||
|
account.userId || "",
|
||||||
|
account.maxGroups ?? DEFAULT_SETTINGS.accountMaxGroups,
|
||||||
|
account.dailyLimit ?? DEFAULT_SETTINGS.accountDailyLimit,
|
||||||
account.status || "ok",
|
account.status || "ok",
|
||||||
account.lastError || "",
|
account.lastError || "",
|
||||||
now,
|
now,
|
||||||
@ -146,26 +240,197 @@ function initStore(userDataPath) {
|
|||||||
.run(status, lastError || "", now, id);
|
.run(status, lastError || "", now, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function enqueueInvite(userId, username, sourceChat) {
|
function updateAccountIdentity(id, userId, phone) {
|
||||||
|
const now = dayjs().toISOString();
|
||||||
|
db.prepare("UPDATE accounts SET user_id = ?, phone = ?, updated_at = ? WHERE id = ?")
|
||||||
|
.run(userId || "", phone || "", now, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAccountCooldown(id, minutes, reason) {
|
||||||
|
const now = dayjs();
|
||||||
|
const until = minutes > 0 ? now.add(minutes, "minute").toISOString() : "";
|
||||||
|
const status = "limited";
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE accounts
|
||||||
|
SET status = ?, last_error = ?, cooldown_until = ?, cooldown_reason = ?, updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(status, reason || "", until, reason || "", now.toISOString(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAccountCooldown(id) {
|
||||||
|
const now = dayjs().toISOString();
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE accounts
|
||||||
|
SET status = 'ok', last_error = '', cooldown_until = '', cooldown_reason = '', updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(now, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAccount(id) {
|
||||||
|
db.prepare("DELETE FROM accounts WHERE id = ?").run(id);
|
||||||
|
db.prepare("DELETE FROM task_accounts WHERE account_id = ?").run(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAccountEvent(accountId, phone, eventType, message) {
|
||||||
|
const now = dayjs().toISOString();
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO account_events (account_id, phone, event_type, message, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
`).run(accountId, phone || "", eventType, message || "", now);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listAccountEvents(limit) {
|
||||||
|
const rows = db.prepare(`
|
||||||
|
SELECT * FROM account_events
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(limit || 200);
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
accountId: row.account_id,
|
||||||
|
phone: row.phone,
|
||||||
|
eventType: row.event_type,
|
||||||
|
message: row.message,
|
||||||
|
createdAt: row.created_at
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueueInvite(taskId, userId, username, sourceChat, accessHash) {
|
||||||
const now = dayjs().toISOString();
|
const now = dayjs().toISOString();
|
||||||
try {
|
try {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO invite_queue (user_id, username, source_chat, status, created_at, updated_at)
|
INSERT INTO invite_queue (task_id, user_id, username, user_access_hash, source_chat, status, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, 'pending', ?, ?)
|
VALUES (?, ?, ?, ?, ?, 'pending', ?, ?)
|
||||||
`).run(userId, username || "", sourceChat, now, now);
|
`).run(taskId || 0, userId, username || "", accessHash || "", sourceChat, now, now);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPendingInvites(limit) {
|
function getPendingInvites(taskId, limit) {
|
||||||
return db.prepare(`
|
return db.prepare(`
|
||||||
SELECT * FROM invite_queue
|
SELECT * FROM invite_queue
|
||||||
WHERE status = 'pending'
|
WHERE status = 'pending' AND task_id = ?
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(limit);
|
`).all(taskId || 0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPendingCount(taskId) {
|
||||||
|
if (taskId == null) {
|
||||||
|
return db.prepare("SELECT COUNT(*) as count FROM invite_queue WHERE status = 'pending'").get().count;
|
||||||
|
}
|
||||||
|
return db.prepare("SELECT COUNT(*) as count FROM invite_queue WHERE status = 'pending' AND task_id = ?")
|
||||||
|
.get(taskId || 0).count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearQueue(taskId) {
|
||||||
|
if (taskId == null) {
|
||||||
|
db.prepare("DELETE FROM invite_queue").run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
db.prepare("DELETE FROM invite_queue WHERE task_id = ?").run(taskId || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listTasks() {
|
||||||
|
return db.prepare("SELECT * FROM tasks ORDER BY id DESC").all();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTask(id) {
|
||||||
|
return db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTask(task) {
|
||||||
|
const now = dayjs().toISOString();
|
||||||
|
if (task.id) {
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE tasks
|
||||||
|
SET name = ?, our_group = ?, min_interval_minutes = ?, max_interval_minutes = ?, daily_limit = ?,
|
||||||
|
history_limit = ?, max_competitor_bots = ?, max_our_bots = ?, random_accounts = ?, multi_accounts_per_run = ?,
|
||||||
|
retry_on_fail = ?, auto_join_competitors = ?, auto_join_our_group = ?, stop_on_blocked = ?, stop_blocked_percent = ?, notes = ?, enabled = ?, updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(
|
||||||
|
task.name,
|
||||||
|
task.ourGroup,
|
||||||
|
task.minIntervalMinutes,
|
||||||
|
task.maxIntervalMinutes,
|
||||||
|
task.dailyLimit,
|
||||||
|
task.historyLimit,
|
||||||
|
task.maxCompetitorBots,
|
||||||
|
task.maxOurBots,
|
||||||
|
task.randomAccounts ? 1 : 0,
|
||||||
|
task.multiAccountsPerRun ? 1 : 0,
|
||||||
|
task.retryOnFail ? 1 : 0,
|
||||||
|
task.autoJoinCompetitors ? 1 : 0,
|
||||||
|
task.autoJoinOurGroup ? 1 : 0,
|
||||||
|
task.stopOnBlocked ? 1 : 0,
|
||||||
|
task.stopBlockedPercent || 25,
|
||||||
|
task.notes || "",
|
||||||
|
task.enabled ? 1 : 0,
|
||||||
|
now,
|
||||||
|
task.id
|
||||||
|
);
|
||||||
|
return task.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = db.prepare(`
|
||||||
|
INSERT INTO tasks (name, our_group, min_interval_minutes, max_interval_minutes, daily_limit, history_limit,
|
||||||
|
max_competitor_bots, max_our_bots, random_accounts, multi_accounts_per_run, retry_on_fail, auto_join_competitors,
|
||||||
|
auto_join_our_group, stop_on_blocked, stop_blocked_percent, notes, enabled, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(
|
||||||
|
task.name,
|
||||||
|
task.ourGroup,
|
||||||
|
task.minIntervalMinutes,
|
||||||
|
task.maxIntervalMinutes,
|
||||||
|
task.dailyLimit,
|
||||||
|
task.historyLimit,
|
||||||
|
task.maxCompetitorBots,
|
||||||
|
task.maxOurBots,
|
||||||
|
task.randomAccounts ? 1 : 0,
|
||||||
|
task.multiAccountsPerRun ? 1 : 0,
|
||||||
|
task.retryOnFail ? 1 : 0,
|
||||||
|
task.autoJoinCompetitors ? 1 : 0,
|
||||||
|
task.autoJoinOurGroup ? 1 : 0,
|
||||||
|
task.stopOnBlocked ? 1 : 0,
|
||||||
|
task.stopBlockedPercent || 25,
|
||||||
|
task.notes || "",
|
||||||
|
task.enabled ? 1 : 0,
|
||||||
|
now,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
return result.lastInsertRowid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTask(id) {
|
||||||
|
db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
||||||
|
db.prepare("DELETE FROM task_competitors WHERE task_id = ?").run(id);
|
||||||
|
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listTaskCompetitors(taskId) {
|
||||||
|
return db.prepare("SELECT * FROM task_competitors WHERE task_id = ? ORDER BY id ASC").all(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTaskCompetitors(taskId, links) {
|
||||||
|
db.prepare("DELETE FROM task_competitors WHERE task_id = ?").run(taskId);
|
||||||
|
const stmt = db.prepare("INSERT INTO task_competitors (task_id, link) VALUES (?, ?)");
|
||||||
|
(links || []).filter(Boolean).forEach((link) => stmt.run(taskId, link));
|
||||||
|
}
|
||||||
|
|
||||||
|
function listTaskAccounts(taskId) {
|
||||||
|
return db.prepare("SELECT * FROM task_accounts WHERE task_id = ?").all(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listAllTaskAccounts() {
|
||||||
|
return db.prepare("SELECT * FROM task_accounts").all();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTaskAccounts(taskId, accountIds) {
|
||||||
|
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId);
|
||||||
|
const stmt = db.prepare("INSERT INTO task_accounts (task_id, account_id) VALUES (?, ?)");
|
||||||
|
(accountIds || []).forEach((accountId) => stmt.run(taskId, accountId));
|
||||||
}
|
}
|
||||||
|
|
||||||
function markInviteStatus(queueId, status) {
|
function markInviteStatus(queueId, status) {
|
||||||
@ -174,26 +439,62 @@ function initStore(userDataPath) {
|
|||||||
.run(status, now, queueId);
|
.run(status, now, queueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordInvite(userId, username, status, error) {
|
function incrementInviteAttempt(queueId) {
|
||||||
const now = dayjs().toISOString();
|
const now = dayjs().toISOString();
|
||||||
db.prepare(`
|
db.prepare("UPDATE invite_queue SET attempts = attempts + 1, updated_at = ? WHERE id = ?")
|
||||||
INSERT INTO invites (user_id, username, invited_at, status, error)
|
.run(now, queueId);
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
`).run(userId, username || "", now, status, error || "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function countInvitesToday() {
|
function recordInvite(taskId, userId, username, accountId, accountPhone, sourceChat, status, error, skippedReason, action) {
|
||||||
|
const now = dayjs().toISOString();
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO invites (task_id, user_id, username, account_id, account_phone, source_chat, action, skipped_reason, invited_at, status, error)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(
|
||||||
|
taskId || 0,
|
||||||
|
userId,
|
||||||
|
username || "",
|
||||||
|
accountId || 0,
|
||||||
|
accountPhone || "",
|
||||||
|
sourceChat || "",
|
||||||
|
action || "invite",
|
||||||
|
skippedReason || "",
|
||||||
|
now,
|
||||||
|
status,
|
||||||
|
error || ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countInvitesToday(taskId) {
|
||||||
const dayStart = dayjs().startOf("day").toISOString();
|
const dayStart = dayjs().startOf("day").toISOString();
|
||||||
|
if (taskId == null) {
|
||||||
return db.prepare(
|
return db.prepare(
|
||||||
"SELECT COUNT(*) as count FROM invites WHERE invited_at >= ? AND status = 'success'"
|
"SELECT COUNT(*) as count FROM invites WHERE invited_at >= ? AND status = 'success'"
|
||||||
).get(dayStart).count;
|
).get(dayStart).count;
|
||||||
}
|
}
|
||||||
|
return db.prepare(
|
||||||
|
"SELECT COUNT(*) as count FROM invites WHERE invited_at >= ? AND status = 'success' AND task_id = ?"
|
||||||
|
).get(dayStart, taskId || 0).count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countInvitesTodayByAccount(accountId, taskId) {
|
||||||
|
const dayStart = dayjs().startOf("day").toISOString();
|
||||||
|
if (taskId == null) {
|
||||||
|
return db.prepare(
|
||||||
|
"SELECT COUNT(*) as count FROM invites WHERE invited_at >= ? AND status = 'success' AND account_id = ?"
|
||||||
|
).get(dayStart, accountId).count;
|
||||||
|
}
|
||||||
|
return db.prepare(
|
||||||
|
"SELECT COUNT(*) as count FROM invites WHERE invited_at >= ? AND status = 'success' AND account_id = ? AND task_id = ?"
|
||||||
|
).get(dayStart, accountId, taskId || 0).count;
|
||||||
|
}
|
||||||
|
|
||||||
function addLog(entry) {
|
function addLog(entry) {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO logs (started_at, finished_at, invited_count, success_ids, error_summary)
|
INSERT INTO logs (task_id, started_at, finished_at, invited_count, success_ids, error_summary)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`).run(
|
`).run(
|
||||||
|
entry.taskId || 0,
|
||||||
entry.startedAt,
|
entry.startedAt,
|
||||||
entry.finishedAt,
|
entry.finishedAt,
|
||||||
entry.invitedCount,
|
entry.invitedCount,
|
||||||
@ -202,10 +503,16 @@ function initStore(userDataPath) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listLogs(limit) {
|
function listLogs(limit, taskId) {
|
||||||
const rows = db.prepare("SELECT * FROM logs ORDER BY id DESC LIMIT ?").all(limit || 100);
|
let rows = [];
|
||||||
|
if (taskId != null) {
|
||||||
|
rows = db.prepare("SELECT * FROM logs WHERE task_id = ? ORDER BY id DESC LIMIT ?").all(taskId || 0, limit || 100);
|
||||||
|
} else {
|
||||||
|
rows = db.prepare("SELECT * FROM logs ORDER BY id DESC LIMIT ?").all(limit || 100);
|
||||||
|
}
|
||||||
return rows.map((row) => ({
|
return rows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
taskId: row.task_id || 0,
|
||||||
startedAt: row.started_at,
|
startedAt: row.started_at,
|
||||||
finishedAt: row.finished_at,
|
finishedAt: row.finished_at,
|
||||||
invitedCount: row.invited_count,
|
invitedCount: row.invited_count,
|
||||||
@ -214,46 +521,91 @@ function initStore(userDataPath) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLogs() {
|
function clearLogs(taskId) {
|
||||||
|
if (taskId == null) {
|
||||||
db.prepare("DELETE FROM logs").run();
|
db.prepare("DELETE FROM logs").run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
db.prepare("DELETE FROM logs WHERE task_id = ?").run(taskId || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listInvites(limit) {
|
function listInvites(limit, taskId) {
|
||||||
const rows = db.prepare(`
|
let rows = [];
|
||||||
|
if (taskId != null) {
|
||||||
|
rows = db.prepare(`
|
||||||
|
SELECT * FROM invites
|
||||||
|
WHERE task_id = ?
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(taskId || 0, limit || 200);
|
||||||
|
} else {
|
||||||
|
rows = db.prepare(`
|
||||||
SELECT * FROM invites
|
SELECT * FROM invites
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(limit || 200);
|
`).all(limit || 200);
|
||||||
|
}
|
||||||
return rows.map((row) => ({
|
return rows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
taskId: row.task_id || 0,
|
||||||
userId: row.user_id,
|
userId: row.user_id,
|
||||||
username: row.username || "",
|
username: row.username || "",
|
||||||
|
accountId: row.account_id || 0,
|
||||||
|
accountPhone: row.account_phone || "",
|
||||||
|
sourceChat: row.source_chat || "",
|
||||||
|
action: row.action || "invite",
|
||||||
|
skippedReason: row.skipped_reason || "",
|
||||||
invitedAt: row.invited_at,
|
invitedAt: row.invited_at,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
error: row.error
|
error: row.error
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearInvites() {
|
function clearInvites(taskId) {
|
||||||
|
if (taskId == null) {
|
||||||
db.prepare("DELETE FROM invites").run();
|
db.prepare("DELETE FROM invites").run();
|
||||||
db.prepare("DELETE FROM invite_queue").run();
|
db.prepare("DELETE FROM invite_queue").run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
db.prepare("DELETE FROM invites WHERE task_id = ?").run(taskId || 0);
|
||||||
|
db.prepare("DELETE FROM invite_queue WHERE task_id = ?").run(taskId || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getSettings,
|
getSettings,
|
||||||
saveSettings,
|
saveSettings,
|
||||||
listAccounts,
|
listAccounts,
|
||||||
|
findAccountByIdentity,
|
||||||
|
listTasks,
|
||||||
|
getTask,
|
||||||
|
saveTask,
|
||||||
|
deleteTask,
|
||||||
|
listTaskCompetitors,
|
||||||
|
setTaskCompetitors,
|
||||||
|
listTaskAccounts,
|
||||||
|
listAllTaskAccounts,
|
||||||
|
setTaskAccounts,
|
||||||
listLogs,
|
listLogs,
|
||||||
listInvites,
|
listInvites,
|
||||||
clearLogs,
|
clearLogs,
|
||||||
clearInvites,
|
clearInvites,
|
||||||
|
setAccountCooldown,
|
||||||
|
clearAccountCooldown,
|
||||||
|
addAccountEvent,
|
||||||
|
listAccountEvents,
|
||||||
|
deleteAccount,
|
||||||
|
updateAccountIdentity,
|
||||||
addAccount,
|
addAccount,
|
||||||
updateAccountStatus,
|
updateAccountStatus,
|
||||||
enqueueInvite,
|
enqueueInvite,
|
||||||
getPendingInvites,
|
getPendingInvites,
|
||||||
|
getPendingCount,
|
||||||
|
clearQueue,
|
||||||
markInviteStatus,
|
markInviteStatus,
|
||||||
|
incrementInviteAttempt,
|
||||||
recordInvite,
|
recordInvite,
|
||||||
countInvitesToday,
|
countInvitesToday,
|
||||||
|
countInvitesTodayByAccount,
|
||||||
addLog
|
addLog
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
151
src/main/taskRunner.js
Normal file
151
src/main/taskRunner.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
const dayjs = require("dayjs");
|
||||||
|
|
||||||
|
class TaskRunner {
|
||||||
|
constructor(store, telegram, task) {
|
||||||
|
this.store = store;
|
||||||
|
this.telegram = telegram;
|
||||||
|
this.task = task;
|
||||||
|
this.running = false;
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning() {
|
||||||
|
return this.running;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
if (this.running) return;
|
||||||
|
this.running = true;
|
||||||
|
await this._initMonitoring();
|
||||||
|
this._scheduleNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.running = false;
|
||||||
|
if (this.timer) clearTimeout(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
this.telegram.stopTaskMonitor(this.task.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _initMonitoring() {
|
||||||
|
const competitors = this.store.listTaskCompetitors(this.task.id).map((row) => row.link);
|
||||||
|
const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id);
|
||||||
|
await this.telegram.joinGroupsForTask(this.task, competitors, accounts);
|
||||||
|
await this.telegram.startTaskMonitor(this.task, competitors, accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
_scheduleNext() {
|
||||||
|
if (!this.running) return;
|
||||||
|
const minMs = Number(this.task.min_interval_minutes || 5) * 60 * 1000;
|
||||||
|
const maxMs = Number(this.task.max_interval_minutes || 10) * 60 * 1000;
|
||||||
|
const jitter = Math.max(minMs, Math.min(maxMs, minMs + Math.random() * (maxMs - minMs)));
|
||||||
|
this.timer = setTimeout(() => this._runBatch(), jitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _runBatch() {
|
||||||
|
const startedAt = dayjs().toISOString();
|
||||||
|
const errors = [];
|
||||||
|
const successIds = [];
|
||||||
|
let invitedCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id);
|
||||||
|
if (!accounts.length) {
|
||||||
|
errors.push("No accounts assigned");
|
||||||
|
}
|
||||||
|
let inviteAccounts = accounts;
|
||||||
|
if (!this.task.multi_accounts_per_run) {
|
||||||
|
const entry = this.telegram.pickInviteAccount(inviteAccounts, Boolean(this.task.random_accounts));
|
||||||
|
inviteAccounts = entry ? [entry.account.id] : [];
|
||||||
|
}
|
||||||
|
const totalAccounts = accounts.length;
|
||||||
|
if (this.task.stop_on_blocked) {
|
||||||
|
const all = this.store.listAccounts().filter((acc) => accounts.includes(acc.id));
|
||||||
|
const blocked = all.filter((acc) => acc.status !== "ok").length;
|
||||||
|
const percent = totalAccounts ? Math.round((blocked / totalAccounts) * 100) : 0;
|
||||||
|
if (percent >= Number(this.task.stop_blocked_percent || 25)) {
|
||||||
|
errors.push(`Stopped: blocked ${percent}% >= ${this.task.stop_blocked_percent}%`);
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dailyLimit = Number(this.task.daily_limit || 100);
|
||||||
|
const alreadyInvited = this.store.countInvitesToday(this.task.id);
|
||||||
|
if (alreadyInvited >= dailyLimit) {
|
||||||
|
errors.push("Daily limit reached");
|
||||||
|
} else {
|
||||||
|
const remaining = dailyLimit - alreadyInvited;
|
||||||
|
const batchSize = Math.min(20, remaining);
|
||||||
|
const pending = this.store.getPendingInvites(this.task.id, batchSize);
|
||||||
|
if (!inviteAccounts.length && pending.length) {
|
||||||
|
errors.push("No available accounts under limits");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of pending) {
|
||||||
|
if (item.attempts >= 2 && this.task.retry_on_fail) {
|
||||||
|
this.store.markInviteStatus(item.id, "failed");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const result = await this.telegram.inviteUserForTask(this.task, item.user_id, inviteAccounts, {
|
||||||
|
randomize: Boolean(this.task.random_accounts),
|
||||||
|
userAccessHash: item.user_access_hash,
|
||||||
|
username: item.username
|
||||||
|
});
|
||||||
|
if (result.ok) {
|
||||||
|
invitedCount += 1;
|
||||||
|
successIds.push(item.user_id);
|
||||||
|
this.store.markInviteStatus(item.id, "invited");
|
||||||
|
this.store.recordInvite(
|
||||||
|
this.task.id,
|
||||||
|
item.user_id,
|
||||||
|
item.username,
|
||||||
|
result.accountId,
|
||||||
|
result.accountPhone,
|
||||||
|
item.source_chat,
|
||||||
|
"success",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"invite"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
errors.push(`${item.user_id}: ${result.error}`);
|
||||||
|
if (this.task.retry_on_fail) {
|
||||||
|
this.store.incrementInviteAttempt(item.id);
|
||||||
|
this.store.markInviteStatus(item.id, "pending");
|
||||||
|
} else {
|
||||||
|
this.store.markInviteStatus(item.id, "failed");
|
||||||
|
}
|
||||||
|
this.store.recordInvite(
|
||||||
|
this.task.id,
|
||||||
|
item.user_id,
|
||||||
|
item.username,
|
||||||
|
result.accountId,
|
||||||
|
result.accountPhone,
|
||||||
|
item.source_chat,
|
||||||
|
"failed",
|
||||||
|
result.error || "",
|
||||||
|
result.error || "",
|
||||||
|
"invite"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(error.message || String(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
const finishedAt = dayjs().toISOString();
|
||||||
|
this.store.addLog({
|
||||||
|
taskId: this.task.id,
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
invitedCount,
|
||||||
|
successIds,
|
||||||
|
errors
|
||||||
|
});
|
||||||
|
|
||||||
|
this._scheduleNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { TaskRunner };
|
||||||
@ -9,7 +9,15 @@ class TelegramManager {
|
|||||||
this.pendingLogins = new Map();
|
this.pendingLogins = new Map();
|
||||||
this.monitorHandler = null;
|
this.monitorHandler = null;
|
||||||
this.monitorClientId = null;
|
this.monitorClientId = null;
|
||||||
|
this.monitorGroups = [];
|
||||||
|
this.monitorTimer = null;
|
||||||
|
this.monitorState = new Map();
|
||||||
|
this.lastMonitorMessageAt = "";
|
||||||
|
this.lastMonitorSource = "";
|
||||||
|
this.taskMonitors = new Map();
|
||||||
this.inviteIndex = 0;
|
this.inviteIndex = 0;
|
||||||
|
this.desktopApiId = 2040;
|
||||||
|
this.desktopApiHash = "b18441a1ff607e10a989891a5462e627";
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@ -25,6 +33,16 @@ class TelegramManager {
|
|||||||
connectionRetries: 3
|
connectionRetries: 3
|
||||||
});
|
});
|
||||||
await client.connect();
|
await client.connect();
|
||||||
|
try {
|
||||||
|
const me = await client.getMe();
|
||||||
|
if (me && me.id) {
|
||||||
|
this.store.updateAccountIdentity(account.id, me.id.toString(), me.phone || account.phone || "");
|
||||||
|
account.user_id = me.id.toString();
|
||||||
|
if (me.phone) account.phone = me.phone;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore identity fetch errors
|
||||||
|
}
|
||||||
this.clients.set(account.id, { client, account });
|
this.clients.set(account.id, { client, account });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,11 +101,88 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sessionString = client.session.save();
|
const sessionString = client.session.save();
|
||||||
|
const settings = this.store.getSettings();
|
||||||
|
const me = await client.getMe();
|
||||||
|
const userId = me && me.id ? me.id.toString() : "";
|
||||||
|
const actualPhone = me && me.phone ? me.phone : phone;
|
||||||
|
const existing = this.store.findAccountByIdentity({
|
||||||
|
userId,
|
||||||
|
phone: actualPhone,
|
||||||
|
session: sessionString
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
this.pendingLogins.delete(loginId);
|
||||||
|
try {
|
||||||
|
await client.disconnect();
|
||||||
|
} catch (error) {
|
||||||
|
// ignore disconnect errors
|
||||||
|
}
|
||||||
|
return { ok: false, error: "DUPLICATE_ACCOUNT", accountId: existing.id };
|
||||||
|
}
|
||||||
const accountId = this.store.addAccount({
|
const accountId = this.store.addAccount({
|
||||||
phone,
|
phone: actualPhone,
|
||||||
apiId,
|
apiId,
|
||||||
apiHash,
|
apiHash,
|
||||||
session: sessionString,
|
session: sessionString,
|
||||||
|
userId,
|
||||||
|
maxGroups: settings.accountMaxGroups,
|
||||||
|
dailyLimit: settings.accountDailyLimit,
|
||||||
|
status: "ok",
|
||||||
|
lastError: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
this.clients.set(accountId, {
|
||||||
|
client,
|
||||||
|
account: {
|
||||||
|
id: accountId,
|
||||||
|
phone: actualPhone,
|
||||||
|
api_id: apiId,
|
||||||
|
api_hash: apiHash,
|
||||||
|
user_id: userId,
|
||||||
|
status: "ok",
|
||||||
|
last_error: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.pendingLogins.delete(loginId);
|
||||||
|
|
||||||
|
return { ok: true, accountId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async importTdataSession({ sessionString, apiId, apiHash }) {
|
||||||
|
const usedApiId = Number(apiId || this.desktopApiId);
|
||||||
|
const usedApiHash = apiHash || this.desktopApiHash;
|
||||||
|
const session = new StringSession(sessionString);
|
||||||
|
const client = new TelegramClient(session, usedApiId, usedApiHash, {
|
||||||
|
connectionRetries: 3
|
||||||
|
});
|
||||||
|
await client.connect();
|
||||||
|
const me = await client.getMe();
|
||||||
|
const phone = me && me.phone ? me.phone : "unknown";
|
||||||
|
const userId = me && me.id ? me.id.toString() : "";
|
||||||
|
const existing = this.store.findAccountByIdentity({
|
||||||
|
userId,
|
||||||
|
phone,
|
||||||
|
session: sessionString
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
try {
|
||||||
|
await client.disconnect();
|
||||||
|
} catch (error) {
|
||||||
|
// ignore disconnect errors
|
||||||
|
}
|
||||||
|
return { ok: false, error: "DUPLICATE_ACCOUNT", accountId: existing.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedSession = client.session.save();
|
||||||
|
const settings = this.store.getSettings();
|
||||||
|
const accountId = this.store.addAccount({
|
||||||
|
phone,
|
||||||
|
apiId: usedApiId,
|
||||||
|
apiHash: usedApiHash,
|
||||||
|
session: savedSession,
|
||||||
|
userId,
|
||||||
|
maxGroups: settings.accountMaxGroups,
|
||||||
|
dailyLimit: settings.accountDailyLimit,
|
||||||
status: "ok",
|
status: "ok",
|
||||||
lastError: ""
|
lastError: ""
|
||||||
});
|
});
|
||||||
@ -97,13 +192,15 @@ class TelegramManager {
|
|||||||
account: {
|
account: {
|
||||||
id: accountId,
|
id: accountId,
|
||||||
phone,
|
phone,
|
||||||
api_id: apiId,
|
api_id: usedApiId,
|
||||||
api_hash: apiHash,
|
api_hash: usedApiHash,
|
||||||
|
user_id: userId,
|
||||||
|
max_groups: settings.accountMaxGroups,
|
||||||
|
daily_limit: settings.accountDailyLimit,
|
||||||
status: "ok",
|
status: "ok",
|
||||||
last_error: ""
|
last_error: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.pendingLogins.delete(loginId);
|
|
||||||
|
|
||||||
return { ok: true, accountId };
|
return { ok: true, accountId };
|
||||||
}
|
}
|
||||||
@ -117,21 +214,55 @@ class TelegramManager {
|
|||||||
if (!accountEntry) return;
|
if (!accountEntry) return;
|
||||||
|
|
||||||
this.monitorClientId = accountEntry.account.id;
|
this.monitorClientId = accountEntry.account.id;
|
||||||
|
this.monitorGroups = groups;
|
||||||
const client = accountEntry.client;
|
const client = accountEntry.client;
|
||||||
|
|
||||||
await this._autoJoinGroups(client, groups, this.store.getSettings().autoJoinCompetitors);
|
const settings = this.store.getSettings();
|
||||||
|
await this._autoJoinGroups(client, groups, settings.autoJoinCompetitors, accountEntry.account);
|
||||||
|
|
||||||
|
const resolved = [];
|
||||||
|
const errors = [];
|
||||||
|
for (const group of groups) {
|
||||||
|
const result = await this._resolveGroupEntity(client, group, settings.autoJoinCompetitors, accountEntry.account);
|
||||||
|
if (result.ok) {
|
||||||
|
resolved.push({ entity: result.entity, source: group });
|
||||||
|
} else {
|
||||||
|
errors.push(`${group}: ${result.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!resolved.length) {
|
||||||
|
return { ok: false, errors: errors.length ? errors : ["No groups to monitor"] };
|
||||||
|
}
|
||||||
|
|
||||||
this.monitorHandler = async (event) => {
|
this.monitorHandler = async (event) => {
|
||||||
const sender = event.message.senderId;
|
const sender = event.message.senderId;
|
||||||
if (!sender) return;
|
if (!sender) return;
|
||||||
const userId = sender.toString();
|
const userId = sender.toString();
|
||||||
const senderEntity = await event.getSender();
|
if (this._isOwnAccount(userId)) return;
|
||||||
|
let senderEntity = null;
|
||||||
|
try {
|
||||||
|
senderEntity = await event.getSender();
|
||||||
|
} catch (error) {
|
||||||
|
senderEntity = null;
|
||||||
|
}
|
||||||
if (senderEntity && senderEntity.bot) return;
|
if (senderEntity && senderEntity.bot) return;
|
||||||
const username = senderEntity && senderEntity.username ? senderEntity.username : "";
|
const username = senderEntity && senderEntity.username ? senderEntity.username : "";
|
||||||
|
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";
|
||||||
this.store.enqueueInvite(userId, username, sourceChat);
|
if (!this.monitorState.has(sourceChat)) return;
|
||||||
|
this.store.enqueueInvite(0, userId, username, sourceChat, accessHash);
|
||||||
|
this.lastMonitorMessageAt = new Date().toISOString();
|
||||||
|
this.lastMonitorSource = sourceChat;
|
||||||
};
|
};
|
||||||
|
|
||||||
client.addEventHandler(this.monitorHandler, new NewMessage({ chats: groups }));
|
this.monitorState.clear();
|
||||||
|
resolved.forEach((item) => {
|
||||||
|
const id = item.entity && item.entity.id != null ? item.entity.id.toString() : item.source;
|
||||||
|
this.monitorState.set(id, { entity: item.entity, source: item.source, lastId: 0 });
|
||||||
|
});
|
||||||
|
client.addEventHandler(this.monitorHandler, new NewMessage({}));
|
||||||
|
this._startMonitorPolling(client);
|
||||||
|
return { ok: true, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopMonitoring() {
|
async stopMonitoring() {
|
||||||
@ -142,12 +273,59 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
this.monitorHandler = null;
|
this.monitorHandler = null;
|
||||||
this.monitorClientId = null;
|
this.monitorClientId = null;
|
||||||
|
this.monitorGroups = [];
|
||||||
|
this.monitorState.clear();
|
||||||
|
if (this.monitorTimer) clearInterval(this.monitorTimer);
|
||||||
|
this.monitorTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeAccount(accountId) {
|
||||||
|
const entry = this.clients.get(accountId);
|
||||||
|
if (!entry) return;
|
||||||
|
if (this.monitorClientId === accountId) {
|
||||||
|
await this.stopMonitoring();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await entry.client.disconnect();
|
||||||
|
} catch (error) {
|
||||||
|
// ignore disconnect errors
|
||||||
|
}
|
||||||
|
this.clients.delete(accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshAccountIdentity(accountId) {
|
||||||
|
const entry = this.clients.get(accountId);
|
||||||
|
if (!entry) return;
|
||||||
|
try {
|
||||||
|
const me = await entry.client.getMe();
|
||||||
|
if (me && me.id) {
|
||||||
|
this.store.updateAccountIdentity(accountId, me.id.toString(), me.phone || entry.account.phone || "");
|
||||||
|
entry.account.user_id = me.id.toString();
|
||||||
|
if (me.phone) entry.account.phone = me.phone;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore identity refresh errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isMonitoring() {
|
||||||
|
return Boolean(this.monitorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMonitorInfo() {
|
||||||
|
return {
|
||||||
|
monitoring: this.isMonitoring(),
|
||||||
|
accountId: this.monitorClientId || 0,
|
||||||
|
groups: this.monitorGroups || [],
|
||||||
|
lastMessageAt: this.lastMonitorMessageAt,
|
||||||
|
lastSource: this.lastMonitorSource
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_pickClient() {
|
_pickClient() {
|
||||||
const entries = Array.from(this.clients.values());
|
const entries = Array.from(this.clients.values());
|
||||||
if (!entries.length) return null;
|
if (!entries.length) return null;
|
||||||
const ordered = entries.filter((entry) => entry.account.status === "ok");
|
const ordered = entries.filter((entry) => entry.account.status === "ok" && !this._isInCooldown(entry.account));
|
||||||
if (!ordered.length) return null;
|
if (!ordered.length) return null;
|
||||||
const entry = ordered[this.inviteIndex % ordered.length];
|
const entry = ordered[this.inviteIndex % ordered.length];
|
||||||
this.inviteIndex += 1;
|
this.inviteIndex += 1;
|
||||||
@ -155,37 +333,172 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async inviteUser(targetGroup, userId) {
|
async inviteUser(targetGroup, userId) {
|
||||||
const entry = this._pickClient();
|
const entry = this._pickClientForInvite();
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return { ok: false, error: "No available accounts" };
|
return { ok: false, error: "No available accounts under limits", accountId: 0, accountPhone: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { client, account } = entry;
|
const { client, account } = entry;
|
||||||
try {
|
try {
|
||||||
await this._autoJoinGroups(client, [targetGroup], this.store.getSettings().autoJoinOurGroup);
|
const allowJoin = this.store.getSettings().autoJoinOurGroup;
|
||||||
const channel = await client.getEntity(targetGroup);
|
await this._autoJoinGroups(client, [targetGroup], allowJoin, account);
|
||||||
|
const resolved = await this._resolveGroupEntity(client, targetGroup, allowJoin, account);
|
||||||
|
if (!resolved.ok) {
|
||||||
|
throw new Error(resolved.error);
|
||||||
|
}
|
||||||
|
const targetEntity = resolved.entity;
|
||||||
const user = await client.getEntity(userId);
|
const user = await client.getEntity(userId);
|
||||||
|
|
||||||
|
if (targetEntity.className === "Channel") {
|
||||||
await client.invoke(
|
await client.invoke(
|
||||||
new Api.channels.InviteToChannel({
|
new Api.channels.InviteToChannel({
|
||||||
channel,
|
channel: targetEntity,
|
||||||
users: [user]
|
users: [user]
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
} else if (targetEntity.className === "Chat") {
|
||||||
|
await client.invoke(
|
||||||
|
new Api.messages.AddChatUser({
|
||||||
|
chatId: targetEntity.id,
|
||||||
|
userId: user,
|
||||||
|
fwdLimit: 0
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported target chat type");
|
||||||
|
}
|
||||||
|
|
||||||
this.store.updateAccountStatus(account.id, "ok", "");
|
this.store.updateAccountStatus(account.id, "ok", "");
|
||||||
return { ok: true };
|
return { ok: true, accountId: account.id, accountPhone: account.phone || "" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error.errorMessage || error.message || String(error);
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) {
|
if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) {
|
||||||
this.store.updateAccountStatus(account.id, "limited", errorText);
|
this._applyFloodCooldown(account, errorText);
|
||||||
} else {
|
} else {
|
||||||
this.store.updateAccountStatus(account.id, account.status || "ok", errorText);
|
this.store.updateAccountStatus(account.id, account.status || "ok", errorText);
|
||||||
}
|
}
|
||||||
return { ok: false, error: errorText };
|
return { ok: false, error: errorText, accountId: account.id, accountPhone: account.phone || "" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async inviteUserForTask(task, userId, allowedAccountIds, options = {}) {
|
||||||
|
const entry = this._pickClientForInvite(allowedAccountIds, options.randomize);
|
||||||
|
if (!entry) {
|
||||||
|
return { ok: false, error: "No available accounts under limits", accountId: 0, accountPhone: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client, account } = entry;
|
||||||
|
try {
|
||||||
|
const accessHash = options.userAccessHash || "";
|
||||||
|
const providedUsername = options.username || "";
|
||||||
|
const allowJoin = Boolean(task.auto_join_our_group);
|
||||||
|
await this._autoJoinGroups(client, [task.our_group], allowJoin, account);
|
||||||
|
const resolved = await this._resolveGroupEntity(client, task.our_group, allowJoin, account);
|
||||||
|
if (!resolved.ok) throw new Error(resolved.error);
|
||||||
|
const targetEntity = resolved.entity;
|
||||||
|
let user = null;
|
||||||
|
if (accessHash) {
|
||||||
|
try {
|
||||||
|
user = new Api.InputUser({
|
||||||
|
userId: BigInt(userId),
|
||||||
|
accessHash: BigInt(accessHash)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
user = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!user && providedUsername) {
|
||||||
|
const username = providedUsername.startsWith("@") ? providedUsername : `@${providedUsername}`;
|
||||||
|
try {
|
||||||
|
user = await client.getEntity(username);
|
||||||
|
} catch (error) {
|
||||||
|
user = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
user = await client.getEntity(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetEntity.className === "Channel") {
|
||||||
|
await client.invoke(
|
||||||
|
new Api.channels.InviteToChannel({
|
||||||
|
channel: targetEntity,
|
||||||
|
users: [user]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (targetEntity.className === "Chat") {
|
||||||
|
await client.invoke(
|
||||||
|
new Api.messages.AddChatUser({
|
||||||
|
chatId: targetEntity.id,
|
||||||
|
userId: user,
|
||||||
|
fwdLimit: 0
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported target chat type");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store.updateAccountStatus(account.id, "ok", "");
|
||||||
|
return { ok: true, accountId: account.id, accountPhone: account.phone || "" };
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
|
if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) {
|
||||||
|
this._applyFloodCooldown(account, errorText);
|
||||||
|
} else {
|
||||||
|
this.store.updateAccountStatus(account.id, account.status || "ok", errorText);
|
||||||
|
}
|
||||||
|
return { ok: false, error: errorText, accountId: account.id, accountPhone: account.phone || "" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_pickClientForInvite(allowedAccountIds, randomize) {
|
||||||
|
const entries = Array.from(this.clients.values()).filter((entry) => entry.account.status === "ok");
|
||||||
|
if (!entries.length) return null;
|
||||||
|
|
||||||
|
const settings = this.store.getSettings();
|
||||||
|
const allowed = Array.isArray(allowedAccountIds) && allowedAccountIds.length
|
||||||
|
? entries.filter((entry) => allowedAccountIds.includes(entry.account.id))
|
||||||
|
: entries;
|
||||||
|
if (!allowed.length) return null;
|
||||||
|
const eligible = allowed.filter((entry) => {
|
||||||
|
if (this._isInCooldown(entry.account)) return false;
|
||||||
|
const limit = Number(entry.account.daily_limit || settings.accountDailyLimit || 0);
|
||||||
|
if (limit > 0) {
|
||||||
|
const used = this.store.countInvitesTodayByAccount(entry.account.id);
|
||||||
|
if (used >= limit) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (!eligible.length) return null;
|
||||||
|
if (randomize) {
|
||||||
|
const entry = eligible[Math.floor(Math.random() * eligible.length)];
|
||||||
|
this.inviteIndex += 1;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < allowed.length; i += 1) {
|
||||||
|
const entry = allowed[(this.inviteIndex + i) % allowed.length];
|
||||||
|
if (!eligible.includes(entry)) continue;
|
||||||
|
this.inviteIndex += 1;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
return eligible[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pickClientFromAllowed(allowedAccountIds) {
|
||||||
|
const entries = Array.from(this.clients.values()).filter((entry) => entry.account.status === "ok");
|
||||||
|
if (!entries.length) return null;
|
||||||
|
const allowed = Array.isArray(allowedAccountIds) && allowedAccountIds.length
|
||||||
|
? entries.filter((entry) => allowedAccountIds.includes(entry.account.id))
|
||||||
|
: entries;
|
||||||
|
if (!allowed.length) return null;
|
||||||
|
const available = allowed.filter((entry) => !this._isInCooldown(entry.account));
|
||||||
|
return available[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pickInviteAccount(allowedAccountIds, randomize) {
|
||||||
|
return this._pickClientForInvite(allowedAccountIds, randomize);
|
||||||
|
}
|
||||||
|
|
||||||
async parseHistory(competitorGroups, limit) {
|
async parseHistory(competitorGroups, limit) {
|
||||||
const groups = Array.isArray(competitorGroups) ? competitorGroups.filter(Boolean) : [competitorGroups].filter(Boolean);
|
const groups = Array.isArray(competitorGroups) ? competitorGroups.filter(Boolean) : [competitorGroups].filter(Boolean);
|
||||||
if (!groups.length) return { ok: false, error: "No competitor groups" };
|
if (!groups.length) return { ok: false, error: "No competitor groups" };
|
||||||
@ -195,26 +508,34 @@ class TelegramManager {
|
|||||||
|
|
||||||
const { client } = entry;
|
const { client } = entry;
|
||||||
const perGroupLimit = Math.max(1, Number(limit) || 200);
|
const perGroupLimit = Math.max(1, Number(limit) || 200);
|
||||||
await this._autoJoinGroups(client, groups, this.store.getSettings().autoJoinCompetitors);
|
await this._autoJoinGroups(client, groups, this.store.getSettings().autoJoinCompetitors, entry.account);
|
||||||
|
const errors = [];
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
const entity = await client.getEntity(group);
|
const resolved = await this._resolveGroupEntity(client, group, this.store.getSettings().autoJoinCompetitors, entry.account);
|
||||||
const messages = await client.getMessages(entity, { limit: perGroupLimit });
|
if (!resolved.ok) {
|
||||||
|
errors.push(`${group}: ${resolved.error}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const messages = await client.getMessages(resolved.entity, { limit: perGroupLimit });
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
const senderId = message.senderId;
|
const senderId = message.senderId;
|
||||||
if (!senderId) continue;
|
if (!senderId) continue;
|
||||||
let username = "";
|
let username = "";
|
||||||
|
let accessHash = "";
|
||||||
try {
|
try {
|
||||||
const sender = await message.getSender();
|
const sender = await message.getSender();
|
||||||
if (sender && sender.bot) continue;
|
if (sender && sender.bot) continue;
|
||||||
username = sender && sender.username ? sender.username : "";
|
username = sender && sender.username ? sender.username : "";
|
||||||
|
accessHash = sender && sender.accessHash ? sender.accessHash.toString() : "";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
username = "";
|
username = "";
|
||||||
|
accessHash = "";
|
||||||
}
|
}
|
||||||
this.store.enqueueInvite(senderId.toString(), username, group);
|
this.store.enqueueInvite(0, senderId.toString(), username, group, accessHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true };
|
return { ok: true, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMembershipStatus(competitorGroups, ourGroup) {
|
async getMembershipStatus(competitorGroups, ourGroup) {
|
||||||
@ -223,28 +544,16 @@ class TelegramManager {
|
|||||||
|
|
||||||
for (const entry of this.clients.values()) {
|
for (const entry of this.clients.values()) {
|
||||||
const { client, account } = entry;
|
const { client, account } = entry;
|
||||||
const me = await client.getMe();
|
|
||||||
|
|
||||||
let competitorCount = 0;
|
let competitorCount = 0;
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
try {
|
const isMember = await this._isParticipant(client, group);
|
||||||
const channel = await client.getEntity(group);
|
if (isMember) competitorCount += 1;
|
||||||
await client.invoke(new Api.channels.GetParticipant({ channel, participant: me }));
|
|
||||||
competitorCount += 1;
|
|
||||||
} catch (error) {
|
|
||||||
// not a participant
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ourGroupMember = false;
|
let ourGroupMember = false;
|
||||||
if (ourGroup) {
|
if (ourGroup) {
|
||||||
try {
|
ourGroupMember = await this._isParticipant(client, ourGroup);
|
||||||
const channel = await client.getEntity(ourGroup);
|
|
||||||
await client.invoke(new Api.channels.GetParticipant({ channel, participant: me }));
|
|
||||||
ourGroupMember = true;
|
|
||||||
} catch (error) {
|
|
||||||
ourGroupMember = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
@ -258,25 +567,446 @@ class TelegramManager {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _autoJoinGroups(client, groups, enabled) {
|
async ensureJoinOurGroup(ourGroup) {
|
||||||
|
if (!ourGroup) return { ok: false, error: "No target group" };
|
||||||
|
const entry = this._pickClient();
|
||||||
|
if (!entry) return { ok: false, error: "No available accounts" };
|
||||||
|
await this._autoJoinGroups(entry.client, [ourGroup], true, entry.account);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkGroupAccess(competitorGroups, ourGroup) {
|
||||||
|
const groups = Array.isArray(competitorGroups) ? competitorGroups.filter(Boolean) : [];
|
||||||
|
const result = [];
|
||||||
|
const entry = this._pickClient();
|
||||||
|
if (!entry) return { ok: false, error: "No available accounts" };
|
||||||
|
|
||||||
|
const { client } = entry;
|
||||||
|
const allGroups = [
|
||||||
|
...groups.map((value) => ({ type: "competitor", value })),
|
||||||
|
...(ourGroup ? [{ type: "our", value: ourGroup }] : [])
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const item of allGroups) {
|
||||||
|
const value = item.value;
|
||||||
|
try {
|
||||||
|
if (this._isInviteLink(value)) {
|
||||||
|
const hash = this._extractInviteHash(value);
|
||||||
|
if (!hash) throw new Error("Invalid invite link");
|
||||||
|
const check = await client.invoke(new Api.messages.CheckChatInvite({ hash }));
|
||||||
|
const title = check && check.chat ? check.chat.title : "";
|
||||||
|
result.push({
|
||||||
|
type: item.type,
|
||||||
|
value,
|
||||||
|
ok: true,
|
||||||
|
title,
|
||||||
|
details: check.className || ""
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const entity = await client.getEntity(value);
|
||||||
|
const title = entity && entity.title ? entity.title : "";
|
||||||
|
result.push({
|
||||||
|
type: item.type,
|
||||||
|
value,
|
||||||
|
ok: true,
|
||||||
|
title,
|
||||||
|
details: "entity"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
result.push({
|
||||||
|
type: item.type,
|
||||||
|
value,
|
||||||
|
ok: false,
|
||||||
|
title: "",
|
||||||
|
details: error.errorMessage || error.message || String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, result };
|
||||||
|
}
|
||||||
|
|
||||||
|
async _autoJoinGroups(client, groups, enabled, account) {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
|
const settings = this.store.getSettings();
|
||||||
|
let maxGroups = Number(account && account.max_groups != null ? account.max_groups : settings.accountMaxGroups);
|
||||||
|
if (!Number.isFinite(maxGroups) || maxGroups <= 0) {
|
||||||
|
maxGroups = Number.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
let memberCount = 0;
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
if (!group) continue;
|
if (!group) continue;
|
||||||
try {
|
try {
|
||||||
|
if (memberCount >= maxGroups) break;
|
||||||
|
const alreadyMember = await this._isParticipant(client, group);
|
||||||
|
if (alreadyMember) {
|
||||||
|
memberCount += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (this._isInviteLink(group)) {
|
if (this._isInviteLink(group)) {
|
||||||
const hash = this._extractInviteHash(group);
|
const hash = this._extractInviteHash(group);
|
||||||
if (hash) {
|
if (hash) {
|
||||||
await client.invoke(new Api.messages.ImportChatInvite({ hash }));
|
await client.invoke(new Api.messages.ImportChatInvite({ hash }));
|
||||||
|
memberCount += 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const entity = await client.getEntity(group);
|
const entity = await client.getEntity(group);
|
||||||
await client.invoke(new Api.channels.JoinChannel({ channel: entity }));
|
await client.invoke(new Api.channels.JoinChannel({ channel: entity }));
|
||||||
|
memberCount += 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ignore join errors (already member or restricted)
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
|
if (account && (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD"))) {
|
||||||
|
this._applyFloodCooldown(account, errorText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _isParticipant(client, group) {
|
||||||
|
try {
|
||||||
|
if (this._isInviteLink(group)) {
|
||||||
|
const hash = this._extractInviteHash(group);
|
||||||
|
if (!hash) return false;
|
||||||
|
const check = await client.invoke(new Api.messages.CheckChatInvite({ hash }));
|
||||||
|
if (check && typeof check.className === "string" && check.className.includes("ChatInviteAlready")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const entity = await client.getEntity(group);
|
||||||
|
const me = await client.getMe();
|
||||||
|
await client.invoke(new Api.channels.GetParticipant({ channel: entity, participant: me }));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _resolveGroupEntity(client, group, allowJoin, account) {
|
||||||
|
try {
|
||||||
|
if (this._isInviteLink(group)) {
|
||||||
|
const hash = this._extractInviteHash(group);
|
||||||
|
if (!hash) return { ok: false, error: "Invalid invite link" };
|
||||||
|
try {
|
||||||
|
const check = await client.invoke(new Api.messages.CheckChatInvite({ hash }));
|
||||||
|
if (check && check.chat) {
|
||||||
|
return { ok: true, entity: await this._normalizeEntity(client, check.chat) };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore and try import
|
||||||
|
}
|
||||||
|
if (!allowJoin) {
|
||||||
|
return { ok: false, error: "Invite link requires auto-join" };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const imported = await client.invoke(new Api.messages.ImportChatInvite({ hash }));
|
||||||
|
if (imported && imported.chats && imported.chats.length) {
|
||||||
|
return { ok: true, entity: await this._normalizeEntity(client, imported.chats[0]) };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
|
if (account && (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD"))) {
|
||||||
|
this._applyFloodCooldown(account, errorText);
|
||||||
|
}
|
||||||
|
if (errorText.includes("USER_ALREADY_PARTICIPANT")) {
|
||||||
|
try {
|
||||||
|
const check = await client.invoke(new Api.messages.CheckChatInvite({ hash }));
|
||||||
|
if (check && check.chat) {
|
||||||
|
return { ok: true, entity: check.chat };
|
||||||
|
}
|
||||||
|
} catch (checkError) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
return { ok: false, error: "USER_ALREADY_PARTICIPANT" };
|
||||||
|
}
|
||||||
|
return { ok: false, error: errorText };
|
||||||
|
}
|
||||||
|
return { ok: false, error: "Unable to resolve invite link" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const entity = await client.getEntity(group);
|
||||||
|
return { ok: true, entity: await this._normalizeEntity(client, entity) };
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
|
if (account && (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD"))) {
|
||||||
|
this._applyFloodCooldown(account, errorText);
|
||||||
|
}
|
||||||
|
return { ok: false, error: errorText };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_startMonitorPolling(client) {
|
||||||
|
if (this.monitorTimer) clearInterval(this.monitorTimer);
|
||||||
|
this.monitorTimer = setInterval(async () => {
|
||||||
|
if (!this.isMonitoring()) return;
|
||||||
|
for (const [key, state] of this.monitorState.entries()) {
|
||||||
|
try {
|
||||||
|
const messages = await client.getMessages(state.entity, { limit: 10 });
|
||||||
|
for (const message of messages.reverse()) {
|
||||||
|
if (state.lastId && message.id <= state.lastId) continue;
|
||||||
|
state.lastId = Math.max(state.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 = "";
|
||||||
|
}
|
||||||
|
this.store.enqueueInvite(0, senderId, username, state.source, accessHash);
|
||||||
|
this.lastMonitorMessageAt = new Date().toISOString();
|
||||||
|
this.lastMonitorSource = state.source;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore polling errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _normalizeEntity(client, entity) {
|
||||||
|
if (!entity) return entity;
|
||||||
|
if (entity.className === "Chat" || entity.className === "Channel") {
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
if (entity.id != null) {
|
||||||
|
return await client.getEntity(entity.id);
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isOwnAccount(userId) {
|
||||||
|
const accounts = this.store.listAccounts();
|
||||||
|
return accounts.some((account) => account.user_id && account.user_id.toString() === userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async joinGroupsForAllAccounts(competitorGroups, ourGroup, settings) {
|
||||||
|
const groups = Array.isArray(competitorGroups) ? competitorGroups.filter(Boolean) : [];
|
||||||
|
for (const entry of this.clients.values()) {
|
||||||
|
if (settings.autoJoinCompetitors) {
|
||||||
|
await this._autoJoinGroups(entry.client, groups, true, entry.account);
|
||||||
|
}
|
||||||
|
if (settings.autoJoinOurGroup && ourGroup) {
|
||||||
|
await this._autoJoinGroups(entry.client, [ourGroup], true, entry.account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async joinGroupsForTask(task, competitorGroups, accountIds) {
|
||||||
|
const accounts = Array.from(this.clients.values()).filter((entry) => accountIds.includes(entry.account.id));
|
||||||
|
const competitorBots = Math.max(1, Number(task.max_competitor_bots || 1));
|
||||||
|
const ourBots = Math.max(1, Number(task.max_our_bots || 1));
|
||||||
|
|
||||||
|
const competitors = competitorGroups || [];
|
||||||
|
let cursor = 0;
|
||||||
|
const usedForCompetitors = new Set();
|
||||||
|
for (const group of competitors) {
|
||||||
|
for (let i = 0; i < competitorBots; i += 1) {
|
||||||
|
if (!accounts.length) break;
|
||||||
|
const entry = accounts[cursor % accounts.length];
|
||||||
|
cursor += 1;
|
||||||
|
usedForCompetitors.add(entry.account.id);
|
||||||
|
if (task.auto_join_competitors) {
|
||||||
|
await this._autoJoinGroups(entry.client, [group], true, entry.account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.our_group) {
|
||||||
|
const available = accounts.filter((entry) => !usedForCompetitors.has(entry.account.id));
|
||||||
|
const pool = available.length ? available : accounts;
|
||||||
|
for (let i = 0; i < Math.min(ourBots, pool.length); i += 1) {
|
||||||
|
const entry = pool[i];
|
||||||
|
if (task.auto_join_our_group) {
|
||||||
|
await this._autoJoinGroups(entry.client, [task.our_group], true, entry.account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startTaskMonitor(task, competitorGroups, accountIds) {
|
||||||
|
const monitorAccount = this._pickClientFromAllowed(accountIds);
|
||||||
|
if (!monitorAccount) return { ok: false, error: "No accounts for task" };
|
||||||
|
const groups = (competitorGroups || []).filter(Boolean);
|
||||||
|
const resolved = [];
|
||||||
|
const errors = [];
|
||||||
|
for (const group of groups) {
|
||||||
|
const result = await this._resolveGroupEntity(
|
||||||
|
monitorAccount.client,
|
||||||
|
group,
|
||||||
|
Boolean(task.auto_join_competitors),
|
||||||
|
monitorAccount.account
|
||||||
|
);
|
||||||
|
if (result.ok) {
|
||||||
|
resolved.push({ entity: result.entity, source: group });
|
||||||
|
} else {
|
||||||
|
errors.push(`${group}: ${result.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!resolved.length) return { ok: false, error: "No groups to monitor", errors };
|
||||||
|
if (errors.length) {
|
||||||
|
this.store.addAccountEvent(
|
||||||
|
monitorAccount.account.id,
|
||||||
|
monitorAccount.account.phone,
|
||||||
|
"monitor_error",
|
||||||
|
errors.join(" | ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = new Map();
|
||||||
|
resolved.forEach((item) => {
|
||||||
|
const id = item.entity && item.entity.id != null ? item.entity.id.toString() : item.source;
|
||||||
|
state.set(id, { entity: item.entity, source: item.source, lastId: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
const monitorEntry = {
|
||||||
|
timer: null,
|
||||||
|
accountId: monitorAccount.account.id,
|
||||||
|
groups,
|
||||||
|
lastMessageAt: "",
|
||||||
|
lastSource: "",
|
||||||
|
lastErrorAt: new Map()
|
||||||
|
};
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
for (const [key, st] of state.entries()) {
|
||||||
|
try {
|
||||||
|
const messages = await monitorAccount.client.getMessages(st.entity, { limit: 10 });
|
||||||
|
for (const message of messages.reverse()) {
|
||||||
|
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 = "";
|
||||||
|
}
|
||||||
|
let messageDate = new Date();
|
||||||
|
if (message.date instanceof Date) {
|
||||||
|
messageDate = message.date;
|
||||||
|
} else if (typeof message.date === "number") {
|
||||||
|
messageDate = new Date(message.date * 1000);
|
||||||
|
}
|
||||||
|
monitorEntry.lastMessageAt = messageDate.toISOString();
|
||||||
|
monitorEntry.lastSource = st.source;
|
||||||
|
this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const now = Date.now();
|
||||||
|
const lastError = monitorEntry.lastErrorAt.get(key) || 0;
|
||||||
|
if (now - lastError > 60000) {
|
||||||
|
monitorEntry.lastErrorAt.set(key, now);
|
||||||
|
this.store.addAccountEvent(
|
||||||
|
monitorAccount.account.id,
|
||||||
|
monitorAccount.account.phone,
|
||||||
|
"monitor_poll_error",
|
||||||
|
`${st.source}: ${error.message || String(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
monitorEntry.timer = timer;
|
||||||
|
this.taskMonitors.set(task.id, monitorEntry);
|
||||||
|
return { ok: true, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseHistoryForTask(task, competitorGroups, accountIds) {
|
||||||
|
const groups = (competitorGroups || []).filter(Boolean);
|
||||||
|
if (!groups.length) return { ok: false, error: "No competitor groups" };
|
||||||
|
const entry = this._pickClientFromAllowed(accountIds);
|
||||||
|
if (!entry) return { ok: false, error: "No available accounts" };
|
||||||
|
const perGroupLimit = Math.max(1, Number(task.history_limit || 200));
|
||||||
|
|
||||||
|
if (task.auto_join_competitors) {
|
||||||
|
await this._autoJoinGroups(entry.client, groups, true, entry.account);
|
||||||
|
}
|
||||||
|
const errors = [];
|
||||||
|
for (const group of groups) {
|
||||||
|
const resolved = await this._resolveGroupEntity(entry.client, group, Boolean(task.auto_join_competitors), entry.account);
|
||||||
|
if (!resolved.ok) {
|
||||||
|
errors.push(`${group}: ${resolved.error}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const messages = await entry.client.getMessages(resolved.entity, { limit: perGroupLimit });
|
||||||
|
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 = "";
|
||||||
|
}
|
||||||
|
this.store.enqueueInvite(task.id, senderStr, username, group, accessHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: true, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTaskMonitor(taskId) {
|
||||||
|
const entry = this.taskMonitors.get(taskId);
|
||||||
|
if (!entry) return;
|
||||||
|
clearInterval(entry.timer);
|
||||||
|
this.taskMonitors.delete(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTaskMonitorInfo(taskId) {
|
||||||
|
const entry = this.taskMonitors.get(taskId);
|
||||||
|
if (!entry) {
|
||||||
|
return { monitoring: false, accountId: 0, groups: [], lastMessageAt: "", lastSource: "" };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
monitoring: true,
|
||||||
|
accountId: entry.accountId || 0,
|
||||||
|
groups: entry.groups || [],
|
||||||
|
lastMessageAt: entry.lastMessageAt || "",
|
||||||
|
lastSource: entry.lastSource || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_isInCooldown(account) {
|
||||||
|
if (!account || !account.cooldown_until) return false;
|
||||||
|
try {
|
||||||
|
return new Date(account.cooldown_until).getTime() > Date.now();
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyFloodCooldown(account, reason) {
|
||||||
|
const settings = this.store.getSettings();
|
||||||
|
const minutes = Number(settings.floodCooldownMinutes || 1440);
|
||||||
|
this.store.setAccountCooldown(account.id, minutes, reason);
|
||||||
|
this.store.addAccountEvent(account.id, account.phone || "", "flood", `FLOOD cooldown: ${minutes} min. ${reason || ""}`);
|
||||||
|
account.status = "limited";
|
||||||
|
account.last_error = reason || "";
|
||||||
|
account.cooldown_until = minutes > 0 ? new Date(Date.now() + minutes * 60000).toISOString() : "";
|
||||||
|
account.cooldown_reason = reason || "";
|
||||||
|
}
|
||||||
|
|
||||||
_isInviteLink(value) {
|
_isInviteLink(value) {
|
||||||
return value.includes("joinchat/") || value.includes("t.me/+");
|
return value.includes("joinchat/") || value.includes("t.me/+");
|
||||||
|
|||||||
2072
src/renderer/App.jsx
2072
src/renderer/App.jsx
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,10 @@ body {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notice.inline {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.notice.info {
|
.notice.info {
|
||||||
background: #e0f2fe;
|
background: #e0f2fe;
|
||||||
color: #0c4a6e;
|
color: #0c4a6e;
|
||||||
@ -71,6 +75,102 @@ body {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.live {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-value {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-block {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-row {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: #f8fafc;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-row.ok {
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-row.fail {
|
||||||
|
border-color: #fecaca;
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-status {
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-error {
|
||||||
|
color: #b91c1c;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -95,12 +195,206 @@ body {
|
|||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-actions button {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #475569;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
border-color: #2563eb;
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bell-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -6px;
|
||||||
|
background: #ef4444;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bell-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 42px;
|
||||||
|
width: 320px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
box-shadow: 0 20px 30px rgba(15, 23, 42, 0.15);
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bell-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bell-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.secondary {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.secondary.active {
|
||||||
|
background: #cbd5f5;
|
||||||
|
color: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 20;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
width: min(640px, 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
box-shadow: 0 30px 60px rgba(15, 23, 42, 0.2);
|
||||||
|
}
|
||||||
|
.task-switcher {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-switcher-label {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-switcher select {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(226, 232, 240, 0.35);
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-caption {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 260px minmax(0, 1fr) 320px;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.left {
|
||||||
|
order: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.right {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky {
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@ -149,6 +443,14 @@ input {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #d1d7e0;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
@ -166,6 +468,15 @@ input {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.row-inline {
|
.row-inline {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@ -176,6 +487,22 @@ input {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-inline {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-inline select {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #d1d7e0;
|
||||||
|
background: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.competitor-list {
|
.competitor-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -201,6 +528,12 @@ input {
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox.inline {
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@ -236,10 +569,14 @@ button.danger {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.busy-accounts {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.account-row {
|
.account-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
@ -276,6 +613,150 @@ button.danger {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 260px) minmax(320px, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-search {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-search input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #475569;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.active {
|
||||||
|
border-color: #2563eb;
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.primary {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.danger {
|
||||||
|
background: #ef4444;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-meta.monitor {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-meta.monitor.on {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-meta.monitor.off {
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item.active {
|
||||||
|
border-color: #2563eb;
|
||||||
|
background: #e0f2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status.ok {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status.off {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.login-box {
|
.login-box {
|
||||||
border-top: 1px solid #e2e8f0;
|
border-top: 1px solid #e2e8f0;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
@ -284,11 +765,88 @@ button.danger {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tdata-report {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #334155;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tdata-errors {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tdata-error-row {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tdata-error-path {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #0f172a;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tdata-error-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.status-text {
|
.status-text {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #1d4ed8;
|
color: #1d4ed8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-actions.expanded {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-stat {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-stat strong {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
.logs .log-row {
|
.logs .log-row {
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
@ -318,6 +876,15 @@ button.danger {
|
|||||||
color: #475569;
|
color: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@ -326,6 +893,17 @@ button.danger {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky {
|
||||||
|
position: static;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.app {
|
.app {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@ -336,4 +914,38 @@ button.danger {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-switcher {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky {
|
||||||
|
position: static;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-actions {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
tgconvertor.spec
Normal file
49
tgconvertor.spec
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
|
datas = []
|
||||||
|
binaries = []
|
||||||
|
hiddenimports = []
|
||||||
|
tmp_ret = collect_all('TGConvertor')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('opentele')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('telethon')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['/Users/ivan/Desktop/projects/work/profi.ru/telegram-forced-entry-group/scripts/tdata_converter.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='tgconvertor',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue
Block a user