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; this.nextRunAt = ""; } isRunning() { return this.running; } getNextRunAt() { return this.nextRunAt || ""; } 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.nextRunAt = ""; 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.nextRunAt = new Date(Date.now() + jitter).toISOString(); this.timer = setTimeout(() => this._runBatch(), jitter); } async _runBatch() { const startedAt = dayjs().toISOString(); const errors = []; const successIds = []; let invitedCount = 0; this.nextRunAt = ""; try { const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id); let inviteAccounts = accounts; if (this.task.separate_bot_roles || this.task.require_same_bot_in_both) { const roles = this.telegram.getTaskRoleAssignments(this.task.id); inviteAccounts = this.task.require_same_bot_in_both ? (roles.competitorIds || []) : (roles.ourIds || []); if (!inviteAccounts.length) { errors.push(this.task.require_same_bot_in_both ? "No invite accounts (same bot required)" : "No invite accounts (separated roles)"); } } else { const limit = Math.max(1, Number(this.task.max_our_bots || accounts.length || 1)); if (inviteAccounts.length > limit) { inviteAccounts = inviteAccounts.slice(0, limit); } } if (!accounts.length) { errors.push("No accounts assigned"); } 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); const accountMap = new Map(this.store.listAccounts().map((account) => [account.id, account])); 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; } let accountsForInvite = inviteAccounts; if (item.watcher_account_id && !inviteAccounts.includes(item.watcher_account_id)) { accountsForInvite = [item.watcher_account_id]; } const watcherAccount = accountMap.get(item.watcher_account_id || 0); const result = await this.telegram.inviteUserForTask(this.task, item.user_id, accountsForInvite, { randomize: Boolean(this.task.random_accounts), userAccessHash: item.user_access_hash, username: item.username, sourceChat: item.source_chat }); 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", item.user_access_hash, watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone : "", result.strategy, result.strategyMeta ); } 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", item.user_access_hash, watcherAccount ? watcherAccount.id : 0, watcherAccount ? watcherAccount.phone : "", result.strategy, result.strategyMeta ); } } } } 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 };