some
This commit is contained in:
parent
10ee8fb3c1
commit
4bba4f3149
@ -204,13 +204,22 @@ ipcMain.handle("tasks:get", (_event, id) => {
|
|||||||
return {
|
return {
|
||||||
task,
|
task,
|
||||||
competitors: store.listTaskCompetitors(id).map((row) => row.link),
|
competitors: store.listTaskCompetitors(id).map((row) => row.link),
|
||||||
accountIds: store.listTaskAccounts(id).map((row) => row.account_id)
|
accountIds: store.listTaskAccounts(id).map((row) => row.account_id),
|
||||||
|
accountRoles: store.listTaskAccounts(id).map((row) => ({
|
||||||
|
accountId: row.account_id,
|
||||||
|
roleMonitor: Boolean(row.role_monitor),
|
||||||
|
roleInvite: Boolean(row.role_invite)
|
||||||
|
}))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
ipcMain.handle("tasks:save", (_event, payload) => {
|
ipcMain.handle("tasks:save", (_event, payload) => {
|
||||||
const taskId = store.saveTask(payload.task);
|
const taskId = store.saveTask(payload.task);
|
||||||
store.setTaskCompetitors(taskId, payload.competitors || []);
|
store.setTaskCompetitors(taskId, payload.competitors || []);
|
||||||
store.setTaskAccounts(taskId, payload.accountIds || []);
|
if (payload.accountRoles && payload.accountRoles.length) {
|
||||||
|
store.setTaskAccountRoles(taskId, payload.accountRoles);
|
||||||
|
} else {
|
||||||
|
store.setTaskAccounts(taskId, payload.accountIds || []);
|
||||||
|
}
|
||||||
return { ok: true, taskId };
|
return { ok: true, taskId };
|
||||||
});
|
});
|
||||||
ipcMain.handle("tasks:delete", (_event, id) => {
|
ipcMain.handle("tasks:delete", (_event, id) => {
|
||||||
@ -250,10 +259,26 @@ ipcMain.handle("tasks:appendAccounts", (_event, payload) => {
|
|||||||
if (!payload || !payload.taskId) return { ok: false, error: "Task not found" };
|
if (!payload || !payload.taskId) return { ok: false, error: "Task not found" };
|
||||||
const task = store.getTask(payload.taskId);
|
const task = store.getTask(payload.taskId);
|
||||||
if (!task) return { ok: false, error: "Task not found" };
|
if (!task) return { ok: false, error: "Task not found" };
|
||||||
const existing = store.listTaskAccounts(payload.taskId).map((row) => row.account_id);
|
const existingRows = store.listTaskAccounts(payload.taskId);
|
||||||
const merged = Array.from(new Set([...(existing || []), ...((payload.accountIds || []))]));
|
const existing = new Map(existingRows.map((row) => [
|
||||||
store.setTaskAccounts(payload.taskId, merged);
|
row.account_id,
|
||||||
return { ok: true, accountIds: merged };
|
{ accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), roleInvite: Boolean(row.role_invite) }
|
||||||
|
]));
|
||||||
|
(payload.accountIds || []).forEach((accountId) => {
|
||||||
|
if (!existing.has(accountId)) {
|
||||||
|
existing.set(accountId, { accountId, roleMonitor: true, roleInvite: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(payload.accountRoles || []).forEach((item) => {
|
||||||
|
existing.set(item.accountId, {
|
||||||
|
accountId: item.accountId,
|
||||||
|
roleMonitor: Boolean(item.roleMonitor),
|
||||||
|
roleInvite: Boolean(item.roleInvite)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const merged = Array.from(existing.values());
|
||||||
|
store.setTaskAccountRoles(payload.taskId, merged);
|
||||||
|
return { ok: true, accountIds: merged.map((item) => item.accountId) };
|
||||||
});
|
});
|
||||||
ipcMain.handle("tasks:removeAccount", (_event, payload) => {
|
ipcMain.handle("tasks:removeAccount", (_event, payload) => {
|
||||||
if (!payload || !payload.taskId || !payload.accountId) {
|
if (!payload || !payload.taskId || !payload.accountId) {
|
||||||
@ -261,10 +286,15 @@ ipcMain.handle("tasks:removeAccount", (_event, payload) => {
|
|||||||
}
|
}
|
||||||
const task = store.getTask(payload.taskId);
|
const task = store.getTask(payload.taskId);
|
||||||
if (!task) return { ok: false, error: "Task not found" };
|
if (!task) return { ok: false, error: "Task not found" };
|
||||||
const existing = store.listTaskAccounts(payload.taskId).map((row) => row.account_id);
|
const existing = store.listTaskAccounts(payload.taskId)
|
||||||
const filtered = existing.filter((id) => id !== payload.accountId);
|
.filter((row) => row.account_id !== payload.accountId)
|
||||||
store.setTaskAccounts(payload.taskId, filtered);
|
.map((row) => ({
|
||||||
return { ok: true, accountIds: filtered };
|
accountId: row.account_id,
|
||||||
|
roleMonitor: Boolean(row.role_monitor),
|
||||||
|
roleInvite: Boolean(row.role_invite)
|
||||||
|
}));
|
||||||
|
store.setTaskAccountRoles(payload.taskId, existing);
|
||||||
|
return { ok: true, accountIds: existing.map((item) => item.accountId) };
|
||||||
});
|
});
|
||||||
ipcMain.handle("tasks:startAll", async () => {
|
ipcMain.handle("tasks:startAll", async () => {
|
||||||
const tasks = store.listTasks();
|
const tasks = store.listTasks();
|
||||||
@ -307,6 +337,33 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
|||||||
const dailyUsed = store.countInvitesToday(id);
|
const dailyUsed = store.countInvitesToday(id);
|
||||||
const task = store.getTask(id);
|
const task = store.getTask(id);
|
||||||
const monitorInfo = telegram.getTaskMonitorInfo(id);
|
const monitorInfo = telegram.getTaskMonitorInfo(id);
|
||||||
|
const warnings = [];
|
||||||
|
if (task) {
|
||||||
|
const accountRows = store.listTaskAccounts(id);
|
||||||
|
if (task.require_same_bot_in_both) {
|
||||||
|
const hasSame = accountRows.some((row) => row.role_monitor && row.role_invite);
|
||||||
|
if (!hasSame) {
|
||||||
|
warnings.push("Режим “один бот в обоих группах” включен, но нет аккаунта с обеими ролями.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const allAssignments = store.listAllTaskAccounts();
|
||||||
|
const accountMap = new Map();
|
||||||
|
allAssignments.forEach((row) => {
|
||||||
|
if (!accountMap.has(row.account_id)) accountMap.set(row.account_id, new Set());
|
||||||
|
accountMap.get(row.account_id).add(row.task_id);
|
||||||
|
});
|
||||||
|
const accountsById = new Map(store.listAccounts().map((acc) => [acc.id, acc]));
|
||||||
|
const seen = new Set();
|
||||||
|
accountRows.forEach((row) => {
|
||||||
|
const tasksForAccount = accountMap.get(row.account_id);
|
||||||
|
if (tasksForAccount && tasksForAccount.size > 1 && !seen.has(row.account_id)) {
|
||||||
|
seen.add(row.account_id);
|
||||||
|
const account = accountsById.get(row.account_id);
|
||||||
|
const label = account ? (account.phone || account.user_id || row.account_id) : row.account_id;
|
||||||
|
warnings.push(`Аккаунт ${label} используется в ${tasksForAccount.size} задачах. Лимиты действий/групп общие.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
running: runner ? runner.isRunning() : false,
|
running: runner ? runner.isRunning() : false,
|
||||||
queueCount,
|
queueCount,
|
||||||
@ -314,7 +371,11 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
|||||||
dailyLimit: task ? task.daily_limit : 0,
|
dailyLimit: task ? task.daily_limit : 0,
|
||||||
dailyRemaining: task ? Math.max(0, Number(task.daily_limit || 0) - dailyUsed) : 0,
|
dailyRemaining: task ? Math.max(0, Number(task.daily_limit || 0) - dailyUsed) : 0,
|
||||||
monitorInfo,
|
monitorInfo,
|
||||||
nextRunAt: runner ? runner.getNextRunAt() : ""
|
nextRunAt: runner ? runner.getNextRunAt() : "",
|
||||||
|
nextInviteAccountId: runner ? runner.getNextInviteAccountId() : 0,
|
||||||
|
lastInviteAccountId: runner ? runner.getLastInviteAccountId() : 0,
|
||||||
|
pendingStats: store.getPendingStats(id),
|
||||||
|
warnings
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -136,7 +136,9 @@ function initStore(userDataPath) {
|
|||||||
CREATE TABLE IF NOT EXISTS task_accounts (
|
CREATE TABLE IF NOT EXISTS task_accounts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
task_id INTEGER NOT NULL,
|
task_id INTEGER NOT NULL,
|
||||||
account_id INTEGER NOT NULL
|
account_id INTEGER NOT NULL,
|
||||||
|
role_monitor INTEGER NOT NULL DEFAULT 1,
|
||||||
|
role_invite INTEGER NOT NULL DEFAULT 1
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@ -177,6 +179,8 @@ function initStore(userDataPath) {
|
|||||||
ensureColumn("tasks", "auto_join_our_group", "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", "separate_bot_roles", "INTEGER NOT NULL DEFAULT 0");
|
||||||
ensureColumn("tasks", "require_same_bot_in_both", "INTEGER NOT NULL DEFAULT 0");
|
ensureColumn("tasks", "require_same_bot_in_both", "INTEGER NOT NULL DEFAULT 0");
|
||||||
|
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");
|
const settingsRow = db.prepare("SELECT value FROM settings WHERE key = ?").get("settings");
|
||||||
if (!settingsRow) {
|
if (!settingsRow) {
|
||||||
@ -356,6 +360,29 @@ function initStore(userDataPath) {
|
|||||||
.get(taskId || 0).count;
|
.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) {
|
function clearQueue(taskId) {
|
||||||
if (taskId == null) {
|
if (taskId == null) {
|
||||||
db.prepare("DELETE FROM invite_queue").run();
|
db.prepare("DELETE FROM invite_queue").run();
|
||||||
@ -465,8 +492,27 @@ function initStore(userDataPath) {
|
|||||||
|
|
||||||
function setTaskAccounts(taskId, accountIds) {
|
function setTaskAccounts(taskId, accountIds) {
|
||||||
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId);
|
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId);
|
||||||
const stmt = db.prepare("INSERT INTO task_accounts (task_id, account_id) VALUES (?, ?)");
|
const stmt = db.prepare(`
|
||||||
(accountIds || []).forEach((accountId) => stmt.run(taskId, accountId));
|
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) {
|
function markInviteStatus(queueId, status) {
|
||||||
@ -665,6 +711,7 @@ function initStore(userDataPath) {
|
|||||||
listTaskAccounts,
|
listTaskAccounts,
|
||||||
listAllTaskAccounts,
|
listAllTaskAccounts,
|
||||||
setTaskAccounts,
|
setTaskAccounts,
|
||||||
|
setTaskAccountRoles,
|
||||||
listLogs,
|
listLogs,
|
||||||
listInvites,
|
listInvites,
|
||||||
clearLogs,
|
clearLogs,
|
||||||
@ -680,6 +727,7 @@ function initStore(userDataPath) {
|
|||||||
enqueueInvite,
|
enqueueInvite,
|
||||||
getPendingInvites,
|
getPendingInvites,
|
||||||
getPendingCount,
|
getPendingCount,
|
||||||
|
getPendingStats,
|
||||||
clearQueue,
|
clearQueue,
|
||||||
markInviteStatus,
|
markInviteStatus,
|
||||||
incrementInviteAttempt,
|
incrementInviteAttempt,
|
||||||
|
|||||||
@ -8,6 +8,8 @@ class TaskRunner {
|
|||||||
this.running = false;
|
this.running = false;
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
this.nextRunAt = "";
|
this.nextRunAt = "";
|
||||||
|
this.nextInviteAccountId = 0;
|
||||||
|
this.lastInviteAccountId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
isRunning() {
|
isRunning() {
|
||||||
@ -18,6 +20,14 @@ class TaskRunner {
|
|||||||
return this.nextRunAt || "";
|
return this.nextRunAt || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNextInviteAccountId() {
|
||||||
|
return this.nextInviteAccountId || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastInviteAccountId() {
|
||||||
|
return this.lastInviteAccountId || 0;
|
||||||
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
if (this.running) return;
|
if (this.running) return;
|
||||||
this.running = true;
|
this.running = true;
|
||||||
@ -30,14 +40,21 @@ class TaskRunner {
|
|||||||
if (this.timer) clearTimeout(this.timer);
|
if (this.timer) clearTimeout(this.timer);
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
this.nextRunAt = "";
|
this.nextRunAt = "";
|
||||||
|
this.nextInviteAccountId = 0;
|
||||||
this.telegram.stopTaskMonitor(this.task.id);
|
this.telegram.stopTaskMonitor(this.task.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _initMonitoring() {
|
async _initMonitoring() {
|
||||||
const competitors = this.store.listTaskCompetitors(this.task.id).map((row) => row.link);
|
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);
|
const accountRows = this.store.listTaskAccounts(this.task.id);
|
||||||
await this.telegram.joinGroupsForTask(this.task, competitors, accounts);
|
const accounts = accountRows.map((row) => row.account_id);
|
||||||
await this.telegram.startTaskMonitor(this.task, competitors, accounts);
|
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() {
|
_scheduleNext() {
|
||||||
@ -55,12 +72,19 @@ class TaskRunner {
|
|||||||
const successIds = [];
|
const successIds = [];
|
||||||
let invitedCount = 0;
|
let invitedCount = 0;
|
||||||
this.nextRunAt = "";
|
this.nextRunAt = "";
|
||||||
|
this.nextInviteAccountId = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id);
|
const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id);
|
||||||
let inviteAccounts = accounts;
|
let inviteAccounts = accounts;
|
||||||
if (this.task.separate_bot_roles || this.task.require_same_bot_in_both) {
|
const roles = this.telegram.getTaskRoleAssignments(this.task.id);
|
||||||
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
|
inviteAccounts = this.task.require_same_bot_in_both
|
||||||
? (roles.competitorIds || [])
|
? (roles.competitorIds || [])
|
||||||
: (roles.ourIds || []);
|
: (roles.ourIds || []);
|
||||||
@ -79,6 +103,9 @@ class TaskRunner {
|
|||||||
if (!this.task.multi_accounts_per_run) {
|
if (!this.task.multi_accounts_per_run) {
|
||||||
const entry = this.telegram.pickInviteAccount(inviteAccounts, Boolean(this.task.random_accounts));
|
const entry = this.telegram.pickInviteAccount(inviteAccounts, Boolean(this.task.random_accounts));
|
||||||
inviteAccounts = entry ? [entry.account.id] : [];
|
inviteAccounts = entry ? [entry.account.id] : [];
|
||||||
|
this.nextInviteAccountId = entry ? entry.account.id : 0;
|
||||||
|
} else if (inviteAccounts.length) {
|
||||||
|
this.nextInviteAccountId = inviteAccounts[0];
|
||||||
}
|
}
|
||||||
const totalAccounts = accounts.length;
|
const totalAccounts = accounts.length;
|
||||||
if (this.task.stop_on_blocked) {
|
if (this.task.stop_on_blocked) {
|
||||||
@ -124,6 +151,7 @@ class TaskRunner {
|
|||||||
invitedCount += 1;
|
invitedCount += 1;
|
||||||
successIds.push(item.user_id);
|
successIds.push(item.user_id);
|
||||||
this.store.markInviteStatus(item.id, "invited");
|
this.store.markInviteStatus(item.id, "invited");
|
||||||
|
this.lastInviteAccountId = result.accountId || this.lastInviteAccountId;
|
||||||
this.store.recordInvite(
|
this.store.recordInvite(
|
||||||
this.task.id,
|
this.task.id,
|
||||||
item.user_id,
|
item.user_id,
|
||||||
|
|||||||
@ -453,11 +453,19 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
if (!user && sourceChat) {
|
if (!user && sourceChat) {
|
||||||
const resolved = await this._resolveUserFromSource(client, sourceChat, userId);
|
const resolved = await this._resolveUserFromSource(client, sourceChat, userId);
|
||||||
if (resolved) {
|
if (resolved && resolved.accessHash) {
|
||||||
user = resolved;
|
try {
|
||||||
attempts.push({ strategy: "participants", ok: true, detail: "from group participants" });
|
user = new Api.InputUser({
|
||||||
|
userId: BigInt(userId),
|
||||||
|
accessHash: BigInt(resolved.accessHash)
|
||||||
|
});
|
||||||
|
attempts.push({ strategy: "participants", ok: true, detail: resolved.detail || "from participants" });
|
||||||
|
} catch (error) {
|
||||||
|
user = null;
|
||||||
|
attempts.push({ strategy: "participants", ok: false, detail: "invalid access_hash" });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
attempts.push({ strategy: "participants", ok: false, detail: "user not in participant cache" });
|
attempts.push({ strategy: "participants", ok: false, detail: resolved ? resolved.detail : "no result" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!user && providedUsername) {
|
if (!user && providedUsername) {
|
||||||
@ -503,14 +511,20 @@ class TelegramManager {
|
|||||||
let fallbackMeta = lastAttempts.length ? JSON.stringify(lastAttempts) : "";
|
let fallbackMeta = lastAttempts.length ? JSON.stringify(lastAttempts) : "";
|
||||||
if (errorText === "USER_ID_INVALID") {
|
if (errorText === "USER_ID_INVALID") {
|
||||||
const username = options.username ? (options.username.startsWith("@") ? options.username : `@${options.username}`) : "";
|
const username = options.username ? (options.username.startsWith("@") ? options.username : `@${options.username}`) : "";
|
||||||
try {
|
try {
|
||||||
let retryUser = null;
|
let retryUser = null;
|
||||||
if (!retryUser && options.sourceChat) {
|
if (!retryUser && options.sourceChat) {
|
||||||
retryUser = await this._resolveUserFromSource(client, options.sourceChat, userId);
|
const resolved = await this._resolveUserFromSource(client, options.sourceChat, userId);
|
||||||
|
if (resolved && resolved.accessHash) {
|
||||||
|
retryUser = new Api.InputUser({
|
||||||
|
userId: BigInt(userId),
|
||||||
|
accessHash: BigInt(resolved.accessHash)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!retryUser && username) {
|
}
|
||||||
try {
|
if (!retryUser && username) {
|
||||||
retryUser = await client.getEntity(username);
|
try {
|
||||||
|
retryUser = await client.getEntity(username);
|
||||||
} catch (resolveError) {
|
} catch (resolveError) {
|
||||||
retryUser = null;
|
retryUser = null;
|
||||||
}
|
}
|
||||||
@ -601,6 +615,15 @@ class TelegramManager {
|
|||||||
return available[0] || null;
|
return available[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_listClientsFromAllowed(allowedAccountIds) {
|
||||||
|
const entries = Array.from(this.clients.values()).filter((entry) => entry.account.status === "ok");
|
||||||
|
if (!entries.length) return [];
|
||||||
|
const allowed = Array.isArray(allowedAccountIds) && allowedAccountIds.length
|
||||||
|
? entries.filter((entry) => allowedAccountIds.includes(entry.account.id))
|
||||||
|
: entries;
|
||||||
|
return allowed.filter((entry) => !this._isInCooldown(entry.account));
|
||||||
|
}
|
||||||
|
|
||||||
pickInviteAccount(allowedAccountIds, randomize) {
|
pickInviteAccount(allowedAccountIds, randomize) {
|
||||||
return this._pickClientForInvite(allowedAccountIds, randomize);
|
return this._pickClientForInvite(allowedAccountIds, randomize);
|
||||||
}
|
}
|
||||||
@ -772,48 +795,76 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _resolveUserFromSource(client, sourceChat, userId) {
|
async _resolveUserFromSource(client, sourceChat, userId) {
|
||||||
if (!client || !sourceChat || !userId) return null;
|
if (!client || !userId) {
|
||||||
|
return { accessHash: "", detail: "no client" };
|
||||||
|
}
|
||||||
|
if (!sourceChat) {
|
||||||
|
return { accessHash: "", detail: "no source chat" };
|
||||||
|
}
|
||||||
const cacheEntry = this.participantCache.get(sourceChat);
|
const cacheEntry = this.participantCache.get(sourceChat);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (cacheEntry && now - cacheEntry.at < 5 * 60 * 1000) {
|
if (cacheEntry && now - cacheEntry.at < 5 * 60 * 1000) {
|
||||||
const cached = cacheEntry.map.get(userId.toString());
|
const cached = cacheEntry.map.get(userId.toString());
|
||||||
if (cached && cached.accessHash) {
|
if (cached && cached.accessHash) {
|
||||||
return new Api.InputUser({
|
return {
|
||||||
userId: BigInt(userId),
|
accessHash: cached.accessHash,
|
||||||
accessHash: BigInt(cached.accessHash)
|
detail: `cache hit (${cacheEntry.map.size})`
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
return { accessHash: "", detail: `cache miss (${cacheEntry.map.size})` };
|
||||||
}
|
}
|
||||||
const resolved = await this._resolveGroupEntity(client, sourceChat, false, null);
|
const resolved = await this._resolveGroupEntity(client, sourceChat, false, null);
|
||||||
if (!resolved || !resolved.ok) return null;
|
if (!resolved || !resolved.ok) {
|
||||||
|
return { accessHash: "", detail: `resolve failed (${resolved ? resolved.error : "unknown"})` };
|
||||||
|
}
|
||||||
const map = await this._loadParticipantCache(client, resolved.entity, 400);
|
const map = await this._loadParticipantCache(client, resolved.entity, 400);
|
||||||
this.participantCache.set(sourceChat, { at: now, map });
|
this.participantCache.set(sourceChat, { at: now, map });
|
||||||
|
if (!map.size) {
|
||||||
|
return { accessHash: "", detail: "participants hidden" };
|
||||||
|
}
|
||||||
const cached = map.get(userId.toString());
|
const cached = map.get(userId.toString());
|
||||||
if (cached && cached.accessHash) {
|
if (cached && cached.accessHash) {
|
||||||
return new Api.InputUser({
|
return { accessHash: cached.accessHash, detail: `participants (${map.size})` };
|
||||||
userId: BigInt(userId),
|
|
||||||
accessHash: BigInt(cached.accessHash)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return null;
|
return { accessHash: "", detail: `not in participants (${map.size})` };
|
||||||
}
|
}
|
||||||
|
|
||||||
async _resolveQueueIdentity(client, sourceChat, userId) {
|
async _resolveQueueIdentity(client, sourceChat, userId) {
|
||||||
if (!client || !userId) return null;
|
const attempts = [];
|
||||||
const fromParticipants = await this._resolveUserFromSource(client, sourceChat, userId);
|
if (!client || !userId) {
|
||||||
if (fromParticipants && fromParticipants.accessHash != null) {
|
return { accessHash: "", strategy: "", detail: "no client or userId", attempts };
|
||||||
return { accessHash: fromParticipants.accessHash.toString(), strategy: "participants" };
|
}
|
||||||
|
const participantResult = await this._resolveUserFromSource(client, sourceChat, userId);
|
||||||
|
attempts.push({
|
||||||
|
strategy: "participants",
|
||||||
|
ok: Boolean(participantResult && participantResult.accessHash),
|
||||||
|
detail: participantResult ? participantResult.detail : "unknown"
|
||||||
|
});
|
||||||
|
if (participantResult && participantResult.accessHash) {
|
||||||
|
return {
|
||||||
|
accessHash: participantResult.accessHash.toString(),
|
||||||
|
strategy: "participants",
|
||||||
|
detail: participantResult.detail,
|
||||||
|
attempts
|
||||||
|
};
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const resolvedUser = await client.getEntity(userId);
|
const resolvedUser = await client.getEntity(userId);
|
||||||
const input = await client.getInputEntity(resolvedUser);
|
const input = await client.getInputEntity(resolvedUser);
|
||||||
if (input && input.accessHash != null) {
|
if (input && input.accessHash != null) {
|
||||||
return { accessHash: input.accessHash.toString(), strategy: "entity" };
|
attempts.push({ strategy: "entity", ok: true, detail: "getEntity(userId)" });
|
||||||
|
return {
|
||||||
|
accessHash: input.accessHash.toString(),
|
||||||
|
strategy: "entity",
|
||||||
|
detail: "getEntity(userId)",
|
||||||
|
attempts
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
attempts.push({ strategy: "entity", ok: false, detail: "no access_hash" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ignore entity resolution errors
|
attempts.push({ strategy: "entity", ok: false, detail: error.message || String(error) });
|
||||||
}
|
}
|
||||||
return null;
|
return { accessHash: "", strategy: "", detail: "", attempts };
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureJoinOurGroup(ourGroup) {
|
async ensureJoinOurGroup(ourGroup) {
|
||||||
@ -883,26 +934,43 @@ class TelegramManager {
|
|||||||
if (!Number.isFinite(maxGroups) || maxGroups <= 0) {
|
if (!Number.isFinite(maxGroups) || maxGroups <= 0) {
|
||||||
maxGroups = Number.POSITIVE_INFINITY;
|
maxGroups = Number.POSITIVE_INFINITY;
|
||||||
}
|
}
|
||||||
let memberCount = 0;
|
const existing = [];
|
||||||
|
const toJoin = [];
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
if (!group) continue;
|
if (!group) continue;
|
||||||
try {
|
try {
|
||||||
if (memberCount >= maxGroups) break;
|
|
||||||
const alreadyMember = await this._isParticipant(client, group);
|
const alreadyMember = await this._isParticipant(client, group);
|
||||||
if (alreadyMember) {
|
if (alreadyMember) {
|
||||||
memberCount += 1;
|
existing.push(group);
|
||||||
continue;
|
} else {
|
||||||
|
toJoin.push(group);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toJoin.push(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const alreadyCount = existing.length;
|
||||||
|
const requiredCount = toJoin.length;
|
||||||
|
let allowedCount = Number.isFinite(maxGroups) ? Math.max(0, maxGroups - alreadyCount) : requiredCount;
|
||||||
|
if (!Number.isFinite(maxGroups)) {
|
||||||
|
allowedCount = requiredCount;
|
||||||
|
}
|
||||||
|
if (Number.isFinite(maxGroups) && account && requiredCount > allowedCount) {
|
||||||
|
const message = `Лимит групп: ${maxGroups}. Уже в группах задачи: ${alreadyCount}. Нужно добавить: ${requiredCount}. Доступно слотов: ${allowedCount}.`;
|
||||||
|
this.store.addAccountEvent(account.id, account.phone || "", "auto_join_limit", message);
|
||||||
|
}
|
||||||
|
if (allowedCount <= 0) return;
|
||||||
|
const joinList = toJoin.slice(0, allowedCount);
|
||||||
|
for (const group of joinList) {
|
||||||
|
try {
|
||||||
if (this._isInviteLink(group)) {
|
if (this._isInviteLink(group)) {
|
||||||
const hash = this._extractInviteHash(group);
|
const hash = this._extractInviteHash(group);
|
||||||
if (hash) {
|
if (hash) {
|
||||||
await client.invoke(new Api.messages.ImportChatInvite({ hash }));
|
await client.invoke(new Api.messages.ImportChatInvite({ hash }));
|
||||||
memberCount += 1;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const entity = await client.getEntity(group);
|
const entity = await client.getEntity(group);
|
||||||
await client.invoke(new Api.channels.JoinChannel({ channel: entity }));
|
await client.invoke(new Api.channels.JoinChannel({ channel: entity }));
|
||||||
memberCount += 1;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error.errorMessage || error.message || String(error);
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
@ -1049,29 +1117,53 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinGroupsForTask(task, competitorGroups, accountIds) {
|
async joinGroupsForTask(task, competitorGroups, accountIds, roleIds = {}) {
|
||||||
const accounts = Array.from(this.clients.values()).filter((entry) => accountIds.includes(entry.account.id));
|
const accounts = Array.from(this.clients.values()).filter((entry) => accountIds.includes(entry.account.id));
|
||||||
const competitorBots = Math.max(1, Number(task.max_competitor_bots || 1));
|
const competitorBots = Math.max(1, Number(task.max_competitor_bots || 1));
|
||||||
const ourBots = Math.max(1, Number(task.max_our_bots || 1));
|
const ourBots = Math.max(1, Number(task.max_our_bots || 1));
|
||||||
|
|
||||||
|
const explicitMonitorIds = Array.isArray(roleIds.monitorIds) ? roleIds.monitorIds : [];
|
||||||
|
const explicitInviteIds = Array.isArray(roleIds.inviteIds) ? roleIds.inviteIds : [];
|
||||||
|
const hasExplicitRoles = explicitMonitorIds.length || explicitInviteIds.length;
|
||||||
|
|
||||||
const competitors = competitorGroups || [];
|
const competitors = competitorGroups || [];
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
const usedForCompetitors = new Set();
|
const usedForCompetitors = new Set();
|
||||||
for (const group of competitors) {
|
if (hasExplicitRoles) {
|
||||||
for (let i = 0; i < competitorBots; i += 1) {
|
for (const group of competitors) {
|
||||||
if (!accounts.length) break;
|
const pool = accounts.filter((entry) => explicitMonitorIds.includes(entry.account.id));
|
||||||
const entry = accounts[cursor % accounts.length];
|
for (const entry of pool) {
|
||||||
cursor += 1;
|
usedForCompetitors.add(entry.account.id);
|
||||||
usedForCompetitors.add(entry.account.id);
|
if (task.auto_join_competitors) {
|
||||||
if (task.auto_join_competitors) {
|
await this._autoJoinGroups(entry.client, [group], true, entry.account);
|
||||||
await this._autoJoinGroups(entry.client, [group], true, entry.account);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const group of competitors) {
|
||||||
|
for (let i = 0; i < competitorBots; i += 1) {
|
||||||
|
if (!accounts.length) break;
|
||||||
|
const entry = accounts[cursor % accounts.length];
|
||||||
|
cursor += 1;
|
||||||
|
usedForCompetitors.add(entry.account.id);
|
||||||
|
if (task.auto_join_competitors) {
|
||||||
|
await this._autoJoinGroups(entry.client, [group], true, entry.account);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const usedForOur = new Set();
|
const usedForOur = new Set();
|
||||||
if (task.our_group) {
|
if (task.our_group) {
|
||||||
if (task.require_same_bot_in_both) {
|
if (hasExplicitRoles) {
|
||||||
|
const pool = accounts.filter((entry) => explicitInviteIds.includes(entry.account.id));
|
||||||
|
for (const entry of pool) {
|
||||||
|
usedForOur.add(entry.account.id);
|
||||||
|
if (task.auto_join_our_group) {
|
||||||
|
await this._autoJoinGroups(entry.client, [task.our_group], true, entry.account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (task.require_same_bot_in_both) {
|
||||||
const pool = accounts.filter((entry) => usedForCompetitors.has(entry.account.id));
|
const pool = accounts.filter((entry) => usedForCompetitors.has(entry.account.id));
|
||||||
const finalPool = pool.length ? pool : accounts;
|
const finalPool = pool.length ? pool : accounts;
|
||||||
const targetCount = Math.max(1, Number(task.max_competitor_bots || 1));
|
const targetCount = Math.max(1, Number(task.max_competitor_bots || 1));
|
||||||
@ -1095,19 +1187,12 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.taskRoleAssignments.set(task.id, {
|
this.taskRoleAssignments.set(task.id, {
|
||||||
competitorIds: Array.from(usedForCompetitors),
|
competitorIds: hasExplicitRoles ? explicitMonitorIds : Array.from(usedForCompetitors),
|
||||||
ourIds: Array.from(usedForOur)
|
ourIds: hasExplicitRoles ? explicitInviteIds : Array.from(usedForOur)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async startTaskMonitor(task, competitorGroups, accountIds) {
|
async _startMonitorEntry(task, monitorAccount, groups) {
|
||||||
const role = this.taskRoleAssignments.get(task.id);
|
|
||||||
const allowed = (task.separate_bot_roles || task.require_same_bot_in_both) && role && role.competitorIds.length
|
|
||||||
? role.competitorIds
|
|
||||||
: accountIds;
|
|
||||||
const monitorAccount = this._pickClientFromAllowed(allowed);
|
|
||||||
if (!monitorAccount) return { ok: false, error: "No accounts for task" };
|
|
||||||
const groups = (competitorGroups || []).filter(Boolean);
|
|
||||||
const resolved = [];
|
const resolved = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
@ -1210,14 +1295,17 @@ class TelegramManager {
|
|||||||
if (!senderInfo || !senderInfo.info) {
|
if (!senderInfo || !senderInfo.info) {
|
||||||
const resolved = rawSenderId
|
const resolved = rawSenderId
|
||||||
? await this._resolveQueueIdentity(monitorAccount.client, st.source, rawSenderId)
|
? await this._resolveQueueIdentity(monitorAccount.client, st.source, rawSenderId)
|
||||||
: null;
|
: { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] };
|
||||||
if (!resolved) {
|
if (!resolved || !resolved.accessHash) {
|
||||||
if (shouldLogEvent(`${chatId}:skip`, 30000)) {
|
if (shouldLogEvent(`${chatId}:skip`, 30000)) {
|
||||||
|
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
||||||
|
const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
|
||||||
|
const extra = strategySummary ? `; стратегии: ${strategySummary}` : "";
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"new_message_skipped",
|
"new_message_skipped",
|
||||||
`${formatGroupLabel(st)}: ${senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя"}, ${this._describeSender(message)}`
|
`${formatGroupLabel(st)}: ${reason}${extra}, ${this._describeSender(message)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -1226,11 +1314,12 @@ class TelegramManager {
|
|||||||
const username = "";
|
const username = "";
|
||||||
const accessHash = resolved.accessHash;
|
const accessHash = resolved.accessHash;
|
||||||
if (shouldLogEvent(`${chatId}:queue`, 30000)) {
|
if (shouldLogEvent(`${chatId}:queue`, 30000)) {
|
||||||
|
const detail = resolved.detail ? `, ${resolved.detail}` : "";
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"queue_strategy",
|
"queue_strategy",
|
||||||
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})`
|
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
monitorEntry.lastMessageAt = new Date().toISOString();
|
monitorEntry.lastMessageAt = new Date().toISOString();
|
||||||
@ -1249,28 +1338,31 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||||
const resolved = await this._resolveQueueIdentity(monitorAccount.client, st.source, senderPayload.userId);
|
const resolved = await this._resolveQueueIdentity(monitorAccount.client, st.source, senderPayload.userId);
|
||||||
if (resolved) {
|
if (resolved && resolved.accessHash) {
|
||||||
senderPayload.accessHash = resolved.accessHash;
|
senderPayload.accessHash = resolved.accessHash;
|
||||||
if (shouldLogEvent(`${chatId}:queue`, 30000)) {
|
if (shouldLogEvent(`${chatId}:queue`, 30000)) {
|
||||||
|
const detail = resolved.detail ? `, ${resolved.detail}` : "";
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"queue_strategy",
|
"queue_strategy",
|
||||||
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})`
|
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
if (shouldLogEvent(`${chatId}:skip`, 30000)) {
|
||||||
if (shouldLogEvent(`${chatId}:skip`, 30000)) {
|
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
||||||
this.store.addAccountEvent(
|
const extra = strategySummary ? `; стратегии: ${strategySummary}` : "";
|
||||||
monitorAccount.account.id,
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.id,
|
||||||
"new_message_skipped",
|
monitorAccount.account.phone,
|
||||||
`${formatGroupLabel(st)}: нет access_hash (нет в списке участников)`
|
"new_message_skipped",
|
||||||
);
|
`${formatGroupLabel(st)}: нет access_hash (нет в списке участников)${extra}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const { userId: senderId, username, accessHash } = senderPayload;
|
const { userId: senderId, username, accessHash } = senderPayload;
|
||||||
if (this._isOwnAccount(senderId)) return;
|
if (this._isOwnAccount(senderId)) return;
|
||||||
@ -1343,42 +1435,46 @@ class TelegramManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
st.lastId = Math.max(st.lastId || 0, message.id || 0);
|
st.lastId = Math.max(st.lastId || 0, message.id || 0);
|
||||||
const senderInfo = await this._getUserInfoFromMessage(monitorAccount.client, message);
|
const senderInfo = await this._getUserInfoFromMessage(monitorAccount.client, message);
|
||||||
const rawSenderId = message && message.senderId != null ? message.senderId.toString() : "";
|
const rawSenderId = message && message.senderId != null ? message.senderId.toString() : "";
|
||||||
if (!senderInfo || !senderInfo.info) {
|
if (!senderInfo || !senderInfo.info) {
|
||||||
const resolved = rawSenderId
|
const resolved = rawSenderId
|
||||||
? await this._resolveQueueIdentity(monitorAccount.client, st.source, rawSenderId)
|
? await this._resolveQueueIdentity(monitorAccount.client, st.source, rawSenderId)
|
||||||
: null;
|
: { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] };
|
||||||
if (!resolved) {
|
if (!resolved || !resolved.accessHash) {
|
||||||
skipped += 1;
|
skipped += 1;
|
||||||
if (shouldLogEvent(`${key}:skip`, 30000)) {
|
if (shouldLogEvent(`${key}:skip`, 30000)) {
|
||||||
this.store.addAccountEvent(
|
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
||||||
monitorAccount.account.id,
|
const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
|
||||||
monitorAccount.account.phone,
|
const extra = strategySummary ? `; стратегии: ${strategySummary}` : "";
|
||||||
"new_message_skipped",
|
|
||||||
`${formatGroupLabel(st)}: ${senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя"}, ${this._describeSender(message)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const senderId = rawSenderId;
|
|
||||||
const username = "";
|
|
||||||
const accessHash = resolved.accessHash;
|
|
||||||
if (shouldLogEvent(`${key}:queue`, 30000)) {
|
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"queue_strategy",
|
"new_message_skipped",
|
||||||
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})`
|
`${formatGroupLabel(st)}: ${reason}${extra}, ${this._describeSender(message)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
monitorEntry.lastMessageAt = new Date().toISOString();
|
|
||||||
monitorEntry.lastSource = st.source;
|
|
||||||
if (this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id)) {
|
|
||||||
enqueued += 1;
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const senderId = rawSenderId;
|
||||||
|
const username = "";
|
||||||
|
const accessHash = resolved.accessHash;
|
||||||
|
if (shouldLogEvent(`${key}:queue`, 30000)) {
|
||||||
|
const detail = resolved.detail ? `, ${resolved.detail}` : "";
|
||||||
|
this.store.addAccountEvent(
|
||||||
|
monitorAccount.account.id,
|
||||||
|
monitorAccount.account.phone,
|
||||||
|
"queue_strategy",
|
||||||
|
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
monitorEntry.lastMessageAt = new Date().toISOString();
|
||||||
|
monitorEntry.lastSource = st.source;
|
||||||
|
if (this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id)) {
|
||||||
|
enqueued += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const senderPayload = { ...senderInfo.info };
|
const senderPayload = { ...senderInfo.info };
|
||||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||||
await ensureParticipantCache(st);
|
await ensureParticipantCache(st);
|
||||||
@ -1390,29 +1486,32 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||||
const resolved = await this._resolveQueueIdentity(monitorAccount.client, st.source, senderPayload.userId);
|
const resolved = await this._resolveQueueIdentity(monitorAccount.client, st.source, senderPayload.userId);
|
||||||
if (resolved) {
|
if (resolved && resolved.accessHash) {
|
||||||
senderPayload.accessHash = resolved.accessHash;
|
senderPayload.accessHash = resolved.accessHash;
|
||||||
if (shouldLogEvent(`${key}:queue`, 30000)) {
|
if (shouldLogEvent(`${key}:queue`, 30000)) {
|
||||||
|
const detail = resolved.detail ? `, ${resolved.detail}` : "";
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"queue_strategy",
|
"queue_strategy",
|
||||||
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})`
|
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
skipped += 1;
|
||||||
skipped += 1;
|
if (shouldLogEvent(`${key}:skip`, 30000)) {
|
||||||
if (shouldLogEvent(`${key}:skip`, 30000)) {
|
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
||||||
this.store.addAccountEvent(
|
const extra = strategySummary ? `; стратегии: ${strategySummary}` : "";
|
||||||
monitorAccount.account.id,
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.id,
|
||||||
"new_message_skipped",
|
monitorAccount.account.phone,
|
||||||
`${formatGroupLabel(st)}: нет access_hash (нет в списке участников)`
|
"new_message_skipped",
|
||||||
);
|
`${formatGroupLabel(st)}: нет access_hash (нет в списке участников)${extra}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
const { userId: senderId, username, accessHash } = senderPayload;
|
const { userId: senderId, username, accessHash } = senderPayload;
|
||||||
if (this._isOwnAccount(senderId)) continue;
|
if (this._isOwnAccount(senderId)) continue;
|
||||||
@ -1482,8 +1581,44 @@ class TelegramManager {
|
|||||||
`Интервал: 20 сек`
|
`Интервал: 20 сек`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.taskMonitors.set(task.id, monitorEntry);
|
return { ok: true, entry: monitorEntry, errors };
|
||||||
return { ok: true, errors };
|
}
|
||||||
|
|
||||||
|
async startTaskMonitor(task, competitorGroups, accountIds, monitorIds = []) {
|
||||||
|
const role = this.taskRoleAssignments.get(task.id);
|
||||||
|
const explicitMonitorIds = Array.isArray(monitorIds) ? monitorIds.filter(Boolean) : [];
|
||||||
|
const allowed = (task.separate_bot_roles || task.require_same_bot_in_both) && role && role.competitorIds.length
|
||||||
|
? role.competitorIds
|
||||||
|
: (explicitMonitorIds.length ? explicitMonitorIds : accountIds);
|
||||||
|
const monitorAccounts = this._listClientsFromAllowed(allowed);
|
||||||
|
if (!monitorAccounts.length) return { ok: false, error: "No accounts for task" };
|
||||||
|
const groups = (competitorGroups || []).filter(Boolean);
|
||||||
|
if (!groups.length) return { ok: false, error: "No groups to monitor" };
|
||||||
|
const targetCount = Math.max(1, Number(task.max_competitor_bots || monitorAccounts.length || 1));
|
||||||
|
const monitorPool = monitorAccounts.slice(0, Math.min(targetCount, monitorAccounts.length));
|
||||||
|
const chunks = monitorPool.map(() => []);
|
||||||
|
groups.forEach((group, index) => {
|
||||||
|
const bucket = index % chunks.length;
|
||||||
|
chunks[bucket].push(group);
|
||||||
|
});
|
||||||
|
|
||||||
|
const entries = [];
|
||||||
|
const errors = [];
|
||||||
|
for (let i = 0; i < monitorPool.length; i += 1) {
|
||||||
|
const accountEntry = monitorPool[i];
|
||||||
|
const chunk = chunks[i] || [];
|
||||||
|
if (!chunk.length) continue;
|
||||||
|
const result = await this._startMonitorEntry(task, accountEntry, chunk);
|
||||||
|
if (result.ok && result.entry) {
|
||||||
|
entries.push(result.entry);
|
||||||
|
}
|
||||||
|
if (result.errors && result.errors.length) {
|
||||||
|
errors.push(...result.errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!entries.length) return { ok: false, error: "No groups to monitor", errors };
|
||||||
|
this.taskMonitors.set(task.id, { entries });
|
||||||
|
return { ok: errors.length === 0, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseHistoryForTask(task, competitorGroups, accountIds) {
|
async parseHistoryForTask(task, competitorGroups, accountIds) {
|
||||||
@ -1511,6 +1646,7 @@ class TelegramManager {
|
|||||||
let enqueued = 0;
|
let enqueued = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
const skipReasons = {};
|
const skipReasons = {};
|
||||||
|
let strategySkipSample = "";
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
total += 1;
|
total += 1;
|
||||||
const senderInfo = await this._getUserInfoFromMessage(entry.client, message);
|
const senderInfo = await this._getUserInfoFromMessage(entry.client, message);
|
||||||
@ -1518,11 +1654,17 @@ class TelegramManager {
|
|||||||
if (!senderInfo || !senderInfo.info) {
|
if (!senderInfo || !senderInfo.info) {
|
||||||
const resolved = rawSenderId
|
const resolved = rawSenderId
|
||||||
? await this._resolveQueueIdentity(entry.client, group, rawSenderId)
|
? await this._resolveQueueIdentity(entry.client, group, rawSenderId)
|
||||||
: null;
|
: { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] };
|
||||||
if (!resolved) {
|
if (!resolved || !resolved.accessHash) {
|
||||||
skipped += 1;
|
skipped += 1;
|
||||||
const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
|
const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
|
||||||
skipReasons[reason] = (skipReasons[reason] || 0) + 1;
|
skipReasons[reason] = (skipReasons[reason] || 0) + 1;
|
||||||
|
if (!strategySkipSample) {
|
||||||
|
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
||||||
|
if (strategySummary) {
|
||||||
|
strategySkipSample = `${group}: ${reason}; стратегии: ${strategySummary}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (this.store.enqueueInvite(task.id, rawSenderId, "", group, resolved.accessHash, entry.account.id)) {
|
if (this.store.enqueueInvite(task.id, rawSenderId, "", group, resolved.accessHash, entry.account.id)) {
|
||||||
@ -1541,9 +1683,15 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||||
const resolved = await this._resolveQueueIdentity(entry.client, group, senderPayload.userId);
|
const resolved = await this._resolveQueueIdentity(entry.client, group, senderPayload.userId);
|
||||||
if (resolved) {
|
if (resolved && resolved.accessHash) {
|
||||||
senderPayload.accessHash = resolved.accessHash;
|
senderPayload.accessHash = resolved.accessHash;
|
||||||
}
|
}
|
||||||
|
if (!senderPayload.accessHash && !senderPayload.username && !strategySkipSample) {
|
||||||
|
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
||||||
|
if (strategySummary) {
|
||||||
|
strategySkipSample = `${group}: нет access_hash; стратегии: ${strategySummary}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||||
skipped += 1;
|
skipped += 1;
|
||||||
@ -1565,6 +1713,14 @@ class TelegramManager {
|
|||||||
summaryLines.push(
|
summaryLines.push(
|
||||||
`${group}: сообщений ${total}, добавлено ${enqueued}, пропущено ${skipped}${skipSummary ? ` (${skipSummary})` : ""}`
|
`${group}: сообщений ${total}, добавлено ${enqueued}, пропущено ${skipped}${skipSummary ? ` (${skipSummary})` : ""}`
|
||||||
);
|
);
|
||||||
|
if (strategySkipSample) {
|
||||||
|
this.store.addAccountEvent(
|
||||||
|
entry.account.id,
|
||||||
|
entry.account.phone,
|
||||||
|
"history_skip_detail",
|
||||||
|
strategySkipSample
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (summaryLines.length) {
|
if (summaryLines.length) {
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
@ -1650,18 +1806,31 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_formatStrategyAttempts(attempts) {
|
||||||
|
if (!Array.isArray(attempts) || attempts.length === 0) return "";
|
||||||
|
const parts = attempts.map((item) => {
|
||||||
|
const status = item.ok ? "ok" : "fail";
|
||||||
|
const detail = item.detail ? String(item.detail).replace(/\s+/g, " ").slice(0, 80) : "";
|
||||||
|
return detail ? `${item.strategy}:${status} (${detail})` : `${item.strategy}:${status}`;
|
||||||
|
});
|
||||||
|
return parts.join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
stopTaskMonitor(taskId) {
|
stopTaskMonitor(taskId) {
|
||||||
const entry = this.taskMonitors.get(taskId);
|
const entry = this.taskMonitors.get(taskId);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
if (entry.timer) clearInterval(entry.timer);
|
const entries = Array.isArray(entry.entries) ? entry.entries : [entry];
|
||||||
const clientEntry = this.clients.get(entry.accountId);
|
entries.forEach((monitorEntry) => {
|
||||||
if (clientEntry && entry.handler) {
|
if (monitorEntry.timer) clearInterval(monitorEntry.timer);
|
||||||
try {
|
const clientEntry = this.clients.get(monitorEntry.accountId);
|
||||||
clientEntry.client.removeEventHandler(entry.handler);
|
if (clientEntry && monitorEntry.handler) {
|
||||||
} catch (error) {
|
try {
|
||||||
// ignore handler removal errors
|
clientEntry.client.removeEventHandler(monitorEntry.handler);
|
||||||
|
} catch (error) {
|
||||||
|
// ignore handler removal errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
this.taskMonitors.delete(taskId);
|
this.taskMonitors.delete(taskId);
|
||||||
this.taskRoleAssignments.delete(taskId);
|
this.taskRoleAssignments.delete(taskId);
|
||||||
}
|
}
|
||||||
@ -1673,14 +1842,25 @@ class TelegramManager {
|
|||||||
getTaskMonitorInfo(taskId) {
|
getTaskMonitorInfo(taskId) {
|
||||||
const entry = this.taskMonitors.get(taskId);
|
const entry = this.taskMonitors.get(taskId);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return { monitoring: false, accountId: 0, groups: [], lastMessageAt: "", lastSource: "" };
|
return { monitoring: false, accountId: 0, accountIds: [], groups: [], lastMessageAt: "", lastSource: "" };
|
||||||
}
|
}
|
||||||
|
const entries = Array.isArray(entry.entries) ? entry.entries : [entry];
|
||||||
|
const accountIds = entries.map((item) => item.accountId).filter(Boolean);
|
||||||
|
let lastMessageAt = "";
|
||||||
|
let lastSource = "";
|
||||||
|
entries.forEach((item) => {
|
||||||
|
if (item.lastMessageAt && (!lastMessageAt || item.lastMessageAt > lastMessageAt)) {
|
||||||
|
lastMessageAt = item.lastMessageAt;
|
||||||
|
lastSource = item.lastSource || "";
|
||||||
|
}
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
monitoring: true,
|
monitoring: true,
|
||||||
accountId: entry.accountId || 0,
|
accountId: accountIds[0] || 0,
|
||||||
groups: entry.groups || [],
|
accountIds,
|
||||||
lastMessageAt: entry.lastMessageAt || "",
|
groups: entries.flatMap((item) => item.groups || []),
|
||||||
lastSource: entry.lastSource || ""
|
lastMessageAt,
|
||||||
|
lastSource
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -94,14 +94,19 @@ export default function App() {
|
|||||||
const [taskForm, setTaskForm] = useState(emptyTaskForm);
|
const [taskForm, setTaskForm] = useState(emptyTaskForm);
|
||||||
const [competitorText, setCompetitorText] = useState("");
|
const [competitorText, setCompetitorText] = useState("");
|
||||||
const [selectedAccountIds, setSelectedAccountIds] = useState([]);
|
const [selectedAccountIds, setSelectedAccountIds] = useState([]);
|
||||||
|
const [taskAccountRoles, setTaskAccountRoles] = useState({});
|
||||||
const [taskStatus, setTaskStatus] = useState({
|
const [taskStatus, setTaskStatus] = useState({
|
||||||
running: false,
|
running: false,
|
||||||
queueCount: 0,
|
queueCount: 0,
|
||||||
dailyRemaining: 0,
|
dailyRemaining: 0,
|
||||||
dailyUsed: 0,
|
dailyUsed: 0,
|
||||||
dailyLimit: 0,
|
dailyLimit: 0,
|
||||||
monitorInfo: { monitoring: false, groups: [], lastMessageAt: "", lastSource: "" },
|
monitorInfo: { monitoring: false, accountId: 0, accountIds: [], groups: [], lastMessageAt: "", lastSource: "" },
|
||||||
nextRunAt: ""
|
nextRunAt: "",
|
||||||
|
nextInviteAccountId: 0,
|
||||||
|
lastInviteAccountId: 0,
|
||||||
|
pendingStats: { total: 0, withUsername: 0, withAccessHash: 0, withoutData: 0 },
|
||||||
|
warnings: []
|
||||||
});
|
});
|
||||||
const [taskStatusMap, setTaskStatusMap] = useState({});
|
const [taskStatusMap, setTaskStatusMap] = useState({});
|
||||||
const [membershipStatus, setMembershipStatus] = useState({});
|
const [membershipStatus, setMembershipStatus] = useState({});
|
||||||
@ -170,11 +175,25 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}, [accounts]);
|
}, [accounts]);
|
||||||
|
const roleSummary = useMemo(() => {
|
||||||
|
const monitor = [];
|
||||||
|
const invite = [];
|
||||||
|
Object.entries(taskAccountRoles).forEach(([id, roles]) => {
|
||||||
|
const accountId = Number(id);
|
||||||
|
if (roles.monitor) monitor.push(accountId);
|
||||||
|
if (roles.invite) invite.push(accountId);
|
||||||
|
});
|
||||||
|
return { monitor, invite };
|
||||||
|
}, [taskAccountRoles]);
|
||||||
const assignedAccountMap = useMemo(() => {
|
const assignedAccountMap = useMemo(() => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
accountAssignments.forEach((row) => {
|
accountAssignments.forEach((row) => {
|
||||||
const list = map.get(row.account_id) || [];
|
const list = map.get(row.account_id) || [];
|
||||||
list.push(row.task_id);
|
list.push({
|
||||||
|
taskId: row.task_id,
|
||||||
|
roleMonitor: Boolean(row.role_monitor),
|
||||||
|
roleInvite: Boolean(row.role_invite)
|
||||||
|
});
|
||||||
map.set(row.account_id, list);
|
map.set(row.account_id, list);
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
@ -190,7 +209,7 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
accounts.forEach((account) => {
|
accounts.forEach((account) => {
|
||||||
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
||||||
const assignedToSelected = selected != null && assignedTasks.includes(selected);
|
const assignedToSelected = selected != null && assignedTasks.some((item) => item.taskId === selected);
|
||||||
const isFree = assignedTasks.length === 0;
|
const isFree = assignedTasks.length === 0;
|
||||||
if (filterFreeAccounts && !isFree && !assignedToSelected) {
|
if (filterFreeAccounts && !isFree && !assignedToSelected) {
|
||||||
busy.push(account);
|
busy.push(account);
|
||||||
@ -243,6 +262,7 @@ export default function App() {
|
|||||||
setTaskForm(emptyTaskForm);
|
setTaskForm(emptyTaskForm);
|
||||||
setCompetitorText("");
|
setCompetitorText("");
|
||||||
setSelectedAccountIds([]);
|
setSelectedAccountIds([]);
|
||||||
|
setTaskAccountRoles({});
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
setInvites([]);
|
setInvites([]);
|
||||||
setGroupVisibility([]);
|
setGroupVisibility([]);
|
||||||
@ -252,8 +272,11 @@ export default function App() {
|
|||||||
dailyRemaining: 0,
|
dailyRemaining: 0,
|
||||||
dailyUsed: 0,
|
dailyUsed: 0,
|
||||||
dailyLimit: 0,
|
dailyLimit: 0,
|
||||||
monitorInfo: { monitoring: false, groups: [], lastMessageAt: "", lastSource: "" },
|
monitorInfo: { monitoring: false, accountId: 0, accountIds: [], groups: [], lastMessageAt: "", lastSource: "" },
|
||||||
nextRunAt: ""
|
nextRunAt: "",
|
||||||
|
nextInviteAccountId: 0,
|
||||||
|
lastInviteAccountId: 0,
|
||||||
|
pendingStats: { total: 0, withUsername: 0, withAccessHash: 0, withoutData: 0 }
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -261,7 +284,21 @@ export default function App() {
|
|||||||
if (!details) return;
|
if (!details) return;
|
||||||
setTaskForm(sanitizeTaskForm({ ...emptyTaskForm, ...normalizeTask(details.task) }));
|
setTaskForm(sanitizeTaskForm({ ...emptyTaskForm, ...normalizeTask(details.task) }));
|
||||||
setCompetitorText((details.competitors || []).join("\n"));
|
setCompetitorText((details.competitors || []).join("\n"));
|
||||||
setSelectedAccountIds(details.accountIds || []);
|
const roleMap = {};
|
||||||
|
if (details.accountRoles && details.accountRoles.length) {
|
||||||
|
details.accountRoles.forEach((item) => {
|
||||||
|
roleMap[item.accountId] = {
|
||||||
|
monitor: Boolean(item.roleMonitor),
|
||||||
|
invite: Boolean(item.roleInvite)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
(details.accountIds || []).forEach((accountId) => {
|
||||||
|
roleMap[accountId] = { monitor: true, invite: true };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTaskAccountRoles(roleMap);
|
||||||
|
setSelectedAccountIds(Object.keys(roleMap).map((id) => Number(id)));
|
||||||
setLogs(await window.api.listLogs({ limit: 100, taskId }));
|
setLogs(await window.api.listLogs({ limit: 100, taskId }));
|
||||||
setInvites(await window.api.listInvites({ limit: 200, taskId }));
|
setInvites(await window.api.listInvites({ limit: 200, taskId }));
|
||||||
setGroupVisibility([]);
|
setGroupVisibility([]);
|
||||||
@ -662,6 +699,7 @@ export default function App() {
|
|||||||
setTaskForm(emptyTaskForm);
|
setTaskForm(emptyTaskForm);
|
||||||
setCompetitorText("");
|
setCompetitorText("");
|
||||||
setSelectedAccountIds([]);
|
setSelectedAccountIds([]);
|
||||||
|
setTaskAccountRoles({});
|
||||||
setAccessStatus([]);
|
setAccessStatus([]);
|
||||||
setMembershipStatus({});
|
setMembershipStatus({});
|
||||||
};
|
};
|
||||||
@ -680,9 +718,15 @@ export default function App() {
|
|||||||
showNotification("Сохраняем задачу...", "info");
|
showNotification("Сохраняем задачу...", "info");
|
||||||
const nextForm = sanitizeTaskForm(taskForm);
|
const nextForm = sanitizeTaskForm(taskForm);
|
||||||
setTaskForm(nextForm);
|
setTaskForm(nextForm);
|
||||||
let accountIds = selectedAccountIds;
|
let accountRolesMap = { ...taskAccountRoles };
|
||||||
|
let accountIds = Object.keys(accountRolesMap).map((id) => Number(id));
|
||||||
if (nextForm.autoAssignAccounts && (!accountIds || accountIds.length === 0)) {
|
if (nextForm.autoAssignAccounts && (!accountIds || accountIds.length === 0)) {
|
||||||
accountIds = accounts.map((account) => account.id);
|
accountIds = accounts.map((account) => account.id);
|
||||||
|
accountRolesMap = {};
|
||||||
|
accountIds.forEach((accountId) => {
|
||||||
|
accountRolesMap[accountId] = { monitor: true, invite: true };
|
||||||
|
});
|
||||||
|
setTaskAccountRoles(accountRolesMap);
|
||||||
setSelectedAccountIds(accountIds);
|
setSelectedAccountIds(accountIds);
|
||||||
if (accountIds.length) {
|
if (accountIds.length) {
|
||||||
setTaskNotice({ text: `Автоназначены аккаунты: ${accountIds.length}`, tone: "success", source });
|
setTaskNotice({ text: `Автоназначены аккаунты: ${accountIds.length}`, tone: "success", source });
|
||||||
@ -692,19 +736,39 @@ export default function App() {
|
|||||||
showNotification("Нет аккаунтов для этой задачи.", "error");
|
showNotification("Нет аккаунтов для этой задачи.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const requiredAccounts = nextForm.requireSameBotInBoth
|
const roleEntries = Object.values(accountRolesMap);
|
||||||
? Math.max(1, Number(nextForm.maxCompetitorBots || 1))
|
if (roleEntries.length) {
|
||||||
: nextForm.separateBotRoles
|
const hasMonitor = roleEntries.some((item) => item.monitor);
|
||||||
? Math.max(1, Number(nextForm.maxCompetitorBots || 1)) + Math.max(1, Number(nextForm.maxOurBots || 1))
|
const hasInvite = roleEntries.some((item) => item.invite);
|
||||||
: 1;
|
if (!hasMonitor) {
|
||||||
if (accountIds.length < requiredAccounts) {
|
showNotification("Нужен хотя бы один аккаунт с ролью мониторинга.", "error");
|
||||||
showNotification(`Нужно минимум ${requiredAccounts} аккаунтов для выбранного режима.`, "error");
|
return;
|
||||||
return;
|
}
|
||||||
|
if (!hasInvite) {
|
||||||
|
showNotification("Нужен хотя бы один аккаунт с ролью инвайта.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const requiredAccounts = nextForm.requireSameBotInBoth
|
||||||
|
? Math.max(1, Number(nextForm.maxCompetitorBots || 1))
|
||||||
|
: nextForm.separateBotRoles
|
||||||
|
? Math.max(1, Number(nextForm.maxCompetitorBots || 1)) + Math.max(1, Number(nextForm.maxOurBots || 1))
|
||||||
|
: 1;
|
||||||
|
if (accountIds.length < requiredAccounts) {
|
||||||
|
showNotification(`Нужно минимум ${requiredAccounts} аккаунтов для выбранного режима.`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
const accountRoles = Object.entries(accountRolesMap).map(([id, roles]) => ({
|
||||||
|
accountId: Number(id),
|
||||||
|
roleMonitor: Boolean(roles.monitor),
|
||||||
|
roleInvite: Boolean(roles.invite)
|
||||||
|
}));
|
||||||
const result = await window.api.saveTask({
|
const result = await window.api.saveTask({
|
||||||
task: nextForm,
|
task: nextForm,
|
||||||
competitors: competitorGroups,
|
competitors: competitorGroups,
|
||||||
accountIds
|
accountIds,
|
||||||
|
accountRoles
|
||||||
});
|
});
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
setTaskNotice({ text: "Задача сохранена.", tone: "success", source });
|
setTaskNotice({ text: "Задача сохранена.", tone: "success", source });
|
||||||
@ -972,6 +1036,7 @@ export default function App() {
|
|||||||
setTaskForm(emptyTaskForm);
|
setTaskForm(emptyTaskForm);
|
||||||
setCompetitorText("");
|
setCompetitorText("");
|
||||||
setSelectedAccountIds([]);
|
setSelectedAccountIds([]);
|
||||||
|
setTaskAccountRoles({});
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
setInvites([]);
|
setInvites([]);
|
||||||
setTaskStatus({
|
setTaskStatus({
|
||||||
@ -980,7 +1045,7 @@ export default function App() {
|
|||||||
dailyRemaining: 0,
|
dailyRemaining: 0,
|
||||||
dailyUsed: 0,
|
dailyUsed: 0,
|
||||||
dailyLimit: 0,
|
dailyLimit: 0,
|
||||||
monitorInfo: { monitoring: false, groups: [], lastMessageAt: "", lastSource: "" }
|
monitorInfo: { monitoring: false, accountId: 0, accountIds: [], groups: [], lastMessageAt: "", lastSource: "" }
|
||||||
});
|
});
|
||||||
await loadBase();
|
await loadBase();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -988,24 +1053,107 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAccountSelection = (accountId) => {
|
const persistAccountRoles = async (next) => {
|
||||||
setSelectedAccountIds((prev) => {
|
if (!window.api || selectedTaskId == null) return;
|
||||||
if (prev.includes(accountId)) {
|
const rolePayload = Object.entries(next).map(([id, roles]) => ({
|
||||||
return prev.filter((id) => id !== accountId);
|
accountId: Number(id),
|
||||||
}
|
roleMonitor: Boolean(roles.monitor),
|
||||||
return [...prev, accountId];
|
roleInvite: Boolean(roles.invite)
|
||||||
|
}));
|
||||||
|
await window.api.appendTaskAccounts({
|
||||||
|
taskId: selectedTaskId,
|
||||||
|
accountRoles: rolePayload
|
||||||
});
|
});
|
||||||
|
await loadAccountAssignments();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAccountRole = (accountId, role, value) => {
|
||||||
|
const next = { ...taskAccountRoles };
|
||||||
|
const existing = next[accountId] || { monitor: false, invite: false };
|
||||||
|
next[accountId] = { ...existing, [role]: value };
|
||||||
|
if (!next[accountId].monitor && !next[accountId].invite) {
|
||||||
|
delete next[accountId];
|
||||||
|
}
|
||||||
|
const ids = Object.keys(next).map((id) => Number(id));
|
||||||
|
setTaskAccountRoles(next);
|
||||||
|
setSelectedAccountIds(ids);
|
||||||
|
persistAccountRoles(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAccountRolesAll = (accountId, value) => {
|
||||||
|
const next = { ...taskAccountRoles };
|
||||||
|
if (value) {
|
||||||
|
next[accountId] = { monitor: true, invite: true };
|
||||||
|
} else {
|
||||||
|
delete next[accountId];
|
||||||
|
}
|
||||||
|
const ids = Object.keys(next).map((id) => Number(id));
|
||||||
|
setTaskAccountRoles(next);
|
||||||
|
setSelectedAccountIds(ids);
|
||||||
|
persistAccountRoles(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyRolePreset = (type) => {
|
||||||
|
if (!hasSelectedTask) return;
|
||||||
|
const availableIds = selectedAccountIds.length
|
||||||
|
? selectedAccountIds
|
||||||
|
: accountBuckets.freeOrSelected.map((account) => account.id);
|
||||||
|
if (!availableIds.length) {
|
||||||
|
showNotification("Нет доступных аккаунтов для назначения.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = {};
|
||||||
|
if (type === "all") {
|
||||||
|
availableIds.forEach((id) => {
|
||||||
|
next[id] = { monitor: true, invite: true };
|
||||||
|
});
|
||||||
|
} else if (type === "one") {
|
||||||
|
const id = availableIds[0];
|
||||||
|
next[id] = { monitor: true, invite: true };
|
||||||
|
} else if (type === "split") {
|
||||||
|
const monitorCount = Math.max(1, Number(taskForm.maxCompetitorBots || 1));
|
||||||
|
const inviteCount = Math.max(1, Number(taskForm.maxOurBots || 1));
|
||||||
|
const monitorIds = availableIds.slice(0, monitorCount);
|
||||||
|
const inviteIds = availableIds.slice(monitorCount, monitorCount + inviteCount);
|
||||||
|
monitorIds.forEach((id) => {
|
||||||
|
next[id] = { monitor: true, invite: false };
|
||||||
|
});
|
||||||
|
inviteIds.forEach((id) => {
|
||||||
|
next[id] = { monitor: false, invite: true };
|
||||||
|
});
|
||||||
|
if (inviteIds.length < inviteCount) {
|
||||||
|
showNotification("Не хватает аккаунтов для роли инвайта.", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ids = Object.keys(next).map((id) => Number(id));
|
||||||
|
setTaskAccountRoles(next);
|
||||||
|
setSelectedAccountIds(ids);
|
||||||
|
persistAccountRoles(next);
|
||||||
|
const label = type === "one" ? "Один бот" : type === "split" ? "Разделить роли" : "Все роли";
|
||||||
|
setTaskNotice({ text: `Пресет: ${label}`, tone: "success", source: "accounts" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const assignAccountsToTask = async (accountIds) => {
|
const assignAccountsToTask = async (accountIds) => {
|
||||||
if (!window.api || selectedTaskId == null) return;
|
if (!window.api || selectedTaskId == null) return;
|
||||||
if (!accountIds.length) return;
|
if (!accountIds.length) return;
|
||||||
|
const nextRoles = { ...taskAccountRoles };
|
||||||
|
accountIds.forEach((accountId) => {
|
||||||
|
if (!nextRoles[accountId]) {
|
||||||
|
nextRoles[accountId] = { monitor: true, invite: true };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const rolePayload = Object.entries(nextRoles).map(([accountId, roles]) => ({
|
||||||
|
accountId: Number(accountId),
|
||||||
|
roleMonitor: Boolean(roles.monitor),
|
||||||
|
roleInvite: Boolean(roles.invite)
|
||||||
|
}));
|
||||||
const result = await window.api.appendTaskAccounts({
|
const result = await window.api.appendTaskAccounts({
|
||||||
taskId: selectedTaskId,
|
taskId: selectedTaskId,
|
||||||
accountIds
|
accountRoles: rolePayload
|
||||||
});
|
});
|
||||||
if (result && result.ok) {
|
if (result && result.ok) {
|
||||||
setSelectedAccountIds(result.accountIds || []);
|
setTaskAccountRoles(nextRoles);
|
||||||
|
setSelectedAccountIds(Object.keys(nextRoles).map((id) => Number(id)));
|
||||||
await loadAccountAssignments();
|
await loadAccountAssignments();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1023,7 +1171,12 @@ export default function App() {
|
|||||||
accountId
|
accountId
|
||||||
});
|
});
|
||||||
if (result && result.ok) {
|
if (result && result.ok) {
|
||||||
setSelectedAccountIds(result.accountIds || []);
|
setTaskAccountRoles((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[accountId];
|
||||||
|
setSelectedAccountIds(Object.keys(next).map((id) => Number(id)));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
await loadAccountAssignments();
|
await loadAccountAssignments();
|
||||||
setTaskNotice({ text: "Аккаунт удален из задачи.", tone: "success", source: "accounts" });
|
setTaskNotice({ text: "Аккаунт удален из задачи.", tone: "success", source: "accounts" });
|
||||||
}
|
}
|
||||||
@ -1504,11 +1657,18 @@ export default function App() {
|
|||||||
? status.monitorInfo.lastSource
|
? status.monitorInfo.lastSource
|
||||||
: "—";
|
: "—";
|
||||||
const monitoring = Boolean(status && status.monitorInfo && status.monitorInfo.monitoring);
|
const monitoring = Boolean(status && status.monitorInfo && status.monitorInfo.monitoring);
|
||||||
const monitorAccountId = status && status.monitorInfo ? status.monitorInfo.accountId : 0;
|
const monitorAccountIds = status && status.monitorInfo && status.monitorInfo.accountIds
|
||||||
const monitorAccount = monitorAccountId ? accountById.get(monitorAccountId) : null;
|
? status.monitorInfo.accountIds
|
||||||
const monitorLabel = monitorAccount
|
: (status && status.monitorInfo && status.monitorInfo.accountId ? [status.monitorInfo.accountId] : []);
|
||||||
? (monitorAccount.phone || monitorAccount.user_id || String(monitorAccountId))
|
const monitorLabels = monitorAccountIds
|
||||||
: (monitorAccountId ? String(monitorAccountId) : "—");
|
.map((id) => {
|
||||||
|
const account = accountById.get(id);
|
||||||
|
return account ? (account.phone || account.user_id || String(id)) : String(id);
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
const monitorLabel = monitorLabels.length
|
||||||
|
? (monitorLabels.length > 2 ? `${monitorLabels.length} аккаунта` : monitorLabels.join(", "))
|
||||||
|
: "—";
|
||||||
const tooltip = [
|
const tooltip = [
|
||||||
`Статус: ${statusLabel}`,
|
`Статус: ${statusLabel}`,
|
||||||
`Очередь: ${status ? status.queueCount : "—"}`,
|
`Очередь: ${status ? status.queueCount : "—"}`,
|
||||||
@ -1613,10 +1773,18 @@ export default function App() {
|
|||||||
<div className="live-label">Мониторит</div>
|
<div className="live-label">Мониторит</div>
|
||||||
<div className="live-value">
|
<div className="live-value">
|
||||||
{(() => {
|
{(() => {
|
||||||
const monitorId = taskStatus.monitorInfo ? taskStatus.monitorInfo.accountId : 0;
|
const monitorIds = taskStatus.monitorInfo && taskStatus.monitorInfo.accountIds
|
||||||
const account = monitorId ? accountById.get(monitorId) : null;
|
? taskStatus.monitorInfo.accountIds
|
||||||
if (!monitorId) return "—";
|
: (taskStatus.monitorInfo && taskStatus.monitorInfo.accountId ? [taskStatus.monitorInfo.accountId] : []);
|
||||||
return account ? (account.phone || account.user_id || String(monitorId)) : String(monitorId);
|
if (!monitorIds.length) return "—";
|
||||||
|
const labels = monitorIds
|
||||||
|
.map((id) => {
|
||||||
|
const account = accountById.get(id);
|
||||||
|
return account ? (account.phone || account.user_id || String(id)) : String(id);
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
if (!labels.length) return "—";
|
||||||
|
return labels.length > 2 ? `${labels.length} аккаунта` : labels.join(", ");
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1632,6 +1800,18 @@ export default function App() {
|
|||||||
<div className="live-label">Очередь инвайтов</div>
|
<div className="live-label">Очередь инвайтов</div>
|
||||||
<div className="live-value">{taskStatus.queueCount}</div>
|
<div className="live-value">{taskStatus.queueCount}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="live-label">Очередь: username</div>
|
||||||
|
<div className="live-value">{taskStatus.pendingStats ? taskStatus.pendingStats.withUsername : 0}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="live-label">Очередь: access_hash</div>
|
||||||
|
<div className="live-value">{taskStatus.pendingStats ? taskStatus.pendingStats.withAccessHash : 0}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="live-label">Очередь: без данных</div>
|
||||||
|
<div className="live-value">{taskStatus.pendingStats ? taskStatus.pendingStats.withoutData : 0}</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="live-label">Лимит в день</div>
|
<div className="live-label">Лимит в день</div>
|
||||||
<div className="live-value">{taskStatus.dailyUsed}/{taskStatus.dailyLimit}</div>
|
<div className="live-value">{taskStatus.dailyUsed}/{taskStatus.dailyLimit}</div>
|
||||||
@ -1644,10 +1824,36 @@ export default function App() {
|
|||||||
<div className="live-label">Следующий цикл</div>
|
<div className="live-label">Следующий цикл</div>
|
||||||
<div className="live-value">{formatCountdown(taskStatus.nextRunAt)}</div>
|
<div className="live-value">{formatCountdown(taskStatus.nextRunAt)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="live-label">Следующий инвайт</div>
|
||||||
|
<div className="live-value">
|
||||||
|
{(() => {
|
||||||
|
const account = accountById.get(taskStatus.nextInviteAccountId);
|
||||||
|
return account ? (account.phone || account.user_id || taskStatus.nextInviteAccountId) : "—";
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="live-label">Последний инвайт</div>
|
||||||
|
<div className="live-value">
|
||||||
|
{(() => {
|
||||||
|
const account = accountById.get(taskStatus.lastInviteAccountId);
|
||||||
|
return account ? (account.phone || account.user_id || taskStatus.lastInviteAccountId) : "—";
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="live-label">Стратегии OK/Fail</div>
|
<div className="live-label">Стратегии OK/Fail</div>
|
||||||
<div className="live-value">{inviteStrategyStats.success}/{inviteStrategyStats.failed}</div>
|
<div className="live-value">{inviteStrategyStats.success}/{inviteStrategyStats.failed}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="live-label">Боты мониторят</div>
|
||||||
|
<div className="live-value">{roleSummary.monitor.length}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="live-label">Боты инвайтят</div>
|
||||||
|
<div className="live-value">{roleSummary.invite.length}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="status-actions">
|
<div className="status-actions">
|
||||||
{taskStatus.running ? (
|
{taskStatus.running ? (
|
||||||
@ -1656,6 +1862,57 @@ export default function App() {
|
|||||||
<button className="primary" onClick={() => startTask("status")} disabled={!hasSelectedTask}>Запустить</button>
|
<button className="primary" onClick={() => startTask("status")} disabled={!hasSelectedTask}>Запустить</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{hasSelectedTask && (
|
||||||
|
<div className="status-text">
|
||||||
|
{taskStatus.warnings && taskStatus.warnings.length > 0 && (
|
||||||
|
<div className="notice inline warn">
|
||||||
|
{taskStatus.warnings.map((warning, index) => (
|
||||||
|
<div key={`warn-${index}`}>{warning}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{roleSummary.monitor.length === 0 && (
|
||||||
|
<div className="notice inline warn">Нет аккаунтов с ролью мониторинга.</div>
|
||||||
|
)}
|
||||||
|
{roleSummary.invite.length === 0 && (
|
||||||
|
<div className="notice inline warn">Нет аккаунтов с ролью инвайта.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasSelectedTask && (
|
||||||
|
<div className="role-panel">
|
||||||
|
<div>
|
||||||
|
<div className="live-label">Мониторинг</div>
|
||||||
|
<div className="role-list">
|
||||||
|
{roleSummary.monitor.length
|
||||||
|
? roleSummary.monitor.map((id) => {
|
||||||
|
const account = accountById.get(id);
|
||||||
|
return (
|
||||||
|
<span key={`mon-${id}`} className="role-pill">
|
||||||
|
{account ? (account.phone || account.user_id || id) : id}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: <span className="role-empty">Нет</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="live-label">Инвайт</div>
|
||||||
|
<div className="role-list">
|
||||||
|
{roleSummary.invite.length
|
||||||
|
? roleSummary.invite.map((id) => {
|
||||||
|
const account = accountById.get(id);
|
||||||
|
return (
|
||||||
|
<span key={`inv-${id}`} className="role-pill">
|
||||||
|
{account ? (account.phone || account.user_id || id) : id}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: <span className="role-empty">Нет</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{groupVisibility.length > 0 && (
|
{groupVisibility.length > 0 && (
|
||||||
<div className="status-text">
|
<div className="status-text">
|
||||||
{groupVisibility.some((item) => item.hidden) && (
|
{groupVisibility.some((item) => item.hidden) && (
|
||||||
@ -1930,26 +2187,29 @@ export default function App() {
|
|||||||
|
|
||||||
{activeTab === "accounts" && (
|
{activeTab === "accounts" && (
|
||||||
<Suspense fallback={<div className="card">Загрузка...</div>}>
|
<Suspense fallback={<div className="card">Загрузка...</div>}>
|
||||||
<AccountsTab
|
<AccountsTab
|
||||||
accounts={accounts}
|
accounts={accounts}
|
||||||
accountStats={accountStats}
|
accountStats={accountStats}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
membershipStatus={membershipStatus}
|
membershipStatus={membershipStatus}
|
||||||
assignedAccountMap={assignedAccountMap}
|
assignedAccountMap={assignedAccountMap}
|
||||||
accountBuckets={accountBuckets}
|
accountBuckets={accountBuckets}
|
||||||
filterFreeAccounts={filterFreeAccounts}
|
filterFreeAccounts={filterFreeAccounts}
|
||||||
selectedAccountIds={selectedAccountIds}
|
selectedAccountIds={selectedAccountIds}
|
||||||
hasSelectedTask={hasSelectedTask}
|
taskAccountRoles={taskAccountRoles}
|
||||||
taskNotice={taskNotice}
|
hasSelectedTask={hasSelectedTask}
|
||||||
refreshMembership={refreshMembership}
|
taskNotice={taskNotice}
|
||||||
refreshIdentity={refreshIdentity}
|
refreshMembership={refreshMembership}
|
||||||
formatAccountStatus={formatAccountStatus}
|
refreshIdentity={refreshIdentity}
|
||||||
resetCooldown={resetCooldown}
|
formatAccountStatus={formatAccountStatus}
|
||||||
deleteAccount={deleteAccount}
|
resetCooldown={resetCooldown}
|
||||||
toggleAccountSelection={toggleAccountSelection}
|
deleteAccount={deleteAccount}
|
||||||
removeAccountFromTask={removeAccountFromTask}
|
updateAccountRole={updateAccountRole}
|
||||||
moveAccountToTask={moveAccountToTask}
|
setAccountRolesAll={setAccountRolesAll}
|
||||||
/>
|
applyRolePreset={applyRolePreset}
|
||||||
|
removeAccountFromTask={removeAccountFromTask}
|
||||||
|
moveAccountToTask={moveAccountToTask}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -651,6 +651,55 @@ button.danger {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-summary {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #475569;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-presets {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-panel {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-list {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-empty {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 6px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
.busy-accounts {
|
.busy-accounts {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
@ -702,6 +751,12 @@ button.danger {
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.role-toggle {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.tasks-layout {
|
.tasks-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(220px, 260px) minmax(320px, 1fr);
|
grid-template-columns: minmax(220px, 260px) minmax(320px, 1fr);
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export default function AccountsTab({
|
|||||||
accountBuckets,
|
accountBuckets,
|
||||||
filterFreeAccounts,
|
filterFreeAccounts,
|
||||||
selectedAccountIds,
|
selectedAccountIds,
|
||||||
|
taskAccountRoles,
|
||||||
hasSelectedTask,
|
hasSelectedTask,
|
||||||
taskNotice,
|
taskNotice,
|
||||||
refreshMembership,
|
refreshMembership,
|
||||||
@ -16,7 +17,9 @@ export default function AccountsTab({
|
|||||||
formatAccountStatus,
|
formatAccountStatus,
|
||||||
resetCooldown,
|
resetCooldown,
|
||||||
deleteAccount,
|
deleteAccount,
|
||||||
toggleAccountSelection,
|
updateAccountRole,
|
||||||
|
setAccountRolesAll,
|
||||||
|
applyRolePreset,
|
||||||
removeAccountFromTask,
|
removeAccountFromTask,
|
||||||
moveAccountToTask
|
moveAccountToTask
|
||||||
}) {
|
}) {
|
||||||
@ -31,6 +34,13 @@ export default function AccountsTab({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildAccountLabel = (account) => `${account.username ? `@${account.username}` : "—"} (${account.user_id || "—"})`;
|
const buildAccountLabel = (account) => `${account.username ? `@${account.username}` : "—"} (${account.user_id || "—"})`;
|
||||||
|
|
||||||
|
const roleStats = React.useMemo(() => {
|
||||||
|
const roles = Object.values(taskAccountRoles || {});
|
||||||
|
const monitor = roles.filter((item) => item.monitor).length;
|
||||||
|
const invite = roles.filter((item) => item.invite).length;
|
||||||
|
return { monitor, invite, total: roles.length };
|
||||||
|
}, [taskAccountRoles]);
|
||||||
return (
|
return (
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="row-header">
|
<div className="row-header">
|
||||||
@ -49,6 +59,18 @@ export default function AccountsTab({
|
|||||||
{taskNotice && taskNotice.source === "accounts" && (
|
{taskNotice && taskNotice.source === "accounts" && (
|
||||||
<div className={`notice inline ${taskNotice.tone}`}>{taskNotice.text}</div>
|
<div className={`notice inline ${taskNotice.tone}`}>{taskNotice.text}</div>
|
||||||
)}
|
)}
|
||||||
|
{hasSelectedTask && (
|
||||||
|
<div className="account-summary">
|
||||||
|
Роли: мониторинг — {roleStats.monitor}, инвайт — {roleStats.invite}, всего — {roleStats.total}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasSelectedTask && (
|
||||||
|
<div className="role-presets">
|
||||||
|
<button className="secondary" type="button" onClick={() => applyRolePreset("one")}>Один бот</button>
|
||||||
|
<button className="secondary" type="button" onClick={() => applyRolePreset("split")}>Разделить роли</button>
|
||||||
|
<button className="secondary" type="button" onClick={() => applyRolePreset("all")}>Все роли</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="account-list">
|
<div className="account-list">
|
||||||
{accounts.length === 0 && <div className="empty">Аккаунты не добавлены.</div>}
|
{accounts.length === 0 && <div className="empty">Аккаунты не добавлены.</div>}
|
||||||
{accountBuckets.freeOrSelected.map((account) => {
|
{accountBuckets.freeOrSelected.map((account) => {
|
||||||
@ -77,8 +99,16 @@ export default function AccountsTab({
|
|||||||
? `В нашей: ${membership.ourGroupMember ? "да" : "нет"}`
|
? `В нашей: ${membership.ourGroupMember ? "да" : "нет"}`
|
||||||
: "В нашей: —";
|
: "В нашей: —";
|
||||||
const selected = selectedAccountIds.includes(account.id);
|
const selected = selectedAccountIds.includes(account.id);
|
||||||
|
const roles = taskAccountRoles[account.id] || { monitor: false, invite: false };
|
||||||
const taskNames = assignedTasks
|
const taskNames = assignedTasks
|
||||||
.map((taskId) => accountBuckets.taskNameMap.get(taskId) || `Задача #${taskId}`)
|
.map((item) => {
|
||||||
|
const name = accountBuckets.taskNameMap.get(item.taskId) || `Задача #${item.taskId}`;
|
||||||
|
const roles = [
|
||||||
|
item.roleMonitor ? "М" : null,
|
||||||
|
item.roleInvite ? "И" : null
|
||||||
|
].filter(Boolean).join("/");
|
||||||
|
return roles ? `${name} (${roles})` : name;
|
||||||
|
})
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -86,6 +116,10 @@ export default function AccountsTab({
|
|||||||
<div>
|
<div>
|
||||||
<div className="account-phone">{account.phone}</div>
|
<div className="account-phone">{account.phone}</div>
|
||||||
<div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div>
|
<div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div>
|
||||||
|
<div className="role-badges">
|
||||||
|
{roles.monitor && <span className="role-pill">Мониторинг</span>}
|
||||||
|
{roles.invite && <span className="role-pill">Инвайт</span>}
|
||||||
|
</div>
|
||||||
<div className="account-meta">User ID: {account.user_id || "—"}</div>
|
<div className="account-meta">User ID: {account.user_id || "—"}</div>
|
||||||
<div className="account-meta membership-row">
|
<div className="account-meta membership-row">
|
||||||
<strong>{competitorInfo}</strong>
|
<strong>{competitorInfo}</strong>
|
||||||
@ -116,7 +150,7 @@ export default function AccountsTab({
|
|||||||
<div className="account-meta">
|
<div className="account-meta">
|
||||||
Осталось действий сегодня: {remaining == null ? "без лимита" : remaining} (использовано: {used}/{limit || "∞"})
|
Осталось действий сегодня: {remaining == null ? "без лимита" : remaining} (использовано: {used}/{limit || "∞"})
|
||||||
</div>
|
</div>
|
||||||
<div className="account-meta">Задачи: {assignedTasks.length ? taskNames : "—"}</div>
|
<div className="account-meta">Работает в задачах: {assignedTasks.length ? taskNames : "—"}</div>
|
||||||
{cooldownActive && (
|
{cooldownActive && (
|
||||||
<div className="account-meta">
|
<div className="account-meta">
|
||||||
Таймер FLOOD: {cooldownMinutes} мин
|
Таймер FLOOD: {cooldownMinutes} мин
|
||||||
@ -128,14 +162,31 @@ export default function AccountsTab({
|
|||||||
)}
|
)}
|
||||||
<div className="account-actions">
|
<div className="account-actions">
|
||||||
{hasSelectedTask && (
|
{hasSelectedTask && (
|
||||||
<label className="checkbox">
|
<div className="role-toggle">
|
||||||
<input
|
<label className="checkbox">
|
||||||
type="checkbox"
|
<input
|
||||||
checked={selected}
|
type="checkbox"
|
||||||
onChange={() => toggleAccountSelection(account.id)}
|
checked={Boolean(roles.monitor)}
|
||||||
/>
|
onChange={(event) => updateAccountRole(account.id, "monitor", event.target.checked)}
|
||||||
В задаче
|
/>
|
||||||
</label>
|
Мониторинг
|
||||||
|
</label>
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(roles.invite)}
|
||||||
|
onChange={(event) => updateAccountRole(account.id, "invite", event.target.checked)}
|
||||||
|
/>
|
||||||
|
Инвайт
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className="ghost tiny"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAccountRolesAll(account.id, !selected)}
|
||||||
|
>
|
||||||
|
{selected ? "Снять" : "Оба"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasSelectedTask && selected && (
|
{hasSelectedTask && selected && (
|
||||||
<button className="ghost" onClick={() => removeAccountFromTask(account.id)}>
|
<button className="ghost" onClick={() => removeAccountFromTask(account.id)}>
|
||||||
@ -166,7 +217,14 @@ export default function AccountsTab({
|
|||||||
{accountBuckets.busy.map((account) => {
|
{accountBuckets.busy.map((account) => {
|
||||||
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
||||||
const taskNames = assignedTasks
|
const taskNames = assignedTasks
|
||||||
.map((taskId) => accountBuckets.taskNameMap.get(taskId) || `Задача #${taskId}`)
|
.map((item) => {
|
||||||
|
const name = accountBuckets.taskNameMap.get(item.taskId) || `Задача #${item.taskId}`;
|
||||||
|
const roles = [
|
||||||
|
item.roleMonitor ? "М" : null,
|
||||||
|
item.roleInvite ? "И" : null
|
||||||
|
].filter(Boolean).join("/");
|
||||||
|
return roles ? `${name} (${roles})` : name;
|
||||||
|
})
|
||||||
.join(", ");
|
.join(", ");
|
||||||
const membership = membershipStatus[account.id];
|
const membership = membershipStatus[account.id];
|
||||||
const stats = accountStats.find((item) => item.id === account.id);
|
const stats = accountStats.find((item) => item.id === account.id);
|
||||||
@ -197,6 +255,10 @@ export default function AccountsTab({
|
|||||||
<div>
|
<div>
|
||||||
<div className="account-phone">{account.phone}</div>
|
<div className="account-phone">{account.phone}</div>
|
||||||
<div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div>
|
<div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div>
|
||||||
|
<div className="role-badges">
|
||||||
|
{roles.monitor && <span className="role-pill">Мониторинг</span>}
|
||||||
|
{roles.invite && <span className="role-pill">Инвайт</span>}
|
||||||
|
</div>
|
||||||
<div className="account-meta">User ID: {account.user_id || "—"}</div>
|
<div className="account-meta">User ID: {account.user_id || "—"}</div>
|
||||||
<div className="account-meta membership-row">
|
<div className="account-meta membership-row">
|
||||||
<strong>{competitorInfo}</strong>
|
<strong>{competitorInfo}</strong>
|
||||||
@ -227,7 +289,7 @@ export default function AccountsTab({
|
|||||||
<div className="account-meta">
|
<div className="account-meta">
|
||||||
Осталось действий сегодня: {remaining == null ? "без лимита" : remaining} (использовано: {used}/{limit || "∞"})
|
Осталось действий сегодня: {remaining == null ? "без лимита" : remaining} (использовано: {used}/{limit || "∞"})
|
||||||
</div>
|
</div>
|
||||||
<div className="account-meta">Задачи: {assignedTasks.length ? taskNames : "—"}</div>
|
<div className="account-meta">Работает в задачах: {assignedTasks.length ? taskNames : "—"}</div>
|
||||||
{cooldownActive && (
|
{cooldownActive && (
|
||||||
<div className="account-meta">
|
<div className="account-meta">
|
||||||
Таймер FLOOD: {cooldownMinutes} мин
|
Таймер FLOOD: {cooldownMinutes} мин
|
||||||
|
|||||||
@ -1,11 +1,51 @@
|
|||||||
import React from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
|
|
||||||
export default function EventsTab({ accountEvents, formatTimestamp }) {
|
export default function EventsTab({ accountEvents, formatTimestamp }) {
|
||||||
|
const [typeFilter, setTypeFilter] = useState("all");
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const eventTypes = useMemo(() => {
|
||||||
|
const types = new Set(accountEvents.map((event) => event.eventType));
|
||||||
|
return ["all", ...Array.from(types)];
|
||||||
|
}, [accountEvents]);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const q = query.trim().toLowerCase();
|
||||||
|
return accountEvents.filter((event) => {
|
||||||
|
if (typeFilter !== "all" && event.eventType !== typeFilter) return false;
|
||||||
|
if (!q) return true;
|
||||||
|
const text = [event.eventType, event.message, event.phone, event.accountId]
|
||||||
|
.join(" ")
|
||||||
|
.toLowerCase();
|
||||||
|
return text.includes(q);
|
||||||
|
});
|
||||||
|
}, [accountEvents, typeFilter, query]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card logs">
|
<section className="card logs">
|
||||||
<h2>События аккаунтов</h2>
|
<h2>События аккаунтов</h2>
|
||||||
{accountEvents.length === 0 && <div className="empty">Событий нет.</div>}
|
<div className="row-inline">
|
||||||
{accountEvents.map((event) => (
|
<input
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
placeholder="Поиск по событиям"
|
||||||
|
/>
|
||||||
|
<div className="task-filters">
|
||||||
|
{eventTypes.map((type) => (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
type="button"
|
||||||
|
className={`chip ${typeFilter === type ? "active" : ""}`}
|
||||||
|
onClick={() => setTypeFilter(type)}
|
||||||
|
>
|
||||||
|
{type === "all" ? "Все" : type}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{filtered.length === 0 && <div className="empty">Событий нет.</div>}
|
||||||
|
{filtered.map((event) => (
|
||||||
<div key={event.id} className="log-row">
|
<div key={event.id} className="log-row">
|
||||||
<div className="log-time">
|
<div className="log-time">
|
||||||
<div>{formatTimestamp(event.createdAt)}</div>
|
<div>{formatTimestamp(event.createdAt)}</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user