diff --git a/package.json b/package.json
index 195f254..cfe88ce 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "telegram-invite-automation",
- "version": "1.6.0",
+ "version": "1.8.0",
"private": true,
"description": "Automated user parsing and invites for Telegram groups",
"main": "src/main/index.js",
diff --git a/src/main/index.js b/src/main/index.js
index 6e3deec..0005bc5 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -1045,6 +1045,35 @@ ipcMain.handle("tasks:checkInviteAccess", async (_event, id) => {
return result;
});
+ipcMain.handle("tasks:checkConfirmAccess", 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_confirm);
+ 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),
+ roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite),
+ inviteLimit: Number(row.invite_limit || 0)
+ }));
+ store.setTaskAccountRoles(id, filtered);
+ }
+ const inviteIdSet = task.separate_confirm_roles
+ ? new Set(store.listTaskAccounts(id).filter((row) => row.role_invite).map((row) => row.account_id))
+ : null;
+ const accountIds = accountRows
+ .filter((row) => existingIds.has(row.account_id))
+ .map((row) => row.account_id)
+ .filter((accountId) => !inviteIdSet || !inviteIdSet.has(accountId));
+ return telegram.checkConfirmAccess(task, accountIds);
+});
+
ipcMain.handle("tasks:groupVisibility", async (_event, id) => {
const task = store.getTask(id);
if (!task) return { ok: false, error: "Task not found" };
diff --git a/src/main/preload.js b/src/main/preload.js
index 544df7b..f6cc700 100644
--- a/src/main/preload.js
+++ b/src/main/preload.js
@@ -56,6 +56,7 @@ contextBridge.exposeInMainWorld("api", {
parseHistoryByTask: (id) => ipcRenderer.invoke("tasks:parseHistory", id),
checkAccessByTask: (id) => ipcRenderer.invoke("tasks:checkAccess", id),
checkInviteAccessByTask: (id) => ipcRenderer.invoke("tasks:checkInviteAccess", id),
+ checkConfirmAccessByTask: (id) => ipcRenderer.invoke("tasks:checkConfirmAccess", id),
membershipStatusByTask: (id) => ipcRenderer.invoke("tasks:membershipStatus", id),
groupVisibilityByTask: (id) => ipcRenderer.invoke("tasks:groupVisibility", id),
joinGroupsByTask: (id) => ipcRenderer.invoke("tasks:joinGroups", id)
diff --git a/src/main/store.js b/src/main/store.js
index 2c7e425..44b0089 100644
--- a/src/main/store.js
+++ b/src/main/store.js
@@ -485,6 +485,31 @@ function initStore(userDataPath) {
}));
}
+ function getAutoJoinStatus(accountId, group) {
+ if (!accountId || !group) return null;
+ const row = db.prepare(`
+ SELECT event_type, message, created_at
+ FROM account_events
+ WHERE account_id = ?
+ AND event_type IN ('auto_join_ok','auto_join_request','auto_join_already','auto_join_failed')
+ AND message LIKE ?
+ ORDER BY id DESC
+ LIMIT 1
+ `).get(accountId, `%${group}%`);
+ if (!row) return null;
+ const eventType = row.event_type || "";
+ if (eventType === "auto_join_request") {
+ return { status: "pending", createdAt: row.created_at };
+ }
+ if (eventType === "auto_join_ok" || eventType === "auto_join_already") {
+ return { status: "ok", createdAt: row.created_at };
+ }
+ if (eventType === "auto_join_failed") {
+ return { status: "failed", createdAt: row.created_at, message: row.message || "" };
+ }
+ return null;
+ }
+
function clearAccountEvents() {
db.prepare("DELETE FROM account_events").run();
}
@@ -1239,6 +1264,7 @@ function initStore(userDataPath) {
clearAccountCooldown,
addAccountEvent,
listAccountEvents,
+ getAutoJoinStatus,
clearAccountEvents,
addTaskAudit,
listTaskAudit,
diff --git a/src/main/taskRunner.js b/src/main/taskRunner.js
index 68b2a5a..95d3368 100644
--- a/src/main/taskRunner.js
+++ b/src/main/taskRunner.js
@@ -60,7 +60,10 @@ class TaskRunner {
const accounts = accountRows.map((row) => row.account_id);
const monitorIds = accountRows.filter((row) => row.role_monitor).map((row) => row.account_id);
const inviteIds = accountRows.filter((row) => row.role_invite).map((row) => row.account_id);
- const confirmIds = accountRows.filter((row) => row.role_confirm).map((row) => row.account_id);
+ const confirmIdsRaw = accountRows.filter((row) => row.role_confirm).map((row) => row.account_id);
+ const confirmIds = this.task.separate_confirm_roles
+ ? confirmIdsRaw.filter((id) => !inviteIds.includes(id))
+ : confirmIdsRaw;
await this.telegram.joinGroupsForTask(this.task, competitors, accounts, {
monitorIds,
inviteIds,
@@ -524,7 +527,7 @@ class TaskRunner {
item.account_id || 0,
"",
"confirm_retry_ok",
- `Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""}`
+ `задача ${this.task.id}: Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""}`
);
continue;
}
@@ -535,7 +538,7 @@ class TaskRunner {
item.account_id || 0,
"",
"confirm_retry_failed",
- `Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""} • лимит попыток`
+ `задача ${this.task.id}: Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""} • лимит попыток`
);
continue;
}
@@ -551,7 +554,7 @@ class TaskRunner {
"",
"confirm_retry_scheduled",
[
- `Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""}`,
+ `задача ${this.task.id}: Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""}`,
"Повторная проверка через 5 минут",
`Попыток: ${attempts}/${item.max_attempts || 2}`
].join("\n")
diff --git a/src/main/telegram.js b/src/main/telegram.js
index 82c6180..2654706 100644
--- a/src/main/telegram.js
+++ b/src/main/telegram.js
@@ -175,7 +175,29 @@ class TelegramManager {
async _grantTempInviteAdmin(masterClient, targetEntity, account, allowAnonymous = false) {
const rights = this._buildInviteAdminRights(allowAnonymous);
- const user = await this._resolveAccountEntityForMaster(masterClient, targetEntity, account);
+ let user = null;
+ const entry = account ? this.clients.get(account.id) : null;
+ if (entry) {
+ try {
+ const me = await entry.client.getMe();
+ if (me && me.id != null && me.accessHash != null) {
+ user = new Api.InputUser({ userId: BigInt(me.id), accessHash: BigInt(me.accessHash) });
+ }
+ } catch {
+ // fallback below
+ }
+ if (!user) {
+ try {
+ const inputMe = await entry.client.getInputEntity("me");
+ user = this._toInputUser(inputMe);
+ } catch {
+ // fallback below
+ }
+ }
+ }
+ if (!user) {
+ user = await this._resolveAccountEntityForMaster(masterClient, targetEntity, account);
+ }
if (!user) {
throw new Error("INVITER_ENTITY_NOT_RESOLVED_BY_MASTER");
}
@@ -712,24 +734,31 @@ class TelegramManager {
return { ok: true, entity: resolvedTarget.entity };
};
const confirmMembership = async (user, confirmClient = client, sourceLabel = "", confirmEntry = null) => {
+ const confirmAccount = confirmEntry && confirmEntry.account ? confirmEntry.account : account;
+ const confirmAccountId = confirmAccount && confirmAccount.id ? confirmAccount.id : 0;
+ const confirmAccountPhone = confirmAccount && confirmAccount.phone ? confirmAccount.phone : "";
const targetForClient = await getTargetEntityForClient(confirmClient, confirmEntry);
if (!targetForClient.ok) {
return {
confirmed: null,
error: targetForClient.error,
- detail: buildConfirmDetail(targetForClient.error, "ошибка подтверждения участия", sourceLabel)
+ detail: buildConfirmDetail(targetForClient.error, "ошибка подтверждения участия", sourceLabel),
+ checkedByAccountId: confirmAccountId,
+ checkedByAccountPhone: confirmAccountPhone
};
}
const confirmTargetEntity = targetForClient.entity;
if (!confirmTargetEntity || confirmTargetEntity.className !== "Channel") {
- return { confirmed: true, error: "", detail: "" };
+ return { confirmed: true, error: "", detail: "", checkedByAccountId: confirmAccountId, checkedByAccountPhone: confirmAccountPhone };
}
const participantForClient = await resolveUserForClient(confirmClient, user);
if (!participantForClient) {
return {
confirmed: null,
error: "INVITED_USER_NOT_RESOLVED_FOR_CONFIRM",
- detail: buildConfirmDetail("INVITED_USER_NOT_RESOLVED_FOR_CONFIRM", "не удалось резолвить пользователя для проверки участия", sourceLabel)
+ detail: buildConfirmDetail("INVITED_USER_NOT_RESOLVED_FOR_CONFIRM", "не удалось резолвить пользователя для проверки участия", sourceLabel),
+ checkedByAccountId: confirmAccountId,
+ checkedByAccountPhone: confirmAccountPhone
};
}
try {
@@ -737,27 +766,33 @@ class TelegramManager {
channel: confirmTargetEntity,
participant: participantForClient
}));
- return { confirmed: true, error: "", detail: sourceLabel ? `OK (${sourceLabel})` : "OK" };
+ return { confirmed: true, error: "", detail: sourceLabel ? `OK (${sourceLabel})` : "OK", checkedByAccountId: confirmAccountId, checkedByAccountPhone: confirmAccountPhone };
} catch (error) {
const errorText = error.errorMessage || error.message || String(error);
if (errorText.includes("USER_NOT_PARTICIPANT")) {
return {
confirmed: false,
error: "USER_NOT_PARTICIPANT",
- detail: buildConfirmDetail("USER_NOT_PARTICIPANT", "пользователь не в группе", sourceLabel)
+ detail: buildConfirmDetail("USER_NOT_PARTICIPANT", "пользователь не в группе", sourceLabel),
+ checkedByAccountId: confirmAccountId,
+ checkedByAccountPhone: confirmAccountPhone
};
}
if (errorText.includes("CHAT_ADMIN_REQUIRED")) {
return {
confirmed: null,
error: "CHAT_ADMIN_REQUIRED",
- detail: buildConfirmDetail("CHAT_ADMIN_REQUIRED", "нет прав для подтверждения участия", sourceLabel)
+ detail: buildConfirmDetail("CHAT_ADMIN_REQUIRED", "нет прав для подтверждения участия", sourceLabel),
+ checkedByAccountId: confirmAccountId,
+ checkedByAccountPhone: confirmAccountPhone
};
}
return {
confirmed: null,
error: errorText,
- detail: buildConfirmDetail(errorText, "ошибка подтверждения участия", sourceLabel)
+ detail: buildConfirmDetail(errorText, "ошибка подтверждения участия", sourceLabel),
+ checkedByAccountId: confirmAccountId,
+ checkedByAccountPhone: confirmAccountPhone
};
}
};
@@ -765,17 +800,21 @@ class TelegramManager {
const attempts = [];
const triedClients = new Set();
const directLabel = formatAccountSource("", inviterEntry);
- const direct = await confirmMembership(user, client, directLabel || "проверка этим аккаунтом", entry);
- if (direct.detail) {
- attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail });
- }
- triedClients.add(client);
- if (direct.confirmed !== null) {
- return { ...direct, attempts };
- }
- let finalResult = direct;
const roleAssignments = this.taskRoleAssignments.get(task.id) || {};
const confirmIds = Array.isArray(roleAssignments.confirmIds) ? roleAssignments.confirmIds : [];
+ const preferConfirmRoles = Boolean(task.separate_confirm_roles) && confirmIds.length > 0;
+ let finalResult = { confirmed: null, detail: "", checkedByAccountId: 0 };
+ if (!preferConfirmRoles) {
+ const direct = await confirmMembership(user, client, directLabel || "проверка этим аккаунтом", entry);
+ if (direct.detail) {
+ attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail });
+ }
+ triedClients.add(client);
+ if (direct.confirmed !== null) {
+ return { ...direct, attempts };
+ }
+ finalResult = direct;
+ }
for (const confirmId of confirmIds) {
const entry = this.clients.get(confirmId);
if (!entry || !entry.client || triedClients.has(entry.client)) continue;
@@ -790,6 +829,9 @@ class TelegramManager {
break;
}
}
+ if (preferConfirmRoles && finalResult.confirmed !== null) {
+ return { ...finalResult, attempts };
+ }
if (finalResult.confirmed === null && !finalResult.detail) {
const masterId = Number(task.invite_admin_master_id || 0);
const masterEntry = masterId ? this.clients.get(masterId) : null;
@@ -802,7 +844,7 @@ class TelegramManager {
finalResult = adminConfirm;
}
}
- if (finalResult.confirmed === null && !finalResult.detail) {
+ if (finalResult.confirmed === null && !finalResult.detail && !preferConfirmRoles) {
await new Promise((resolve) => setTimeout(resolve, 10000));
const retryLabel = directLabel ? `${directLabel}, повтор через 10с` : "проверка этим аккаунтом, повтор через 10с";
const retry = await confirmMembership(user, client, retryLabel, entry);
@@ -1194,21 +1236,23 @@ class TelegramManager {
].filter(Boolean).join("\n")
);
- if (confirm.error === "USER_NOT_PARTICIPANT") {
+ if (confirm.confirmed === false) {
const nextCheckAt = dayjs().add(5, "minute").toISOString();
const username = resolvedUser && resolvedUser.username ? resolvedUser.username : (user && user.username ? user.username : "");
+ const confirmAccountId = confirm.checkedByAccountId || account.id;
+ const confirmAccountPhone = confirm.checkedByAccountPhone || account.phone || "";
this.store.addConfirmQueue(
task.id,
userId,
username,
- account.id,
+ confirmAccountId,
options.watcherAccountId || 0,
nextCheckAt,
2
);
this.store.addAccountEvent(
- account.id,
- account.phone || "",
+ confirmAccountId,
+ confirmAccountPhone,
"confirm_retry_scheduled",
[
`Пользователь: ${userId}${username ? ` (@${username})` : ""}`,
@@ -1456,17 +1500,37 @@ class TelegramManager {
for (const group of groups) {
const isMember = await this._isParticipant(client, group);
if (isMember) competitorCount += 1;
+ let pending = false;
+ let pendingAt = "";
+ if (!isMember) {
+ const joinStatus = this.store.getAutoJoinStatus(account.id, group);
+ if (joinStatus && joinStatus.status === "pending") {
+ pending = true;
+ pendingAt = joinStatus.createdAt || "";
+ }
+ }
competitorGroupsInfo.push({
link: group,
title: titleMap.get(group) || "",
- isMember
+ isMember,
+ pending,
+ pendingAt
});
}
let ourGroupMember = false;
+ let ourGroupPending = false;
+ let ourGroupPendingAt = "";
let ourGroupInfo = null;
if (ourGroup) {
ourGroupMember = await this._isParticipant(client, ourGroup);
+ if (!ourGroupMember) {
+ const joinStatus = this.store.getAutoJoinStatus(account.id, ourGroup);
+ if (joinStatus && joinStatus.status === "pending") {
+ ourGroupPending = true;
+ ourGroupPendingAt = joinStatus.createdAt || "";
+ }
+ }
ourGroupInfo = {
link: ourGroup,
title: titleMap.get(ourGroup) || "",
@@ -1479,6 +1543,8 @@ class TelegramManager {
competitorCount,
competitorTotal: groups.length,
ourGroupMember,
+ ourGroupPending,
+ ourGroupPendingAt,
competitorGroups: competitorGroupsInfo,
ourGroup: ourGroupInfo
});
@@ -1894,6 +1960,111 @@ class TelegramManager {
return { ok: true, result: results };
}
+ async checkConfirmAccess(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 confirm 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,
+ 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,
+ 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";
+ }
+ if (className !== "Channel") {
+ results.push({
+ accountId,
+ accountPhone: account.phone || "",
+ ok: true,
+ member: true,
+ reason: "Проверка участия не требуется для обычной группы",
+ targetType,
+ title,
+ targetChat: task.our_group
+ });
+ continue;
+ }
+ try {
+ const me = await client.getMe();
+ await client.invoke(new Api.channels.GetParticipant({
+ channel: entity,
+ participant: me
+ }));
+ results.push({
+ accountId,
+ accountPhone: account.phone || "",
+ ok: true,
+ member: true,
+ reason: "",
+ targetType,
+ title,
+ targetChat: task.our_group
+ });
+ } catch (error) {
+ const errorText = error.errorMessage || error.message || String(error);
+ let reason = errorText;
+ let member = false;
+ if (errorText.includes("USER_NOT_PARTICIPANT")) {
+ reason = "Аккаунт не состоит в нашей группе";
+ member = false;
+ } else if (errorText.includes("CHAT_ADMIN_REQUIRED")) {
+ reason = "Нет прав для проверки участия";
+ }
+ results.push({
+ accountId,
+ accountPhone: account.phone || "",
+ ok: false,
+ member,
+ reason,
+ targetType,
+ title,
+ targetChat: task.our_group
+ });
+ }
+ }
+ return { ok: true, result: results };
+ }
+
async confirmUserInGroup(task, userId, accountId) {
if (!task || !task.our_group) {
return { ok: false, error: "No target group" };
@@ -1956,6 +2127,20 @@ class TelegramManager {
return { ok: false, error: "Admin invite поддерживается только для супергрупп" };
}
+ try {
+ const diag = await this._collectInviteDiagnostics(client, targetEntity);
+ if (diag) {
+ this.store.addAccountEvent(
+ masterAccountId,
+ masterAccount.phone || "",
+ "admin_grant_master_diag",
+ diag
+ );
+ }
+ } catch {
+ // ignore diagnostics failures
+ }
+
const rights = this._buildInviteAdminRights(Boolean(task.invite_admin_anonymous));
const accounts = this.store.listAccounts();
const accountMap = new Map(accounts.map((acc) => [acc.id, acc]));
@@ -1963,13 +2148,17 @@ class TelegramManager {
const results = [];
for (const accountId of targetIds) {
const record = accountMap.get(accountId);
+ const label = record ? (record.phone || record.username || record.id) : accountId;
+ const diagParts = [`inviter=${label}`, `group=${task.our_group}`];
if (!record) {
results.push({ accountId, ok: false, reason: "Аккаунт не найден" });
+ this.store.addAccountEvent(masterAccountId, masterAccount.phone || "", "admin_grant_detail", `${diagParts.join(" | ")} | error=account_not_found`);
continue;
}
const targetEntry = this.clients.get(accountId);
if (!targetEntry) {
results.push({ accountId, ok: false, reason: "Сессия инвайтера не подключена" });
+ this.store.addAccountEvent(masterAccountId, masterAccount.phone || "", "admin_grant_detail", `${diagParts.join(" | ")} | error=session_not_connected`);
continue;
}
const targetAccess = await this._resolveGroupEntity(
@@ -1984,20 +2173,84 @@ class TelegramManager {
ok: false,
reason: `Инвайтер не имеет доступа к целевой группе: ${targetAccess.error || "неизвестно"}`
});
+ this.store.addAccountEvent(
+ masterAccountId,
+ masterAccount.phone || "",
+ "admin_grant_detail",
+ `${diagParts.join(" | ")} | access=fail (${targetAccess.error || "unknown"})`
+ );
+ continue;
+ }
+ let memberStatus = "ok";
+ let memberError = "";
+ try {
+ const selfUser = await targetEntry.client.getMe();
+ await targetEntry.client.invoke(new Api.channels.GetParticipant({
+ channel: targetAccess.entity,
+ participant: selfUser
+ }));
+ } catch (error) {
+ const errorText = error.errorMessage || error.message || String(error);
+ memberError = errorText;
+ if (errorText.includes("USER_NOT_PARTICIPANT")) {
+ memberStatus = "not_member";
+ } else {
+ memberStatus = "error";
+ }
+ const reason = memberStatus === "not_member"
+ ? "Инвайтер не состоит в нашей группе или ожидает одобрения"
+ : `Не удалось проверить участие инвайтера: ${errorText}`;
+ results.push({ accountId, ok: false, reason });
+ this.store.addAccountEvent(
+ masterAccountId,
+ masterAccount.phone || "",
+ "admin_grant_detail",
+ `${diagParts.join(" | ")} | member=${memberStatus}${memberError ? ` (${memberError})` : ""}`
+ );
continue;
}
if (!record.user_id && !record.username && !targetEntry.account.username && !targetEntry.account.user_id) {
results.push({ accountId, ok: false, reason: "Нет user_id/username" });
+ this.store.addAccountEvent(masterAccountId, masterAccount.phone || "", "admin_grant_detail", `${diagParts.join(" | ")} | error=no_identity`);
continue;
}
try {
- const user = await this._resolveAccountEntityForMaster(client, targetEntity, {
- ...record,
- username: record.username || targetEntry.account.username || "",
- user_id: record.user_id || targetEntry.account.user_id || ""
- });
+ let user = null;
+ let resolveMethod = "";
+ try {
+ const me = await targetEntry.client.getMe();
+ if (me && me.id != null && me.accessHash != null) {
+ user = new Api.InputUser({ userId: BigInt(me.id), accessHash: BigInt(me.accessHash) });
+ resolveMethod = "self:getMe";
+ }
+ } catch {
+ // fallback to resolver below
+ }
+ if (!user) {
+ try {
+ const inputMe = await targetEntry.client.getInputEntity("me");
+ user = this._toInputUser(inputMe);
+ if (user) resolveMethod = "self:input";
+ } catch {
+ // fallback to resolver below
+ }
+ }
+ if (!user) {
+ user = await this._resolveAccountEntityForMaster(client, targetEntity, {
+ ...record,
+ username: record.username || targetEntry.account.username || "",
+ user_id: record.user_id || targetEntry.account.user_id || ""
+ });
+ if (user) resolveMethod = "master:resolve";
+ }
if (!user) {
results.push({ accountId, ok: false, reason: "Мастер-админ не смог резолвить аккаунт инвайтера" });
+ this.store.addAccountEvent(
+ masterAccountId,
+ masterAccount.phone || "",
+ "admin_grant_detail",
+ `${diagParts.join(" | ")} | member=${memberStatus} | resolve=failed`
+ );
continue;
}
await client.invoke(new Api.channels.EditAdmin({
@@ -2012,6 +2265,12 @@ class TelegramManager {
"admin_grant",
`${record.phone || record.username || record.id} -> ${task.our_group}`
);
+ this.store.addAccountEvent(
+ masterAccountId,
+ masterAccount.phone || "",
+ "admin_grant_detail",
+ `${diagParts.join(" | ")} | member=${memberStatus} | resolve=${resolveMethod} | result=ok`
+ );
results.push({ accountId, ok: true });
} catch (error) {
const errorText = error.errorMessage || error.message || String(error);
@@ -2021,6 +2280,12 @@ class TelegramManager {
"admin_grant_failed",
`${record.phone || record.username || record.id} -> ${task.our_group} | ${errorText}`
);
+ this.store.addAccountEvent(
+ masterAccountId,
+ masterAccount.phone || "",
+ "admin_grant_detail",
+ `${diagParts.join(" | ")} | member=ok | error=${errorText}`
+ );
results.push({ accountId, ok: false, reason: errorText });
}
}
@@ -2280,7 +2545,10 @@ class TelegramManager {
const explicitMonitorIds = Array.isArray(roleIds.monitorIds) ? roleIds.monitorIds : [];
const explicitInviteIds = Array.isArray(roleIds.inviteIds) ? roleIds.inviteIds : [];
- const explicitConfirmIds = Array.isArray(roleIds.confirmIds) ? roleIds.confirmIds : [];
+ const explicitConfirmIdsRaw = Array.isArray(roleIds.confirmIds) ? roleIds.confirmIds : [];
+ const explicitConfirmIds = task.separate_confirm_roles
+ ? explicitConfirmIdsRaw.filter((id) => !explicitInviteIds.includes(id))
+ : explicitConfirmIdsRaw;
const hasExplicitRoles = explicitMonitorIds.length || explicitInviteIds.length || explicitConfirmIds.length;
const competitors = competitorGroups || [];
diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx
index a69ae14..5db57bb 100644
--- a/src/renderer/App.jsx
+++ b/src/renderer/App.jsx
@@ -1,4 +1,4 @@
-import React, { useDeferredValue, useRef } from "react";
+import React, { useDeferredValue, useMemo, useRef } from "react";
import { emptyTaskForm, normalizeIntervals, sanitizeTaskForm } from "./appDefaults.js";
import { formatAccountLabel, formatAccountStatus, formatTimestamp, formatCountdown } from "./utils/formatters.js";
import { copyToClipboard } from "./utils/clipboard.js";
@@ -83,6 +83,10 @@ export default function App() {
setInviteAccessStatus,
inviteAccessCheckedAt,
setInviteAccessCheckedAt,
+ confirmAccessStatus,
+ setConfirmAccessStatus,
+ confirmAccessCheckedAt,
+ setConfirmAccessCheckedAt,
accountEvents,
setAccountEvents,
taskAudit,
@@ -377,11 +381,29 @@ export default function App() {
confirmQueue,
queueItems
});
- const { checkAccess, checkInviteAccess } = useAccessChecks({
+ const confirmStats = useMemo(() => {
+ const stats = {
+ total: confirmQueue.length,
+ confirmed: 0,
+ failed: 0
+ };
+ if (!selectedTaskId) return stats;
+ const prefix = `задача ${selectedTaskId}:`;
+ (accountEvents || []).forEach((event) => {
+ if (!event || !event.message || typeof event.message !== "string") return;
+ if (!event.message.startsWith(prefix)) return;
+ if (event.eventType === "confirm_retry_ok") stats.confirmed += 1;
+ if (event.eventType === "confirm_retry_failed") stats.failed += 1;
+ });
+ return stats;
+ }, [accountEvents, confirmQueue.length, selectedTaskId]);
+ const { checkAccess, checkInviteAccess, checkConfirmAccess } = useAccessChecks({
selectedTaskId,
setAccessStatus,
setInviteAccessStatus,
setInviteAccessCheckedAt,
+ setConfirmAccessStatus,
+ setConfirmAccessCheckedAt,
setTaskNotice,
showNotification
});
@@ -510,7 +532,8 @@ export default function App() {
taskActionLoading,
loadBase,
createTask,
- setActiveTab
+ setActiveTab,
+ checkConfirmAccess
});
const {
accountById,
@@ -772,6 +795,8 @@ export default function App() {
tasksLength: tasks.length,
runTestSafe: () => runTest("safe"),
exportTaskBundle,
+ setInfoOpen,
+ setInfoTab,
nowLine,
nowExpanded,
setNowExpanded,
@@ -789,7 +814,8 @@ export default function App() {
checklistItems,
activeTab,
logsTab,
- setLogsTab
+ setLogsTab,
+ confirmStats
});
const { taskSettings, accountsTab, logsTab: logsTabGroup, queueTab: queueTabGroup, eventsTab, settingsTab } = useAppTabGroups({
selectedTaskId,
@@ -811,8 +837,11 @@ export default function App() {
hasSelectedTask,
inviteAccessStatus,
inviteAccessCheckedAt,
+ confirmAccessStatus,
+ confirmAccessCheckedAt,
formatTimestamp,
checkInviteAccess,
+ checkConfirmAccess,
accounts,
showNotification,
copyToClipboard,
@@ -881,6 +910,7 @@ export default function App() {
setConfirmPage,
confirmPageCount,
pagedConfirmQueue,
+ confirmStats,
queueItems,
queueStats,
queueSearch,
diff --git a/src/renderer/components/HelpTip.jsx b/src/renderer/components/HelpTip.jsx
new file mode 100644
index 0000000..3fe932e
--- /dev/null
+++ b/src/renderer/components/HelpTip.jsx
@@ -0,0 +1,10 @@
+import React from "react";
+
+export default function HelpTip({ text }) {
+ if (!text) return null;
+ return (
+
+ ?
+
+ );
+}
diff --git a/src/renderer/components/InfoModal.jsx b/src/renderer/components/InfoModal.jsx
index ceb7fd3..f3dd7ea 100644
--- a/src/renderer/components/InfoModal.jsx
+++ b/src/renderer/components/InfoModal.jsx
@@ -25,6 +25,27 @@ export default function InfoModal({ open, onClose, infoTab, setInfoTab }) {
>
Функции
+
+
+
+
{infoTab === "usage" && (
@@ -63,12 +91,55 @@ export default function InfoModal({ open, onClose, infoTab, setInfoTab }) {
)}
{infoTab === "features" && (
-
Функции и режимы:
-
1) Мониторинг: отслеживает новые сообщения в группах конкурентов и ставит авторов в очередь.
-
2) Инвайт по расписанию: приглашает с интервалом и дневным лимитом.
-
3) Инвайт через админов: временно выдает право “Приглашать”, затем снимает.
-
4) Прогрев лимита: плавно увеличивает нагрузку, чтобы снизить риск ограничений.
-
5) История/Очередь/События: показывает, что произошло и почему.
+
Функции и кнопки:
+
+ - Сохранить — сохраняет настройки задачи.
+ - Экспорт логов — выгружает логи/очередь/ошибки по задаче.
+ - Собрать историю — добавляет авторов последних сообщений конкурентов в очередь.
+ - Добавить ботов в Telegram группы — вводит аккаунты в конкурентов/нашу группу.
+ - Проверить всё — проверяет доступы и права инвайта у аккаунтов.
+ - Тестовый прогон — один реальный инвайт из очереди для проверки логики.
+ - Проверить участие — обновляет статусы участия аккаунтов в группах.
+ - Обновить ID — подтягивает актуальные user_id/username аккаунтов.
+ - Очистить очередь — удаляет пользователей в ожидании.
+ - Сбросить сессии — переподключает аккаунты Telegram.
+
+
+ )}
+ {infoTab === "admin" && (
+
+
Инвайт через админов:
+
+ - Мастер‑админ должен быть участником целевой группы и иметь право “Добавлять админов”.
+ - Перед инвайтом мастер‑админ временно выдает права “Приглашать” инвайтеру.
+ - Инвайтер выполняет приглашение пользователя в группу.
+ - После попытки права у инвайтера снимаются.
+
+
+ Если master‑admin не может резолвить инвайтера или не имеет прав — инвайт через админов не сработает.
+
+
+ )}
+ {infoTab === "confirm" && (
+
+
Подтверждение участия:
+
+ - Успех — пользователь найден в группе (OK).
+ - Не подтверждено — инвайт отправлен, но вступление не найдено.
+ - Ошибка — подтвердить участие нельзя (нет прав, приватность, цель недоступна).
+ - При USER_NOT_PARTICIPANT автоматически ставится повторная проверка через 5 минут (до 2 попыток).
+
+
+ )}
+ {infoTab === "queue" && (
+
+
Очередь:
+
+ - Pending — пользователь ожидает инвайт.
+ - Unconfirmed — инвайт отправлен, участие не подтверждено.
+ - Failed — ошибка инвайта (с кодом причины).
+ - Skipped — пользователь пропущен (например, админ/бот конкурента).
+
)}
{infoTab === "terms" && (
@@ -95,16 +166,28 @@ export default function InfoModal({ open, onClose, infoTab, setInfoTab }) {
После успешного инвайта выполняется проверка фактического вступления.
)}
+ {infoTab === "faq" && (
+
+
Частые вопросы:
+
• Почему “Не подтверждено”? — Инвайт отправлен, но Telegram ещё не видит пользователя в группе.
+
• Почему нет прав инвайта? — Аккаунт не в группе или нет права “Приглашать”.
+
• Почему USER_NOT_MUTUAL_CONTACT? — У пользователя закрыт приём инвайтов или в группе “только контакты”.
+
• Почему CHANNEL_INVALID? — Цель недоступна в этой сессии или ссылка некорректна.
+
• Что делать при FLOOD? — Уменьшить лимиты и увеличить интервалы.
+
• Почему мониторинг не даёт username? — Пользователь скрывает username, нужен access_hash.
+
+ )}
{infoTab === "limits" && (
-
Особенности Telegram и ошибки:
-
1) AUTH_KEY_DUPLICATED: tdata уже используется — выйдите из аккаунта на других устройствах и пересоберите tdata.
-
2) CHAT_ADMIN_REQUIRED: аккаунт должен быть админом с правом “добавлять участников”.
-
3) USER_ID_INVALID: скрытые/анонимные авторы, удаленные аккаунты — инвайт возможен только по username.
-
4) USER_NOT_MUTUAL_CONTACT: ограничения Telegram/приватность пользователя — помогает инвайт‑ссылка или другой аккаунт.
-
5) USER_PRIVACY_RESTRICTED: пользователь запретил инвайты в чаты.
-
6) FLOOD/PEER_FLOOD: снизить лимиты, увеличить интервалы, распределить нагрузку.
-
7) CHANNEL_PRIVATE/INVITE_HASH_INVALID: ссылка недействительна или чат приватный.
+
Ошибки и ограничения Telegram:
+
1) AUTH_KEY_DUPLICATED — tdata уже используется. Выйдите из аккаунта на других устройствах.
+
2) CHAT_ADMIN_REQUIRED — у аккаунта нет прав админа для инвайта.
+
3) USER_ID_INVALID — скрытый/удалённый автор, нужен username.
+
4) USER_NOT_MUTUAL_CONTACT — приватность или «только контакты».
+
5) USER_PRIVACY_RESTRICTED — пользователь запретил инвайты.
+
6) CHAT_WRITE_FORBIDDEN — аккаунт не в группе или не имеет прав.
+
7) CHANNEL_INVALID / CHANNEL_PRIVATE — цель не резолвится в сессии.
+
8) FLOOD / PEER_FLOOD — снизить лимиты и увеличить интервалы.
)}
diff --git a/src/renderer/components/NowStatusCard.jsx b/src/renderer/components/NowStatusCard.jsx
index 57150bc..80e4bdf 100644
--- a/src/renderer/components/NowStatusCard.jsx
+++ b/src/renderer/components/NowStatusCard.jsx
@@ -12,7 +12,9 @@ export default function NowStatusCard({
taskStatus,
groupVisibility,
lastEvents,
- formatTimestamp
+ formatTimestamp,
+ confirmStats,
+ openConfirmTab
}) {
return (
@@ -55,6 +57,14 @@ export default function NowStatusCard({
{taskStatus.lastStopAt ? ` (${formatTimestamp(taskStatus.lastStopAt)})` : ""}
)}
+ {confirmStats && (
+
+ Неподтвержденные: очередь {confirmStats.total} · подтвердилось {confirmStats.confirmed} · не подтвердилось {confirmStats.failed}
+
+
+ )}
{taskStatus.warnings && taskStatus.warnings.length > 0 && (
{taskStatus.warnings.map((warning, index) => (
diff --git a/src/renderer/components/QuickActionsBar.jsx b/src/renderer/components/QuickActionsBar.jsx
index 904d086..8391e94 100644
--- a/src/renderer/components/QuickActionsBar.jsx
+++ b/src/renderer/components/QuickActionsBar.jsx
@@ -25,7 +25,8 @@ export default function QuickActionsBar({
setActiveTab,
tasksLength,
runTestSafe,
- exportTaskBundle
+ exportTaskBundle,
+ openHelp
}) {
return (
@@ -50,6 +51,7 @@ export default function QuickActionsBar({
+
{taskStatus.running ? (