telegram-invite-automation/src/main/taskRunner.js
2026-01-17 22:54:41 +04:00

191 lines
6.6 KiB
JavaScript

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 };