telegram-invite-automation/src/main/store.js
2026-02-04 14:21:07 +04:00

1269 lines
43 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const Database = require("better-sqlite3");
const dayjs = require("dayjs");
const DEFAULT_SETTINGS = {
competitorGroups: [""],
ourGroup: "",
minIntervalMinutes: 10,
maxIntervalMinutes: 20,
dailyLimit: 100,
historyLimit: 200,
accountMaxGroups: 10,
accountDailyLimit: 50,
floodCooldownMinutes: 1440,
queueTtlHours: 24,
quietModeMinutes: 10,
autoJoinCompetitors: false,
autoJoinOurGroup: false
};
function initStore(userDataPath) {
const dataDir = path.join(userDataPath, "data");
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
const dbPath = path.join(dataDir, "app.db");
const db = new Database(dbPath);
db.pragma("journal_mode = WAL");
db.exec(`
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
phone TEXT NOT NULL,
api_id INTEGER NOT NULL,
api_hash TEXT NOT NULL,
session TEXT NOT NULL,
user_id TEXT DEFAULT '',
username TEXT DEFAULT '',
max_groups INTEGER DEFAULT 10,
daily_limit INTEGER DEFAULT 50,
status TEXT NOT NULL DEFAULT 'ok',
last_error TEXT DEFAULT '',
cooldown_until TEXT DEFAULT '',
cooldown_reason TEXT DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS invite_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER DEFAULT 0,
user_id TEXT NOT NULL,
username TEXT DEFAULT '',
user_access_hash TEXT DEFAULT '',
watcher_account_id INTEGER DEFAULT 0,
source_chat TEXT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(user_id, source_chat)
);
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER DEFAULT 0,
started_at TEXT NOT NULL,
finished_at TEXT NOT NULL,
invited_count INTEGER NOT NULL,
success_ids TEXT NOT NULL,
error_summary TEXT NOT NULL,
meta TEXT NOT NULL DEFAULT ''
);
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 task_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
action TEXT NOT NULL,
details TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS invites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER DEFAULT 0,
user_id TEXT NOT NULL,
username TEXT DEFAULT '',
user_access_hash TEXT DEFAULT '',
account_id INTEGER DEFAULT 0,
account_phone TEXT DEFAULT '',
watcher_account_id INTEGER DEFAULT 0,
watcher_phone TEXT DEFAULT '',
strategy TEXT DEFAULT '',
strategy_meta TEXT DEFAULT '',
source_chat TEXT DEFAULT '',
target_chat TEXT DEFAULT '',
target_type TEXT DEFAULT '',
action TEXT DEFAULT 'invite',
skipped_reason TEXT DEFAULT '',
invited_at TEXT NOT NULL,
status TEXT NOT NULL,
error TEXT NOT NULL,
confirmed INTEGER NOT NULL DEFAULT 1,
confirm_error TEXT NOT NULL DEFAULT '',
archived INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS fallback_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER DEFAULT 0,
user_id TEXT NOT NULL,
username TEXT DEFAULT '',
source_chat TEXT DEFAULT '',
target_chat TEXT DEFAULT '',
reason TEXT NOT NULL,
route TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL,
UNIQUE(user_id, target_chat)
);
CREATE TABLE IF NOT EXISTS confirm_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER DEFAULT 0,
user_id TEXT NOT NULL,
username TEXT DEFAULT '',
account_id INTEGER DEFAULT 0,
watcher_account_id INTEGER DEFAULT 0,
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 2,
next_check_at TEXT NOT NULL,
last_error TEXT DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(task_id, user_id)
);
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 DEFAULT 10,
max_interval_minutes INTEGER NOT NULL DEFAULT 20,
daily_limit INTEGER NOT NULL DEFAULT 15,
history_limit INTEGER NOT NULL DEFAULT 35,
max_invites_per_cycle INTEGER NOT NULL DEFAULT 1,
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,
task_invite_access TEXT NOT NULL DEFAULT '',
task_invite_access_at TEXT NOT NULL DEFAULT '',
allow_start_without_invite_rights INTEGER NOT NULL DEFAULT 1,
parse_participants INTEGER NOT NULL DEFAULT 0,
invite_via_admins INTEGER NOT NULL DEFAULT 0,
invite_admin_master_id INTEGER NOT NULL DEFAULT 0,
invite_admin_allow_flood INTEGER NOT NULL DEFAULT 0,
invite_admin_anonymous INTEGER NOT NULL DEFAULT 1,
separate_confirm_roles INTEGER NOT NULL DEFAULT 0,
max_confirm_bots INTEGER NOT NULL DEFAULT 1,
use_watcher_invite_no_username INTEGER NOT NULL DEFAULT 1,
warmup_enabled INTEGER NOT NULL DEFAULT 1,
warmup_start_limit INTEGER NOT NULL DEFAULT 3,
warmup_daily_increase INTEGER NOT NULL DEFAULT 2,
cycle_competitors INTEGER NOT NULL DEFAULT 0,
competitor_cursor INTEGER NOT NULL DEFAULT 0,
invite_link_on_fail INTEGER NOT NULL DEFAULT 0,
role_mode TEXT NOT NULL DEFAULT 'manual',
last_stop_reason TEXT NOT NULL DEFAULT '',
last_stop_at TEXT NOT NULL DEFAULT '',
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,
role_monitor INTEGER NOT NULL DEFAULT 1,
role_invite INTEGER NOT NULL DEFAULT 1,
role_confirm INTEGER NOT NULL DEFAULT 1,
invite_limit INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS task_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
action TEXT NOT NULL,
details TEXT NOT NULL,
created_at TEXT NOT NULL
);
`);
const ensureColumn = (table, column, definition) => {
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
const exists = columns.some((col) => col.name === column);
if (!exists) {
db.prepare(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`).run();
}
};
const ensureTable = (table, ddl) => {
const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
if (!row) {
db.exec(ddl);
}
};
ensureColumn("invite_queue", "username", "TEXT DEFAULT ''");
ensureColumn("invite_queue", "user_access_hash", "TEXT DEFAULT ''");
ensureColumn("invite_queue", "watcher_account_id", "INTEGER DEFAULT 0");
ensureColumn("invites", "username", "TEXT DEFAULT ''");
ensureColumn("invites", "user_access_hash", "TEXT DEFAULT ''");
ensureColumn("invites", "confirmed", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("invites", "confirm_error", "TEXT NOT NULL DEFAULT ''");
ensureTable("confirm_queue", `
CREATE TABLE IF NOT EXISTS confirm_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER DEFAULT 0,
user_id TEXT NOT NULL,
username TEXT DEFAULT '',
account_id INTEGER DEFAULT 0,
watcher_account_id INTEGER DEFAULT 0,
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 2,
next_check_at TEXT NOT NULL,
last_error TEXT DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(task_id, user_id)
)
`);
ensureColumn("logs", "meta", "TEXT NOT NULL DEFAULT ''");
ensureColumn("tasks", "max_invites_per_cycle", "INTEGER NOT NULL DEFAULT 20");
ensureColumn("fallback_queue", "username", "TEXT DEFAULT ''");
ensureColumn("fallback_queue", "source_chat", "TEXT DEFAULT ''");
ensureColumn("fallback_queue", "target_chat", "TEXT DEFAULT ''");
ensureColumn("fallback_queue", "reason", "TEXT NOT NULL DEFAULT ''");
ensureColumn("fallback_queue", "route", "TEXT NOT NULL DEFAULT ''");
ensureColumn("fallback_queue", "status", "TEXT NOT NULL DEFAULT 'pending'");
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("accounts", "username", "TEXT DEFAULT ''");
ensureColumn("invites", "watcher_account_id", "INTEGER DEFAULT 0");
ensureColumn("invites", "watcher_phone", "TEXT DEFAULT ''");
ensureColumn("invites", "strategy", "TEXT DEFAULT ''");
ensureColumn("invites", "strategy_meta", "TEXT DEFAULT ''");
ensureColumn("invites", "source_chat", "TEXT DEFAULT ''");
ensureColumn("invites", "target_chat", "TEXT DEFAULT ''");
ensureColumn("invites", "target_type", "TEXT DEFAULT ''");
ensureColumn("invites", "action", "TEXT DEFAULT 'invite'");
ensureColumn("invites", "skipped_reason", "TEXT DEFAULT ''");
ensureColumn("invites", "archived", "INTEGER NOT NULL DEFAULT 0");
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");
ensureColumn("tasks", "separate_bot_roles", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "require_same_bot_in_both", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "task_invite_access", "TEXT NOT NULL DEFAULT ''");
ensureColumn("tasks", "task_invite_access_at", "TEXT NOT NULL DEFAULT ''");
ensureColumn("tasks", "allow_start_without_invite_rights", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("tasks", "parse_participants", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "invite_via_admins", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "invite_admin_master_id", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "invite_admin_allow_flood", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "invite_admin_anonymous", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("tasks", "separate_confirm_roles", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "max_confirm_bots", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("tasks", "use_watcher_invite_no_username", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("task_accounts", "role_confirm", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("tasks", "warmup_enabled", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "warmup_start_limit", "INTEGER NOT NULL DEFAULT 3");
ensureColumn("tasks", "warmup_daily_increase", "INTEGER NOT NULL DEFAULT 2");
ensureColumn("tasks", "cycle_competitors", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "competitor_cursor", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "invite_link_on_fail", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "role_mode", "TEXT NOT NULL DEFAULT 'manual'");
ensureColumn("tasks", "last_stop_reason", "TEXT NOT NULL DEFAULT ''");
ensureColumn("tasks", "last_stop_at", "TEXT NOT NULL DEFAULT ''");
ensureColumn("task_accounts", "role_monitor", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("task_accounts", "role_invite", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("task_accounts", "invite_limit", "INTEGER NOT NULL DEFAULT 0");
const settingsRow = db.prepare("SELECT value FROM settings WHERE key = ?").get("settings");
if (!settingsRow) {
db.prepare("INSERT INTO settings (key, value) VALUES (?, ?)")
.run("settings", JSON.stringify(DEFAULT_SETTINGS));
}
function getSettings() {
const row = db.prepare("SELECT value FROM settings WHERE key = ?").get("settings");
if (!row) return { ...DEFAULT_SETTINGS };
try {
const parsed = JSON.parse(row.value);
const normalized = { ...DEFAULT_SETTINGS, ...parsed };
if (typeof normalized.competitorGroup === "string" && normalized.competitorGroups == null) {
normalized.competitorGroups = [normalized.competitorGroup];
}
if (!Array.isArray(normalized.competitorGroups) || normalized.competitorGroups.length === 0) {
normalized.competitorGroups = [""];
}
return normalized;
} catch (error) {
return { ...DEFAULT_SETTINGS };
}
}
function saveSettings(settings) {
const payload = { ...DEFAULT_SETTINGS, ...settings };
if (typeof payload.competitorGroup === "string" && payload.competitorGroups == null) {
payload.competitorGroups = [payload.competitorGroup];
}
if (!Array.isArray(payload.competitorGroups) || payload.competitorGroups.length === 0) {
payload.competitorGroups = [""];
}
db.prepare("UPDATE settings SET value = ? WHERE key = ?")
.run(JSON.stringify(payload), "settings");
return payload;
}
function listAccounts() {
return db.prepare("SELECT * FROM accounts ORDER BY id DESC").all();
}
function clearAllData() {
db.prepare("DELETE FROM task_accounts").run();
db.prepare("DELETE FROM task_competitors").run();
db.prepare("DELETE FROM tasks").run();
db.prepare("DELETE FROM invite_queue").run();
db.prepare("DELETE FROM invites").run();
db.prepare("DELETE FROM fallback_queue").run();
db.prepare("DELETE FROM logs").run();
db.prepare("DELETE FROM account_events").run();
db.prepare("DELETE FROM accounts").run();
db.prepare("DELETE FROM settings").run();
db.prepare("INSERT INTO settings (key, value) VALUES (?, ?)")
.run("settings", JSON.stringify(DEFAULT_SETTINGS));
}
function clearAllSessions() {
db.prepare("DELETE FROM task_accounts").run();
db.prepare("DELETE FROM accounts").run();
}
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) {
const now = dayjs().toISOString();
const result = db.prepare(`
INSERT INTO accounts (phone, api_id, api_hash, session, user_id, username, max_groups, daily_limit, status, last_error, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
account.phone,
account.apiId,
account.apiHash,
account.session,
account.userId || "",
account.username || "",
account.maxGroups ?? DEFAULT_SETTINGS.accountMaxGroups,
account.dailyLimit ?? DEFAULT_SETTINGS.accountDailyLimit,
account.status || "ok",
account.lastError || "",
now,
now
);
return result.lastInsertRowid;
}
function updateAccountStatus(id, status, lastError) {
const now = dayjs().toISOString();
db.prepare("UPDATE accounts SET status = ?, last_error = ?, updated_at = ? WHERE id = ?")
.run(status, lastError || "", now, id);
}
function updateAccountIdentity(id, userId, phone, username) {
const now = dayjs().toISOString();
db.prepare("UPDATE accounts SET user_id = ?, phone = ?, username = ?, updated_at = ? WHERE id = ?")
.run(userId || "", phone || "", username || "", 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 settings = getSettings();
const quietMinutes = Number(settings.quietModeMinutes || 0);
if (quietMinutes > 0) {
const last = db.prepare(`
SELECT created_at FROM account_events
WHERE account_id = ? AND event_type = ? AND message = ?
ORDER BY id DESC LIMIT 1
`).get(accountId, eventType, message);
if (last && last.created_at) {
const lastAt = dayjs(last.created_at);
if (dayjs().diff(lastAt, "minute") < quietMinutes) {
return;
}
}
}
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 clearAccountEvents() {
db.prepare("DELETE FROM account_events").run();
}
function addTaskAudit(taskId, action, details) {
const now = dayjs().toISOString();
db.prepare(`
INSERT INTO task_audit (task_id, action, details, created_at)
VALUES (?, ?, ?, ?)
`).run(taskId || 0, action || "", details || "", now);
}
function listTaskAudit(taskId, limit) {
return db.prepare(`
SELECT * FROM task_audit
WHERE task_id = ?
ORDER BY id DESC
LIMIT ?
`).all(taskId || 0, limit || 200).map((row) => ({
id: row.id,
taskId: row.task_id,
action: row.action,
details: row.details,
createdAt: row.created_at
}));
}
function enqueueInvite(taskId, userId, username, sourceChat, accessHash, watcherAccountId) {
const now = dayjs().toISOString();
try {
if (taskId) {
const existing = db.prepare(
"SELECT status FROM invites WHERE task_id = ? AND user_id = ? ORDER BY invited_at DESC LIMIT 1"
).get(taskId || 0, userId);
if (existing && existing.status === "success") {
return false;
}
}
const result = db.prepare(`
INSERT OR IGNORE INTO invite_queue (task_id, user_id, username, user_access_hash, watcher_account_id, source_chat, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)
`).run(taskId || 0, userId, username || "", accessHash || "", watcherAccountId || 0, sourceChat, now, now);
return result.changes > 0;
} catch (error) {
return false;
}
}
function getInviteStatus(taskId, userId, sourceChat) {
const row = db.prepare(
"SELECT status FROM invite_queue WHERE task_id = ? AND user_id = ? AND source_chat = ?"
).get(taskId || 0, userId, sourceChat || "");
return row ? row.status : "";
}
function getLastInviteError(taskId, userId, sourceChat) {
const row = db.prepare(`
SELECT error
FROM invites
WHERE task_id = ? AND user_id = ? AND source_chat = ?
ORDER BY invited_at DESC
LIMIT 1
`).get(taskId || 0, userId, sourceChat || "");
return row ? row.error || "" : "";
}
function getPendingInvites(taskId, limit, offset = 0) {
return db.prepare(`
SELECT * FROM invite_queue
WHERE status = 'pending' AND task_id = ?
ORDER BY id ASC
LIMIT ? OFFSET ?
`).all(taskId || 0, limit, offset);
}
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 getPendingStats(taskId) {
const base = taskId == null
? "WHERE status = 'pending'"
: "WHERE status = 'pending' AND task_id = ?";
const params = taskId == null ? [] : [taskId || 0];
const totalRow = db.prepare(`SELECT COUNT(*) as count FROM invite_queue ${base}`).get(...params);
const usernameRow = db.prepare(
`SELECT COUNT(*) as count FROM invite_queue ${base} AND username != ''`
).get(...params);
const hashRow = db.prepare(
`SELECT COUNT(*) as count FROM invite_queue ${base} AND user_access_hash != ''`
).get(...params);
const emptyRow = db.prepare(
`SELECT COUNT(*) as count FROM invite_queue ${base} AND username = '' AND user_access_hash = ''`
).get(...params);
return {
total: totalRow.count || 0,
withUsername: usernameRow.count || 0,
withAccessHash: hashRow.count || 0,
withoutData: emptyRow.count || 0
};
}
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 clearQueueItems(taskId, ids) {
const list = Array.isArray(ids)
? ids.map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0)
: [];
if (!list.length) return 0;
const placeholders = list.map(() => "?").join(",");
if (taskId == null) {
const result = db.prepare(`DELETE FROM invite_queue WHERE id IN (${placeholders})`).run(...list);
return result.changes || 0;
}
const result = db
.prepare(`DELETE FROM invite_queue WHERE task_id = ? AND id IN (${placeholders})`)
.run(taskId || 0, ...list);
return result.changes || 0;
}
function clearQueueOlderThan(taskId, hours) {
const limit = Number(hours || 0);
if (!Number.isFinite(limit) || limit <= 0) return 0;
const cutoff = dayjs().subtract(limit, "hour").toISOString();
if (taskId == null) {
const result = db.prepare("DELETE FROM invite_queue WHERE created_at < ?").run(cutoff);
return result.changes || 0;
}
const result = db.prepare("DELETE FROM invite_queue WHERE task_id = ? AND created_at < ?").run(taskId || 0, cutoff);
return result.changes || 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 setTaskCompetitorCursor(taskId, cursor) {
const now = dayjs().toISOString();
db.prepare("UPDATE tasks SET competitor_cursor = ?, updated_at = ? WHERE id = ?")
.run(Number(cursor || 0), now, taskId || 0);
}
function getEffectiveDailyLimit(task) {
if (!task) return 0;
const baseLimit = Number(task.daily_limit || 0);
if (!task.warmup_enabled) return baseLimit;
const createdAt = task.created_at ? new Date(task.created_at).getTime() : Date.now();
const days = Math.max(0, Math.floor((Date.now() - createdAt) / (24 * 60 * 60 * 1000)));
const dayIndex = days + 1;
let warmed = 7;
if (dayIndex <= 3) warmed = 1;
else if (dayIndex <= 7) warmed = 2;
else if (dayIndex <= 12) warmed = 3;
else if (dayIndex <= 18) warmed = 4;
else if (dayIndex <= 25) warmed = 5;
else if (dayIndex <= 33) warmed = 6;
const startLimit = Number(task.warmup_start_limit || 0);
const warmedLimit = startLimit > 0 ? Math.max(1, startLimit + (warmed - 1)) : warmed;
if (baseLimit > 0) {
return Math.min(baseLimit, warmedLimit);
}
return warmedLimit;
}
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_invites_per_cycle = ?, max_competitor_bots = ?, max_our_bots = ?, random_accounts = ?, multi_accounts_per_run = ?,
retry_on_fail = ?, auto_join_competitors = ?, auto_join_our_group = ?, separate_bot_roles = ?,
require_same_bot_in_both = ?, stop_on_blocked = ?, stop_blocked_percent = ?, notes = ?, enabled = ?,
allow_start_without_invite_rights = ?, parse_participants = ?, invite_via_admins = ?, invite_admin_master_id = ?,
invite_admin_allow_flood = ?, invite_admin_anonymous = ?, separate_confirm_roles = ?, max_confirm_bots = ?,
use_watcher_invite_no_username = ?,
warmup_enabled = ?, warmup_start_limit = ?, warmup_daily_increase = ?,
cycle_competitors = ?, competitor_cursor = ?, invite_link_on_fail = ?, role_mode = ?, updated_at = ?
WHERE id = ?
`).run(
task.name,
task.ourGroup,
task.minIntervalMinutes,
task.maxIntervalMinutes,
task.dailyLimit,
task.historyLimit,
task.maxInvitesPerCycle || 1,
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.separateBotRoles ? 1 : 0,
task.requireSameBotInBoth ? 1 : 0,
task.stopOnBlocked ? 1 : 0,
task.stopBlockedPercent || 25,
task.notes || "",
task.enabled ? 1 : 0,
task.allowStartWithoutInviteRights ? 1 : 0,
task.parseParticipants ? 1 : 0,
task.inviteViaAdmins ? 1 : 0,
task.inviteAdminMasterId || 0,
task.inviteAdminAllowFlood ? 1 : 0,
task.inviteAdminAnonymous ? 1 : 0,
task.separateConfirmRoles ? 1 : 0,
task.maxConfirmBots || 1,
task.useWatcherInviteNoUsername ? 1 : 0,
task.warmupEnabled ? 1 : 0,
task.warmupStartLimit || 3,
task.warmupDailyIncrease || 2,
task.cycleCompetitors ? 1 : 0,
task.competitorCursor || 0,
task.inviteLinkOnFail ? 1 : 0,
task.rolesMode || "manual",
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_invites_per_cycle, max_competitor_bots, max_our_bots, random_accounts, multi_accounts_per_run, retry_on_fail, auto_join_competitors,
auto_join_our_group, separate_bot_roles, require_same_bot_in_both, stop_on_blocked, stop_blocked_percent, notes, enabled,
allow_start_without_invite_rights, parse_participants, invite_via_admins, invite_admin_master_id,
invite_admin_allow_flood, invite_admin_anonymous, separate_confirm_roles, max_confirm_bots,
use_watcher_invite_no_username,
warmup_enabled, warmup_start_limit, warmup_daily_increase, cycle_competitors,
competitor_cursor, invite_link_on_fail, role_mode, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
task.name,
task.ourGroup,
task.minIntervalMinutes,
task.maxIntervalMinutes,
task.dailyLimit,
task.historyLimit,
task.maxInvitesPerCycle || 1,
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.separateBotRoles ? 1 : 0,
task.requireSameBotInBoth ? 1 : 0,
task.stopOnBlocked ? 1 : 0,
task.stopBlockedPercent || 25,
task.notes || "",
task.enabled ? 1 : 0,
task.allowStartWithoutInviteRights ? 1 : 0,
task.parseParticipants ? 1 : 0,
task.inviteViaAdmins ? 1 : 0,
task.inviteAdminMasterId || 0,
task.inviteAdminAllowFlood ? 1 : 0,
task.inviteAdminAnonymous ? 1 : 0,
task.separateConfirmRoles ? 1 : 0,
task.maxConfirmBots || 1,
task.useWatcherInviteNoUsername ? 1 : 0,
task.warmupEnabled ? 1 : 0,
task.warmupStartLimit || 3,
task.warmupDailyIncrease || 2,
task.cycleCompetitors ? 1 : 0,
task.competitorCursor || 0,
task.inviteLinkOnFail ? 1 : 0,
task.rolesMode || "manual",
now,
now
);
return result.lastInsertRowid;
}
function setTaskInviteAccess(taskId, payload) {
const now = dayjs().toISOString();
const value = payload ? JSON.stringify(payload) : "";
db.prepare("UPDATE tasks SET task_invite_access = ?, task_invite_access_at = ?, updated_at = ? WHERE id = ?")
.run(value, value ? now : "", now, taskId);
}
function setTaskStopReason(taskId, reason) {
const now = dayjs().toISOString();
db.prepare("UPDATE tasks SET last_stop_reason = ?, last_stop_at = ?, updated_at = ? WHERE id = ?")
.run(reason || "", reason ? now : "", now, taskId);
}
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, role_monitor, role_invite, role_confirm, invite_limit)
VALUES (?, ?, ?, ?, ?, ?)
`);
(accountIds || []).forEach((accountId) => stmt.run(taskId, accountId, 1, 1, 1, 7));
}
function setTaskAccountRoles(taskId, roles) {
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId);
const stmt = db.prepare(`
INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite, role_confirm, invite_limit)
VALUES (?, ?, ?, ?, ?, ?)
`);
(roles || []).forEach((item) => {
const roleConfirm = item.roleConfirm != null ? item.roleConfirm : item.roleInvite;
stmt.run(
taskId,
item.accountId,
item.roleMonitor ? 1 : 0,
item.roleInvite ? 1 : 0,
roleConfirm ? 1 : 0,
Number(item.inviteLimit || 0)
);
});
}
function markInviteStatus(queueId, status) {
const now = dayjs().toISOString();
db.prepare("UPDATE invite_queue SET status = ?, updated_at = ? WHERE id = ?")
.run(status, now, queueId);
}
function incrementInviteAttempt(queueId) {
const now = dayjs().toISOString();
db.prepare("UPDATE invite_queue SET attempts = attempts + 1, updated_at = ? WHERE id = ?")
.run(now, queueId);
}
function recordInvite(
taskId,
userId,
username,
accountId,
accountPhone,
sourceChat,
status,
error,
skippedReason,
action,
userAccessHash,
watcherAccountId,
watcherPhone,
strategy,
strategyMeta,
targetChat,
targetType,
confirmed = true,
confirmError = ""
) {
const now = dayjs().toISOString();
db.prepare(`
INSERT INTO invites (
task_id,
user_id,
username,
user_access_hash,
account_id,
account_phone,
watcher_account_id,
watcher_phone,
strategy,
strategy_meta,
source_chat,
target_chat,
target_type,
action,
skipped_reason,
invited_at,
status,
error,
confirmed,
confirm_error,
archived
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
`).run(
taskId || 0,
userId,
username || "",
userAccessHash || "",
accountId || 0,
accountPhone || "",
watcherAccountId || 0,
watcherPhone || "",
strategy || "",
strategyMeta || "",
sourceChat || "",
targetChat || "",
targetType || "",
action || "invite",
skippedReason || "",
now,
status,
error || "",
confirmed ? 1 : 0,
confirmError || ""
);
}
function addFallback(taskId, userId, username, sourceChat, targetChat, reason, route) {
const now = dayjs().toISOString();
if (!userId) return false;
try {
const result = db.prepare(`
INSERT OR IGNORE INTO fallback_queue
(task_id, user_id, username, source_chat, target_chat, reason, route, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?)
`).run(
taskId || 0,
userId,
username || "",
sourceChat || "",
targetChat || "",
reason || "",
route || "",
now
);
return result.changes > 0;
} catch (error) {
return false;
}
}
function addConfirmQueue(taskId, userId, username, accountId, watcherAccountId, nextCheckAt, maxAttempts = 2) {
const now = dayjs().toISOString();
db.prepare(`
INSERT OR REPLACE INTO confirm_queue
(task_id, user_id, username, account_id, watcher_account_id, attempts, max_attempts, next_check_at, last_error, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
taskId || 0,
userId,
username || "",
accountId || 0,
watcherAccountId || 0,
0,
maxAttempts,
nextCheckAt,
"",
now,
now
);
}
function listConfirmQueue(taskId, limit = 200) {
if (taskId) {
return db.prepare("SELECT * FROM confirm_queue WHERE task_id = ? ORDER BY next_check_at ASC LIMIT ?")
.all(taskId, limit);
}
return db.prepare("SELECT * FROM confirm_queue ORDER BY next_check_at ASC LIMIT ?")
.all(limit);
}
function listDueConfirmQueue(taskId, nowIso, limit = 50) {
return db.prepare(`
SELECT * FROM confirm_queue
WHERE task_id = ?
AND next_check_at <= ?
AND attempts < max_attempts
ORDER BY next_check_at ASC
LIMIT ?
`).all(taskId, nowIso, limit);
}
function updateConfirmQueue(id, fields) {
if (!id) return;
const now = dayjs().toISOString();
db.prepare(`
UPDATE confirm_queue
SET attempts = ?, next_check_at = ?, last_error = ?, updated_at = ?
WHERE id = ?
`).run(
fields.attempts,
fields.nextCheckAt,
fields.lastError || "",
now,
id
);
}
function deleteConfirmQueue(id) {
db.prepare("DELETE FROM confirm_queue WHERE id = ?").run(id);
}
function clearConfirmQueue(taskId) {
if (taskId) {
db.prepare("DELETE FROM confirm_queue WHERE task_id = ?").run(taskId);
} else {
db.prepare("DELETE FROM confirm_queue").run();
}
}
function updateInviteConfirmation(taskId, userId, confirmed, confirmError) {
const row = db.prepare(`
SELECT id FROM invites
WHERE task_id = ? AND user_id = ?
ORDER BY invited_at DESC
LIMIT 1
`).get(taskId || 0, String(userId || ""));
if (!row || !row.id) return;
db.prepare("UPDATE invites SET confirmed = ?, confirm_error = ? WHERE id = ?")
.run(confirmed ? 1 : 0, confirmError || "", row.id);
}
function listFallback(limit, taskId) {
let rows = [];
if (taskId != null) {
rows = db.prepare(`
SELECT * FROM fallback_queue
WHERE task_id = ?
ORDER BY id DESC
LIMIT ?
`).all(taskId || 0, limit || 500);
} else {
rows = db.prepare(`
SELECT * FROM fallback_queue
ORDER BY id DESC
LIMIT ?
`).all(limit || 500);
}
return rows.map((row) => ({
id: row.id,
taskId: row.task_id || 0,
userId: row.user_id,
username: row.username || "",
sourceChat: row.source_chat || "",
targetChat: row.target_chat || "",
reason: row.reason || "",
route: row.route || "",
status: row.status || "pending",
createdAt: row.created_at
}));
}
function updateFallbackStatus(id, status) {
if (!id) return;
db.prepare("UPDATE fallback_queue SET status = ? WHERE id = ?")
.run(status || "done", id);
}
function clearFallback(taskId) {
if (taskId == null) {
db.prepare("DELETE FROM fallback_queue").run();
return;
}
db.prepare("DELETE FROM fallback_queue WHERE task_id = ?").run(taskId || 0);
}
function countInvitesToday(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'"
).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 countInvitesByStatus(taskId, status) {
if (!status) return 0;
if (taskId == null) {
return db.prepare(
"SELECT COUNT(*) as count FROM invites WHERE status = ? AND archived = 0"
).get(status).count;
}
return db.prepare(
"SELECT COUNT(*) as count FROM invites WHERE status = ? AND task_id = ? AND archived = 0"
).get(status, taskId || 0).count;
}
function addLog(entry) {
db.prepare(`
INSERT INTO logs (task_id, started_at, finished_at, invited_count, success_ids, error_summary, meta)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
entry.taskId || 0,
entry.startedAt,
entry.finishedAt,
entry.invitedCount,
JSON.stringify(entry.successIds || []),
JSON.stringify(entry.errors || []),
JSON.stringify(entry.meta || {})
);
}
function listLogs(limit, taskId) {
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) => ({
id: row.id,
taskId: row.task_id || 0,
startedAt: row.started_at,
finishedAt: row.finished_at,
invitedCount: row.invited_count,
successIds: JSON.parse(row.success_ids || "[]"),
errors: JSON.parse(row.error_summary || "[]"),
meta: JSON.parse(row.meta || "{}")
}));
}
function clearLogs(taskId) {
if (taskId == null) {
db.prepare("DELETE FROM logs").run();
return;
}
db.prepare("DELETE FROM logs WHERE task_id = ?").run(taskId || 0);
}
function listInvites(limit, taskId) {
let rows = [];
if (taskId != null) {
rows = db.prepare(`
SELECT * FROM invites
WHERE task_id = ? AND archived = 0
ORDER BY id DESC
LIMIT ?
`).all(taskId || 0, limit || 200);
} else {
rows = db.prepare(`
SELECT * FROM invites
WHERE archived = 0
ORDER BY id DESC
LIMIT ?
`).all(limit || 200);
}
return rows.map((row) => ({
id: row.id,
taskId: row.task_id || 0,
userId: row.user_id,
username: row.username || "",
userAccessHash: row.user_access_hash || "",
accountId: row.account_id || 0,
accountPhone: row.account_phone || "",
watcherAccountId: row.watcher_account_id || 0,
watcherPhone: row.watcher_phone || "",
strategy: row.strategy || "",
strategyMeta: row.strategy_meta || "",
sourceChat: row.source_chat || "",
targetChat: row.target_chat || "",
targetType: row.target_type || "",
action: row.action || "invite",
skippedReason: row.skipped_reason || "",
invitedAt: row.invited_at,
status: row.status,
error: row.error,
confirmed: row.confirmed !== 0,
confirmError: row.confirm_error || ""
}));
}
function clearInvites(taskId) {
if (taskId == null) {
db.prepare("UPDATE invites SET archived = 1").run();
return;
}
db.prepare("UPDATE invites SET archived = 1 WHERE task_id = ?").run(taskId || 0);
}
return {
getSettings,
saveSettings,
listAccounts,
findAccountByIdentity,
clearAllData,
clearAllSessions,
listTasks,
getTask,
saveTask,
setTaskInviteAccess,
setTaskStopReason,
deleteTask,
listTaskCompetitors,
setTaskCompetitors,
listTaskAccounts,
listAllTaskAccounts,
getEffectiveDailyLimit,
setTaskCompetitorCursor,
setTaskAccounts,
setTaskAccountRoles,
listLogs,
listInvites,
clearLogs,
clearInvites,
addFallback,
listFallback,
updateFallbackStatus,
clearFallback,
addConfirmQueue,
listConfirmQueue,
listDueConfirmQueue,
updateConfirmQueue,
deleteConfirmQueue,
clearConfirmQueue,
updateInviteConfirmation,
setAccountCooldown,
clearAccountCooldown,
addAccountEvent,
listAccountEvents,
clearAccountEvents,
addTaskAudit,
listTaskAudit,
deleteAccount,
updateAccountIdentity,
addAccount,
updateAccountStatus,
enqueueInvite,
getInviteStatus,
getLastInviteError,
getPendingInvites,
getPendingCount,
getPendingStats,
clearQueue,
clearQueueItems,
clearQueueOlderThan,
markInviteStatus,
incrementInviteAttempt,
recordInvite,
countInvitesToday,
countInvitesTodayByAccount,
countInvitesByStatus,
addLog
};
}
module.exports = { initStore };