some
This commit is contained in:
parent
7ed57048da
commit
591b4f3a89
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "telegram-invite-automation",
|
"name": "telegram-invite-automation",
|
||||||
"version": "1.9.5",
|
"version": "1.9.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Automated user parsing and invites for Telegram groups",
|
"description": "Automated user parsing and invites for Telegram groups",
|
||||||
"main": "src/main/index.js",
|
"main": "src/main/index.js",
|
||||||
|
|||||||
@ -455,7 +455,7 @@ ipcMain.handle("sessions:reset", async () => {
|
|||||||
runner.stop();
|
runner.stop();
|
||||||
}
|
}
|
||||||
taskRunners.clear();
|
taskRunners.clear();
|
||||||
telegram.resetAllSessions();
|
telegram.resetAllSessions("manual_user", { source: "ipc:sessions:reset" });
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
ipcMain.handle("accounts:startLogin", async (_event, payload) => {
|
ipcMain.handle("accounts:startLogin", async (_event, payload) => {
|
||||||
@ -558,9 +558,6 @@ ipcMain.handle("accounts:importTdata", async (_event, payload) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authKeyDuplicatedCount > 0) {
|
|
||||||
telegram.resetAllSessions();
|
|
||||||
}
|
|
||||||
return { ok: true, imported, skipped, failed, authKeyDuplicatedCount };
|
return { ok: true, imported, skipped, failed, authKeyDuplicatedCount };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,8 @@ const DEFAULT_SETTINGS = {
|
|||||||
accountMaxGroups: 10,
|
accountMaxGroups: 10,
|
||||||
accountDailyLimit: 50,
|
accountDailyLimit: 50,
|
||||||
floodCooldownMinutes: 1440,
|
floodCooldownMinutes: 1440,
|
||||||
|
inviteFloodCooldownDays: 3,
|
||||||
|
generalFloodCooldownDays: 1,
|
||||||
queueTtlHours: 24,
|
queueTtlHours: 24,
|
||||||
quietModeMinutes: 10,
|
quietModeMinutes: 10,
|
||||||
autoJoinCompetitors: false,
|
autoJoinCompetitors: false,
|
||||||
@ -447,7 +449,25 @@ function initStore(userDataPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listAccounts() {
|
function listAccounts() {
|
||||||
return db.prepare("SELECT * FROM accounts ORDER BY id DESC").all();
|
const rows = db.prepare("SELECT * FROM accounts ORDER BY id DESC").all();
|
||||||
|
const now = Date.now();
|
||||||
|
rows.forEach((row) => {
|
||||||
|
if (!row || !row.cooldown_until) return;
|
||||||
|
let until = 0;
|
||||||
|
try {
|
||||||
|
until = new Date(row.cooldown_until).getTime();
|
||||||
|
} catch (error) {
|
||||||
|
until = 0;
|
||||||
|
}
|
||||||
|
if (!until || until <= now) {
|
||||||
|
clearAccountCooldown(row.id);
|
||||||
|
row.status = "ok";
|
||||||
|
row.last_error = "";
|
||||||
|
row.cooldown_until = "";
|
||||||
|
row.cooldown_reason = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAllData() {
|
function clearAllData() {
|
||||||
@ -525,6 +545,19 @@ function initStore(userDataPath) {
|
|||||||
`).run(status, reason || "", until, reason || "", now.toISOString(), id);
|
`).run(status, reason || "", until, reason || "", now.toISOString(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setAccountInviteCooldown(id, minutes, reason) {
|
||||||
|
const now = dayjs();
|
||||||
|
const until = minutes > 0 ? now.add(minutes, "minute").toISOString() : "";
|
||||||
|
const scopedReason = reason && String(reason).startsWith("INVITE_ONLY|")
|
||||||
|
? String(reason)
|
||||||
|
: `INVITE_ONLY|${reason || ""}`;
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE accounts
|
||||||
|
SET last_error = ?, cooldown_until = ?, cooldown_reason = ?, updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(scopedReason, until, scopedReason, now.toISOString(), id);
|
||||||
|
}
|
||||||
|
|
||||||
function clearAccountCooldown(id) {
|
function clearAccountCooldown(id) {
|
||||||
const now = dayjs().toISOString();
|
const now = dayjs().toISOString();
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
@ -1512,6 +1545,7 @@ function initStore(userDataPath) {
|
|||||||
clearConfirmQueue,
|
clearConfirmQueue,
|
||||||
updateInviteConfirmation,
|
updateInviteConfirmation,
|
||||||
setAccountCooldown,
|
setAccountCooldown,
|
||||||
|
setAccountInviteCooldown,
|
||||||
clearAccountCooldown,
|
clearAccountCooldown,
|
||||||
addAccountEvent,
|
addAccountEvent,
|
||||||
listAccountEvents,
|
listAccountEvents,
|
||||||
|
|||||||
@ -40,6 +40,27 @@ class TaskRunner {
|
|||||||
return "—";
|
return "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isInviteAccountReady(accountId) {
|
||||||
|
const id = Number(accountId || 0);
|
||||||
|
if (!id) return false;
|
||||||
|
return this.telegram.isInviteAccountAvailable(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_filterInviteAccounts(accountIds) {
|
||||||
|
const ids = Array.isArray(accountIds) ? accountIds.map((id) => Number(id)).filter(Boolean) : [];
|
||||||
|
return ids.filter((id) => this._isInviteAccountReady(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
_getTaskUnavailableStats(accountIds) {
|
||||||
|
const ids = Array.isArray(accountIds) ? accountIds.map((id) => Number(id)).filter(Boolean) : [];
|
||||||
|
const unavailable = ids.filter((id) => this.telegram.isAccountFullyUnavailable(id));
|
||||||
|
return {
|
||||||
|
total: ids.length,
|
||||||
|
unavailableCount: unavailable.length,
|
||||||
|
unavailableIds: unavailable
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
if (this.running) return;
|
if (this.running) return;
|
||||||
this.running = true;
|
this.running = true;
|
||||||
@ -157,6 +178,7 @@ class TaskRunner {
|
|||||||
inviteAccounts = inviteAccounts.slice(0, limit);
|
inviteAccounts = inviteAccounts.slice(0, limit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
inviteAccounts = this._filterInviteAccounts(inviteAccounts);
|
||||||
if (!accounts.length) {
|
if (!accounts.length) {
|
||||||
errors.push("No accounts assigned");
|
errors.push("No accounts assigned");
|
||||||
}
|
}
|
||||||
@ -177,14 +199,14 @@ class TaskRunner {
|
|||||||
} else if (inviteAccounts.length) {
|
} else if (inviteAccounts.length) {
|
||||||
this.nextInviteAccountId = inviteAccounts[0];
|
this.nextInviteAccountId = inviteAccounts[0];
|
||||||
}
|
}
|
||||||
const totalAccounts = accounts.length;
|
|
||||||
if (this.task.stop_on_blocked) {
|
if (this.task.stop_on_blocked) {
|
||||||
const all = this.store.listAccounts().filter((acc) => accounts.includes(acc.id));
|
const unavailableStats = this._getTaskUnavailableStats(accounts);
|
||||||
const blocked = all.filter((acc) => acc.status !== "ok").length;
|
if (unavailableStats.total > 0 && unavailableStats.unavailableCount >= unavailableStats.total) {
|
||||||
const percent = totalAccounts ? Math.round((blocked / totalAccounts) * 100) : 0;
|
errors.push(`Stopped: all task accounts unavailable (${unavailableStats.unavailableCount}/${unavailableStats.total})`);
|
||||||
if (percent >= Number(this.task.stop_blocked_percent || 25)) {
|
this.store.setTaskStopReason(
|
||||||
errors.push(`Stopped: blocked ${percent}% >= ${this.task.stop_blocked_percent}%`);
|
this.task.id,
|
||||||
this.store.setTaskStopReason(this.task.id, `Блокировки ${percent}% >= ${this.task.stop_blocked_percent}%`);
|
`Остановлено: недоступны все аккаунты задачи (${unavailableStats.unavailableCount}/${unavailableStats.total})`
|
||||||
|
);
|
||||||
this.stop();
|
this.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,7 +246,7 @@ class TaskRunner {
|
|||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
"invite_skipped",
|
"invite_skipped",
|
||||||
`задача ${this.task.id}: нет аккаунтов с ролью инвайта`
|
`задача ${this.task.id}: нет доступных аккаунтов для инвайта (invite-only cooldown/спам/нет сессии)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (inviteLimitRows.length) {
|
if (inviteLimitRows.length) {
|
||||||
@ -283,11 +305,12 @@ class TaskRunner {
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let accountsForInvite = inviteAccounts;
|
let accountsForInvite = this._filterInviteAccounts(inviteAccounts);
|
||||||
let fixedInviteAccountId = 0;
|
let fixedInviteAccountId = 0;
|
||||||
if (inviteOrder.length) {
|
if (inviteOrder.length) {
|
||||||
fixedInviteAccountId = inviteOrder.shift() || 0;
|
fixedInviteAccountId = inviteOrder.shift() || 0;
|
||||||
if (fixedInviteAccountId) {
|
if (fixedInviteAccountId) {
|
||||||
|
if (this._isInviteAccountReady(fixedInviteAccountId)) {
|
||||||
accountsForInvite = [fixedInviteAccountId];
|
accountsForInvite = [fixedInviteAccountId];
|
||||||
const pickedAccount = accountMap.get(fixedInviteAccountId);
|
const pickedAccount = accountMap.get(fixedInviteAccountId);
|
||||||
const label = this._formatAccountLabel(pickedAccount, String(fixedInviteAccountId));
|
const label = this._formatAccountLabel(pickedAccount, String(fixedInviteAccountId));
|
||||||
@ -297,10 +320,23 @@ class TaskRunner {
|
|||||||
"invite_pick",
|
"invite_pick",
|
||||||
`выбран: ${label}; лимит на цикл: ${inviteLimitRows.find((row) => row.accountId === fixedInviteAccountId)?.limit || 0}`
|
`выбран: ${label}; лимит на цикл: ${inviteLimitRows.find((row) => row.accountId === fixedInviteAccountId)?.limit || 0}`
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
const fallbackPool = this._filterInviteAccounts(inviteAccounts.filter((id) => Number(id) !== Number(fixedInviteAccountId)));
|
||||||
|
accountsForInvite = fallbackPool;
|
||||||
|
const blockedAccount = accountMap.get(fixedInviteAccountId);
|
||||||
|
const blockedLabel = this._formatAccountLabel(blockedAccount, String(fixedInviteAccountId));
|
||||||
|
this.store.addAccountEvent(
|
||||||
|
fixedInviteAccountId,
|
||||||
|
blockedAccount ? blockedAccount.phone || "" : "",
|
||||||
|
"invite_pick_fallback",
|
||||||
|
`пропуск инвайтера ${blockedLabel}: недоступен для инвайта; резерв: ${fallbackPool.length ? fallbackPool.join(", ") : "нет"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!item.username && this.task.use_watcher_invite_no_username && item.watcher_account_id) {
|
if (!item.username && this.task.use_watcher_invite_no_username && item.watcher_account_id) {
|
||||||
const watcherCanInvite = inviteAccounts.includes(Number(item.watcher_account_id));
|
const watcherCanInvite = inviteAccounts.includes(Number(item.watcher_account_id))
|
||||||
|
&& this._isInviteAccountReady(item.watcher_account_id);
|
||||||
if (watcherCanInvite) {
|
if (watcherCanInvite) {
|
||||||
accountsForInvite = [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);
|
||||||
@ -331,7 +367,7 @@ class TaskRunner {
|
|||||||
const currentPool = Array.isArray(accountsForInvite) ? accountsForInvite.map((id) => Number(id)).filter(Boolean) : [];
|
const currentPool = Array.isArray(accountsForInvite) ? accountsForInvite.map((id) => Number(id)).filter(Boolean) : [];
|
||||||
let alternatives = currentPool.filter((id) => id !== previousAccountId);
|
let alternatives = currentPool.filter((id) => id !== previousAccountId);
|
||||||
if (!alternatives.length) {
|
if (!alternatives.length) {
|
||||||
alternatives = inviteAccounts.map((id) => Number(id)).filter((id) => id !== previousAccountId);
|
alternatives = this._filterInviteAccounts(inviteAccounts.map((id) => Number(id)).filter((id) => id !== previousAccountId));
|
||||||
}
|
}
|
||||||
if (alternatives.length) {
|
if (alternatives.length) {
|
||||||
accountsForInvite = alternatives;
|
accountsForInvite = alternatives;
|
||||||
|
|||||||
@ -21,7 +21,6 @@ class TelegramManager {
|
|||||||
this.desktopApiId = 2040;
|
this.desktopApiId = 2040;
|
||||||
this.desktopApiHash = "b18441a1ff607e10a989891a5462e627";
|
this.desktopApiHash = "b18441a1ff607e10a989891a5462e627";
|
||||||
this.participantCache = new Map();
|
this.participantCache = new Map();
|
||||||
this.authKeyResetDone = false;
|
|
||||||
this.apiTraceEnabled = false;
|
this.apiTraceEnabled = false;
|
||||||
try {
|
try {
|
||||||
const settings = this.store.getSettings();
|
const settings = this.store.getSettings();
|
||||||
@ -587,8 +586,8 @@ class TelegramManager {
|
|||||||
await this._connectAccount(account);
|
await this._connectAccount(account);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error && (error.errorMessage || error.message) ? (error.errorMessage || error.message) : String(error);
|
const errorText = error && (error.errorMessage || error.message) ? (error.errorMessage || error.message) : String(error);
|
||||||
if (this._handleAuthKeyDuplicated(errorText)) {
|
if (this._handleAuthKeyDuplicated(errorText, account, "init_connect")) {
|
||||||
break;
|
continue;
|
||||||
}
|
}
|
||||||
this.store.updateAccountStatus(account.id, "error", errorText);
|
this.store.updateAccountStatus(account.id, "error", errorText);
|
||||||
this.store.addAccountEvent(account.id, account.phone || "", "connect_failed", errorText);
|
this.store.addAccountEvent(account.id, account.phone || "", "connect_failed", errorText);
|
||||||
@ -596,15 +595,67 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleAuthKeyDuplicated(errorText) {
|
_handleAuthKeyDuplicated(errorText, account = null, context = "") {
|
||||||
if (!errorText || !String(errorText).includes("AUTH_KEY_DUPLICATED")) return false;
|
if (!errorText || !String(errorText).includes("AUTH_KEY_DUPLICATED")) return false;
|
||||||
if (this.authKeyResetDone) return true;
|
const details = {
|
||||||
this.authKeyResetDone = true;
|
error: String(errorText),
|
||||||
this.resetAllSessions();
|
context: context || "",
|
||||||
|
accountId: Number(account && account.id ? account.id : 0),
|
||||||
|
phone: account && account.phone ? account.phone : ""
|
||||||
|
};
|
||||||
|
const detailsJson = JSON.stringify(details);
|
||||||
|
if (account && account.id) {
|
||||||
|
const accountId = Number(account.id);
|
||||||
|
const clientEntry = this.clients.get(accountId);
|
||||||
|
if (clientEntry && clientEntry.client) {
|
||||||
|
try {
|
||||||
|
clientEntry.client.disconnect();
|
||||||
|
} catch (_disconnectError) {
|
||||||
|
// ignore disconnect errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.clients.delete(accountId);
|
||||||
|
this.store.updateAccountStatus(accountId, "error", "AUTH_KEY_DUPLICATED");
|
||||||
|
this.store.addAccountEvent(accountId, account.phone || "", "auth_key_duplicated", detailsJson);
|
||||||
|
const taskRows = this.store.listAllTaskAccounts ? this.store.listAllTaskAccounts() : [];
|
||||||
|
const taskIds = Array.from(new Set(
|
||||||
|
taskRows
|
||||||
|
.filter((row) => Number(row && row.account_id ? row.account_id : 0) === accountId)
|
||||||
|
.map((row) => Number(row.task_id || 0))
|
||||||
|
.filter(Boolean)
|
||||||
|
));
|
||||||
|
taskIds.forEach((taskId) => {
|
||||||
|
this.store.addTaskAudit(taskId, "account_auth_key_duplicated", detailsJson);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.store.addAccountEvent(0, "", "auth_key_duplicated", detailsJson);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetAllSessions() {
|
_auditSessionsReset(reason, extra = {}, accountsSnapshot = [], taskRowsSnapshot = []) {
|
||||||
|
const accountIds = accountsSnapshot.map((item) => Number(item && item.id ? item.id : 0)).filter(Boolean);
|
||||||
|
const taskIds = Array.from(new Set(taskRowsSnapshot.map((row) => Number(row && row.task_id ? row.task_id : 0)).filter(Boolean)));
|
||||||
|
const payload = {
|
||||||
|
reason: reason || "unknown",
|
||||||
|
totalAccounts: accountIds.length,
|
||||||
|
accountIdsPreview: accountIds.slice(0, 20),
|
||||||
|
totalTaskBindings: taskRowsSnapshot.length,
|
||||||
|
taskIds,
|
||||||
|
extra: extra || {}
|
||||||
|
};
|
||||||
|
const details = JSON.stringify(payload);
|
||||||
|
this.store.addAccountEvent(0, "", "sessions_reset", details);
|
||||||
|
this.store.addTaskAudit(0, "sessions_reset", details);
|
||||||
|
taskIds.forEach((taskId) => {
|
||||||
|
this.store.addTaskAudit(taskId, "sessions_reset", details);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAllSessions(reason = "manual", extra = {}) {
|
||||||
|
const accountsSnapshot = this.store.listAccounts();
|
||||||
|
const taskRowsSnapshot = this.store.listAllTaskAccounts ? this.store.listAllTaskAccounts() : [];
|
||||||
|
this._auditSessionsReset(reason, extra, accountsSnapshot, taskRowsSnapshot);
|
||||||
for (const entry of this.clients.values()) {
|
for (const entry of this.clients.values()) {
|
||||||
try {
|
try {
|
||||||
entry.client.disconnect();
|
entry.client.disconnect();
|
||||||
@ -765,7 +816,7 @@ class TelegramManager {
|
|||||||
me = await client.getMe();
|
me = await client.getMe();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error && (error.errorMessage || error.message) ? (error.errorMessage || error.message) : String(error);
|
const errorText = error && (error.errorMessage || error.message) ? (error.errorMessage || error.message) : String(error);
|
||||||
this._handleAuthKeyDuplicated(errorText);
|
this._handleAuthKeyDuplicated(errorText, null, "import_tdata_connect");
|
||||||
try {
|
try {
|
||||||
await client.disconnect();
|
await client.disconnect();
|
||||||
} catch (disconnectError) {
|
} catch (disconnectError) {
|
||||||
@ -951,7 +1002,7 @@ class TelegramManager {
|
|||||||
_pickClient() {
|
_pickClient() {
|
||||||
const entries = Array.from(this.clients.values());
|
const entries = Array.from(this.clients.values());
|
||||||
if (!entries.length) return null;
|
if (!entries.length) return null;
|
||||||
const ordered = entries.filter((entry) => entry.account.status === "ok" && !this._isInCooldown(entry.account));
|
const ordered = entries.filter((entry) => !this._isGeneralRoleBlocked(entry.account));
|
||||||
if (!ordered.length) return null;
|
if (!ordered.length) return null;
|
||||||
const entry = ordered[this.inviteIndex % ordered.length];
|
const entry = ordered[this.inviteIndex % ordered.length];
|
||||||
this.inviteIndex += 1;
|
this.inviteIndex += 1;
|
||||||
@ -999,9 +1050,9 @@ class TelegramManager {
|
|||||||
return { ok: true, accountId: account.id, accountPhone: account.phone || "" };
|
return { ok: true, accountId: account.id, accountPhone: account.phone || "" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error.errorMessage || error.message || String(error);
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
this._handleAuthKeyDuplicated(errorText);
|
this._handleAuthKeyDuplicated(errorText, account, "invite_user");
|
||||||
if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) {
|
if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) {
|
||||||
this._applyFloodCooldown(account, errorText);
|
this._applyFloodCooldown(account, errorText, "invite");
|
||||||
} else {
|
} else {
|
||||||
this.store.updateAccountStatus(account.id, account.status || "ok", errorText);
|
this.store.updateAccountStatus(account.id, account.status || "ok", errorText);
|
||||||
}
|
}
|
||||||
@ -2039,7 +2090,7 @@ class TelegramManager {
|
|||||||
// ignore diagnostics errors
|
// ignore diagnostics errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._handleAuthKeyDuplicated(errorText);
|
this._handleAuthKeyDuplicated(errorText, account, "invite_user_for_task");
|
||||||
let fallbackMeta = lastAttempts.length ? JSON.stringify(lastAttempts) : "";
|
let fallbackMeta = lastAttempts.length ? JSON.stringify(lastAttempts) : "";
|
||||||
if (errorText === "USER_NOT_MUTUAL_CONTACT") {
|
if (errorText === "USER_NOT_MUTUAL_CONTACT") {
|
||||||
try {
|
try {
|
||||||
@ -2112,7 +2163,7 @@ class TelegramManager {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) {
|
if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) {
|
||||||
this._applyFloodCooldown(account, errorText);
|
this._applyFloodCooldown(account, errorText, "invite");
|
||||||
} else {
|
} else {
|
||||||
this.store.updateAccountStatus(account.id, account.status || "ok", errorText);
|
this.store.updateAccountStatus(account.id, account.status || "ok", errorText);
|
||||||
}
|
}
|
||||||
@ -2129,7 +2180,7 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pickClientForInvite(allowedAccountIds, randomize) {
|
_pickClientForInvite(allowedAccountIds, randomize) {
|
||||||
const entries = Array.from(this.clients.values()).filter((entry) => entry.account.status === "ok");
|
const entries = Array.from(this.clients.values());
|
||||||
if (!entries.length) return null;
|
if (!entries.length) return null;
|
||||||
|
|
||||||
const settings = this.store.getSettings();
|
const settings = this.store.getSettings();
|
||||||
@ -2138,7 +2189,7 @@ class TelegramManager {
|
|||||||
: entries;
|
: entries;
|
||||||
if (!allowed.length) return null;
|
if (!allowed.length) return null;
|
||||||
const eligible = allowed.filter((entry) => {
|
const eligible = allowed.filter((entry) => {
|
||||||
if (this._isInCooldown(entry.account)) return false;
|
if (this._isInviteRoleBlocked(entry.account)) return false;
|
||||||
const limit = Number(entry.account.daily_limit || settings.accountDailyLimit || 0);
|
const limit = Number(entry.account.daily_limit || settings.accountDailyLimit || 0);
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
const used = this.store.countInvitesTodayByAccount(entry.account.id);
|
const used = this.store.countInvitesTodayByAccount(entry.account.id);
|
||||||
@ -2162,23 +2213,23 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pickClientFromAllowed(allowedAccountIds) {
|
_pickClientFromAllowed(allowedAccountIds) {
|
||||||
const entries = Array.from(this.clients.values()).filter((entry) => entry.account.status === "ok");
|
const entries = Array.from(this.clients.values());
|
||||||
if (!entries.length) return null;
|
if (!entries.length) return null;
|
||||||
const allowed = Array.isArray(allowedAccountIds) && allowedAccountIds.length
|
const allowed = Array.isArray(allowedAccountIds) && allowedAccountIds.length
|
||||||
? entries.filter((entry) => allowedAccountIds.includes(entry.account.id))
|
? entries.filter((entry) => allowedAccountIds.includes(entry.account.id))
|
||||||
: entries;
|
: entries;
|
||||||
if (!allowed.length) return null;
|
if (!allowed.length) return null;
|
||||||
const available = allowed.filter((entry) => !this._isInCooldown(entry.account));
|
const available = allowed.filter((entry) => !this._isGeneralRoleBlocked(entry.account));
|
||||||
return available[0] || null;
|
return available[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_listClientsFromAllowed(allowedAccountIds) {
|
_listClientsFromAllowed(allowedAccountIds) {
|
||||||
const entries = Array.from(this.clients.values()).filter((entry) => entry.account.status === "ok");
|
const entries = Array.from(this.clients.values());
|
||||||
if (!entries.length) return [];
|
if (!entries.length) return [];
|
||||||
const allowed = Array.isArray(allowedAccountIds) && allowedAccountIds.length
|
const allowed = Array.isArray(allowedAccountIds) && allowedAccountIds.length
|
||||||
? entries.filter((entry) => allowedAccountIds.includes(entry.account.id))
|
? entries.filter((entry) => allowedAccountIds.includes(entry.account.id))
|
||||||
: entries;
|
: entries;
|
||||||
return allowed.filter((entry) => !this._isInCooldown(entry.account));
|
return allowed.filter((entry) => !this._isGeneralRoleBlocked(entry.account));
|
||||||
}
|
}
|
||||||
|
|
||||||
pickInviteAccount(allowedAccountIds, randomize) {
|
pickInviteAccount(allowedAccountIds, randomize) {
|
||||||
@ -3098,10 +3149,10 @@ class TelegramManager {
|
|||||||
this.store.addAccountEvent(masterAccountId, masterAccount.phone || "", "admin_grant_detail", `${diagParts.join(" | ")} | error=session_not_connected`);
|
this.store.addAccountEvent(masterAccountId, masterAccount.phone || "", "admin_grant_detail", `${diagParts.join(" | ")} | error=session_not_connected`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (targetEntry.account && (targetEntry.account.status !== "ok" || this._isInCooldown(targetEntry.account))) {
|
if (targetEntry.account && this._isInviteRoleBlocked(targetEntry.account)) {
|
||||||
const reason = targetEntry.account.status !== "ok"
|
const reason = targetEntry.account.status !== "ok"
|
||||||
? "Аккаунт в спаме/ограничении"
|
? "Аккаунт в спаме/ограничении"
|
||||||
: "Аккаунт в FLOOD‑кулдауне";
|
: "Аккаунт в FLOOD‑кулдауне (invite-only)";
|
||||||
results.push({ accountId, label, ok: false, reason });
|
results.push({ accountId, label, ok: false, reason });
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
masterAccountId,
|
masterAccountId,
|
||||||
@ -3497,7 +3548,12 @@ class TelegramManager {
|
|||||||
const hash = this._extractInviteHash(group);
|
const hash = this._extractInviteHash(group);
|
||||||
if (!hash) return { ok: false, error: "Invalid invite link" };
|
if (!hash) return { ok: false, error: "Invalid invite link" };
|
||||||
try {
|
try {
|
||||||
const check = await client.invoke(new Api.messages.CheckChatInvite({ hash }));
|
const check = await this._callWithTlRecovery(
|
||||||
|
client,
|
||||||
|
account,
|
||||||
|
`resolve_group:check_invite:${group}`,
|
||||||
|
() => client.invoke(new Api.messages.CheckChatInvite({ hash }))
|
||||||
|
);
|
||||||
if (check && check.chat) {
|
if (check && check.chat) {
|
||||||
return { ok: true, entity: await this._normalizeEntity(client, check.chat) };
|
return { ok: true, entity: await this._normalizeEntity(client, check.chat) };
|
||||||
}
|
}
|
||||||
@ -3508,12 +3564,17 @@ class TelegramManager {
|
|||||||
return { ok: false, error: "Invite link requires auto-join" };
|
return { ok: false, error: "Invite link requires auto-join" };
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const imported = await client.invoke(new Api.messages.ImportChatInvite({ hash }));
|
const imported = await this._callWithTlRecovery(
|
||||||
|
client,
|
||||||
|
account,
|
||||||
|
`resolve_group:import_invite:${group}`,
|
||||||
|
() => client.invoke(new Api.messages.ImportChatInvite({ hash }))
|
||||||
|
);
|
||||||
if (imported && imported.chats && imported.chats.length) {
|
if (imported && imported.chats && imported.chats.length) {
|
||||||
return { ok: true, entity: await this._normalizeEntity(client, imported.chats[0]) };
|
return { ok: true, entity: await this._normalizeEntity(client, imported.chats[0]) };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error.errorMessage || error.message || String(error);
|
const errorText = this._extractErrorText(error);
|
||||||
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);
|
||||||
}
|
}
|
||||||
@ -3528,19 +3589,24 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
return { ok: false, error: "USER_ALREADY_PARTICIPANT" };
|
return { ok: false, error: "USER_ALREADY_PARTICIPANT" };
|
||||||
}
|
}
|
||||||
return { ok: false, error: errorText };
|
return { ok: false, error: this._normalizeHistoryErrorText(errorText) };
|
||||||
}
|
}
|
||||||
return { ok: false, error: "Unable to resolve invite link" };
|
return { ok: false, error: "Unable to resolve invite link" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const entity = await client.getEntity(group);
|
const entity = await this._callWithTlRecovery(
|
||||||
|
client,
|
||||||
|
account,
|
||||||
|
`resolve_group:get_entity:${group}`,
|
||||||
|
() => client.getEntity(group)
|
||||||
|
);
|
||||||
return { ok: true, entity: await this._normalizeEntity(client, entity) };
|
return { ok: true, entity: await this._normalizeEntity(client, entity) };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error.errorMessage || error.message || String(error);
|
const errorText = this._extractErrorText(error);
|
||||||
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);
|
||||||
}
|
}
|
||||||
return { ok: false, error: errorText };
|
return { ok: false, error: this._normalizeHistoryErrorText(errorText) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3611,7 +3677,7 @@ class TelegramManager {
|
|||||||
const forceJoin = Boolean(options.forceJoin);
|
const forceJoin = Boolean(options.forceJoin);
|
||||||
const accounts = Array.from(this.clients.values())
|
const accounts = Array.from(this.clients.values())
|
||||||
.filter((entry) => accountIds.includes(entry.account.id))
|
.filter((entry) => accountIds.includes(entry.account.id))
|
||||||
.filter((entry) => entry.account.status === "ok" && !this._isInCooldown(entry.account));
|
.filter((entry) => !this._isGeneralRoleBlocked(entry.account));
|
||||||
const competitorBots = Math.max(1, Number(task.max_competitor_bots || 1));
|
const competitorBots = Math.max(1, Number(task.max_competitor_bots || 1));
|
||||||
const ourBots = Math.max(1, Number(task.max_our_bots || 1));
|
const ourBots = Math.max(1, Number(task.max_our_bots || 1));
|
||||||
|
|
||||||
@ -4224,7 +4290,7 @@ class TelegramManager {
|
|||||||
const label = acc && acc.username ? `${labelBase} (@${acc.username})` : String(labelBase);
|
const label = acc && acc.username ? `${labelBase} (@${acc.username})` : String(labelBase);
|
||||||
if (!acc) return `${label}: аккаунт не найден`;
|
if (!acc) return `${label}: аккаунт не найден`;
|
||||||
if (acc.status && acc.status !== "ok") return `${label}: статус ${acc.status}${acc.last_error ? ` (${acc.last_error})` : ""}`;
|
if (acc.status && acc.status !== "ok") return `${label}: статус ${acc.status}${acc.last_error ? ` (${acc.last_error})` : ""}`;
|
||||||
if (this._isInCooldown(acc)) {
|
if (this._isGeneralRoleBlocked(acc)) {
|
||||||
const until = acc.cooldown_until ? new Date(acc.cooldown_until).toLocaleString("ru-RU") : "—";
|
const until = acc.cooldown_until ? new Date(acc.cooldown_until).toLocaleString("ru-RU") : "—";
|
||||||
return `${label}: cooldown до ${until}${acc.cooldown_reason ? ` (${acc.cooldown_reason})` : ""}`;
|
return `${label}: cooldown до ${until}${acc.cooldown_reason ? ` (${acc.cooldown_reason})` : ""}`;
|
||||||
}
|
}
|
||||||
@ -4248,7 +4314,7 @@ class TelegramManager {
|
|||||||
for (const group of targetGroups) {
|
for (const group of targetGroups) {
|
||||||
const resolved = await this._resolveGroupEntity(entry.client, group, Boolean(task.auto_join_competitors), entry.account);
|
const resolved = await this._resolveGroupEntity(entry.client, group, Boolean(task.auto_join_competitors), entry.account);
|
||||||
if (!resolved.ok) {
|
if (!resolved.ok) {
|
||||||
errors.push(`${group}: ${resolved.error}`);
|
errors.push(`${group}: ${this._normalizeHistoryErrorText(resolved.error)}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const participantCache = await this._loadParticipantCache(entry.client, resolved.entity, Math.max(200, perGroupLimit));
|
const participantCache = await this._loadParticipantCache(entry.client, resolved.entity, Math.max(200, perGroupLimit));
|
||||||
@ -4256,7 +4322,12 @@ class TelegramManager {
|
|||||||
let participantsEnqueued = 0;
|
let participantsEnqueued = 0;
|
||||||
if (task.parse_participants) {
|
if (task.parse_participants) {
|
||||||
try {
|
try {
|
||||||
const participants = await entry.client.getParticipants(resolved.entity, { limit: perGroupLimit });
|
const participants = await this._callWithTlRecovery(
|
||||||
|
entry.client,
|
||||||
|
entry.account,
|
||||||
|
`parse_history:get_participants:${group}`,
|
||||||
|
() => entry.client.getParticipants(resolved.entity, { limit: perGroupLimit })
|
||||||
|
);
|
||||||
for (const user of participants || []) {
|
for (const user of participants || []) {
|
||||||
if (!user || user.className !== "User" || user.bot) continue;
|
if (!user || user.className !== "User" || user.bot) continue;
|
||||||
if (user.username && user.username.toLowerCase().includes("bot")) continue;
|
if (user.username && user.username.toLowerCase().includes("bot")) continue;
|
||||||
@ -4273,10 +4344,29 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push(`${group}: участники не доступны (${error.errorMessage || error.message || String(error)})`);
|
const errorText = this._extractErrorText(error);
|
||||||
|
if (entry.account && (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD"))) {
|
||||||
|
this._applyFloodCooldown(entry.account, errorText);
|
||||||
|
}
|
||||||
|
errors.push(`${group}: участники не доступны (${this._normalizeHistoryErrorText(errorText)})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const messages = await entry.client.getMessages(resolved.entity, { limit: perGroupLimit });
|
let messages = [];
|
||||||
|
try {
|
||||||
|
messages = await this._callWithTlRecovery(
|
||||||
|
entry.client,
|
||||||
|
entry.account,
|
||||||
|
`parse_history:get_messages:${group}`,
|
||||||
|
() => entry.client.getMessages(resolved.entity, { limit: perGroupLimit })
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = this._extractErrorText(error);
|
||||||
|
if (entry.account && (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD"))) {
|
||||||
|
this._applyFloodCooldown(entry.account, errorText);
|
||||||
|
}
|
||||||
|
errors.push(`${group}: история не доступна (${this._normalizeHistoryErrorText(errorText)})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let total = 0;
|
let total = 0;
|
||||||
let enqueued = 0;
|
let enqueued = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
@ -4579,24 +4669,191 @@ class TelegramManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_isInCooldown(account) {
|
_resolveAccount(accountOrId) {
|
||||||
|
if (!accountOrId) return null;
|
||||||
|
if (typeof accountOrId === "object" && accountOrId.id != null) return accountOrId;
|
||||||
|
const id = Number(accountOrId || 0);
|
||||||
|
if (!id) return null;
|
||||||
|
const clientEntry = this.clients.get(id);
|
||||||
|
if (clientEntry && clientEntry.account) return clientEntry.account;
|
||||||
|
const fromStore = this.store.listAccounts().find((item) => Number(item.id) === id);
|
||||||
|
return fromStore || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCooldownScope(account) {
|
||||||
|
const reason = String(account && account.cooldown_reason ? account.cooldown_reason : "");
|
||||||
|
return reason.startsWith("INVITE_ONLY|") ? "invite" : "general";
|
||||||
|
}
|
||||||
|
|
||||||
|
_isInCooldown(account, scope = "any") {
|
||||||
if (!account || !account.cooldown_until) return false;
|
if (!account || !account.cooldown_until) return false;
|
||||||
try {
|
try {
|
||||||
return new Date(account.cooldown_until).getTime() > Date.now();
|
const active = new Date(account.cooldown_until).getTime() > Date.now();
|
||||||
|
if (!active) return false;
|
||||||
|
if (scope === "any") return true;
|
||||||
|
const cooldownScope = this._getCooldownScope(account);
|
||||||
|
if (scope === "invite") return true;
|
||||||
|
return cooldownScope !== "invite";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyFloodCooldown(account, reason) {
|
_clearCooldownInMemory(account) {
|
||||||
|
if (!account) return;
|
||||||
|
account.status = "ok";
|
||||||
|
account.last_error = "";
|
||||||
|
account.cooldown_until = "";
|
||||||
|
account.cooldown_reason = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureActiveCooldown(account) {
|
||||||
|
if (!account || !account.cooldown_until) return;
|
||||||
|
let ts = 0;
|
||||||
|
try {
|
||||||
|
ts = new Date(account.cooldown_until).getTime();
|
||||||
|
} catch (error) {
|
||||||
|
ts = 0;
|
||||||
|
}
|
||||||
|
if (!ts || ts <= Date.now()) {
|
||||||
|
this.store.clearAccountCooldown(account.id);
|
||||||
|
this._clearCooldownInMemory(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_isGeneralRoleBlocked(account) {
|
||||||
|
const acc = this._resolveAccount(account);
|
||||||
|
if (!acc) return true;
|
||||||
|
this._ensureActiveCooldown(acc);
|
||||||
|
if (acc.status && acc.status !== "ok") return true;
|
||||||
|
return this._isInCooldown(acc, "general");
|
||||||
|
}
|
||||||
|
|
||||||
|
_isInviteRoleBlocked(account) {
|
||||||
|
const acc = this._resolveAccount(account);
|
||||||
|
if (!acc) return true;
|
||||||
|
this._ensureActiveCooldown(acc);
|
||||||
|
if (acc.status && acc.status !== "ok") return true;
|
||||||
|
return this._isInCooldown(acc, "invite");
|
||||||
|
}
|
||||||
|
|
||||||
|
isAccountInviteBlocked(accountOrId) {
|
||||||
|
return this._isInviteRoleBlocked(accountOrId);
|
||||||
|
}
|
||||||
|
|
||||||
|
isAccountGeneralBlocked(accountOrId) {
|
||||||
|
return this._isGeneralRoleBlocked(accountOrId);
|
||||||
|
}
|
||||||
|
|
||||||
|
isAccountConnected(accountOrId) {
|
||||||
|
const acc = this._resolveAccount(accountOrId);
|
||||||
|
const id = Number(acc && acc.id ? acc.id : accountOrId || 0);
|
||||||
|
if (!id) return false;
|
||||||
|
return this.clients.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
isInviteAccountAvailable(accountOrId) {
|
||||||
|
const acc = this._resolveAccount(accountOrId);
|
||||||
|
if (!acc) return false;
|
||||||
|
return this.isAccountConnected(acc.id) && !this._isInviteRoleBlocked(acc);
|
||||||
|
}
|
||||||
|
|
||||||
|
isAccountFullyUnavailable(accountOrId) {
|
||||||
|
const acc = this._resolveAccount(accountOrId);
|
||||||
|
if (!acc) return true;
|
||||||
|
if (!this.isAccountConnected(acc.id)) return true;
|
||||||
|
return this._isGeneralRoleBlocked(acc);
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyFloodCooldown(account, reason, scope = "general") {
|
||||||
const settings = this.store.getSettings();
|
const settings = this.store.getSettings();
|
||||||
const minutes = Number(settings.floodCooldownMinutes || 1440);
|
const inviteOnly = String(scope || "").toLowerCase() === "invite";
|
||||||
|
const daysRaw = inviteOnly
|
||||||
|
? Number(settings.inviteFloodCooldownDays || 0)
|
||||||
|
: Number(settings.generalFloodCooldownDays || 0);
|
||||||
|
const minutes = daysRaw > 0
|
||||||
|
? Math.round(daysRaw * 24 * 60)
|
||||||
|
: Number(settings.floodCooldownMinutes || 1440);
|
||||||
|
if (inviteOnly) {
|
||||||
|
this.store.setAccountInviteCooldown(account.id, minutes, reason);
|
||||||
|
} else {
|
||||||
this.store.setAccountCooldown(account.id, minutes, reason);
|
this.store.setAccountCooldown(account.id, minutes, reason);
|
||||||
this.store.addAccountEvent(account.id, account.phone || "", "flood", `FLOOD cooldown: ${minutes} min. ${reason || ""}`);
|
}
|
||||||
|
const scopedReason = inviteOnly ? `INVITE_ONLY|${reason || ""}` : (reason || "");
|
||||||
|
this.store.addAccountEvent(
|
||||||
|
account.id,
|
||||||
|
account.phone || "",
|
||||||
|
inviteOnly ? "flood_invite" : "flood",
|
||||||
|
`FLOOD cooldown: ${minutes} min${daysRaw > 0 ? ` (${daysRaw} дн.)` : ""}. ${inviteOnly ? "[invite-only] " : ""}${reason || ""}`
|
||||||
|
);
|
||||||
|
if (!inviteOnly) {
|
||||||
account.status = "limited";
|
account.status = "limited";
|
||||||
account.last_error = reason || "";
|
}
|
||||||
|
account.last_error = scopedReason;
|
||||||
account.cooldown_until = minutes > 0 ? new Date(Date.now() + minutes * 60000).toISOString() : "";
|
account.cooldown_until = minutes > 0 ? new Date(Date.now() + minutes * 60000).toISOString() : "";
|
||||||
account.cooldown_reason = reason || "";
|
account.cooldown_reason = scopedReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
_extractErrorText(error) {
|
||||||
|
return error && (error.errorMessage || error.message) ? (error.errorMessage || error.message) : String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTlConstructorMismatch(errorText) {
|
||||||
|
return String(errorText || "").includes("Could not find a matching Constructor ID for the TLObject");
|
||||||
|
}
|
||||||
|
|
||||||
|
_normalizeHistoryErrorText(errorText) {
|
||||||
|
const text = String(errorText || "").trim();
|
||||||
|
if (!text) return "UNKNOWN_ERROR";
|
||||||
|
if (this._isTlConstructorMismatch(text)) return "TL_CONSTRUCTOR_ID_MISMATCH (нужно переподключение сессии/повтор)";
|
||||||
|
if (text.includes("No user has") && text.includes("as username")) return "USERNAME_NOT_OCCUPIED";
|
||||||
|
if (text.includes("CHANNEL_PRIVATE")) return "CHANNEL_PRIVATE";
|
||||||
|
if (text.includes("FLOOD_WAIT_") || text.includes("FLOOD_PREMIUM_WAIT_") || text.includes("FLOOD") || text.includes("PEER_FLOOD")) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _callWithTlRecovery(client, account, context, run, retries = 1) {
|
||||||
|
try {
|
||||||
|
return await run();
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = this._extractErrorText(error);
|
||||||
|
if (!this._isTlConstructorMismatch(errorText) || retries <= 0 || !client) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (account) {
|
||||||
|
this.store.addAccountEvent(
|
||||||
|
account.id,
|
||||||
|
account.phone || "",
|
||||||
|
"tl_recover",
|
||||||
|
`${context}: TL constructor mismatch -> reconnect`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await client.disconnect();
|
||||||
|
} catch (_disconnectError) {
|
||||||
|
// ignore disconnect errors
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
if (account) {
|
||||||
|
this._instrumentClientInvoke(client, account.id, account.phone || "", 0);
|
||||||
|
}
|
||||||
|
} catch (reconnectError) {
|
||||||
|
const reconnectText = this._extractErrorText(reconnectError);
|
||||||
|
if (account) {
|
||||||
|
this.store.addAccountEvent(
|
||||||
|
account.id,
|
||||||
|
account.phone || "",
|
||||||
|
"tl_recover_failed",
|
||||||
|
`${context}: reconnect failed (${reconnectText})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw reconnectError;
|
||||||
|
}
|
||||||
|
return this._callWithTlRecovery(client, account, context, run, retries - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_isInviteLink(value) {
|
_isInviteLink(value) {
|
||||||
|
|||||||
@ -8,6 +8,8 @@ export const emptySettings = {
|
|||||||
accountMaxGroups: 10,
|
accountMaxGroups: 10,
|
||||||
accountDailyLimit: 50,
|
accountDailyLimit: 50,
|
||||||
floodCooldownMinutes: 1440,
|
floodCooldownMinutes: 1440,
|
||||||
|
inviteFloodCooldownDays: 3,
|
||||||
|
generalFloodCooldownDays: 1,
|
||||||
queueTtlHours: 24,
|
queueTtlHours: 24,
|
||||||
apiTraceEnabled: false
|
apiTraceEnabled: false
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1645,6 +1645,10 @@ button.danger {
|
|||||||
color: #f97316;
|
color: #f97316;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
.account-error {
|
.account-error {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
@ -1708,6 +1712,11 @@ button.danger {
|
|||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-row.auth-dup {
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
background: #fff7f7;
|
||||||
|
}
|
||||||
|
|
||||||
.role-toggle {
|
.role-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -1953,6 +1962,11 @@ button.danger {
|
|||||||
color: #b45309;
|
color: #b45309;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-badge.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.pending-badge {
|
.pending-badge {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
|
|||||||
@ -36,6 +36,7 @@ function AccountsTab({
|
|||||||
const [membershipModal, setMembershipModal] = useState(null);
|
const [membershipModal, setMembershipModal] = useState(null);
|
||||||
const [usageModal, setUsageModal] = useState(null);
|
const [usageModal, setUsageModal] = useState(null);
|
||||||
const [bulkInviteLimit, setBulkInviteLimit] = useState(7);
|
const [bulkInviteLimit, setBulkInviteLimit] = useState(7);
|
||||||
|
const [healthFilter, setHealthFilter] = useState("all");
|
||||||
const inviteAccessById = React.useMemo(() => {
|
const inviteAccessById = React.useMemo(() => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
(inviteAccessStatus || []).forEach((item) => {
|
(inviteAccessStatus || []).forEach((item) => {
|
||||||
@ -73,12 +74,39 @@ function AccountsTab({
|
|||||||
return access.role;
|
return access.role;
|
||||||
};
|
};
|
||||||
const formatBool = (value) => (value ? "да" : "нет");
|
const formatBool = (value) => (value ? "да" : "нет");
|
||||||
|
const isAuthKeyDuplicatedAccount = (account) => (
|
||||||
|
String(account && account.status ? account.status : "").toLowerCase() === "error"
|
||||||
|
&& String(account && account.last_error ? account.last_error : "").includes("AUTH_KEY_DUPLICATED")
|
||||||
|
);
|
||||||
|
const healthFilterMatch = (account) => {
|
||||||
|
if (healthFilter === "auth_dup") return isAuthKeyDuplicatedAccount(account);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const visibleFreeOrSelected = (accountBuckets.freeOrSelected || []).filter(healthFilterMatch);
|
||||||
|
const visibleBusy = (accountBuckets.busy || []).filter(healthFilterMatch);
|
||||||
|
const authDupCount = (accounts || []).filter(isAuthKeyDuplicatedAccount).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="row-header">
|
<div className="row-header">
|
||||||
<h3>Аккаунты</h3>
|
<h3>Аккаунты</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="task-filters">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`chip ${healthFilter === "all" ? "active" : ""}`}
|
||||||
|
onClick={() => setHealthFilter("all")}
|
||||||
|
>
|
||||||
|
Все ({accounts.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`chip ${healthFilter === "auth_dup" ? "active" : ""}`}
|
||||||
|
onClick={() => setHealthFilter("auth_dup")}
|
||||||
|
>
|
||||||
|
AUTH_KEY_DUPLICATED ({authDupCount})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{!hasSelectedTask && (
|
{!hasSelectedTask && (
|
||||||
<div className="hint">Выберите задачу, чтобы управлять аккаунтами.</div>
|
<div className="hint">Выберите задачу, чтобы управлять аккаунтами.</div>
|
||||||
)}
|
)}
|
||||||
@ -193,13 +221,13 @@ function AccountsTab({
|
|||||||
<div className="account-section">
|
<div className="account-section">
|
||||||
<div className="row-header">
|
<div className="row-header">
|
||||||
<h3>Свободные аккаунты</h3>
|
<h3>Свободные аккаунты</h3>
|
||||||
<div className="status-caption">Доступны для выбранной задачи · {accountBuckets.freeOrSelected.length}</div>
|
<div className="status-caption">Доступны для выбранной задачи · {visibleFreeOrSelected.length}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="account-list">
|
<div className="account-list">
|
||||||
{accounts.length === 0 && <div className="empty">Аккаунты не добавлены.</div>}
|
{accounts.length === 0 && <div className="empty">Аккаунты не добавлены.</div>}
|
||||||
{accountBuckets.freeOrSelected.map((account) => {
|
{visibleFreeOrSelected.map((account) => {
|
||||||
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
||||||
const membership = membershipStatus[account.id];
|
const membership = membershipStatus[account.id];
|
||||||
const stats = accountStatsMap.get(account.id);
|
const stats = accountStatsMap.get(account.id);
|
||||||
@ -239,6 +267,7 @@ function AccountsTab({
|
|||||||
const selected = selectedAccountIds.includes(account.id);
|
const selected = selectedAccountIds.includes(account.id);
|
||||||
const roles = taskAccountRoles[account.id] || { monitor: false, invite: false, confirm: false, inviteLimit: 0 };
|
const roles = taskAccountRoles[account.id] || { monitor: false, invite: false, confirm: false, inviteLimit: 0 };
|
||||||
const isMasterAdmin = hasSelectedTask && inviteAdminMasterId && Number(inviteAdminMasterId) === account.id;
|
const isMasterAdmin = hasSelectedTask && inviteAdminMasterId && Number(inviteAdminMasterId) === account.id;
|
||||||
|
const isAuthDup = isAuthKeyDuplicatedAccount(account);
|
||||||
const taskNames = assignedTasks
|
const taskNames = assignedTasks
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const name = accountBuckets.taskNameMap.get(item.taskId) || `Задача #${item.taskId}`;
|
const name = accountBuckets.taskNameMap.get(item.taskId) || `Задача #${item.taskId}`;
|
||||||
@ -252,15 +281,21 @@ function AccountsTab({
|
|||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={account.id} className="account-row free">
|
<div key={account.id} className={`account-row free ${isAuthDup ? "auth-dup" : ""}`}>
|
||||||
<div className="account-main">
|
<div className="account-main">
|
||||||
<div className="account-header">
|
<div className="account-header">
|
||||||
<div>
|
<div>
|
||||||
<div className="account-phone">{formatAccountLabel(account)}</div>
|
<div className="account-phone">{formatAccountLabel(account)}</div>
|
||||||
<div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div>
|
<div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div>
|
||||||
{account.status && account.status !== "ok" && (
|
{isAuthDup && (
|
||||||
|
<span className="task-badge error pending-badge">AUTH_KEY_DUPLICATED</span>
|
||||||
|
)}
|
||||||
|
{account.status === "limited" && (
|
||||||
<span className="task-badge warn pending-badge">в спаме</span>
|
<span className="task-badge warn pending-badge">в спаме</span>
|
||||||
)}
|
)}
|
||||||
|
{account.status === "error" && !isAuthDup && (
|
||||||
|
<span className="task-badge warn pending-badge">ошибка</span>
|
||||||
|
)}
|
||||||
<div className={`account-status-pill ${selected ? "busy" : "free"}`}>
|
<div className={`account-status-pill ${selected ? "busy" : "free"}`}>
|
||||||
{selected ? "В задаче" : "Свободен"}
|
{selected ? "В задаче" : "Свободен"}
|
||||||
</div>
|
</div>
|
||||||
@ -414,14 +449,14 @@ function AccountsTab({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filterFreeAccounts && accountBuckets.busy.length > 0 && (
|
{filterFreeAccounts && visibleBusy.length > 0 && (
|
||||||
<div className="busy-accounts">
|
<div className="busy-accounts">
|
||||||
<div className="row-header">
|
<div className="row-header">
|
||||||
<h3>Занятые аккаунты</h3>
|
<h3>Занятые аккаунты</h3>
|
||||||
<div className="status-caption">Используются в других задачах · {accountBuckets.busy.length}</div>
|
<div className="status-caption">Используются в других задачах · {visibleBusy.length}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="account-list">
|
<div className="account-list">
|
||||||
{accountBuckets.busy.map((account) => {
|
{visibleBusy.map((account) => {
|
||||||
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
||||||
const roles = {
|
const roles = {
|
||||||
monitor: assignedTasks.some((item) => item.roleMonitor),
|
monitor: assignedTasks.some((item) => item.roleMonitor),
|
||||||
@ -474,17 +509,24 @@ function AccountsTab({
|
|||||||
? `В нашей: ${membership.ourGroupMember ? "да" : membership.ourGroupPending ? "ожидает одобрения" : "нет"}`
|
? `В нашей: ${membership.ourGroupMember ? "да" : membership.ourGroupPending ? "ожидает одобрения" : "нет"}`
|
||||||
: "В нашей: —";
|
: "В нашей: —";
|
||||||
const inviteAccess = inviteAccessById.get(account.id);
|
const inviteAccess = inviteAccessById.get(account.id);
|
||||||
|
const isAuthDup = isAuthKeyDuplicatedAccount(account);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={account.id} className="account-row busy">
|
<div key={account.id} className={`account-row busy ${isAuthDup ? "auth-dup" : ""}`}>
|
||||||
<div className="account-main">
|
<div className="account-main">
|
||||||
<div className="account-header">
|
<div className="account-header">
|
||||||
<div>
|
<div>
|
||||||
<div className="account-phone">{formatAccountLabel(account)}</div>
|
<div className="account-phone">{formatAccountLabel(account)}</div>
|
||||||
<div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div>
|
<div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div>
|
||||||
{account.status && account.status !== "ok" && (
|
{isAuthDup && (
|
||||||
|
<span className="task-badge error pending-badge">AUTH_KEY_DUPLICATED</span>
|
||||||
|
)}
|
||||||
|
{account.status === "limited" && (
|
||||||
<span className="task-badge warn pending-badge">в спаме</span>
|
<span className="task-badge warn pending-badge">в спаме</span>
|
||||||
)}
|
)}
|
||||||
|
{account.status === "error" && !isAuthDup && (
|
||||||
|
<span className="task-badge warn pending-badge">ошибка</span>
|
||||||
|
)}
|
||||||
<div className="account-status-pill busy">
|
<div className="account-status-pill busy">
|
||||||
Занят
|
Занят
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -41,7 +41,7 @@ function SettingsTab({ settings, onSettingsChange, saveSettings }) {
|
|||||||
<span className="label-line">Таймер после FLOOD (мин)</span>
|
<span className="label-line">Таймер после FLOOD (мин)</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="0"
|
||||||
value={settings.floodCooldownMinutes === "" ? "" : settings.floodCooldownMinutes}
|
value={settings.floodCooldownMinutes === "" ? "" : settings.floodCooldownMinutes}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
@ -49,7 +49,39 @@ function SettingsTab({ settings, onSettingsChange, saveSettings }) {
|
|||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
const value = Number(settings.floodCooldownMinutes);
|
const value = Number(settings.floodCooldownMinutes);
|
||||||
onSettingsChange("floodCooldownMinutes", Number.isFinite(value) && value > 0 ? value : 1);
|
onSettingsChange("floodCooldownMinutes", Number.isFinite(value) && value >= 0 ? value : 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span className="label-line">Отлёжка инвайта (дни)</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={settings.inviteFloodCooldownDays === "" ? "" : settings.inviteFloodCooldownDays}
|
||||||
|
onChange={(event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
onSettingsChange("inviteFloodCooldownDays", value === "" ? "" : Number(value));
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const value = Number(settings.inviteFloodCooldownDays);
|
||||||
|
onSettingsChange("inviteFloodCooldownDays", Number.isFinite(value) && value >= 0 ? value : 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span className="label-line">Отлёжка общая (дни)</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={settings.generalFloodCooldownDays === "" ? "" : settings.generalFloodCooldownDays}
|
||||||
|
onChange={(event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
onSettingsChange("generalFloodCooldownDays", value === "" ? "" : Number(value));
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const value = Number(settings.generalFloodCooldownDays);
|
||||||
|
onSettingsChange("generalFloodCooldownDays", Number.isFinite(value) && value >= 0 ? value : 0);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user