236 lines
8.4 KiB
JavaScript
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 };
|