telegram-invite-automation/src/main/store.js
2026-01-21 01:00:03 +04:00

1042 lines
35 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: 5,
maxIntervalMinutes: 10,
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
);
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 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,
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,
warmup_enabled INTEGER NOT NULL DEFAULT 0,
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,
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
);
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();
}
};
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 ''");
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", "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", "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");
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 {
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 getPendingInvites(taskId, limit) {
return db.prepare(`
SELECT * FROM invite_queue
WHERE status = 'pending' AND task_id = ?
ORDER BY id ASC
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 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 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 startLimit = Math.max(1, Number(task.warmup_start_limit || 1));
const step = Math.max(0, Number(task.warmup_daily_increase || 0));
const warmed = startLimit + days * step;
return Math.min(baseLimit || warmed, warmed);
}
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 = ?, 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 = ?, warmup_enabled = ?, warmup_start_limit = ?, warmup_daily_increase = ?,
cycle_competitors = ?, competitor_cursor = ?, invite_link_on_fail = ?, 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.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.warmupEnabled ? 1 : 0,
task.warmupStartLimit || 3,
task.warmupDailyIncrease || 2,
task.cycleCompetitors ? 1 : 0,
task.competitorCursor || 0,
task.inviteLinkOnFail ? 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, 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, warmup_enabled, warmup_start_limit, warmup_daily_increase, cycle_competitors,
competitor_cursor, invite_link_on_fail, 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.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.warmupEnabled ? 1 : 0,
task.warmupStartLimit || 3,
task.warmupDailyIncrease || 2,
task.cycleCompetitors ? 1 : 0,
task.competitorCursor || 0,
task.inviteLinkOnFail ? 1 : 0,
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)
VALUES (?, ?, ?, ?)
`);
(accountIds || []).forEach((accountId) => stmt.run(taskId, accountId, 1, 1));
}
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)
VALUES (?, ?, ?, ?)
`);
(roles || []).forEach((item) => {
stmt.run(
taskId,
item.accountId,
item.roleMonitor ? 1 : 0,
item.roleInvite ? 1 : 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 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 addLog(entry) {
db.prepare(`
INSERT INTO logs (task_id, started_at, finished_at, invited_count, success_ids, error_summary)
VALUES (?, ?, ?, ?, ?, ?)
`).run(
entry.taskId || 0,
entry.startedAt,
entry.finishedAt,
entry.invitedCount,
JSON.stringify(entry.successIds || []),
JSON.stringify(entry.errors || [])
);
}
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 || "[]")
}));
}
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,
setAccountCooldown,
clearAccountCooldown,
addAccountEvent,
listAccountEvents,
clearAccountEvents,
addTaskAudit,
listTaskAudit,
deleteAccount,
updateAccountIdentity,
addAccount,
updateAccountStatus,
enqueueInvite,
getInviteStatus,
getPendingInvites,
getPendingCount,
getPendingStats,
clearQueue,
clearQueueOlderThan,
markInviteStatus,
incrementInviteAttempt,
recordInvite,
countInvitesToday,
countInvitesTodayByAccount,
addLog
};
}
module.exports = { initStore };