some
This commit is contained in:
parent
10ee8fb3c1
commit
4bba4f3149
@ -204,13 +204,22 @@ ipcMain.handle("tasks:get", (_event, id) => {
|
||||
return {
|
||||
task,
|
||||
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) => {
|
||||
const taskId = store.saveTask(payload.task);
|
||||
store.setTaskCompetitors(taskId, payload.competitors || []);
|
||||
if (payload.accountRoles && payload.accountRoles.length) {
|
||||
store.setTaskAccountRoles(taskId, payload.accountRoles);
|
||||
} else {
|
||||
store.setTaskAccounts(taskId, payload.accountIds || []);
|
||||
}
|
||||
return { ok: true, taskId };
|
||||
});
|
||||
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" };
|
||||
const task = store.getTask(payload.taskId);
|
||||
if (!task) return { ok: false, error: "Task not found" };
|
||||
const existing = store.listTaskAccounts(payload.taskId).map((row) => row.account_id);
|
||||
const merged = Array.from(new Set([...(existing || []), ...((payload.accountIds || []))]));
|
||||
store.setTaskAccounts(payload.taskId, merged);
|
||||
return { ok: true, accountIds: merged };
|
||||
const existingRows = store.listTaskAccounts(payload.taskId);
|
||||
const existing = new Map(existingRows.map((row) => [
|
||||
row.account_id,
|
||||
{ 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) => {
|
||||
if (!payload || !payload.taskId || !payload.accountId) {
|
||||
@ -261,10 +286,15 @@ ipcMain.handle("tasks:removeAccount", (_event, payload) => {
|
||||
}
|
||||
const task = store.getTask(payload.taskId);
|
||||
if (!task) return { ok: false, error: "Task not found" };
|
||||
const existing = store.listTaskAccounts(payload.taskId).map((row) => row.account_id);
|
||||
const filtered = existing.filter((id) => id !== payload.accountId);
|
||||
store.setTaskAccounts(payload.taskId, filtered);
|
||||
return { ok: true, accountIds: filtered };
|
||||
const existing = store.listTaskAccounts(payload.taskId)
|
||||
.filter((row) => row.account_id !== payload.accountId)
|
||||
.map((row) => ({
|
||||
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 () => {
|
||||
const tasks = store.listTasks();
|
||||
@ -307,6 +337,33 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
||||
const dailyUsed = store.countInvitesToday(id);
|
||||
const task = store.getTask(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 {
|
||||
running: runner ? runner.isRunning() : false,
|
||||
queueCount,
|
||||
@ -314,7 +371,11 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
||||
dailyLimit: task ? task.daily_limit : 0,
|
||||
dailyRemaining: task ? Math.max(0, Number(task.daily_limit || 0) - dailyUsed) : 0,
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
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", "separate_bot_roles", "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");
|
||||
if (!settingsRow) {
|
||||
@ -356,6 +360,29 @@ function initStore(userDataPath) {
|
||||
.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) {
|
||||
if (taskId == null) {
|
||||
db.prepare("DELETE FROM invite_queue").run();
|
||||
@ -465,8 +492,27 @@ function initStore(userDataPath) {
|
||||
|
||||
function setTaskAccounts(taskId, accountIds) {
|
||||
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId);
|
||||
const stmt = db.prepare("INSERT INTO task_accounts (task_id, account_id) VALUES (?, ?)");
|
||||
(accountIds || []).forEach((accountId) => stmt.run(taskId, accountId));
|
||||
const stmt = db.prepare(`
|
||||
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) {
|
||||
@ -665,6 +711,7 @@ function initStore(userDataPath) {
|
||||
listTaskAccounts,
|
||||
listAllTaskAccounts,
|
||||
setTaskAccounts,
|
||||
setTaskAccountRoles,
|
||||
listLogs,
|
||||
listInvites,
|
||||
clearLogs,
|
||||
@ -680,6 +727,7 @@ function initStore(userDataPath) {
|
||||
enqueueInvite,
|
||||
getPendingInvites,
|
||||
getPendingCount,
|
||||
getPendingStats,
|
||||
clearQueue,
|
||||
markInviteStatus,
|
||||
incrementInviteAttempt,
|
||||
|
||||
@ -8,6 +8,8 @@ class TaskRunner {
|
||||
this.running = false;
|
||||
this.timer = null;
|
||||
this.nextRunAt = "";
|
||||
this.nextInviteAccountId = 0;
|
||||
this.lastInviteAccountId = 0;
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
@ -18,6 +20,14 @@ class TaskRunner {
|
||||
return this.nextRunAt || "";
|
||||
}
|
||||
|
||||
getNextInviteAccountId() {
|
||||
return this.nextInviteAccountId || 0;
|
||||
}
|
||||
|
||||
getLastInviteAccountId() {
|
||||
return this.lastInviteAccountId || 0;
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.running) return;
|
||||
this.running = true;
|
||||
@ -30,14 +40,21 @@ class TaskRunner {
|
||||
if (this.timer) clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
this.nextRunAt = "";
|
||||
this.nextInviteAccountId = 0;
|
||||
this.telegram.stopTaskMonitor(this.task.id);
|
||||
}
|
||||
|
||||
async _initMonitoring() {
|
||||
const competitors = this.store.listTaskCompetitors(this.task.id).map((row) => row.link);
|
||||
const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id);
|
||||
await this.telegram.joinGroupsForTask(this.task, competitors, accounts);
|
||||
await this.telegram.startTaskMonitor(this.task, competitors, accounts);
|
||||
const accountRows = this.store.listTaskAccounts(this.task.id);
|
||||
const accounts = accountRows.map((row) => row.account_id);
|
||||
const monitorIds = accountRows.filter((row) => row.role_monitor).map((row) => row.account_id);
|
||||
const inviteIds = accountRows.filter((row) => row.role_invite).map((row) => row.account_id);
|
||||
await this.telegram.joinGroupsForTask(this.task, competitors, accounts, {
|
||||
monitorIds,
|
||||
inviteIds
|
||||
});
|
||||
await this.telegram.startTaskMonitor(this.task, competitors, accounts, monitorIds);
|
||||
}
|
||||
|
||||
_scheduleNext() {
|
||||
@ -55,12 +72,19 @@ class TaskRunner {
|
||||
const successIds = [];
|
||||
let invitedCount = 0;
|
||||
this.nextRunAt = "";
|
||||
this.nextInviteAccountId = 0;
|
||||
|
||||
try {
|
||||
const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id);
|
||||
let inviteAccounts = accounts;
|
||||
if (this.task.separate_bot_roles || this.task.require_same_bot_in_both) {
|
||||
const roles = this.telegram.getTaskRoleAssignments(this.task.id);
|
||||
const hasExplicitRoles = roles && ((roles.ourIds || []).length || (roles.competitorIds || []).length);
|
||||
if (hasExplicitRoles) {
|
||||
inviteAccounts = (roles.ourIds || []).length ? roles.ourIds : (roles.competitorIds || []);
|
||||
if (!inviteAccounts.length) {
|
||||
errors.push("No invite accounts (role-based)");
|
||||
}
|
||||
} else if (this.task.separate_bot_roles || this.task.require_same_bot_in_both) {
|
||||
inviteAccounts = this.task.require_same_bot_in_both
|
||||
? (roles.competitorIds || [])
|
||||
: (roles.ourIds || []);
|
||||
@ -79,6 +103,9 @@ class TaskRunner {
|
||||
if (!this.task.multi_accounts_per_run) {
|
||||
const entry = this.telegram.pickInviteAccount(inviteAccounts, Boolean(this.task.random_accounts));
|
||||
inviteAccounts = entry ? [entry.account.id] : [];
|
||||
this.nextInviteAccountId = entry ? entry.account.id : 0;
|
||||
} else if (inviteAccounts.length) {
|
||||
this.nextInviteAccountId = inviteAccounts[0];
|
||||
}
|
||||
const totalAccounts = accounts.length;
|
||||
if (this.task.stop_on_blocked) {
|
||||
@ -124,6 +151,7 @@ class TaskRunner {
|
||||
invitedCount += 1;
|
||||
successIds.push(item.user_id);
|
||||
this.store.markInviteStatus(item.id, "invited");
|
||||
this.lastInviteAccountId = result.accountId || this.lastInviteAccountId;
|
||||
this.store.recordInvite(
|
||||
this.task.id,
|
||||
item.user_id,
|
||||
|
||||
@ -453,11 +453,19 @@ class TelegramManager {
|
||||
}
|
||||
if (!user && sourceChat) {
|
||||
const resolved = await this._resolveUserFromSource(client, sourceChat, userId);
|
||||
if (resolved) {
|
||||
user = resolved;
|
||||
attempts.push({ strategy: "participants", ok: true, detail: "from group participants" });
|
||||
if (resolved && resolved.accessHash) {
|
||||
try {
|
||||
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 {
|
||||
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) {
|
||||
@ -506,7 +514,13 @@ class TelegramManager {
|
||||
try {
|
||||
let retryUser = null;
|
||||
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 {
|
||||
@ -601,6 +615,15 @@ class TelegramManager {
|
||||
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) {
|
||||
return this._pickClientForInvite(allowedAccountIds, randomize);
|
||||
}
|
||||
@ -772,48 +795,76 @@ class TelegramManager {
|
||||
}
|
||||
|
||||
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 now = Date.now();
|
||||
if (cacheEntry && now - cacheEntry.at < 5 * 60 * 1000) {
|
||||
const cached = cacheEntry.map.get(userId.toString());
|
||||
if (cached && cached.accessHash) {
|
||||
return new Api.InputUser({
|
||||
userId: BigInt(userId),
|
||||
accessHash: BigInt(cached.accessHash)
|
||||
});
|
||||
return {
|
||||
accessHash: 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);
|
||||
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);
|
||||
this.participantCache.set(sourceChat, { at: now, map });
|
||||
if (!map.size) {
|
||||
return { accessHash: "", detail: "participants hidden" };
|
||||
}
|
||||
const cached = map.get(userId.toString());
|
||||
if (cached && cached.accessHash) {
|
||||
return new Api.InputUser({
|
||||
userId: BigInt(userId),
|
||||
accessHash: BigInt(cached.accessHash)
|
||||
});
|
||||
return { accessHash: cached.accessHash, detail: `participants (${map.size})` };
|
||||
}
|
||||
return null;
|
||||
return { accessHash: "", detail: `not in participants (${map.size})` };
|
||||
}
|
||||
|
||||
async _resolveQueueIdentity(client, sourceChat, userId) {
|
||||
if (!client || !userId) return null;
|
||||
const fromParticipants = await this._resolveUserFromSource(client, sourceChat, userId);
|
||||
if (fromParticipants && fromParticipants.accessHash != null) {
|
||||
return { accessHash: fromParticipants.accessHash.toString(), strategy: "participants" };
|
||||
const attempts = [];
|
||||
if (!client || !userId) {
|
||||
return { accessHash: "", strategy: "", detail: "no client or userId", attempts };
|
||||
}
|
||||
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 {
|
||||
const resolvedUser = await client.getEntity(userId);
|
||||
const input = await client.getInputEntity(resolvedUser);
|
||||
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) {
|
||||
// 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) {
|
||||
@ -883,26 +934,43 @@ class TelegramManager {
|
||||
if (!Number.isFinite(maxGroups) || maxGroups <= 0) {
|
||||
maxGroups = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
let memberCount = 0;
|
||||
const existing = [];
|
||||
const toJoin = [];
|
||||
for (const group of groups) {
|
||||
if (!group) continue;
|
||||
try {
|
||||
if (memberCount >= maxGroups) break;
|
||||
const alreadyMember = await this._isParticipant(client, group);
|
||||
if (alreadyMember) {
|
||||
memberCount += 1;
|
||||
continue;
|
||||
existing.push(group);
|
||||
} 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)) {
|
||||
const hash = this._extractInviteHash(group);
|
||||
if (hash) {
|
||||
await client.invoke(new Api.messages.ImportChatInvite({ hash }));
|
||||
memberCount += 1;
|
||||
}
|
||||
} else {
|
||||
const entity = await client.getEntity(group);
|
||||
await client.invoke(new Api.channels.JoinChannel({ channel: entity }));
|
||||
memberCount += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
const errorText = error.errorMessage || error.message || String(error);
|
||||
@ -1049,14 +1117,29 @@ 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 competitorBots = Math.max(1, Number(task.max_competitor_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 || [];
|
||||
let cursor = 0;
|
||||
const usedForCompetitors = new Set();
|
||||
if (hasExplicitRoles) {
|
||||
for (const group of competitors) {
|
||||
const pool = accounts.filter((entry) => explicitMonitorIds.includes(entry.account.id));
|
||||
for (const entry of pool) {
|
||||
usedForCompetitors.add(entry.account.id);
|
||||
if (task.auto_join_competitors) {
|
||||
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;
|
||||
@ -1068,10 +1151,19 @@ class TelegramManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const usedForOur = new Set();
|
||||
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 finalPool = pool.length ? pool : accounts;
|
||||
const targetCount = Math.max(1, Number(task.max_competitor_bots || 1));
|
||||
@ -1095,19 +1187,12 @@ class TelegramManager {
|
||||
}
|
||||
}
|
||||
this.taskRoleAssignments.set(task.id, {
|
||||
competitorIds: Array.from(usedForCompetitors),
|
||||
ourIds: Array.from(usedForOur)
|
||||
competitorIds: hasExplicitRoles ? explicitMonitorIds : Array.from(usedForCompetitors),
|
||||
ourIds: hasExplicitRoles ? explicitInviteIds : Array.from(usedForOur)
|
||||
});
|
||||
}
|
||||
|
||||
async startTaskMonitor(task, competitorGroups, accountIds) {
|
||||
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);
|
||||
async _startMonitorEntry(task, monitorAccount, groups) {
|
||||
const resolved = [];
|
||||
const errors = [];
|
||||
for (const group of groups) {
|
||||
@ -1210,14 +1295,17 @@ class TelegramManager {
|
||||
if (!senderInfo || !senderInfo.info) {
|
||||
const resolved = rawSenderId
|
||||
? await this._resolveQueueIdentity(monitorAccount.client, st.source, rawSenderId)
|
||||
: null;
|
||||
if (!resolved) {
|
||||
: { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] };
|
||||
if (!resolved || !resolved.accessHash) {
|
||||
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(
|
||||
monitorAccount.account.id,
|
||||
monitorAccount.account.phone,
|
||||
"new_message_skipped",
|
||||
`${formatGroupLabel(st)}: ${senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя"}, ${this._describeSender(message)}`
|
||||
`${formatGroupLabel(st)}: ${reason}${extra}, ${this._describeSender(message)}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
@ -1226,11 +1314,12 @@ class TelegramManager {
|
||||
const username = "";
|
||||
const accessHash = resolved.accessHash;
|
||||
if (shouldLogEvent(`${chatId}: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})`
|
||||
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
|
||||
);
|
||||
}
|
||||
monitorEntry.lastMessageAt = new Date().toISOString();
|
||||
@ -1249,29 +1338,32 @@ class TelegramManager {
|
||||
}
|
||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||
const resolved = await this._resolveQueueIdentity(monitorAccount.client, st.source, senderPayload.userId);
|
||||
if (resolved) {
|
||||
if (resolved && resolved.accessHash) {
|
||||
senderPayload.accessHash = resolved.accessHash;
|
||||
if (shouldLogEvent(`${chatId}: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})`
|
||||
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||
if (shouldLogEvent(`${chatId}:skip`, 30000)) {
|
||||
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
||||
const extra = strategySummary ? `; стратегии: ${strategySummary}` : "";
|
||||
this.store.addAccountEvent(
|
||||
monitorAccount.account.id,
|
||||
monitorAccount.account.phone,
|
||||
"new_message_skipped",
|
||||
`${formatGroupLabel(st)}: нет access_hash (нет в списке участников)`
|
||||
`${formatGroupLabel(st)}: нет access_hash (нет в списке участников)${extra}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { userId: senderId, username, accessHash } = senderPayload;
|
||||
if (this._isOwnAccount(senderId)) return;
|
||||
let messageDate = new Date();
|
||||
@ -1348,15 +1440,18 @@ class TelegramManager {
|
||||
if (!senderInfo || !senderInfo.info) {
|
||||
const resolved = rawSenderId
|
||||
? await this._resolveQueueIdentity(monitorAccount.client, st.source, rawSenderId)
|
||||
: null;
|
||||
if (!resolved) {
|
||||
: { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] };
|
||||
if (!resolved || !resolved.accessHash) {
|
||||
skipped += 1;
|
||||
if (shouldLogEvent(`${key}:skip`, 30000)) {
|
||||
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
||||
const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
|
||||
const extra = strategySummary ? `; стратегии: ${strategySummary}` : "";
|
||||
this.store.addAccountEvent(
|
||||
monitorAccount.account.id,
|
||||
monitorAccount.account.phone,
|
||||
"new_message_skipped",
|
||||
`${formatGroupLabel(st)}: ${senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя"}, ${this._describeSender(message)}`
|
||||
`${formatGroupLabel(st)}: ${reason}${extra}, ${this._describeSender(message)}`
|
||||
);
|
||||
}
|
||||
continue;
|
||||
@ -1365,11 +1460,12 @@ class TelegramManager {
|
||||
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})`
|
||||
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
|
||||
);
|
||||
}
|
||||
monitorEntry.lastMessageAt = new Date().toISOString();
|
||||
@ -1390,30 +1486,33 @@ class TelegramManager {
|
||||
}
|
||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||
const resolved = await this._resolveQueueIdentity(monitorAccount.client, st.source, senderPayload.userId);
|
||||
if (resolved) {
|
||||
if (resolved && resolved.accessHash) {
|
||||
senderPayload.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})`
|
||||
`${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||
skipped += 1;
|
||||
if (shouldLogEvent(`${key}:skip`, 30000)) {
|
||||
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
||||
const extra = strategySummary ? `; стратегии: ${strategySummary}` : "";
|
||||
this.store.addAccountEvent(
|
||||
monitorAccount.account.id,
|
||||
monitorAccount.account.phone,
|
||||
"new_message_skipped",
|
||||
`${formatGroupLabel(st)}: нет access_hash (нет в списке участников)`
|
||||
`${formatGroupLabel(st)}: нет access_hash (нет в списке участников)${extra}`
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const { userId: senderId, username, accessHash } = senderPayload;
|
||||
if (this._isOwnAccount(senderId)) continue;
|
||||
let messageDate = new Date();
|
||||
@ -1482,8 +1581,44 @@ class TelegramManager {
|
||||
`Интервал: 20 сек`
|
||||
);
|
||||
|
||||
this.taskMonitors.set(task.id, monitorEntry);
|
||||
return { ok: true, errors };
|
||||
return { ok: true, entry: monitorEntry, 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) {
|
||||
@ -1511,6 +1646,7 @@ class TelegramManager {
|
||||
let enqueued = 0;
|
||||
let skipped = 0;
|
||||
const skipReasons = {};
|
||||
let strategySkipSample = "";
|
||||
for (const message of messages) {
|
||||
total += 1;
|
||||
const senderInfo = await this._getUserInfoFromMessage(entry.client, message);
|
||||
@ -1518,11 +1654,17 @@ class TelegramManager {
|
||||
if (!senderInfo || !senderInfo.info) {
|
||||
const resolved = rawSenderId
|
||||
? await this._resolveQueueIdentity(entry.client, group, rawSenderId)
|
||||
: null;
|
||||
if (!resolved) {
|
||||
: { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] };
|
||||
if (!resolved || !resolved.accessHash) {
|
||||
skipped += 1;
|
||||
const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
|
||||
skipReasons[reason] = (skipReasons[reason] || 0) + 1;
|
||||
if (!strategySkipSample) {
|
||||
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
||||
if (strategySummary) {
|
||||
strategySkipSample = `${group}: ${reason}; стратегии: ${strategySummary}`;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (this.store.enqueueInvite(task.id, rawSenderId, "", group, resolved.accessHash, entry.account.id)) {
|
||||
@ -1541,9 +1683,15 @@ class TelegramManager {
|
||||
}
|
||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||
const resolved = await this._resolveQueueIdentity(entry.client, group, senderPayload.userId);
|
||||
if (resolved) {
|
||||
if (resolved && 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) {
|
||||
skipped += 1;
|
||||
@ -1565,6 +1713,14 @@ class TelegramManager {
|
||||
summaryLines.push(
|
||||
`${group}: сообщений ${total}, добавлено ${enqueued}, пропущено ${skipped}${skipSummary ? ` (${skipSummary})` : ""}`
|
||||
);
|
||||
if (strategySkipSample) {
|
||||
this.store.addAccountEvent(
|
||||
entry.account.id,
|
||||
entry.account.phone,
|
||||
"history_skip_detail",
|
||||
strategySkipSample
|
||||
);
|
||||
}
|
||||
}
|
||||
if (summaryLines.length) {
|
||||
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) {
|
||||
const entry = this.taskMonitors.get(taskId);
|
||||
if (!entry) return;
|
||||
if (entry.timer) clearInterval(entry.timer);
|
||||
const clientEntry = this.clients.get(entry.accountId);
|
||||
if (clientEntry && entry.handler) {
|
||||
const entries = Array.isArray(entry.entries) ? entry.entries : [entry];
|
||||
entries.forEach((monitorEntry) => {
|
||||
if (monitorEntry.timer) clearInterval(monitorEntry.timer);
|
||||
const clientEntry = this.clients.get(monitorEntry.accountId);
|
||||
if (clientEntry && monitorEntry.handler) {
|
||||
try {
|
||||
clientEntry.client.removeEventHandler(entry.handler);
|
||||
clientEntry.client.removeEventHandler(monitorEntry.handler);
|
||||
} catch (error) {
|
||||
// ignore handler removal errors
|
||||
}
|
||||
}
|
||||
});
|
||||
this.taskMonitors.delete(taskId);
|
||||
this.taskRoleAssignments.delete(taskId);
|
||||
}
|
||||
@ -1673,14 +1842,25 @@ class TelegramManager {
|
||||
getTaskMonitorInfo(taskId) {
|
||||
const entry = this.taskMonitors.get(taskId);
|
||||
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 {
|
||||
monitoring: true,
|
||||
accountId: entry.accountId || 0,
|
||||
groups: entry.groups || [],
|
||||
lastMessageAt: entry.lastMessageAt || "",
|
||||
lastSource: entry.lastSource || ""
|
||||
accountId: accountIds[0] || 0,
|
||||
accountIds,
|
||||
groups: entries.flatMap((item) => item.groups || []),
|
||||
lastMessageAt,
|
||||
lastSource
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -94,14 +94,19 @@ export default function App() {
|
||||
const [taskForm, setTaskForm] = useState(emptyTaskForm);
|
||||
const [competitorText, setCompetitorText] = useState("");
|
||||
const [selectedAccountIds, setSelectedAccountIds] = useState([]);
|
||||
const [taskAccountRoles, setTaskAccountRoles] = useState({});
|
||||
const [taskStatus, setTaskStatus] = useState({
|
||||
running: false,
|
||||
queueCount: 0,
|
||||
dailyRemaining: 0,
|
||||
dailyUsed: 0,
|
||||
dailyLimit: 0,
|
||||
monitorInfo: { monitoring: false, groups: [], lastMessageAt: "", lastSource: "" },
|
||||
nextRunAt: ""
|
||||
monitorInfo: { monitoring: false, accountId: 0, accountIds: [], groups: [], lastMessageAt: "", lastSource: "" },
|
||||
nextRunAt: "",
|
||||
nextInviteAccountId: 0,
|
||||
lastInviteAccountId: 0,
|
||||
pendingStats: { total: 0, withUsername: 0, withAccessHash: 0, withoutData: 0 },
|
||||
warnings: []
|
||||
});
|
||||
const [taskStatusMap, setTaskStatusMap] = useState({});
|
||||
const [membershipStatus, setMembershipStatus] = useState({});
|
||||
@ -170,11 +175,25 @@ export default function App() {
|
||||
});
|
||||
return map;
|
||||
}, [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 map = new Map();
|
||||
accountAssignments.forEach((row) => {
|
||||
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);
|
||||
});
|
||||
return map;
|
||||
@ -190,7 +209,7 @@ export default function App() {
|
||||
});
|
||||
accounts.forEach((account) => {
|
||||
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;
|
||||
if (filterFreeAccounts && !isFree && !assignedToSelected) {
|
||||
busy.push(account);
|
||||
@ -243,6 +262,7 @@ export default function App() {
|
||||
setTaskForm(emptyTaskForm);
|
||||
setCompetitorText("");
|
||||
setSelectedAccountIds([]);
|
||||
setTaskAccountRoles({});
|
||||
setLogs([]);
|
||||
setInvites([]);
|
||||
setGroupVisibility([]);
|
||||
@ -252,8 +272,11 @@ export default function App() {
|
||||
dailyRemaining: 0,
|
||||
dailyUsed: 0,
|
||||
dailyLimit: 0,
|
||||
monitorInfo: { monitoring: false, groups: [], lastMessageAt: "", lastSource: "" },
|
||||
nextRunAt: ""
|
||||
monitorInfo: { monitoring: false, accountId: 0, accountIds: [], groups: [], lastMessageAt: "", lastSource: "" },
|
||||
nextRunAt: "",
|
||||
nextInviteAccountId: 0,
|
||||
lastInviteAccountId: 0,
|
||||
pendingStats: { total: 0, withUsername: 0, withAccessHash: 0, withoutData: 0 }
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -261,7 +284,21 @@ export default function App() {
|
||||
if (!details) return;
|
||||
setTaskForm(sanitizeTaskForm({ ...emptyTaskForm, ...normalizeTask(details.task) }));
|
||||
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 }));
|
||||
setInvites(await window.api.listInvites({ limit: 200, taskId }));
|
||||
setGroupVisibility([]);
|
||||
@ -662,6 +699,7 @@ export default function App() {
|
||||
setTaskForm(emptyTaskForm);
|
||||
setCompetitorText("");
|
||||
setSelectedAccountIds([]);
|
||||
setTaskAccountRoles({});
|
||||
setAccessStatus([]);
|
||||
setMembershipStatus({});
|
||||
};
|
||||
@ -680,9 +718,15 @@ export default function App() {
|
||||
showNotification("Сохраняем задачу...", "info");
|
||||
const nextForm = sanitizeTaskForm(taskForm);
|
||||
setTaskForm(nextForm);
|
||||
let accountIds = selectedAccountIds;
|
||||
let accountRolesMap = { ...taskAccountRoles };
|
||||
let accountIds = Object.keys(accountRolesMap).map((id) => Number(id));
|
||||
if (nextForm.autoAssignAccounts && (!accountIds || accountIds.length === 0)) {
|
||||
accountIds = accounts.map((account) => account.id);
|
||||
accountRolesMap = {};
|
||||
accountIds.forEach((accountId) => {
|
||||
accountRolesMap[accountId] = { monitor: true, invite: true };
|
||||
});
|
||||
setTaskAccountRoles(accountRolesMap);
|
||||
setSelectedAccountIds(accountIds);
|
||||
if (accountIds.length) {
|
||||
setTaskNotice({ text: `Автоназначены аккаунты: ${accountIds.length}`, tone: "success", source });
|
||||
@ -692,6 +736,19 @@ export default function App() {
|
||||
showNotification("Нет аккаунтов для этой задачи.", "error");
|
||||
return;
|
||||
}
|
||||
const roleEntries = Object.values(accountRolesMap);
|
||||
if (roleEntries.length) {
|
||||
const hasMonitor = roleEntries.some((item) => item.monitor);
|
||||
const hasInvite = roleEntries.some((item) => item.invite);
|
||||
if (!hasMonitor) {
|
||||
showNotification("Нужен хотя бы один аккаунт с ролью мониторинга.", "error");
|
||||
return;
|
||||
}
|
||||
if (!hasInvite) {
|
||||
showNotification("Нужен хотя бы один аккаунт с ролью инвайта.", "error");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const requiredAccounts = nextForm.requireSameBotInBoth
|
||||
? Math.max(1, Number(nextForm.maxCompetitorBots || 1))
|
||||
: nextForm.separateBotRoles
|
||||
@ -701,10 +758,17 @@ export default function App() {
|
||||
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({
|
||||
task: nextForm,
|
||||
competitors: competitorGroups,
|
||||
accountIds
|
||||
accountIds,
|
||||
accountRoles
|
||||
});
|
||||
if (result.ok) {
|
||||
setTaskNotice({ text: "Задача сохранена.", tone: "success", source });
|
||||
@ -972,6 +1036,7 @@ export default function App() {
|
||||
setTaskForm(emptyTaskForm);
|
||||
setCompetitorText("");
|
||||
setSelectedAccountIds([]);
|
||||
setTaskAccountRoles({});
|
||||
setLogs([]);
|
||||
setInvites([]);
|
||||
setTaskStatus({
|
||||
@ -980,7 +1045,7 @@ export default function App() {
|
||||
dailyRemaining: 0,
|
||||
dailyUsed: 0,
|
||||
dailyLimit: 0,
|
||||
monitorInfo: { monitoring: false, groups: [], lastMessageAt: "", lastSource: "" }
|
||||
monitorInfo: { monitoring: false, accountId: 0, accountIds: [], groups: [], lastMessageAt: "", lastSource: "" }
|
||||
});
|
||||
await loadBase();
|
||||
} catch (error) {
|
||||
@ -988,24 +1053,107 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAccountSelection = (accountId) => {
|
||||
setSelectedAccountIds((prev) => {
|
||||
if (prev.includes(accountId)) {
|
||||
return prev.filter((id) => id !== accountId);
|
||||
}
|
||||
return [...prev, accountId];
|
||||
const persistAccountRoles = async (next) => {
|
||||
if (!window.api || selectedTaskId == null) return;
|
||||
const rolePayload = Object.entries(next).map(([id, roles]) => ({
|
||||
accountId: Number(id),
|
||||
roleMonitor: Boolean(roles.monitor),
|
||||
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) => {
|
||||
if (!window.api || selectedTaskId == null) 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({
|
||||
taskId: selectedTaskId,
|
||||
accountIds
|
||||
accountRoles: rolePayload
|
||||
});
|
||||
if (result && result.ok) {
|
||||
setSelectedAccountIds(result.accountIds || []);
|
||||
setTaskAccountRoles(nextRoles);
|
||||
setSelectedAccountIds(Object.keys(nextRoles).map((id) => Number(id)));
|
||||
await loadAccountAssignments();
|
||||
}
|
||||
};
|
||||
@ -1023,7 +1171,12 @@ export default function App() {
|
||||
accountId
|
||||
});
|
||||
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();
|
||||
setTaskNotice({ text: "Аккаунт удален из задачи.", tone: "success", source: "accounts" });
|
||||
}
|
||||
@ -1504,11 +1657,18 @@ export default function App() {
|
||||
? status.monitorInfo.lastSource
|
||||
: "—";
|
||||
const monitoring = Boolean(status && status.monitorInfo && status.monitorInfo.monitoring);
|
||||
const monitorAccountId = status && status.monitorInfo ? status.monitorInfo.accountId : 0;
|
||||
const monitorAccount = monitorAccountId ? accountById.get(monitorAccountId) : null;
|
||||
const monitorLabel = monitorAccount
|
||||
? (monitorAccount.phone || monitorAccount.user_id || String(monitorAccountId))
|
||||
: (monitorAccountId ? String(monitorAccountId) : "—");
|
||||
const monitorAccountIds = status && status.monitorInfo && status.monitorInfo.accountIds
|
||||
? status.monitorInfo.accountIds
|
||||
: (status && status.monitorInfo && status.monitorInfo.accountId ? [status.monitorInfo.accountId] : []);
|
||||
const monitorLabels = monitorAccountIds
|
||||
.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 = [
|
||||
`Статус: ${statusLabel}`,
|
||||
`Очередь: ${status ? status.queueCount : "—"}`,
|
||||
@ -1613,10 +1773,18 @@ export default function App() {
|
||||
<div className="live-label">Мониторит</div>
|
||||
<div className="live-value">
|
||||
{(() => {
|
||||
const monitorId = taskStatus.monitorInfo ? taskStatus.monitorInfo.accountId : 0;
|
||||
const account = monitorId ? accountById.get(monitorId) : null;
|
||||
if (!monitorId) return "—";
|
||||
return account ? (account.phone || account.user_id || String(monitorId)) : String(monitorId);
|
||||
const monitorIds = taskStatus.monitorInfo && taskStatus.monitorInfo.accountIds
|
||||
? taskStatus.monitorInfo.accountIds
|
||||
: (taskStatus.monitorInfo && taskStatus.monitorInfo.accountId ? [taskStatus.monitorInfo.accountId] : []);
|
||||
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>
|
||||
@ -1632,6 +1800,18 @@ export default function App() {
|
||||
<div className="live-label">Очередь инвайтов</div>
|
||||
<div className="live-value">{taskStatus.queueCount}</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 className="live-label">Лимит в день</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-value">{formatCountdown(taskStatus.nextRunAt)}</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 className="live-label">Стратегии OK/Fail</div>
|
||||
<div className="live-value">{inviteStrategyStats.success}/{inviteStrategyStats.failed}</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 className="status-actions">
|
||||
{taskStatus.running ? (
|
||||
@ -1656,6 +1862,57 @@ export default function App() {
|
||||
<button className="primary" onClick={() => startTask("status")} disabled={!hasSelectedTask}>Запустить</button>
|
||||
)}
|
||||
</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 && (
|
||||
<div className="status-text">
|
||||
{groupVisibility.some((item) => item.hidden) && (
|
||||
@ -1939,6 +2196,7 @@ export default function App() {
|
||||
accountBuckets={accountBuckets}
|
||||
filterFreeAccounts={filterFreeAccounts}
|
||||
selectedAccountIds={selectedAccountIds}
|
||||
taskAccountRoles={taskAccountRoles}
|
||||
hasSelectedTask={hasSelectedTask}
|
||||
taskNotice={taskNotice}
|
||||
refreshMembership={refreshMembership}
|
||||
@ -1946,7 +2204,9 @@ export default function App() {
|
||||
formatAccountStatus={formatAccountStatus}
|
||||
resetCooldown={resetCooldown}
|
||||
deleteAccount={deleteAccount}
|
||||
toggleAccountSelection={toggleAccountSelection}
|
||||
updateAccountRole={updateAccountRole}
|
||||
setAccountRolesAll={setAccountRolesAll}
|
||||
applyRolePreset={applyRolePreset}
|
||||
removeAccountFromTask={removeAccountFromTask}
|
||||
moveAccountToTask={moveAccountToTask}
|
||||
/>
|
||||
|
||||
@ -651,6 +651,55 @@ button.danger {
|
||||
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 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
@ -702,6 +751,12 @@ button.danger {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.role-toggle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tasks-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 260px) minmax(320px, 1fr);
|
||||
|
||||
@ -9,6 +9,7 @@ export default function AccountsTab({
|
||||
accountBuckets,
|
||||
filterFreeAccounts,
|
||||
selectedAccountIds,
|
||||
taskAccountRoles,
|
||||
hasSelectedTask,
|
||||
taskNotice,
|
||||
refreshMembership,
|
||||
@ -16,7 +17,9 @@ export default function AccountsTab({
|
||||
formatAccountStatus,
|
||||
resetCooldown,
|
||||
deleteAccount,
|
||||
toggleAccountSelection,
|
||||
updateAccountRole,
|
||||
setAccountRolesAll,
|
||||
applyRolePreset,
|
||||
removeAccountFromTask,
|
||||
moveAccountToTask
|
||||
}) {
|
||||
@ -31,6 +34,13 @@ export default function AccountsTab({
|
||||
};
|
||||
|
||||
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 (
|
||||
<section className="card">
|
||||
<div className="row-header">
|
||||
@ -49,6 +59,18 @@ export default function AccountsTab({
|
||||
{taskNotice && taskNotice.source === "accounts" && (
|
||||
<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">
|
||||
{accounts.length === 0 && <div className="empty">Аккаунты не добавлены.</div>}
|
||||
{accountBuckets.freeOrSelected.map((account) => {
|
||||
@ -77,8 +99,16 @@ export default function AccountsTab({
|
||||
? `В нашей: ${membership.ourGroupMember ? "да" : "нет"}`
|
||||
: "В нашей: —";
|
||||
const selected = selectedAccountIds.includes(account.id);
|
||||
const roles = taskAccountRoles[account.id] || { monitor: false, invite: false };
|
||||
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(", ");
|
||||
|
||||
return (
|
||||
@ -86,6 +116,10 @@ export default function AccountsTab({
|
||||
<div>
|
||||
<div className="account-phone">{account.phone}</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 membership-row">
|
||||
<strong>{competitorInfo}</strong>
|
||||
@ -116,7 +150,7 @@ export default function AccountsTab({
|
||||
<div className="account-meta">
|
||||
Осталось действий сегодня: {remaining == null ? "без лимита" : remaining} (использовано: {used}/{limit || "∞"})
|
||||
</div>
|
||||
<div className="account-meta">Задачи: {assignedTasks.length ? taskNames : "—"}</div>
|
||||
<div className="account-meta">Работает в задачах: {assignedTasks.length ? taskNames : "—"}</div>
|
||||
{cooldownActive && (
|
||||
<div className="account-meta">
|
||||
Таймер FLOOD: {cooldownMinutes} мин
|
||||
@ -128,14 +162,31 @@ export default function AccountsTab({
|
||||
)}
|
||||
<div className="account-actions">
|
||||
{hasSelectedTask && (
|
||||
<div className="role-toggle">
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
onChange={() => toggleAccountSelection(account.id)}
|
||||
checked={Boolean(roles.monitor)}
|
||||
onChange={(event) => updateAccountRole(account.id, "monitor", event.target.checked)}
|
||||
/>
|
||||
В задаче
|
||||
Мониторинг
|
||||
</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 && (
|
||||
<button className="ghost" onClick={() => removeAccountFromTask(account.id)}>
|
||||
@ -166,7 +217,14 @@ export default function AccountsTab({
|
||||
{accountBuckets.busy.map((account) => {
|
||||
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
||||
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(", ");
|
||||
const membership = membershipStatus[account.id];
|
||||
const stats = accountStats.find((item) => item.id === account.id);
|
||||
@ -197,6 +255,10 @@ export default function AccountsTab({
|
||||
<div>
|
||||
<div className="account-phone">{account.phone}</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 membership-row">
|
||||
<strong>{competitorInfo}</strong>
|
||||
@ -227,7 +289,7 @@ export default function AccountsTab({
|
||||
<div className="account-meta">
|
||||
Осталось действий сегодня: {remaining == null ? "без лимита" : remaining} (использовано: {used}/{limit || "∞"})
|
||||
</div>
|
||||
<div className="account-meta">Задачи: {assignedTasks.length ? taskNames : "—"}</div>
|
||||
<div className="account-meta">Работает в задачах: {assignedTasks.length ? taskNames : "—"}</div>
|
||||
{cooldownActive && (
|
||||
<div className="account-meta">
|
||||
Таймер FLOOD: {cooldownMinutes} мин
|
||||
|
||||
@ -1,11 +1,51 @@
|
||||
import React from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
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 (
|
||||
<section className="card logs">
|
||||
<h2>События аккаунтов</h2>
|
||||
{accountEvents.length === 0 && <div className="empty">Событий нет.</div>}
|
||||
{accountEvents.map((event) => (
|
||||
<div className="row-inline">
|
||||
<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 className="log-time">
|
||||
<div>{formatTimestamp(event.createdAt)}</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user