telegram-invite-automation/src/main/taskRunner.js
2026-01-18 23:29:16 +04:00

236 lines
8.4 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 = "";
this.nextInviteAccountId = 0;
this.lastInviteAccountId = 0;
}
isRunning() {
return this.running;
}
getNextRunAt() {
return this.nextRunAt || "";
}
getNextInviteAccountId() {
return this.nextInviteAccountId || 0;
}
getLastInviteAccountId() {
return this.lastInviteAccountId || 0;
}
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.nextInviteAccountId = 0;
this.telegram.stopTaskMonitor(this.task.id);
}
async _initMonitoring() {
const competitors = this.store.listTaskCompetitors(this.task.id).map((row) => row.link);
const accountRows = this.store.listTaskAccounts(this.task.id);
const accounts = accountRows.map((row) => row.account_id);
const monitorIds = accountRows.filter((row) => row.role_monitor).map((row) => row.account_id);
const inviteIds = accountRows.filter((row) => row.role_invite).map((row) => row.account_id);
await this.telegram.joinGroupsForTask(this.task, competitors, accounts, {
monitorIds,
inviteIds
});
await this.telegram.startTaskMonitor(this.task, competitors, accounts, monitorIds);
}
_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 = "";
this.nextInviteAccountId = 0;
try {
const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id);
let inviteAccounts = accounts;
const roles = this.telegram.getTaskRoleAssignments(this.task.id);
const hasExplicitRoles = roles && ((roles.ourIds || []).length || (roles.competitorIds || []).length);
if (hasExplicitRoles) {
inviteAccounts = (roles.ourIds || []).length ? roles.ourIds : (roles.competitorIds || []);
if (!inviteAccounts.length) {
errors.push("No invite accounts (role-based)");
}
} else if (this.task.separate_bot_roles || this.task.require_same_bot_in_both) {
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] : [];
this.nextInviteAccountId = entry ? entry.account.id : 0;
} else if (inviteAccounts.length) {
this.nextInviteAccountId = inviteAccounts[0];
}
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.lastInviteAccountId = result.accountId || this.lastInviteAccountId;
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
);
const detailed = [
`user=${item.user_id}`,
`error=${result.error || "unknown"}`,
`strategy=${result.strategy || "—"}`,
`meta=${result.strategyMeta || "—"}`,
`source=${item.source_chat || "—"}`,
`account=${result.accountPhone || result.accountId || "—"}`
].join(" | ");
this.store.addAccountEvent(
watcherAccount ? watcherAccount.id : 0,
watcherAccount ? watcherAccount.phone : "",
"invite_failed",
detailed
);
}
}
if (!pending.length) {
errors.push("queue empty");
}
}
} 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 };