This commit is contained in:
Ivan Neplokhov 2026-01-28 01:11:14 +04:00
parent 77baea862f
commit ac63ce91aa
7 changed files with 260 additions and 66 deletions

View File

@ -99,6 +99,7 @@ const startTaskWithChecks = async (id) => {
runner.task = task; runner.task = task;
} }
store.setTaskStopReason(id, ""); store.setTaskStopReason(id, "");
store.addAccountEvent(0, "", "task_start", `задача ${id}: запуск`);
await runner.start(); await runner.start();
const warnings = []; const warnings = [];
if (accessCheck && accessCheck.ok) { if (accessCheck && accessCheck.ok) {
@ -527,6 +528,7 @@ ipcMain.handle("tasks:stop", (_event, id) => {
} }
store.setTaskStopReason(id, "Остановлено пользователем"); store.setTaskStopReason(id, "Остановлено пользователем");
store.addTaskAudit(id, "stop", "Остановлено пользователем"); store.addTaskAudit(id, "stop", "Остановлено пользователем");
store.addAccountEvent(0, "", "task_stop", `задача ${id}: остановлена пользователем`);
return { ok: true }; return { ok: true };
}); });
ipcMain.handle("tasks:accountAssignments", () => { ipcMain.handle("tasks:accountAssignments", () => {
@ -1045,6 +1047,11 @@ ipcMain.handle("fallback:clear", (_event, taskId) => {
ipcMain.handle("accounts:events", async (_event, limit) => { ipcMain.handle("accounts:events", async (_event, limit) => {
return store.listAccountEvents(limit || 200); return store.listAccountEvents(limit || 200);
}); });
ipcMain.handle("accounts:eventAdd", (_event, payload) => {
if (!payload) return { ok: false };
store.addAccountEvent(payload.accountId || 0, payload.phone || "", payload.action || "custom", payload.details || "");
return { ok: true };
});
ipcMain.handle("accounts:events:clear", async () => { ipcMain.handle("accounts:events:clear", async () => {
store.clearAccountEvents(); store.clearAccountEvents();

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),
addAccountEvent: (payload) => ipcRenderer.invoke("accounts:eventAdd", payload),
clearAccountEvents: () => ipcRenderer.invoke("accounts:events:clear"), clearAccountEvents: () => ipcRenderer.invoke("accounts:events:clear"),
deleteAccount: (accountId) => ipcRenderer.invoke("accounts:delete", accountId), deleteAccount: (accountId) => ipcRenderer.invoke("accounts:delete", accountId),
resetSessions: () => ipcRenderer.invoke("sessions:reset"), resetSessions: () => ipcRenderer.invoke("sessions:reset"),

View File

@ -163,6 +163,7 @@ function initStore(userDataPath) {
invite_admin_anonymous INTEGER NOT NULL DEFAULT 1, invite_admin_anonymous INTEGER NOT NULL DEFAULT 1,
separate_confirm_roles INTEGER NOT NULL DEFAULT 0, separate_confirm_roles INTEGER NOT NULL DEFAULT 0,
max_confirm_bots INTEGER NOT NULL DEFAULT 1, max_confirm_bots INTEGER NOT NULL DEFAULT 1,
use_watcher_invite_no_username INTEGER NOT NULL DEFAULT 1,
warmup_enabled INTEGER NOT NULL DEFAULT 1, warmup_enabled INTEGER NOT NULL DEFAULT 1,
warmup_start_limit INTEGER NOT NULL DEFAULT 3, warmup_start_limit INTEGER NOT NULL DEFAULT 3,
warmup_daily_increase INTEGER NOT NULL DEFAULT 2, warmup_daily_increase INTEGER NOT NULL DEFAULT 2,
@ -258,6 +259,7 @@ function initStore(userDataPath) {
ensureColumn("tasks", "invite_admin_anonymous", "INTEGER NOT NULL DEFAULT 1"); ensureColumn("tasks", "invite_admin_anonymous", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("tasks", "separate_confirm_roles", "INTEGER NOT NULL DEFAULT 0"); ensureColumn("tasks", "separate_confirm_roles", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "max_confirm_bots", "INTEGER NOT NULL DEFAULT 1"); ensureColumn("tasks", "max_confirm_bots", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("tasks", "use_watcher_invite_no_username", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("task_accounts", "role_confirm", "INTEGER NOT NULL DEFAULT 1"); ensureColumn("task_accounts", "role_confirm", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("tasks", "warmup_enabled", "INTEGER NOT NULL DEFAULT 0"); ensureColumn("tasks", "warmup_enabled", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("tasks", "warmup_start_limit", "INTEGER NOT NULL DEFAULT 3"); ensureColumn("tasks", "warmup_start_limit", "INTEGER NOT NULL DEFAULT 3");
@ -469,6 +471,14 @@ function initStore(userDataPath) {
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 {
if (taskId) {
const existing = db.prepare(
"SELECT status FROM invites WHERE task_id = ? AND user_id = ? ORDER BY invited_at DESC LIMIT 1"
).get(taskId || 0, userId);
if (existing && existing.status === "success") {
return false;
}
}
const result = db.prepare(` const result = db.prepare(`
INSERT OR IGNORE INTO invite_queue (task_id, user_id, username, user_access_hash, watcher_account_id, source_chat, status, created_at, updated_at) INSERT OR IGNORE INTO invite_queue (task_id, user_id, username, user_access_hash, watcher_account_id, source_chat, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)
@ -591,6 +601,7 @@ function initStore(userDataPath) {
require_same_bot_in_both = ?, stop_on_blocked = ?, stop_blocked_percent = ?, notes = ?, enabled = ?, require_same_bot_in_both = ?, stop_on_blocked = ?, stop_blocked_percent = ?, notes = ?, enabled = ?,
allow_start_without_invite_rights = ?, parse_participants = ?, invite_via_admins = ?, invite_admin_master_id = ?, allow_start_without_invite_rights = ?, parse_participants = ?, invite_via_admins = ?, invite_admin_master_id = ?,
invite_admin_allow_flood = ?, invite_admin_anonymous = ?, separate_confirm_roles = ?, max_confirm_bots = ?, invite_admin_allow_flood = ?, invite_admin_anonymous = ?, separate_confirm_roles = ?, max_confirm_bots = ?,
use_watcher_invite_no_username = ?,
warmup_enabled = ?, warmup_start_limit = ?, warmup_daily_increase = ?, warmup_enabled = ?, warmup_start_limit = ?, warmup_daily_increase = ?,
cycle_competitors = ?, competitor_cursor = ?, invite_link_on_fail = ?, updated_at = ? cycle_competitors = ?, competitor_cursor = ?, invite_link_on_fail = ?, updated_at = ?
WHERE id = ? WHERE id = ?
@ -623,6 +634,7 @@ function initStore(userDataPath) {
task.inviteAdminAnonymous ? 1 : 0, task.inviteAdminAnonymous ? 1 : 0,
task.separateConfirmRoles ? 1 : 0, task.separateConfirmRoles ? 1 : 0,
task.maxConfirmBots || 1, task.maxConfirmBots || 1,
task.useWatcherInviteNoUsername ? 1 : 0,
task.warmupEnabled ? 1 : 0, task.warmupEnabled ? 1 : 0,
task.warmupStartLimit || 3, task.warmupStartLimit || 3,
task.warmupDailyIncrease || 2, task.warmupDailyIncrease || 2,
@ -641,9 +653,10 @@ function initStore(userDataPath) {
auto_join_our_group, separate_bot_roles, require_same_bot_in_both, stop_on_blocked, stop_blocked_percent, notes, enabled, auto_join_our_group, separate_bot_roles, require_same_bot_in_both, stop_on_blocked, stop_blocked_percent, notes, enabled,
allow_start_without_invite_rights, parse_participants, invite_via_admins, invite_admin_master_id, allow_start_without_invite_rights, parse_participants, invite_via_admins, invite_admin_master_id,
invite_admin_allow_flood, invite_admin_anonymous, separate_confirm_roles, max_confirm_bots, invite_admin_allow_flood, invite_admin_anonymous, separate_confirm_roles, max_confirm_bots,
use_watcher_invite_no_username,
warmup_enabled, warmup_start_limit, warmup_daily_increase, cycle_competitors, warmup_enabled, warmup_start_limit, warmup_daily_increase, cycle_competitors,
competitor_cursor, invite_link_on_fail, created_at, updated_at) competitor_cursor, invite_link_on_fail, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run( `).run(
task.name, task.name,
task.ourGroup, task.ourGroup,
@ -673,6 +686,7 @@ function initStore(userDataPath) {
task.inviteAdminAnonymous ? 1 : 0, task.inviteAdminAnonymous ? 1 : 0,
task.separateConfirmRoles ? 1 : 0, task.separateConfirmRoles ? 1 : 0,
task.maxConfirmBots || 1, task.maxConfirmBots || 1,
task.useWatcherInviteNoUsername ? 1 : 0,
task.warmupEnabled ? 1 : 0, task.warmupEnabled ? 1 : 0,
task.warmupStartLimit || 3, task.warmupStartLimit || 3,
task.warmupDailyIncrease || 2, task.warmupDailyIncrease || 2,

View File

@ -131,6 +131,16 @@ class TaskRunner {
const entry = this.telegram.pickInviteAccount(inviteAccounts, Boolean(this.task.random_accounts)); const entry = this.telegram.pickInviteAccount(inviteAccounts, Boolean(this.task.random_accounts));
inviteAccounts = entry ? [entry.account.id] : []; inviteAccounts = entry ? [entry.account.id] : [];
this.nextInviteAccountId = entry ? entry.account.id : 0; this.nextInviteAccountId = entry ? entry.account.id : 0;
if (entry && entry.account) {
const label = entry.account.phone || entry.account.username || entry.account.id;
const poolLabel = inviteAccounts.length ? inviteAccounts.join(",") : "—";
this.store.addAccountEvent(
entry.account.id,
entry.account.phone || "",
"invite_pick",
`выбран: ${label}; режим: ${this.task.random_accounts ? "случайный" : "по очереди"}`
);
}
} else if (inviteAccounts.length) { } else if (inviteAccounts.length) {
this.nextInviteAccountId = inviteAccounts[0]; this.nextInviteAccountId = inviteAccounts[0];
} }
@ -150,6 +160,12 @@ class TaskRunner {
const alreadyInvited = this.store.countInvitesToday(this.task.id); const alreadyInvited = this.store.countInvitesToday(this.task.id);
if (alreadyInvited >= dailyLimit) { if (alreadyInvited >= dailyLimit) {
errors.push("Daily limit reached"); errors.push("Daily limit reached");
this.store.addAccountEvent(
0,
"",
"invite_skipped",
`задача ${this.task.id}: дневной лимит ${alreadyInvited}/${dailyLimit}`
);
} else { } else {
const remaining = dailyLimit - alreadyInvited; const remaining = dailyLimit - alreadyInvited;
const perCycle = perCycleLimit; const perCycle = perCycleLimit;
@ -170,6 +186,20 @@ class TaskRunner {
} }
if (!inviteAccounts.length && pending.length) { if (!inviteAccounts.length && pending.length) {
errors.push("No available accounts under limits"); errors.push("No available accounts under limits");
this.store.addAccountEvent(
0,
"",
"invite_skipped",
`задача ${this.task.id}: нет аккаунтов с ролью инвайта`
);
}
if (!pending.length) {
this.store.addAccountEvent(
0,
"",
"invite_skipped",
`задача ${this.task.id}: очередь пуста`
);
} }
const fallbackRoute = (error, confirmed) => { const fallbackRoute = (error, confirmed) => {
@ -197,9 +227,18 @@ class TaskRunner {
for (const item of pending) { for (const item of pending) {
if (item.attempts >= 2 && this.task.retry_on_fail) { if (item.attempts >= 2 && this.task.retry_on_fail) {
this.store.markInviteStatus(item.id, "failed"); this.store.markInviteStatus(item.id, "failed");
this.store.addAccountEvent(
0,
"",
"invite_skipped",
`задача ${this.task.id}: превышен лимит повторов для ${item.user_id}`
);
continue; continue;
} }
const accountsForInvite = inviteAccounts; let accountsForInvite = inviteAccounts;
if (!item.username && this.task.use_watcher_invite_no_username && item.watcher_account_id) {
accountsForInvite = [item.watcher_account_id];
}
const watcherAccount = accountMap.get(item.watcher_account_id || 0); const watcherAccount = accountMap.get(item.watcher_account_id || 0);
const result = await this.telegram.inviteUserForTask(this.task, item.user_id, accountsForInvite, { const result = await this.telegram.inviteUserForTask(this.task, item.user_id, accountsForInvite, {
randomize: Boolean(this.task.random_accounts), randomize: Boolean(this.task.random_accounts),

View File

@ -60,6 +60,7 @@ class TelegramManager {
const inviteUsers = rights ? Boolean(rights.inviteUsers || rights.addUsers) : false; const inviteUsers = rights ? Boolean(rights.inviteUsers || rights.addUsers) : false;
const addAdmins = rights ? Boolean(rights.addAdmins) : false; const addAdmins = rights ? Boolean(rights.addAdmins) : false;
lines.push(`Роль: ${isCreator ? "creator" : isAdmin ? "admin" : "member"}`); lines.push(`Роль: ${isCreator ? "creator" : isAdmin ? "admin" : "member"}`);
lines.push(`Права: inviteUsers=${inviteUsers}; addUsers=${rights ? Boolean(rights.addUsers) : false}; addAdmins=${addAdmins}`);
lines.push(`inviteUsers: ${inviteUsers}`); lines.push(`inviteUsers: ${inviteUsers}`);
lines.push(`addAdmins: ${addAdmins}`); lines.push(`addAdmins: ${addAdmins}`);
} catch (error) { } catch (error) {
@ -101,6 +102,14 @@ class TelegramManager {
adminRights: rights, adminRights: rights,
rank: "invite" rank: "invite"
})); }));
if (account) {
this.store.addAccountEvent(
account.id,
account.phone || "",
"temp_admin_granted",
`${targetEntity && targetEntity.title ? targetEntity.title : "цель"}`
);
}
} }
async _revokeTempInviteAdmin(masterClient, targetEntity, account) { async _revokeTempInviteAdmin(masterClient, targetEntity, account) {
@ -648,6 +657,10 @@ class TelegramManager {
} }
finalResult = retry; finalResult = retry;
} }
if (finalResult.confirmed !== true && !finalResult.detail) {
const label = directLabel || "проверка этим аккаунтом";
finalResult.detail = buildConfirmDetail("CONFIRM_UNKNOWN", "результат проверки не определен", label);
}
return { ...finalResult, attempts }; return { ...finalResult, attempts };
}; };
const attemptInvite = async (user) => { const attemptInvite = async (user) => {
@ -737,19 +750,40 @@ class TelegramManager {
const sourceChat = options.sourceChat || ""; const sourceChat = options.sourceChat || "";
const attempts = []; const attempts = [];
let user = null; let user = null;
if (accessHash) { const resolveEvents = [];
if (providedUsername) {
const username = providedUsername.startsWith("@") ? providedUsername : `@${providedUsername}`;
try {
user = await client.getEntity(username);
attempts.push({ strategy: "username", ok: true, detail: username });
resolveEvents.push(`username: ok (${username})`);
} catch (error) {
user = null;
attempts.push({ strategy: "username", ok: false, detail: "resolve failed" });
resolveEvents.push("username: fail (resolve failed)");
}
} else {
attempts.push({ strategy: "username", ok: false, detail: "username not provided" });
resolveEvents.push("username: skip (no username)");
}
if (!user && accessHash) {
try { try {
user = new Api.InputUser({ user = new Api.InputUser({
userId: BigInt(userId), userId: BigInt(userId),
accessHash: BigInt(accessHash) accessHash: BigInt(accessHash)
}); });
attempts.push({ strategy: "access_hash", ok: true, detail: "from message" }); attempts.push({ strategy: "access_hash", ok: true, detail: "from message" });
resolveEvents.push("access_hash: ok (from message)");
} catch (error) { } catch (error) {
user = null; user = null;
attempts.push({ strategy: "access_hash", ok: false, detail: "invalid access_hash" }); attempts.push({ strategy: "access_hash", ok: false, detail: "invalid access_hash" });
resolveEvents.push("access_hash: fail (invalid access_hash)");
} }
} else { } else {
if (!accessHash) {
attempts.push({ strategy: "access_hash", ok: false, detail: "not provided" }); attempts.push({ strategy: "access_hash", ok: false, detail: "not provided" });
resolveEvents.push("access_hash: fail (not provided)");
}
} }
if (!user && sourceChat) { if (!user && sourceChat) {
const resolved = await this._resolveUserFromSource(client, sourceChat, userId); const resolved = await this._resolveUserFromSource(client, sourceChat, userId);
@ -760,32 +794,34 @@ class TelegramManager {
accessHash: BigInt(resolved.accessHash) accessHash: BigInt(resolved.accessHash)
}); });
attempts.push({ strategy: "participants", ok: true, detail: resolved.detail || "from participants" }); attempts.push({ strategy: "participants", ok: true, detail: resolved.detail || "from participants" });
resolveEvents.push(`participants: ok (${resolved.detail || "from participants"})`);
} catch (error) { } catch (error) {
user = null; user = null;
attempts.push({ strategy: "participants", ok: false, detail: "invalid access_hash" }); attempts.push({ strategy: "participants", ok: false, detail: "invalid access_hash" });
resolveEvents.push("participants: fail (invalid access_hash)");
} }
} else { } else {
attempts.push({ strategy: "participants", ok: false, detail: resolved ? resolved.detail : "no result" }); attempts.push({ strategy: "participants", ok: false, detail: resolved ? resolved.detail : "no result" });
resolveEvents.push(`participants: fail (${resolved ? resolved.detail : "no result"})`);
} }
} else if (!user && !sourceChat) { } else if (!user && !sourceChat) {
attempts.push({ strategy: "participants", ok: false, detail: "source chat not provided" }); attempts.push({ strategy: "participants", ok: false, detail: "source chat not provided" });
resolveEvents.push("participants: skip (no source chat)");
} }
if (!user && providedUsername) { // username already attempted above
const username = providedUsername.startsWith("@") ? providedUsername : `@${providedUsername}`;
try {
user = await client.getEntity(username);
attempts.push({ strategy: "username", ok: true, detail: username });
} catch (error) {
user = null;
attempts.push({ strategy: "username", ok: false, detail: "resolve failed" });
}
} else if (!user && !providedUsername) {
attempts.push({ strategy: "username", ok: false, detail: "username not provided" });
}
if (!user) { if (!user) {
const resolvedUser = await client.getEntity(userId); const resolvedUser = await client.getEntity(userId);
user = await client.getInputEntity(resolvedUser); user = await client.getInputEntity(resolvedUser);
attempts.push({ strategy: "entity", ok: true, detail: "getEntity(userId)" }); attempts.push({ strategy: "entity", ok: true, detail: "getEntity(userId)" });
resolveEvents.push("entity: ok (getEntity(userId))");
}
if (account) {
this.store.addAccountEvent(
account.id,
account.phone || "",
"resolve_user",
resolveEvents.join(" | ")
);
} }
return { user, attempts }; return { user, attempts };
}; };
@ -806,6 +842,12 @@ class TelegramManager {
} else { } else {
targetType = targetEntity && targetEntity.className ? targetEntity.className : ""; targetType = targetEntity && targetEntity.className ? targetEntity.className : "";
} }
this.store.addAccountEvent(
account.id,
account.phone || "",
"invite_admin_path",
`invite_via_admins=${task.invite_via_admins ? "on" : "off"}; targetType=${targetType || "unknown"}`
);
const resolved = await resolveInputUser(); const resolved = await resolveInputUser();
lastAttempts = resolved.attempts || []; lastAttempts = resolved.attempts || [];
const user = resolved.user; const user = resolved.user;
@ -905,7 +947,19 @@ class TelegramManager {
lastAttempts.push({ strategy: "admin_invite", ok: false, detail: adminText }); lastAttempts.push({ strategy: "admin_invite", ok: false, detail: adminText });
} }
} }
this.store.addAccountEvent(
account.id,
account.phone || "",
"invite_attempt",
`${userId} -> ${task.our_group || "цель"}`
);
await attemptInvite(user); await attemptInvite(user);
this.store.addAccountEvent(
account.id,
account.phone || "",
"invite_sent",
`${userId} -> ${task.our_group || "цель"}`
);
const confirm = await confirmMembershipWithFallback(user, entry); const confirm = await confirmMembershipWithFallback(user, entry);
if (confirm.confirmed !== true && !confirm.detail) { if (confirm.confirmed !== true && !confirm.detail) {
const label = formatAccountSource("", entry) || "проверка этим аккаунтом"; const label = formatAccountSource("", entry) || "проверка этим аккаунтом";
@ -914,6 +968,12 @@ class TelegramManager {
if (confirm.attempts && confirm.attempts.length) { if (confirm.attempts && confirm.attempts.length) {
lastAttempts.push(...confirm.attempts); lastAttempts.push(...confirm.attempts);
} }
this.store.addAccountEvent(
account.id,
account.phone || "",
confirm.confirmed === true ? "confirm_ok" : "confirm_unconfirmed",
`${userId} -> ${confirm.detail || "не подтверждено"}`
);
this.store.updateAccountStatus(account.id, "ok", ""); this.store.updateAccountStatus(account.id, "ok", "");
const last = lastAttempts.filter((item) => item.ok).slice(-1)[0]; const last = lastAttempts.filter((item) => item.ok).slice(-1)[0];
@ -929,6 +989,12 @@ class TelegramManager {
}; };
} catch (error) { } catch (error) {
const errorText = error.errorMessage || error.message || String(error); const errorText = error.errorMessage || error.message || String(error);
this.store.addAccountEvent(
account.id,
account.phone || "",
"invite_failed",
`${userId} -> ${task.our_group || "цель"} | ${errorText}`
);
if (errorText.includes("CHAT_WRITE_FORBIDDEN")) { if (errorText.includes("CHAT_WRITE_FORBIDDEN")) {
try { try {
const reason = await explainWriteForbidden(); const reason = await explainWriteForbidden();
@ -999,50 +1065,15 @@ class TelegramManager {
ok: false, ok: false,
detail: `username=${options.username || "—"}; hash=${options.userAccessHash || "—"}; source=${options.sourceChat || "—"}` detail: `username=${options.username || "—"}; hash=${options.userAccessHash || "—"}; source=${options.sourceChat || "—"}`
}); });
const username = options.username ? (options.username.startsWith("@") ? options.username : `@${options.username}`) : "";
try {
let retryUser = null;
if (!retryUser && options.sourceChat) {
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 {
retryUser = await client.getEntity(username);
} catch (resolveError) {
retryUser = null;
}
}
if (!retryUser) {
const resolvedUser = await client.getEntity(userId);
retryUser = await client.getInputEntity(resolvedUser);
}
await attemptInvite(retryUser);
this.store.updateAccountStatus(account.id, "ok", "");
return { ok: true, accountId: account.id, accountPhone: account.phone || "" };
} catch (retryError) {
const retryText = retryError.errorMessage || retryError.message || String(retryError);
this.store.addAccountEvent(
account.id,
account.phone || "",
"invite_user_invalid",
`USER_ID_INVALID -> retry failed (${retryText}); user=${userId}; username=${options.username || "—"}; hash=${options.userAccessHash || "—"}`
);
fallbackMeta = JSON.stringify([
{ strategy: "retry", ok: false, detail: retryText }
]);
}
this.store.addAccountEvent( this.store.addAccountEvent(
account.id, account.id,
account.phone || "", account.phone || "",
"invite_user_invalid", "invite_user_invalid",
`USER_ID_INVALID; user=${userId}; username=${options.username || "—"}; hash=${options.userAccessHash || "—"}` `USER_ID_INVALID; user=${userId}; username=${options.username || "—"}; hash=${options.userAccessHash || "—"}`
); );
fallbackMeta = JSON.stringify([
{ strategy: "retry", ok: false, detail: "skip retry: user invalid" }
]);
} }
if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) { if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) {
this._applyFloodCooldown(account, errorText); this._applyFloodCooldown(account, errorText);
@ -1582,9 +1613,21 @@ class TelegramManager {
adminRights: rights, adminRights: rights,
rank: "invite" rank: "invite"
})); }));
this.store.addAccountEvent(
masterAccountId,
masterAccount.phone || "",
"admin_grant",
`${record.phone || record.username || record.id} -> ${task.our_group}`
);
results.push({ accountId, ok: true }); results.push({ accountId, ok: true });
} catch (error) { } catch (error) {
const errorText = error.errorMessage || error.message || String(error); const errorText = error.errorMessage || error.message || String(error);
this.store.addAccountEvent(
masterAccountId,
masterAccount.phone || "",
"admin_grant_failed",
`${record.phone || record.username || record.id} -> ${task.our_group} | ${errorText}`
);
results.push({ accountId, ok: false, reason: errorText }); results.push({ accountId, ok: false, reason: errorText });
} }
} }
@ -1592,7 +1635,18 @@ class TelegramManager {
} }
async _autoJoinGroups(client, groups, enabled, account) { async _autoJoinGroups(client, groups, enabled, account) {
if (!enabled) return; if (!enabled) {
if (account && (groups || []).length) {
const list = (groups || []).filter(Boolean).join(", ");
this.store.addAccountEvent(
account.id,
account.phone || "",
"auto_join_skipped",
`автовступление выключено: ${list || "без групп"}`
);
}
return;
}
const settings = this.store.getSettings(); const settings = this.store.getSettings();
let maxGroups = Number(account && account.max_groups != null ? account.max_groups : settings.accountMaxGroups); let maxGroups = Number(account && account.max_groups != null ? account.max_groups : settings.accountMaxGroups);
if (!Number.isFinite(maxGroups) || maxGroups <= 0) { if (!Number.isFinite(maxGroups) || maxGroups <= 0) {
@ -1630,14 +1684,39 @@ class TelegramManager {
if (this._isInviteLink(group)) { if (this._isInviteLink(group)) {
const hash = this._extractInviteHash(group); const hash = this._extractInviteHash(group);
if (hash) { if (hash) {
await client.invoke(new Api.messages.ImportChatInvite({ hash })); const result = await client.invoke(new Api.messages.ImportChatInvite({ hash }));
if (account) {
const className = result && result.className ? result.className : "";
if (className.includes("ChatInviteAlready")) {
this.store.addAccountEvent(account.id, account.phone || "", "auto_join_already", group);
} else if (className.includes("ChatInvite") || className.includes("ChatInvitePeek")) {
this.store.addAccountEvent(account.id, account.phone || "", "auto_join_request", group);
} else {
this.store.addAccountEvent(account.id, account.phone || "", "auto_join_ok", group);
}
}
} }
} else { } else {
const entity = await client.getEntity(group); const entity = await client.getEntity(group);
await client.invoke(new Api.channels.JoinChannel({ channel: entity })); const result = await client.invoke(new Api.channels.JoinChannel({ channel: entity }));
if (account) {
const className = result && result.className ? result.className : "";
if (className.includes("ChatInviteAlready")) {
this.store.addAccountEvent(account.id, account.phone || "", "auto_join_already", group);
} else if (className.includes("ChatInvite") || className.includes("ChatInvitePeek")) {
this.store.addAccountEvent(account.id, account.phone || "", "auto_join_request", group);
} else {
this.store.addAccountEvent(account.id, account.phone || "", "auto_join_ok", group);
}
}
} }
} catch (error) { } catch (error) {
const errorText = error.errorMessage || error.message || String(error); const errorText = error.errorMessage || error.message || String(error);
if (account) {
const isRequest = errorText.includes("INVITE_REQUEST_SENT");
const action = isRequest ? "auto_join_request" : "auto_join_failed";
this.store.addAccountEvent(account.id, account.phone || "", action, `${group} | ${errorText}`);
}
if (account && (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD"))) { if (account && (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD"))) {
this._applyFloodCooldown(account, errorText); this._applyFloodCooldown(account, errorText);
} }

View File

@ -43,6 +43,7 @@ const emptySettings = {
inviteAdminAnonymous: true, inviteAdminAnonymous: true,
separateConfirmRoles: false, separateConfirmRoles: false,
maxConfirmBots: 1, maxConfirmBots: 1,
useWatcherInviteNoUsername: true,
warmupEnabled: true, warmupEnabled: true,
warmupStartLimit: 3, warmupStartLimit: 3,
warmupDailyIncrease: 2, warmupDailyIncrease: 2,
@ -82,6 +83,7 @@ const emptySettings = {
inviteAdminAnonymous: row.invite_admin_anonymous == null ? true : Boolean(row.invite_admin_anonymous), inviteAdminAnonymous: row.invite_admin_anonymous == null ? true : Boolean(row.invite_admin_anonymous),
separateConfirmRoles: Boolean(row.separate_confirm_roles), separateConfirmRoles: Boolean(row.separate_confirm_roles),
maxConfirmBots: Number(row.max_confirm_bots || 1), maxConfirmBots: Number(row.max_confirm_bots || 1),
useWatcherInviteNoUsername: row.use_watcher_invite_no_username == null ? true : Boolean(row.use_watcher_invite_no_username),
warmupEnabled: row.warmup_enabled == null ? true : Boolean(row.warmup_enabled), warmupEnabled: row.warmup_enabled == null ? true : Boolean(row.warmup_enabled),
warmupStartLimit: Number(row.warmup_start_limit || 3), warmupStartLimit: Number(row.warmup_start_limit || 3),
warmupDailyIncrease: Number(row.warmup_daily_increase || 2), warmupDailyIncrease: Number(row.warmup_daily_increase || 2),
@ -188,6 +190,7 @@ export default function App() {
const toastTimers = useRef(new Map()); const toastTimers = useRef(new Map());
const [notifications, setNotifications] = useState([]); const [notifications, setNotifications] = useState([]);
const [notificationsOpen, setNotificationsOpen] = useState(false); const [notificationsOpen, setNotificationsOpen] = useState(false);
const notificationsModalRef = useRef(null);
const [manualLoginOpen, setManualLoginOpen] = useState(false); const [manualLoginOpen, setManualLoginOpen] = useState(false);
const [taskSearch, setTaskSearch] = useState(""); const [taskSearch, setTaskSearch] = useState("");
const [taskFilter, setTaskFilter] = useState("all"); const [taskFilter, setTaskFilter] = useState("all");
@ -872,6 +875,9 @@ export default function App() {
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if (!notificationsOpen) return; if (!notificationsOpen) return;
if (!bellRef.current) return; if (!bellRef.current) return;
if (notificationsModalRef.current && notificationsModalRef.current.contains(event.target)) {
return;
}
if (!bellRef.current.contains(event.target)) { if (!bellRef.current.contains(event.target)) {
setNotificationsOpen(false); setNotificationsOpen(false);
} }
@ -1350,8 +1356,14 @@ export default function App() {
if (taskActionLoading) return; if (taskActionLoading) return;
setTaskActionLoading(true); setTaskActionLoading(true);
showNotification("Запуск...", "info"); showNotification("Запуск...", "info");
const withTimeout = (promise, ms) => (
Promise.race([
promise,
new Promise((_, reject) => setTimeout(() => reject(new Error("TIMEOUT")), ms))
])
);
try { try {
const result = await window.api.startTaskById(selectedTaskId); const result = await withTimeout(window.api.startTaskById(selectedTaskId), 15000);
if (result && result.ok) { if (result && result.ok) {
setTaskNotice({ text: "Запущено.", tone: "success", source }); setTaskNotice({ text: "Запущено.", tone: "success", source });
if (result.warnings && result.warnings.length) { if (result.warnings && result.warnings.length) {
@ -1362,7 +1374,9 @@ export default function App() {
showNotification(result.error || "Не удалось запустить", "error"); showNotification(result.error || "Не удалось запустить", "error");
} }
} catch (error) { } catch (error) {
const message = error.message || String(error); const message = error.message === "TIMEOUT"
? "Запуск не ответил за 15 секунд. Проверьте логи/события и попробуйте снова."
: (error.message || String(error));
setTaskNotice({ text: message, tone: "error", source }); setTaskNotice({ text: message, tone: "error", source });
showNotification(message, "error"); showNotification(message, "error");
} finally { } finally {
@ -1404,11 +1418,19 @@ export default function App() {
} }
setTaskActionLoading(true); setTaskActionLoading(true);
showNotification("Остановка...", "info"); showNotification("Остановка...", "info");
const withTimeout = (promise, ms) => (
Promise.race([
promise,
new Promise((_, reject) => setTimeout(() => reject(new Error("TIMEOUT")), ms))
])
);
try { try {
await window.api.stopTaskById(selectedTaskId); await withTimeout(window.api.stopTaskById(selectedTaskId), 15000);
setTaskNotice({ text: "Остановлено.", tone: "success", source }); setTaskNotice({ text: "Остановлено.", tone: "success", source });
} catch (error) { } catch (error) {
const message = error.message || String(error); const message = error.message === "TIMEOUT"
? "Остановка не ответила за 15 секунд. Проверьте логи/события и попробуйте снова."
: (error.message || String(error));
setTaskNotice({ text: message, tone: "error", source }); setTaskNotice({ text: message, tone: "error", source });
showNotification(message, "error"); showNotification(message, "error");
} finally { } finally {
@ -1722,6 +1744,12 @@ export default function App() {
taskId: selectedTaskId, taskId: selectedTaskId,
accountRoles: rolePayload accountRoles: rolePayload
}); });
await window.api.addAccountEvent({
accountId: 0,
phone: "",
action: "roles_changed",
details: `задача ${selectedTaskId}: обновлены роли`
});
await loadAccountAssignments(); await loadAccountAssignments();
}; };
@ -1867,11 +1895,19 @@ export default function App() {
}); });
const signature = buildPresetSignature(nextForm, roleMap); const signature = buildPresetSignature(nextForm, roleMap);
presetSignatureRef.current = signature; presetSignatureRef.current = signature;
const label = type === "admin" ? "Автораспределение + Инвайт через админа" : "Автораспределение + Без админки";
setTaskForm(nextForm); setTaskForm(nextForm);
setTaskAccountRoles(roleMap); setTaskAccountRoles(roleMap);
setSelectedAccountIds(Object.keys(roleMap).map((id) => Number(id))); setSelectedAccountIds(Object.keys(roleMap).map((id) => Number(id)));
persistAccountRoles(roleMap); persistAccountRoles(roleMap);
const label = type === "admin" ? "Автораспределение + Инвайт через админа" : "Автораспределение + Без админки"; if (window.api) {
window.api.addAccountEvent({
accountId: 0,
phone: "",
action: "preset_applied",
details: `задача ${selectedTaskId}: ${label}`
});
}
setTaskNotice({ text: `Пресет: ${label}`, tone: "success", source: "task" }); setTaskNotice({ text: `Пресет: ${label}`, tone: "success", source: "task" });
setActivePreset(type); setActivePreset(type);
}; };
@ -2292,7 +2328,7 @@ export default function App() {
</div> </div>
{notificationsOpen && ( {notificationsOpen && (
<div className="modal-overlay" onClick={() => setNotificationsOpen(false)}> <div className="modal-overlay" onClick={() => setNotificationsOpen(false)}>
<div className="modal" onClick={(event) => event.stopPropagation()}> <div className="modal" ref={notificationsModalRef} onClick={(event) => event.stopPropagation()}>
<div className="row-header"> <div className="row-header">
<h2>Уведомления</h2> <h2>Уведомления</h2>
<button type="button" className="ghost" onClick={() => setNotifications([])}> <button type="button" className="ghost" onClick={() => setNotifications([])}>
@ -2883,7 +2919,7 @@ export default function App() {
<span className="hint">Нужен для прогрева новых аккаунтов и снижения риска флуда.</span> <span className="hint">Нужен для прогрева новых аккаунтов и снижения риска флуда.</span>
<span className="hint">График: 1/д ×3, 2/д ×4, 3/д ×5, 4/д ×6, 5/д ×7, 6/д ×8, далее 7/д.</span> <span className="hint">График: 1/д ×3, 2/д ×4, 3/д ×5, 4/д ×6, 5/д ×7, 6/д ×8, далее 7/д.</span>
</label> </label>
{!taskForm.warmupEnabled && ( {taskForm.warmupEnabled && (
<> <>
<label> <label>
<span className="label-line">Стартовый лимит</span> <span className="label-line">Стартовый лимит</span>
@ -3237,6 +3273,20 @@ export default function App() {
Разрешать запуск без прав инвайта Разрешать запуск без прав инвайта
<span className="hint">Полезно, если вы выдаёте админов после автодобавления.</span> <span className="hint">Полезно, если вы выдаёте админов после автодобавления.</span>
</label> </label>
<label className="checkbox">
<input
type="checkbox"
checked={Boolean(taskForm.useWatcherInviteNoUsername)}
onChange={(event) => setTaskForm({ ...taskForm, useWatcherInviteNoUsername: event.target.checked })}
/>
Инвайт через наблюдателя, если нет username
<span className="hint">
Правило 1: сначала резолвим @username в сессии инвайтера участие в группе конкурентов не требуется.
</span>
<span className="hint">
Правило 2: если username нет инвайтим наблюдателем, потому что access_hash валиден только в его сессии.
</span>
</label>
</div> </div>
<div className="row"> <div className="row">
<label> <label>

View File

@ -1269,7 +1269,9 @@ button.danger {
.task-title-row { .task-title-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 6px;
flex-direction: column;
align-items: flex-start;
} }
.task-meta { .task-meta {
@ -1336,13 +1338,15 @@ button.danger {
border-radius: 999px; border-radius: 999px;
background: #e2e8f0; background: #e2e8f0;
color: #475569; color: #475569;
white-space: nowrap;
} }
.task-badge-row { .task-badge-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
flex-wrap: wrap; flex-direction: column;
align-items: flex-start;
} }
.task-badge.ok { .task-badge.ok {