This commit is contained in:
Ivan Neplokhov 2026-01-20 00:04:52 +04:00
parent a3a259bd3b
commit a5d55012a8
11 changed files with 394 additions and 114 deletions

View File

@ -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);
try {
await telegram.init(); 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) {

View File

@ -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)
}); });

View File

@ -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 || "",
"" ""
); );
} }

View File

@ -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,

View File

@ -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 : "",

View File

@ -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) {
try {
await this._connectAccount(account); 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();

View File

@ -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" && (

View File

@ -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;

View File

@ -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">

View File

@ -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">
<div className="row-header">
<h2>События аккаунтов</h2> <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"

View File

@ -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>