some
This commit is contained in:
parent
a3a259bd3b
commit
a5d55012a8
@ -37,7 +37,11 @@ function createWindow() {
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
store = initStore(app.getPath("userData"));
|
store = initStore(app.getPath("userData"));
|
||||||
telegram = new TelegramManager(store);
|
telegram = new TelegramManager(store);
|
||||||
await telegram.init();
|
try {
|
||||||
|
await telegram.init();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to initialize Telegram clients:", error);
|
||||||
|
}
|
||||||
scheduler = new Scheduler(store, telegram);
|
scheduler = new Scheduler(store, telegram);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -401,6 +405,29 @@ ipcMain.handle("tasks:membershipStatus", async (_event, id) => {
|
|||||||
return telegram.getMembershipStatus(competitors, task.our_group);
|
return telegram.getMembershipStatus(competitors, task.our_group);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("tasks:checkInviteAccess", async (_event, id) => {
|
||||||
|
const task = store.getTask(id);
|
||||||
|
if (!task) return { ok: false, error: "Task not found" };
|
||||||
|
const accountRows = store.listTaskAccounts(id).filter((row) => row.role_invite);
|
||||||
|
const existingAccounts = store.listAccounts();
|
||||||
|
const existingIds = new Set(existingAccounts.map((account) => account.id));
|
||||||
|
const missing = accountRows.filter((row) => !existingIds.has(row.account_id));
|
||||||
|
if (missing.length) {
|
||||||
|
const filtered = accountRows
|
||||||
|
.filter((row) => existingIds.has(row.account_id))
|
||||||
|
.map((row) => ({
|
||||||
|
accountId: row.account_id,
|
||||||
|
roleMonitor: Boolean(row.role_monitor),
|
||||||
|
roleInvite: Boolean(row.role_invite)
|
||||||
|
}));
|
||||||
|
store.setTaskAccountRoles(id, filtered);
|
||||||
|
}
|
||||||
|
const accountIds = accountRows
|
||||||
|
.filter((row) => existingIds.has(row.account_id))
|
||||||
|
.map((row) => row.account_id);
|
||||||
|
return telegram.checkInvitePermissions(task, accountIds);
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("tasks:groupVisibility", async (_event, id) => {
|
ipcMain.handle("tasks:groupVisibility", async (_event, id) => {
|
||||||
const task = store.getTask(id);
|
const task = store.getTask(id);
|
||||||
if (!task) return { ok: false, error: "Task not found" };
|
if (!task) return { ok: false, error: "Task not found" };
|
||||||
@ -475,6 +502,11 @@ ipcMain.handle("accounts:events", async (_event, limit) => {
|
|||||||
return store.listAccountEvents(limit || 200);
|
return store.listAccountEvents(limit || 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("accounts:events:clear", async () => {
|
||||||
|
store.clearAccountEvents();
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("accounts:refreshIdentity", async () => {
|
ipcMain.handle("accounts:refreshIdentity", async () => {
|
||||||
const accounts = store.listAccounts();
|
const accounts = store.listAccounts();
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ contextBridge.exposeInMainWorld("api", {
|
|||||||
listAccounts: () => ipcRenderer.invoke("accounts:list"),
|
listAccounts: () => ipcRenderer.invoke("accounts:list"),
|
||||||
resetAccountCooldown: (accountId) => ipcRenderer.invoke("accounts:resetCooldown", accountId),
|
resetAccountCooldown: (accountId) => ipcRenderer.invoke("accounts:resetCooldown", accountId),
|
||||||
listAccountEvents: (limit) => ipcRenderer.invoke("accounts:events", limit),
|
listAccountEvents: (limit) => ipcRenderer.invoke("accounts:events", limit),
|
||||||
|
clearAccountEvents: () => ipcRenderer.invoke("accounts:events:clear"),
|
||||||
deleteAccount: (accountId) => ipcRenderer.invoke("accounts:delete", accountId),
|
deleteAccount: (accountId) => ipcRenderer.invoke("accounts:delete", accountId),
|
||||||
refreshAccountIdentity: () => ipcRenderer.invoke("accounts:refreshIdentity"),
|
refreshAccountIdentity: () => ipcRenderer.invoke("accounts:refreshIdentity"),
|
||||||
startLogin: (payload) => ipcRenderer.invoke("accounts:startLogin", payload),
|
startLogin: (payload) => ipcRenderer.invoke("accounts:startLogin", payload),
|
||||||
@ -39,6 +40,7 @@ contextBridge.exposeInMainWorld("api", {
|
|||||||
taskStatus: (id) => ipcRenderer.invoke("tasks:status", id),
|
taskStatus: (id) => ipcRenderer.invoke("tasks:status", id),
|
||||||
parseHistoryByTask: (id) => ipcRenderer.invoke("tasks:parseHistory", id),
|
parseHistoryByTask: (id) => ipcRenderer.invoke("tasks:parseHistory", id),
|
||||||
checkAccessByTask: (id) => ipcRenderer.invoke("tasks:checkAccess", id),
|
checkAccessByTask: (id) => ipcRenderer.invoke("tasks:checkAccess", id),
|
||||||
|
checkInviteAccessByTask: (id) => ipcRenderer.invoke("tasks:checkInviteAccess", id),
|
||||||
membershipStatusByTask: (id) => ipcRenderer.invoke("tasks:membershipStatus", id),
|
membershipStatusByTask: (id) => ipcRenderer.invoke("tasks:membershipStatus", id),
|
||||||
groupVisibilityByTask: (id) => ipcRenderer.invoke("tasks:groupVisibility", id)
|
groupVisibilityByTask: (id) => ipcRenderer.invoke("tasks:groupVisibility", id)
|
||||||
});
|
});
|
||||||
|
|||||||
@ -68,6 +68,8 @@ class Scheduler {
|
|||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
|
this.settings.ourGroup || "",
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@ -92,6 +94,8 @@ class Scheduler {
|
|||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
|
this.settings.ourGroup || "",
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -112,6 +116,8 @@ class Scheduler {
|
|||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
|
this.settings.ourGroup || "",
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,6 +96,8 @@ function initStore(userDataPath) {
|
|||||||
strategy TEXT DEFAULT '',
|
strategy TEXT DEFAULT '',
|
||||||
strategy_meta TEXT DEFAULT '',
|
strategy_meta TEXT DEFAULT '',
|
||||||
source_chat TEXT DEFAULT '',
|
source_chat TEXT DEFAULT '',
|
||||||
|
target_chat TEXT DEFAULT '',
|
||||||
|
target_type TEXT DEFAULT '',
|
||||||
action TEXT DEFAULT 'invite',
|
action TEXT DEFAULT 'invite',
|
||||||
skipped_reason TEXT DEFAULT '',
|
skipped_reason TEXT DEFAULT '',
|
||||||
invited_at TEXT NOT NULL,
|
invited_at TEXT NOT NULL,
|
||||||
@ -165,6 +167,8 @@ function initStore(userDataPath) {
|
|||||||
ensureColumn("invites", "strategy", "TEXT DEFAULT ''");
|
ensureColumn("invites", "strategy", "TEXT DEFAULT ''");
|
||||||
ensureColumn("invites", "strategy_meta", "TEXT DEFAULT ''");
|
ensureColumn("invites", "strategy_meta", "TEXT DEFAULT ''");
|
||||||
ensureColumn("invites", "source_chat", "TEXT DEFAULT ''");
|
ensureColumn("invites", "source_chat", "TEXT DEFAULT ''");
|
||||||
|
ensureColumn("invites", "target_chat", "TEXT DEFAULT ''");
|
||||||
|
ensureColumn("invites", "target_type", "TEXT DEFAULT ''");
|
||||||
ensureColumn("invites", "action", "TEXT DEFAULT 'invite'");
|
ensureColumn("invites", "action", "TEXT DEFAULT 'invite'");
|
||||||
ensureColumn("invites", "skipped_reason", "TEXT DEFAULT ''");
|
ensureColumn("invites", "skipped_reason", "TEXT DEFAULT ''");
|
||||||
ensureColumn("invites", "archived", "INTEGER NOT NULL DEFAULT 0");
|
ensureColumn("invites", "archived", "INTEGER NOT NULL DEFAULT 0");
|
||||||
@ -330,6 +334,10 @@ function initStore(userDataPath) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearAccountEvents() {
|
||||||
|
db.prepare("DELETE FROM account_events").run();
|
||||||
|
}
|
||||||
|
|
||||||
function enqueueInvite(taskId, userId, username, sourceChat, accessHash, watcherAccountId) {
|
function enqueueInvite(taskId, userId, username, sourceChat, accessHash, watcherAccountId) {
|
||||||
const now = dayjs().toISOString();
|
const now = dayjs().toISOString();
|
||||||
try {
|
try {
|
||||||
@ -542,7 +550,9 @@ function initStore(userDataPath) {
|
|||||||
watcherAccountId,
|
watcherAccountId,
|
||||||
watcherPhone,
|
watcherPhone,
|
||||||
strategy,
|
strategy,
|
||||||
strategyMeta
|
strategyMeta,
|
||||||
|
targetChat,
|
||||||
|
targetType
|
||||||
) {
|
) {
|
||||||
const now = dayjs().toISOString();
|
const now = dayjs().toISOString();
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
@ -558,6 +568,8 @@ function initStore(userDataPath) {
|
|||||||
strategy,
|
strategy,
|
||||||
strategy_meta,
|
strategy_meta,
|
||||||
source_chat,
|
source_chat,
|
||||||
|
target_chat,
|
||||||
|
target_type,
|
||||||
action,
|
action,
|
||||||
skipped_reason,
|
skipped_reason,
|
||||||
invited_at,
|
invited_at,
|
||||||
@ -565,7 +577,7 @@ function initStore(userDataPath) {
|
|||||||
error,
|
error,
|
||||||
archived
|
archived
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
||||||
`).run(
|
`).run(
|
||||||
taskId || 0,
|
taskId || 0,
|
||||||
userId,
|
userId,
|
||||||
@ -578,6 +590,8 @@ function initStore(userDataPath) {
|
|||||||
strategy || "",
|
strategy || "",
|
||||||
strategyMeta || "",
|
strategyMeta || "",
|
||||||
sourceChat || "",
|
sourceChat || "",
|
||||||
|
targetChat || "",
|
||||||
|
targetType || "",
|
||||||
action || "invite",
|
action || "invite",
|
||||||
skippedReason || "",
|
skippedReason || "",
|
||||||
now,
|
now,
|
||||||
@ -680,6 +694,8 @@ function initStore(userDataPath) {
|
|||||||
strategy: row.strategy || "",
|
strategy: row.strategy || "",
|
||||||
strategyMeta: row.strategy_meta || "",
|
strategyMeta: row.strategy_meta || "",
|
||||||
sourceChat: row.source_chat || "",
|
sourceChat: row.source_chat || "",
|
||||||
|
targetChat: row.target_chat || "",
|
||||||
|
targetType: row.target_type || "",
|
||||||
action: row.action || "invite",
|
action: row.action || "invite",
|
||||||
skippedReason: row.skipped_reason || "",
|
skippedReason: row.skipped_reason || "",
|
||||||
invitedAt: row.invited_at,
|
invitedAt: row.invited_at,
|
||||||
@ -720,6 +736,7 @@ function initStore(userDataPath) {
|
|||||||
clearAccountCooldown,
|
clearAccountCooldown,
|
||||||
addAccountEvent,
|
addAccountEvent,
|
||||||
listAccountEvents,
|
listAccountEvents,
|
||||||
|
clearAccountEvents,
|
||||||
deleteAccount,
|
deleteAccount,
|
||||||
updateAccountIdentity,
|
updateAccountIdentity,
|
||||||
addAccount,
|
addAccount,
|
||||||
|
|||||||
@ -167,7 +167,9 @@ class TaskRunner {
|
|||||||
watcherAccount ? watcherAccount.id : 0,
|
watcherAccount ? watcherAccount.id : 0,
|
||||||
watcherAccount ? watcherAccount.phone : "",
|
watcherAccount ? watcherAccount.phone : "",
|
||||||
result.strategy,
|
result.strategy,
|
||||||
result.strategyMeta
|
result.strategyMeta,
|
||||||
|
this.task.our_group,
|
||||||
|
result.targetType
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
errors.push(`${item.user_id}: ${result.error}`);
|
errors.push(`${item.user_id}: ${result.error}`);
|
||||||
@ -192,16 +194,33 @@ class TaskRunner {
|
|||||||
watcherAccount ? watcherAccount.id : 0,
|
watcherAccount ? watcherAccount.id : 0,
|
||||||
watcherAccount ? watcherAccount.phone : "",
|
watcherAccount ? watcherAccount.phone : "",
|
||||||
result.strategy,
|
result.strategy,
|
||||||
result.strategyMeta
|
result.strategyMeta,
|
||||||
|
this.task.our_group,
|
||||||
|
result.targetType
|
||||||
);
|
);
|
||||||
|
let strategyLine = result.strategy || "—";
|
||||||
|
if (result.strategyMeta) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(result.strategyMeta);
|
||||||
|
if (Array.isArray(parsed) && parsed.length) {
|
||||||
|
const steps = parsed
|
||||||
|
.map((step) => `${step.strategy}:${step.ok ? "ok" : "fail"}`)
|
||||||
|
.join(", ");
|
||||||
|
strategyLine = `${strategyLine} (${steps})`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
const detailed = [
|
const detailed = [
|
||||||
`user=${item.user_id}`,
|
`Пользователь: ${item.user_id || "—"}`,
|
||||||
`error=${result.error || "unknown"}`,
|
`Ошибка: ${result.error || "unknown"}`,
|
||||||
`strategy=${result.strategy || "—"}`,
|
`Стратегия: ${strategyLine}`,
|
||||||
`meta=${result.strategyMeta || "—"}`,
|
`Источник: ${item.source_chat || "—"}`,
|
||||||
`source=${item.source_chat || "—"}`,
|
`Цель: ${this.task.our_group || "—"}`,
|
||||||
`account=${result.accountPhone || result.accountId || "—"}`
|
`Тип цели: ${result.targetType || "—"}`,
|
||||||
].join(" | ");
|
`Аккаунт: ${result.accountPhone || result.accountId || "—"}`
|
||||||
|
].join("\n");
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
watcherAccount ? watcherAccount.id : 0,
|
watcherAccount ? watcherAccount.id : 0,
|
||||||
watcherAccount ? watcherAccount.phone : "",
|
watcherAccount ? watcherAccount.phone : "",
|
||||||
|
|||||||
@ -25,7 +25,13 @@ class TelegramManager {
|
|||||||
async init() {
|
async init() {
|
||||||
const accounts = this.store.listAccounts();
|
const accounts = this.store.listAccounts();
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
await this._connectAccount(account);
|
try {
|
||||||
|
await this._connectAccount(account);
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = error && (error.errorMessage || error.message) ? (error.errorMessage || error.message) : String(error);
|
||||||
|
this.store.updateAccountStatus(account.id, "error", errorText);
|
||||||
|
this.store.addAccountEvent(account.id, account.phone || "", "connect_failed", errorText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,11 +414,12 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { client, account } = entry;
|
const { client, account } = entry;
|
||||||
|
let targetEntity = null;
|
||||||
|
let targetType = "";
|
||||||
const attemptInvite = async (user) => {
|
const attemptInvite = async (user) => {
|
||||||
const resolved = await this._resolveGroupEntity(client, task.our_group, Boolean(task.auto_join_our_group), account);
|
if (!targetEntity) {
|
||||||
if (!resolved.ok) throw new Error(resolved.error);
|
throw new Error("Target group not resolved");
|
||||||
const targetEntity = resolved.entity;
|
}
|
||||||
|
|
||||||
if (targetEntity.className === "Channel") {
|
if (targetEntity.className === "Channel") {
|
||||||
await client.invoke(
|
await client.invoke(
|
||||||
new Api.channels.InviteToChannel({
|
new Api.channels.InviteToChannel({
|
||||||
@ -492,6 +499,16 @@ class TelegramManager {
|
|||||||
const providedUsername = options.username || "";
|
const providedUsername = options.username || "";
|
||||||
const allowJoin = Boolean(task.auto_join_our_group);
|
const allowJoin = Boolean(task.auto_join_our_group);
|
||||||
await this._autoJoinGroups(client, [task.our_group], allowJoin, account);
|
await this._autoJoinGroups(client, [task.our_group], allowJoin, account);
|
||||||
|
const resolvedTarget = await this._resolveGroupEntity(client, task.our_group, allowJoin, account);
|
||||||
|
if (!resolvedTarget.ok) throw new Error(resolvedTarget.error);
|
||||||
|
targetEntity = resolvedTarget.entity;
|
||||||
|
if (targetEntity && targetEntity.className === "Channel") {
|
||||||
|
targetType = targetEntity.megagroup ? "megagroup" : "channel";
|
||||||
|
} else if (targetEntity && targetEntity.className === "Chat") {
|
||||||
|
targetType = "group";
|
||||||
|
} else {
|
||||||
|
targetType = targetEntity && targetEntity.className ? targetEntity.className : "";
|
||||||
|
}
|
||||||
const resolved = await resolveInputUser();
|
const resolved = await resolveInputUser();
|
||||||
lastAttempts = resolved.attempts || [];
|
lastAttempts = resolved.attempts || [];
|
||||||
const user = resolved.user;
|
const user = resolved.user;
|
||||||
@ -504,7 +521,8 @@ class TelegramManager {
|
|||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
accountPhone: account.phone || "",
|
accountPhone: account.phone || "",
|
||||||
strategy: last ? last.strategy : "",
|
strategy: last ? last.strategy : "",
|
||||||
strategyMeta: JSON.stringify(lastAttempts)
|
strategyMeta: JSON.stringify(lastAttempts),
|
||||||
|
targetType
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error.errorMessage || error.message || String(error);
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
@ -566,7 +584,8 @@ class TelegramManager {
|
|||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
accountPhone: account.phone || "",
|
accountPhone: account.phone || "",
|
||||||
strategy: "",
|
strategy: "",
|
||||||
strategyMeta: fallbackMeta
|
strategyMeta: fallbackMeta,
|
||||||
|
targetType
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -927,6 +946,124 @@ class TelegramManager {
|
|||||||
return { ok: true, result };
|
return { ok: true, result };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkInvitePermissions(task, accountIds) {
|
||||||
|
if (!task || !task.our_group) {
|
||||||
|
return { ok: false, error: "No target group" };
|
||||||
|
}
|
||||||
|
const ids = Array.isArray(accountIds) ? accountIds.filter(Boolean) : [];
|
||||||
|
if (!ids.length) {
|
||||||
|
return { ok: false, error: "No invite accounts" };
|
||||||
|
}
|
||||||
|
const accounts = this.store.listAccounts();
|
||||||
|
const accountMap = new Map(accounts.map((account) => [account.id, account]));
|
||||||
|
const results = [];
|
||||||
|
for (const accountId of ids) {
|
||||||
|
const entry = this.clients.get(accountId);
|
||||||
|
const accountRecord = accountMap.get(accountId);
|
||||||
|
if (!entry) {
|
||||||
|
results.push({
|
||||||
|
accountId,
|
||||||
|
accountPhone: accountRecord ? (accountRecord.phone || "") : "",
|
||||||
|
ok: false,
|
||||||
|
canInvite: false,
|
||||||
|
member: false,
|
||||||
|
reason: "Сессия не подключена",
|
||||||
|
targetType: "",
|
||||||
|
title: "",
|
||||||
|
targetChat: task.our_group
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { client, account } = entry;
|
||||||
|
const resolved = await this._resolveGroupEntity(client, task.our_group, Boolean(task.auto_join_our_group), account);
|
||||||
|
if (!resolved.ok) {
|
||||||
|
results.push({
|
||||||
|
accountId,
|
||||||
|
accountPhone: account.phone || "",
|
||||||
|
ok: false,
|
||||||
|
canInvite: false,
|
||||||
|
member: false,
|
||||||
|
reason: resolved.error || "Не удалось получить группу",
|
||||||
|
targetType: "",
|
||||||
|
title: "",
|
||||||
|
targetChat: task.our_group
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entity = resolved.entity;
|
||||||
|
const title = entity && entity.title ? entity.title : "";
|
||||||
|
const className = entity && entity.className ? entity.className : "";
|
||||||
|
let targetType = className;
|
||||||
|
if (className === "Channel") {
|
||||||
|
targetType = entity && entity.megagroup ? "megagroup" : "channel";
|
||||||
|
} else if (className === "Chat") {
|
||||||
|
targetType = "group";
|
||||||
|
}
|
||||||
|
let canInvite = false;
|
||||||
|
let member = true;
|
||||||
|
let reason = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (className === "Channel") {
|
||||||
|
const me = await client.getMe();
|
||||||
|
const participant = await client.invoke(new Api.channels.GetParticipant({
|
||||||
|
channel: entity,
|
||||||
|
participant: me
|
||||||
|
}));
|
||||||
|
const part = participant && participant.participant ? participant.participant : participant;
|
||||||
|
const className = part && part.className ? part.className : "";
|
||||||
|
const isAdmin = className.includes("Admin") || className.includes("Creator");
|
||||||
|
const addUsers = part && part.adminRights ? Boolean(part.adminRights.addUsers) : isAdmin;
|
||||||
|
canInvite = Boolean(isAdmin && addUsers);
|
||||||
|
if (!canInvite) {
|
||||||
|
reason = "Нужны права администратора на добавление участников";
|
||||||
|
}
|
||||||
|
} else if (className === "Chat") {
|
||||||
|
let fullChat = null;
|
||||||
|
try {
|
||||||
|
fullChat = await client.invoke(new Api.messages.GetFullChat({ chatId: entity.id }));
|
||||||
|
} catch (error) {
|
||||||
|
fullChat = null;
|
||||||
|
}
|
||||||
|
const full = fullChat && fullChat.fullChat ? fullChat.fullChat : null;
|
||||||
|
const restricted = Boolean(full && full.defaultBannedRights && full.defaultBannedRights.inviteUsers);
|
||||||
|
if (restricted) {
|
||||||
|
canInvite = false;
|
||||||
|
reason = "Добавление пользователей запрещено для участников";
|
||||||
|
} else {
|
||||||
|
canInvite = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
canInvite = false;
|
||||||
|
reason = "Не удалось распознать тип цели (не группа/канал)";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
|
if (errorText.includes("USER_NOT_PARTICIPANT")) {
|
||||||
|
member = false;
|
||||||
|
reason = "Аккаунт не состоит в группе";
|
||||||
|
} else if (errorText.includes("CHAT_ADMIN_REQUIRED")) {
|
||||||
|
reason = "Нужны права администратора";
|
||||||
|
} else {
|
||||||
|
reason = errorText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
accountId,
|
||||||
|
accountPhone: account.phone || "",
|
||||||
|
ok: true,
|
||||||
|
canInvite,
|
||||||
|
member,
|
||||||
|
reason,
|
||||||
|
targetType,
|
||||||
|
title,
|
||||||
|
targetChat: task.our_group
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { ok: true, result: results };
|
||||||
|
}
|
||||||
|
|
||||||
async _autoJoinGroups(client, groups, enabled, account) {
|
async _autoJoinGroups(client, groups, enabled, account) {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
const settings = this.store.getSettings();
|
const settings = this.store.getSettings();
|
||||||
|
|||||||
@ -114,6 +114,7 @@ export default function App() {
|
|||||||
const [membershipStatus, setMembershipStatus] = useState({});
|
const [membershipStatus, setMembershipStatus] = useState({});
|
||||||
const [groupVisibility, setGroupVisibility] = useState([]);
|
const [groupVisibility, setGroupVisibility] = useState([]);
|
||||||
const [accessStatus, setAccessStatus] = useState([]);
|
const [accessStatus, setAccessStatus] = useState([]);
|
||||||
|
const [inviteAccessStatus, setInviteAccessStatus] = useState([]);
|
||||||
const [accountEvents, setAccountEvents] = useState([]);
|
const [accountEvents, setAccountEvents] = useState([]);
|
||||||
const [loginForm, setLoginForm] = useState({
|
const [loginForm, setLoginForm] = useState({
|
||||||
apiId: "",
|
apiId: "",
|
||||||
@ -345,6 +346,7 @@ export default function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSelectedTask(selectedTaskId);
|
loadSelectedTask(selectedTaskId);
|
||||||
setAccessStatus([]);
|
setAccessStatus([]);
|
||||||
|
setInviteAccessStatus([]);
|
||||||
setMembershipStatus({});
|
setMembershipStatus({});
|
||||||
setTaskNotice(null);
|
setTaskNotice(null);
|
||||||
}, [selectedTaskId]);
|
}, [selectedTaskId]);
|
||||||
@ -516,6 +518,7 @@ export default function App() {
|
|||||||
|
|
||||||
const formatAccountStatus = (status) => {
|
const formatAccountStatus = (status) => {
|
||||||
if (status === "limited") return "В спаме";
|
if (status === "limited") return "В спаме";
|
||||||
|
if (status === "error") return "Ошибка";
|
||||||
if (status === "ok") return "ОК";
|
if (status === "ok") return "ОК";
|
||||||
return status || "Неизвестно";
|
return status || "Неизвестно";
|
||||||
};
|
};
|
||||||
@ -829,6 +832,19 @@ export default function App() {
|
|||||||
setTaskForm(nextForm);
|
setTaskForm(nextForm);
|
||||||
let accountRolesMap = { ...taskAccountRoles };
|
let accountRolesMap = { ...taskAccountRoles };
|
||||||
let accountIds = Object.keys(accountRolesMap).map((id) => Number(id));
|
let accountIds = Object.keys(accountRolesMap).map((id) => Number(id));
|
||||||
|
if (nextForm.requireSameBotInBoth) {
|
||||||
|
const required = Math.max(1, Number(nextForm.maxCompetitorBots || 1));
|
||||||
|
const pool = (selectedAccountIds && selectedAccountIds.length ? selectedAccountIds : accounts.map((account) => account.id))
|
||||||
|
.filter((id) => Number.isFinite(id));
|
||||||
|
const chosen = pool.slice(0, required);
|
||||||
|
accountRolesMap = {};
|
||||||
|
chosen.forEach((accountId) => {
|
||||||
|
accountRolesMap[accountId] = { monitor: true, invite: true };
|
||||||
|
});
|
||||||
|
accountIds = chosen;
|
||||||
|
setTaskAccountRoles(accountRolesMap);
|
||||||
|
setSelectedAccountIds(chosen);
|
||||||
|
}
|
||||||
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 = {};
|
accountRolesMap = {};
|
||||||
@ -899,8 +915,12 @@ export default function App() {
|
|||||||
try {
|
try {
|
||||||
await window.api.deleteTask(selectedTaskId);
|
await window.api.deleteTask(selectedTaskId);
|
||||||
setTaskNotice({ text: "Задача удалена.", tone: "success", source: "tasks" });
|
setTaskNotice({ text: "Задача удалена.", tone: "success", source: "tasks" });
|
||||||
await loadTasks();
|
const tasksData = await loadTasks();
|
||||||
await loadAccountAssignments();
|
await loadAccountAssignments();
|
||||||
|
if (!tasksData.length) {
|
||||||
|
createTask();
|
||||||
|
setActiveTab("task");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotification(error.message || String(error), "error");
|
showNotification(error.message || String(error), "error");
|
||||||
}
|
}
|
||||||
@ -1059,6 +1079,26 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkInviteAccess = async (source = "editor") => {
|
||||||
|
if (!window.api || selectedTaskId == null) {
|
||||||
|
showNotification("Сначала выберите задачу.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInviteAccessStatus([]);
|
||||||
|
showNotification("Проверяем права инвайта...", "info");
|
||||||
|
try {
|
||||||
|
const result = await window.api.checkInviteAccessByTask(selectedTaskId);
|
||||||
|
if (!result.ok) {
|
||||||
|
showNotification(result.error || "Не удалось проверить права", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInviteAccessStatus(result.result || []);
|
||||||
|
setTaskNotice({ text: "Проверка прав инвайта завершена.", tone: "success", source });
|
||||||
|
} catch (error) {
|
||||||
|
showNotification(error.message || String(error), "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const clearLogs = async (source = "editor") => {
|
const clearLogs = async (source = "editor") => {
|
||||||
if (!window.api || selectedTaskId == null) {
|
if (!window.api || selectedTaskId == null) {
|
||||||
showNotification("Сначала выберите задачу.", "error");
|
showNotification("Сначала выберите задачу.", "error");
|
||||||
@ -1087,6 +1127,20 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearAccountEvents = async () => {
|
||||||
|
if (!window.api) {
|
||||||
|
showNotification("Electron API недоступен. Откройте приложение в Electron.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await window.api.clearAccountEvents();
|
||||||
|
setAccountEvents([]);
|
||||||
|
showNotification("События очищены.", "success");
|
||||||
|
} catch (error) {
|
||||||
|
showNotification(error.message || String(error), "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const exportLogs = async (source = "editor") => {
|
const exportLogs = async (source = "editor") => {
|
||||||
if (!window.api || selectedTaskId == null) {
|
if (!window.api || selectedTaskId == null) {
|
||||||
showNotification("Сначала выберите задачу.", "error");
|
showNotification("Сначала выберите задачу.", "error");
|
||||||
@ -1883,21 +1937,13 @@ export default function App() {
|
|||||||
<div className="live-value">{taskStatus.monitorInfo && taskStatus.monitorInfo.groups ? taskStatus.monitorInfo.groups.length : 0}</div>
|
<div className="live-value">{taskStatus.monitorInfo && taskStatus.monitorInfo.groups ? taskStatus.monitorInfo.groups.length : 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="live-label">Мониторит</div>
|
<div className="live-label">Наблюдает</div>
|
||||||
<div className="live-value">
|
<div className="live-value">
|
||||||
{(() => {
|
{(() => {
|
||||||
const monitorIds = taskStatus.monitorInfo && taskStatus.monitorInfo.accountIds
|
const monitorIds = taskStatus.monitorInfo && taskStatus.monitorInfo.accountIds
|
||||||
? taskStatus.monitorInfo.accountIds
|
? taskStatus.monitorInfo.accountIds
|
||||||
: (taskStatus.monitorInfo && taskStatus.monitorInfo.accountId ? [taskStatus.monitorInfo.accountId] : []);
|
: (taskStatus.monitorInfo && taskStatus.monitorInfo.accountId ? [taskStatus.monitorInfo.accountId] : []);
|
||||||
if (!monitorIds.length) return "—";
|
return monitorIds.length;
|
||||||
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>
|
||||||
@ -1913,18 +1959,6 @@ 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>
|
||||||
@ -1937,36 +1971,6 @@ 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 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>
|
||||||
<div className="status-actions">
|
<div className="status-actions">
|
||||||
{taskStatus.running ? (
|
{taskStatus.running ? (
|
||||||
@ -1992,40 +1996,6 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
</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) && (
|
||||||
@ -2055,6 +2025,7 @@ export default function App() {
|
|||||||
<button className="secondary task-toolbar" onClick={() => saveTask("editor")} disabled={!canSaveTask}>💾 Сохранить</button>
|
<button className="secondary task-toolbar" onClick={() => saveTask("editor")} disabled={!canSaveTask}>💾 Сохранить</button>
|
||||||
<button className="secondary task-toolbar" onClick={() => parseHistory("editor")} disabled={!hasSelectedTask}>📥 История</button>
|
<button className="secondary task-toolbar" onClick={() => parseHistory("editor")} disabled={!hasSelectedTask}>📥 История</button>
|
||||||
<button className="secondary task-toolbar" onClick={() => checkAccess("editor")} disabled={!hasSelectedTask}>🔎 Доступ</button>
|
<button className="secondary task-toolbar" onClick={() => checkAccess("editor")} disabled={!hasSelectedTask}>🔎 Доступ</button>
|
||||||
|
<button className="secondary task-toolbar" onClick={() => checkInviteAccess("editor")} disabled={!hasSelectedTask}>✅ Права инвайта</button>
|
||||||
<button className="secondary task-toolbar" onClick={() => refreshMembership("editor")} disabled={!hasSelectedTask}>👥 Участие</button>
|
<button className="secondary task-toolbar" onClick={() => refreshMembership("editor")} disabled={!hasSelectedTask}>👥 Участие</button>
|
||||||
</div>
|
</div>
|
||||||
{taskNotice && taskNotice.source === "editor" && (
|
{taskNotice && taskNotice.source === "editor" && (
|
||||||
@ -2371,7 +2342,7 @@ export default function App() {
|
|||||||
|
|
||||||
{activeTab === "events" && (
|
{activeTab === "events" && (
|
||||||
<Suspense fallback={<div className="card">Загрузка...</div>}>
|
<Suspense fallback={<div className="card">Загрузка...</div>}>
|
||||||
<EventsTab accountEvents={accountEvents} formatTimestamp={formatTimestamp} />
|
<EventsTab accountEvents={accountEvents} formatTimestamp={formatTimestamp} onClearEvents={clearAccountEvents} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -2423,6 +2394,7 @@ export default function App() {
|
|||||||
<button className="secondary" onClick={() => saveTask("sidebar")} disabled={!canSaveTask}>Сохранить задачу</button>
|
<button className="secondary" onClick={() => saveTask("sidebar")} disabled={!canSaveTask}>Сохранить задачу</button>
|
||||||
<button className="secondary" onClick={() => parseHistory("sidebar")} disabled={!hasSelectedTask}>Собрать историю</button>
|
<button className="secondary" onClick={() => parseHistory("sidebar")} disabled={!hasSelectedTask}>Собрать историю</button>
|
||||||
<button className="secondary" onClick={() => checkAccess("sidebar")} disabled={!hasSelectedTask}>Проверить доступ</button>
|
<button className="secondary" onClick={() => checkAccess("sidebar")} disabled={!hasSelectedTask}>Проверить доступ</button>
|
||||||
|
<button className="secondary" onClick={() => checkInviteAccess("sidebar")} disabled={!hasSelectedTask}>Проверить права инвайта</button>
|
||||||
<button className="secondary" onClick={() => refreshMembership("sidebar")} disabled={!hasSelectedTask}>Проверить участие</button>
|
<button className="secondary" onClick={() => refreshMembership("sidebar")} disabled={!hasSelectedTask}>Проверить участие</button>
|
||||||
<button className="secondary" onClick={() => clearQueue("sidebar")} disabled={!hasSelectedTask}>Очистить очередь</button>
|
<button className="secondary" onClick={() => clearQueue("sidebar")} disabled={!hasSelectedTask}>Очистить очередь</button>
|
||||||
{accessStatus.length > 0 && (
|
{accessStatus.length > 0 && (
|
||||||
@ -2443,6 +2415,25 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{inviteAccessStatus.length > 0 && (
|
||||||
|
<div className="access-block">
|
||||||
|
<div className="access-title">Права инвайта</div>
|
||||||
|
<div className="access-list">
|
||||||
|
{inviteAccessStatus.map((item, index) => (
|
||||||
|
<div key={`${item.accountId}-${index}`} className={`access-row ${item.canInvite ? "ok" : "fail"}`}>
|
||||||
|
<div className="access-title">
|
||||||
|
{item.accountPhone || item.accountId}: {item.title || item.targetChat}
|
||||||
|
{item.targetType ? ` (${item.targetType === "channel" ? "канал" : item.targetType === "megagroup" ? "супергруппа" : item.targetType === "group" ? "группа" : item.targetType})` : ""}
|
||||||
|
</div>
|
||||||
|
<div className="access-status">
|
||||||
|
{item.canInvite ? "Можно инвайтить" : "Нет прав"}
|
||||||
|
</div>
|
||||||
|
{!item.canInvite && <div className="access-error">{item.reason || "—"}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{taskNotice && taskNotice.source === "sidebar" && (
|
{taskNotice && taskNotice.source === "sidebar" && (
|
||||||
|
|||||||
@ -1122,6 +1122,25 @@ label .hint {
|
|||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-result-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-result {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-result.success {
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-result.error {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.invite-details {
|
.invite-details {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
@ -36,11 +36,19 @@ 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 roleStats = React.useMemo(() => {
|
||||||
const roles = Object.values(taskAccountRoles || {});
|
const knownIds = new Set((accounts || []).map((account) => account.id));
|
||||||
const monitor = roles.filter((item) => item.monitor).length;
|
let monitor = 0;
|
||||||
const invite = roles.filter((item) => item.invite).length;
|
let invite = 0;
|
||||||
return { monitor, invite, total: roles.length };
|
let total = 0;
|
||||||
}, [taskAccountRoles]);
|
Object.entries(taskAccountRoles || {}).forEach(([id, roles]) => {
|
||||||
|
const accountId = Number(id);
|
||||||
|
if (!knownIds.has(accountId)) return;
|
||||||
|
if (roles.monitor) monitor += 1;
|
||||||
|
if (roles.invite) invite += 1;
|
||||||
|
if (roles.monitor || roles.invite) total += 1;
|
||||||
|
});
|
||||||
|
return { monitor, invite, total };
|
||||||
|
}, [taskAccountRoles, accounts]);
|
||||||
return (
|
return (
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="row-header">
|
<div className="row-header">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { memo, useMemo, useState } from "react";
|
import React, { memo, useMemo, useState } from "react";
|
||||||
|
|
||||||
function EventsTab({ accountEvents, formatTimestamp }) {
|
function EventsTab({ accountEvents, formatTimestamp, onClearEvents }) {
|
||||||
const [typeFilter, setTypeFilter] = useState("all");
|
const [typeFilter, setTypeFilter] = useState("all");
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
@ -23,7 +23,10 @@ function EventsTab({ accountEvents, formatTimestamp }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card logs">
|
<section className="card logs">
|
||||||
<h2>События аккаунтов</h2>
|
<div className="row-header">
|
||||||
|
<h2>События аккаунтов</h2>
|
||||||
|
<button type="button" className="danger" onClick={onClearEvents}>Сбросить</button>
|
||||||
|
</div>
|
||||||
<div className="row-inline column">
|
<div className="row-inline column">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@ -69,6 +69,14 @@ function LogsTab({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatTargetType = (value) => {
|
||||||
|
if (!value) return "—";
|
||||||
|
if (value === "channel") return "канал";
|
||||||
|
if (value === "megagroup") return "супергруппа";
|
||||||
|
if (value === "group") return "группа";
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
const getDurationMs = (start, finish) => {
|
const getDurationMs = (start, finish) => {
|
||||||
const startMs = new Date(start).getTime();
|
const startMs = new Date(start).getTime();
|
||||||
const finishMs = new Date(finish).getTime();
|
const finishMs = new Date(finish).getTime();
|
||||||
@ -149,6 +157,27 @@ function LogsTab({
|
|||||||
{pagedLogs.map((log) => {
|
{pagedLogs.map((log) => {
|
||||||
const successIds = Array.isArray(log.successIds) ? log.successIds : [];
|
const successIds = Array.isArray(log.successIds) ? log.successIds : [];
|
||||||
const errors = Array.isArray(log.errors) ? log.errors : [];
|
const errors = Array.isArray(log.errors) ? log.errors : [];
|
||||||
|
const errorMap = new Map();
|
||||||
|
errors.forEach((err) => {
|
||||||
|
const parts = String(err).split(":");
|
||||||
|
if (parts.length < 2) return;
|
||||||
|
const id = parts[0].trim();
|
||||||
|
const code = parts.slice(1).join(":").trim();
|
||||||
|
if (!id) return;
|
||||||
|
errorMap.set(id, code);
|
||||||
|
});
|
||||||
|
const resultRows = [
|
||||||
|
...successIds.map((id) => ({
|
||||||
|
id: String(id),
|
||||||
|
status: "success",
|
||||||
|
message: "успех"
|
||||||
|
})),
|
||||||
|
...Array.from(errorMap.entries()).map(([id, code]) => ({
|
||||||
|
id,
|
||||||
|
status: "error",
|
||||||
|
message: `${code} (${explainInviteError(code) || "Причина не определена"})`
|
||||||
|
}))
|
||||||
|
];
|
||||||
return (
|
return (
|
||||||
<div key={log.id} className="log-row">
|
<div key={log.id} className="log-row">
|
||||||
<div className="log-time">
|
<div className="log-time">
|
||||||
@ -160,6 +189,18 @@ function LogsTab({
|
|||||||
<div className="log-users wrap">
|
<div className="log-users wrap">
|
||||||
Пользователи: {successIds.length ? successIds.join(", ") : "—"}
|
Пользователи: {successIds.length ? successIds.join(", ") : "—"}
|
||||||
</div>
|
</div>
|
||||||
|
{resultRows.length > 0 && (
|
||||||
|
<div className="log-users">
|
||||||
|
<div>Результаты:</div>
|
||||||
|
<div className="log-result-list">
|
||||||
|
{resultRows.map((row) => (
|
||||||
|
<div key={`${log.id}-${row.id}-${row.status}`} className={`log-result ${row.status}`}>
|
||||||
|
{row.id} — {row.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{log.invitedCount === 0 && errors.length === 0 && (
|
{log.invitedCount === 0 && errors.length === 0 && (
|
||||||
<div className="log-errors">Причина: цикл завершён сразу — очередь пуста</div>
|
<div className="log-errors">Причина: цикл завершён сразу — очередь пуста</div>
|
||||||
)}
|
)}
|
||||||
@ -274,6 +315,9 @@ function LogsTab({
|
|||||||
<div className="log-users wrap">
|
<div className="log-users wrap">
|
||||||
Источник: {invite.sourceChat || "—"}
|
Источник: {invite.sourceChat || "—"}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="log-users wrap">
|
||||||
|
Цель: {invite.targetChat || "—"}{invite.targetType ? ` (${formatTargetType(invite.targetType)})` : ""}
|
||||||
|
</div>
|
||||||
<div className="log-users">
|
<div className="log-users">
|
||||||
Инвайт: {invite.accountPhone || "—"}
|
Инвайт: {invite.accountPhone || "—"}
|
||||||
{invite.watcherPhone && invite.accountPhone && (
|
{invite.watcherPhone && invite.accountPhone && (
|
||||||
@ -318,6 +362,8 @@ function LogsTab({
|
|||||||
<div>Аккаунт ID: {invite.accountId || "—"}</div>
|
<div>Аккаунт ID: {invite.accountId || "—"}</div>
|
||||||
<div>Наблюдатель ID: {invite.watcherAccountId || "—"}</div>
|
<div>Наблюдатель ID: {invite.watcherAccountId || "—"}</div>
|
||||||
<div>Наблюдатель: {invite.watcherPhone || "—"}</div>
|
<div>Наблюдатель: {invite.watcherPhone || "—"}</div>
|
||||||
|
<div>Цель: {invite.targetChat || "—"}</div>
|
||||||
|
<div>Тип цели: {formatTargetType(invite.targetType)}</div>
|
||||||
<div>Действие: {invite.action || "invite"}</div>
|
<div>Действие: {invite.action || "invite"}</div>
|
||||||
<div>Статус: {invite.status}</div>
|
<div>Статус: {invite.status}</div>
|
||||||
<div>Пропуск: {invite.skippedReason || "—"}</div>
|
<div>Пропуск: {invite.skippedReason || "—"}</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user