some
This commit is contained in:
parent
e43eed7f9c
commit
6af9c974be
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "telegram-invite-automation",
|
"name": "telegram-invite-automation",
|
||||||
"version": "1.3.0",
|
"version": "1.4.0",
|
||||||
"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",
|
||||||
|
|||||||
@ -1092,6 +1092,12 @@ const explainInviteError = (error) => {
|
|||||||
if (error === "CHAT_MEMBER_ADD_FAILED") {
|
if (error === "CHAT_MEMBER_ADD_FAILED") {
|
||||||
return "Telegram отказал в добавлении пользователя. Возможные причины: пользователь недоступен для инвайта, неверные данные, ограничения чата или лимиты аккаунта.";
|
return "Telegram отказал в добавлении пользователя. Возможные причины: пользователь недоступен для инвайта, неверные данные, ограничения чата или лимиты аккаунта.";
|
||||||
}
|
}
|
||||||
|
if (error === "SOURCE_ADMIN_SKIPPED") {
|
||||||
|
return "Пользователь является администратором в группе конкурента и пропущен по фильтру.";
|
||||||
|
}
|
||||||
|
if (error === "SOURCE_BOT_SKIPPED") {
|
||||||
|
return "Пользователь является ботом в группе конкурента и пропущен по фильтру.";
|
||||||
|
}
|
||||||
if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") {
|
if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") {
|
||||||
return "Инвайт-ссылка недействительна или истекла.";
|
return "Инвайт-ссылка недействительна или истекла.";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,8 @@ const dayjs = require("dayjs");
|
|||||||
const DEFAULT_SETTINGS = {
|
const DEFAULT_SETTINGS = {
|
||||||
competitorGroups: [""],
|
competitorGroups: [""],
|
||||||
ourGroup: "",
|
ourGroup: "",
|
||||||
minIntervalMinutes: 5,
|
minIntervalMinutes: 10,
|
||||||
maxIntervalMinutes: 10,
|
maxIntervalMinutes: 20,
|
||||||
dailyLimit: 100,
|
dailyLimit: 100,
|
||||||
historyLimit: 200,
|
historyLimit: 200,
|
||||||
accountMaxGroups: 10,
|
accountMaxGroups: 10,
|
||||||
@ -153,8 +153,8 @@ function initStore(userDataPath) {
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
our_group TEXT NOT NULL,
|
our_group TEXT NOT NULL,
|
||||||
min_interval_minutes INTEGER NOT NULL DEFAULT 1,
|
min_interval_minutes INTEGER NOT NULL DEFAULT 10,
|
||||||
max_interval_minutes INTEGER NOT NULL DEFAULT 3,
|
max_interval_minutes INTEGER NOT NULL DEFAULT 20,
|
||||||
daily_limit INTEGER NOT NULL DEFAULT 15,
|
daily_limit INTEGER NOT NULL DEFAULT 15,
|
||||||
history_limit INTEGER NOT NULL DEFAULT 35,
|
history_limit INTEGER NOT NULL DEFAULT 35,
|
||||||
max_invites_per_cycle INTEGER NOT NULL DEFAULT 1,
|
max_invites_per_cycle INTEGER NOT NULL DEFAULT 1,
|
||||||
@ -817,7 +817,7 @@ function initStore(userDataPath) {
|
|||||||
INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite, role_confirm, invite_limit)
|
INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite, role_confirm, invite_limit)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
(accountIds || []).forEach((accountId) => stmt.run(taskId, accountId, 1, 1, 1, 1));
|
(accountIds || []).forEach((accountId) => stmt.run(taskId, accountId, 1, 1, 1, 7));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTaskAccountRoles(taskId, roles) {
|
function setTaskAccountRoles(taskId, roles) {
|
||||||
|
|||||||
@ -363,6 +363,38 @@ class TaskRunner {
|
|||||||
false,
|
false,
|
||||||
result.error || ""
|
result.error || ""
|
||||||
);
|
);
|
||||||
|
} else if (result.error === "SOURCE_ADMIN_SKIPPED" || result.error === "SOURCE_BOT_SKIPPED") {
|
||||||
|
this.store.markInviteStatus(item.id, "skipped");
|
||||||
|
this.store.recordInvite(
|
||||||
|
this.task.id,
|
||||||
|
item.user_id,
|
||||||
|
item.username,
|
||||||
|
result.accountId,
|
||||||
|
result.accountPhone,
|
||||||
|
item.source_chat,
|
||||||
|
"skipped",
|
||||||
|
"",
|
||||||
|
result.error || "",
|
||||||
|
"invite",
|
||||||
|
item.user_access_hash,
|
||||||
|
watcherAccount ? watcherAccount.id : 0,
|
||||||
|
watcherAccount ? watcherAccount.phone : "",
|
||||||
|
result.strategy,
|
||||||
|
result.strategyMeta,
|
||||||
|
this.task.our_group,
|
||||||
|
result.targetType,
|
||||||
|
false,
|
||||||
|
result.error || ""
|
||||||
|
);
|
||||||
|
const reasonText = result.error === "SOURCE_ADMIN_SKIPPED"
|
||||||
|
? "пропущен администратор группы конкурента"
|
||||||
|
: "пропущен бот из группы конкурента";
|
||||||
|
this.store.addAccountEvent(
|
||||||
|
watcherAccount ? watcherAccount.id : 0,
|
||||||
|
watcherAccount ? watcherAccount.phone : "",
|
||||||
|
"invite_skipped",
|
||||||
|
`задача ${this.task.id}: ${item.user_id}${item.username ? ` (@${item.username})` : ""} — ${reasonText}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
errors.push(`${item.user_id}: ${result.error}`);
|
errors.push(`${item.user_id}: ${result.error}`);
|
||||||
if (this.task.retry_on_fail) {
|
if (this.task.retry_on_fail) {
|
||||||
|
|||||||
@ -866,6 +866,40 @@ class TelegramManager {
|
|||||||
lastAttempts = resolved.attempts || [];
|
lastAttempts = resolved.attempts || [];
|
||||||
const user = resolved.user;
|
const user = resolved.user;
|
||||||
resolvedUser = user;
|
resolvedUser = user;
|
||||||
|
const sourceRestrictions = await this._checkSourceUserRestrictions(
|
||||||
|
client,
|
||||||
|
options.sourceChat || "",
|
||||||
|
userId,
|
||||||
|
options.username || "",
|
||||||
|
resolvedUser
|
||||||
|
);
|
||||||
|
if (sourceRestrictions && sourceRestrictions.skip) {
|
||||||
|
lastAttempts.push({
|
||||||
|
strategy: "source_filter",
|
||||||
|
ok: false,
|
||||||
|
detail: sourceRestrictions.detail || sourceRestrictions.code
|
||||||
|
});
|
||||||
|
this.store.addAccountEvent(
|
||||||
|
account.id,
|
||||||
|
account.phone || "",
|
||||||
|
"invite_skipped_source",
|
||||||
|
[
|
||||||
|
`Пользователь: ${userId}${options.username ? ` (@${options.username})` : ""}`,
|
||||||
|
`Причина: ${sourceRestrictions.code}`,
|
||||||
|
`Детали: ${sourceRestrictions.detail || "—"}`,
|
||||||
|
`Источник: ${options.sourceChat || "—"}`
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: sourceRestrictions.code,
|
||||||
|
accountId: account.id,
|
||||||
|
accountPhone: account.phone || "",
|
||||||
|
strategy: "source_filter",
|
||||||
|
strategyMeta: JSON.stringify(lastAttempts),
|
||||||
|
targetType
|
||||||
|
};
|
||||||
|
}
|
||||||
if (task.invite_via_admins && task.invite_admin_allow_flood && targetEntity.className === "Channel") {
|
if (task.invite_via_admins && task.invite_admin_allow_flood && targetEntity.className === "Channel") {
|
||||||
const masterId = Number(task.invite_admin_master_id || 0);
|
const masterId = Number(task.invite_admin_master_id || 0);
|
||||||
const masterEntry = masterId ? this.clients.get(masterId) : null;
|
const masterEntry = masterId ? this.clients.get(masterId) : null;
|
||||||
@ -1335,6 +1369,78 @@ class TelegramManager {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _checkSourceUserRestrictions(client, sourceChat, userId, username, resolvedUser) {
|
||||||
|
const normalizeUsername = (value) => {
|
||||||
|
if (!value) return "";
|
||||||
|
return String(value).startsWith("@") ? String(value) : `@${value}`;
|
||||||
|
};
|
||||||
|
const normalizedUserId = userId != null ? String(userId) : "";
|
||||||
|
const normalizedUsername = normalizeUsername(username);
|
||||||
|
let userEntity = null;
|
||||||
|
|
||||||
|
if (resolvedUser && resolvedUser.className === "User") {
|
||||||
|
userEntity = resolvedUser;
|
||||||
|
}
|
||||||
|
if (!userEntity && normalizedUsername) {
|
||||||
|
try {
|
||||||
|
userEntity = await client.getEntity(normalizedUsername);
|
||||||
|
} catch (error) {
|
||||||
|
userEntity = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!userEntity && normalizedUserId) {
|
||||||
|
try {
|
||||||
|
userEntity = await client.getEntity(BigInt(normalizedUserId));
|
||||||
|
} catch (error) {
|
||||||
|
userEntity = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userEntity && userEntity.className === "User" && userEntity.bot) {
|
||||||
|
return {
|
||||||
|
skip: true,
|
||||||
|
code: "SOURCE_BOT_SKIPPED",
|
||||||
|
detail: "пользователь является ботом"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceChat) return { skip: false };
|
||||||
|
const resolvedSource = await this._resolveGroupEntity(client, sourceChat, false, null);
|
||||||
|
if (!resolvedSource || !resolvedSource.ok || !resolvedSource.entity || resolvedSource.entity.className !== "Channel") {
|
||||||
|
return { skip: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const participantRef = userEntity || resolvedUser;
|
||||||
|
if (!participantRef) return { skip: false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const participantResult = await client.invoke(new Api.channels.GetParticipant({
|
||||||
|
channel: resolvedSource.entity,
|
||||||
|
participant: participantRef
|
||||||
|
}));
|
||||||
|
const sourceParticipant = participantResult && participantResult.participant ? participantResult.participant : participantResult;
|
||||||
|
const sourceClass = sourceParticipant && sourceParticipant.className ? sourceParticipant.className : "";
|
||||||
|
const isCreator = sourceClass.includes("Creator");
|
||||||
|
const isAdmin = sourceClass.includes("Admin") || isCreator;
|
||||||
|
if (isAdmin) {
|
||||||
|
return {
|
||||||
|
skip: true,
|
||||||
|
code: "SOURCE_ADMIN_SKIPPED",
|
||||||
|
detail: isCreator
|
||||||
|
? "пользователь является владельцем в группе-источнике"
|
||||||
|
: "пользователь является администратором в группе-источнике"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
|
if (errorText.includes("USER_NOT_PARTICIPANT") || errorText.includes("PARTICIPANT_ID_INVALID")) {
|
||||||
|
return { skip: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { skip: false };
|
||||||
|
}
|
||||||
|
|
||||||
async getGroupVisibility(task, competitorGroups) {
|
async getGroupVisibility(task, competitorGroups) {
|
||||||
const groups = (competitorGroups || []).filter(Boolean);
|
const groups = (competitorGroups || []).filter(Boolean);
|
||||||
if (!groups.length) return [];
|
if (!groups.length) return [];
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import useAppState from "./hooks/useAppState.js";
|
|||||||
import useAppTaskDerived from "./hooks/useAppTaskDerived.js";
|
import useAppTaskDerived from "./hooks/useAppTaskDerived.js";
|
||||||
import useOpenLogsTabListener from "./hooks/useOpenLogsTabListener.js";
|
import useOpenLogsTabListener from "./hooks/useOpenLogsTabListener.js";
|
||||||
import useAppOutsideClicks from "./hooks/useAppOutsideClicks.js";
|
import useAppOutsideClicks from "./hooks/useAppOutsideClicks.js";
|
||||||
|
import { APP_VERSION } from "./constants/consts.js";
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@ -536,6 +537,7 @@ export default function App() {
|
|||||||
refreshIdentity,
|
refreshIdentity,
|
||||||
updateAccountRole,
|
updateAccountRole,
|
||||||
updateAccountInviteLimit,
|
updateAccountInviteLimit,
|
||||||
|
setInviteLimitForAllInviters,
|
||||||
setAccountRolesAll,
|
setAccountRolesAll,
|
||||||
applyRolePreset,
|
applyRolePreset,
|
||||||
assignAccountsToTask,
|
assignAccountsToTask,
|
||||||
@ -834,6 +836,7 @@ export default function App() {
|
|||||||
deleteAccount,
|
deleteAccount,
|
||||||
updateAccountRole,
|
updateAccountRole,
|
||||||
updateAccountInviteLimit,
|
updateAccountInviteLimit,
|
||||||
|
setInviteLimitForAllInviters,
|
||||||
setAccountRolesAll,
|
setAccountRolesAll,
|
||||||
applyRolePreset,
|
applyRolePreset,
|
||||||
removeAccountFromTask,
|
removeAccountFromTask,
|
||||||
@ -1009,6 +1012,7 @@ export default function App() {
|
|||||||
settingsTab={settingsTab}
|
settingsTab={settingsTab}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="app-footer">Версия: {APP_VERSION}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,8 +15,8 @@ export const emptyTaskForm = {
|
|||||||
id: null,
|
id: null,
|
||||||
name: "",
|
name: "",
|
||||||
ourGroup: "",
|
ourGroup: "",
|
||||||
minIntervalMinutes: 1,
|
minIntervalMinutes: 10,
|
||||||
maxIntervalMinutes: 3,
|
maxIntervalMinutes: 20,
|
||||||
dailyLimit: 15,
|
dailyLimit: 15,
|
||||||
historyLimit: 35,
|
historyLimit: 35,
|
||||||
maxInvitesPerCycle: 1,
|
maxInvitesPerCycle: 1,
|
||||||
@ -80,8 +80,8 @@ export const normalizeTask = (row) => ({
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name || "",
|
name: row.name || "",
|
||||||
ourGroup: row.our_group || "",
|
ourGroup: row.our_group || "",
|
||||||
minIntervalMinutes: Number(row.min_interval_minutes || 1),
|
minIntervalMinutes: Number(row.min_interval_minutes || 10),
|
||||||
maxIntervalMinutes: Number(row.max_interval_minutes || 3),
|
maxIntervalMinutes: Number(row.max_interval_minutes || 20),
|
||||||
dailyLimit: Number(row.daily_limit || 15),
|
dailyLimit: Number(row.daily_limit || 15),
|
||||||
historyLimit: Number(row.history_limit || 35),
|
historyLimit: Number(row.history_limit || 35),
|
||||||
maxInvitesPerCycle: Number(row.max_invites_per_cycle || 1),
|
maxInvitesPerCycle: Number(row.max_invites_per_cycle || 1),
|
||||||
|
|||||||
3
src/renderer/constants/consts.js
Normal file
3
src/renderer/constants/consts.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import packageJson from "../../../package.json";
|
||||||
|
|
||||||
|
export const APP_VERSION = packageJson.version || "dev";
|
||||||
@ -15,6 +15,8 @@ export default function useAccountManagement({
|
|||||||
membershipStatus,
|
membershipStatus,
|
||||||
refreshMembership
|
refreshMembership
|
||||||
}) {
|
}) {
|
||||||
|
const DEFAULT_INVITE_LIMIT = 7;
|
||||||
|
|
||||||
const persistAccountRoles = async (next) => {
|
const persistAccountRoles = async (next) => {
|
||||||
if (!window.api || selectedTaskId == null) return;
|
if (!window.api || selectedTaskId == null) return;
|
||||||
const rolePayload = Object.entries(next).map(([id, roles]) => ({
|
const rolePayload = Object.entries(next).map(([id, roles]) => ({
|
||||||
@ -42,7 +44,7 @@ export default function useAccountManagement({
|
|||||||
const existing = next[accountId] || { monitor: false, invite: false, confirm: false, inviteLimit: 0 };
|
const existing = next[accountId] || { monitor: false, invite: false, confirm: false, inviteLimit: 0 };
|
||||||
let inviteLimit = existing.inviteLimit || 0;
|
let inviteLimit = existing.inviteLimit || 0;
|
||||||
if (role === "invite" && value && inviteLimit === 0) {
|
if (role === "invite" && value && inviteLimit === 0) {
|
||||||
inviteLimit = 1;
|
inviteLimit = DEFAULT_INVITE_LIMIT;
|
||||||
}
|
}
|
||||||
next[accountId] = { ...existing, [role]: value, inviteLimit };
|
next[accountId] = { ...existing, [role]: value, inviteLimit };
|
||||||
if (!next[accountId].monitor && !next[accountId].invite && !next[accountId].confirm) {
|
if (!next[accountId].monitor && !next[accountId].invite && !next[accountId].confirm) {
|
||||||
@ -68,7 +70,7 @@ export default function useAccountManagement({
|
|||||||
const next = { ...taskAccountRoles };
|
const next = { ...taskAccountRoles };
|
||||||
if (value) {
|
if (value) {
|
||||||
const existing = next[accountId] || { inviteLimit: 0 };
|
const existing = next[accountId] || { inviteLimit: 0 };
|
||||||
next[accountId] = { monitor: true, invite: true, confirm: true, inviteLimit: existing.inviteLimit || 1 };
|
next[accountId] = { monitor: true, invite: true, confirm: true, inviteLimit: existing.inviteLimit || DEFAULT_INVITE_LIMIT };
|
||||||
} else {
|
} else {
|
||||||
delete next[accountId];
|
delete next[accountId];
|
||||||
}
|
}
|
||||||
@ -91,12 +93,12 @@ export default function useAccountManagement({
|
|||||||
if (type === "all") {
|
if (type === "all") {
|
||||||
availableIds.forEach((id) => {
|
availableIds.forEach((id) => {
|
||||||
const existing = taskAccountRoles[id] || {};
|
const existing = taskAccountRoles[id] || {};
|
||||||
next[id] = { monitor: true, invite: true, confirm: true, inviteLimit: existing.inviteLimit || 1 };
|
next[id] = { monitor: true, invite: true, confirm: true, inviteLimit: existing.inviteLimit || DEFAULT_INVITE_LIMIT };
|
||||||
});
|
});
|
||||||
} else if (type === "one") {
|
} else if (type === "one") {
|
||||||
const id = availableIds[0];
|
const id = availableIds[0];
|
||||||
const existing = taskAccountRoles[id] || {};
|
const existing = taskAccountRoles[id] || {};
|
||||||
next[id] = { monitor: true, invite: true, confirm: true, inviteLimit: existing.inviteLimit || 1 };
|
next[id] = { monitor: true, invite: true, confirm: true, inviteLimit: existing.inviteLimit || DEFAULT_INVITE_LIMIT };
|
||||||
} else if (type === "split") {
|
} else if (type === "split") {
|
||||||
const monitorCount = Math.max(1, Number(taskForm.maxCompetitorBots || 1));
|
const monitorCount = Math.max(1, Number(taskForm.maxCompetitorBots || 1));
|
||||||
const inviteCount = Math.max(1, Number(taskForm.maxOurBots || 1));
|
const inviteCount = Math.max(1, Number(taskForm.maxOurBots || 1));
|
||||||
@ -112,7 +114,7 @@ export default function useAccountManagement({
|
|||||||
});
|
});
|
||||||
inviteIds.forEach((id) => {
|
inviteIds.forEach((id) => {
|
||||||
const existing = taskAccountRoles[id] || {};
|
const existing = taskAccountRoles[id] || {};
|
||||||
next[id] = { monitor: false, invite: true, confirm: true, inviteLimit: existing.inviteLimit || 1 };
|
next[id] = { monitor: false, invite: true, confirm: true, inviteLimit: existing.inviteLimit || DEFAULT_INVITE_LIMIT };
|
||||||
});
|
});
|
||||||
confirmIds.forEach((id) => {
|
confirmIds.forEach((id) => {
|
||||||
const existing = taskAccountRoles[id] || {};
|
const existing = taskAccountRoles[id] || {};
|
||||||
@ -184,13 +186,45 @@ export default function useAccountManagement({
|
|||||||
setTaskNotice({ text: `Пресет: ${label}`, tone: "success", source: "accounts" });
|
setTaskNotice({ text: `Пресет: ${label}`, tone: "success", source: "accounts" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setInviteLimitForAllInviters = (value) => {
|
||||||
|
if (!hasSelectedTask) return;
|
||||||
|
const normalized = Number(value);
|
||||||
|
const nextLimit = Number.isFinite(normalized) && normalized >= 0 ? Math.floor(normalized) : 0;
|
||||||
|
const next = { ...taskAccountRoles };
|
||||||
|
let inviters = 0;
|
||||||
|
let changed = 0;
|
||||||
|
Object.entries(next).forEach(([id, roles]) => {
|
||||||
|
if (!roles || !roles.invite) return;
|
||||||
|
inviters += 1;
|
||||||
|
const prevLimit = Number(roles.inviteLimit || 0);
|
||||||
|
if (prevLimit !== nextLimit) {
|
||||||
|
changed += 1;
|
||||||
|
}
|
||||||
|
next[id] = { ...roles, inviteLimit: nextLimit };
|
||||||
|
});
|
||||||
|
if (!inviters) {
|
||||||
|
showNotification("Нет аккаунтов с ролью инвайта.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTaskAccountRoles(next);
|
||||||
|
setSelectedAccountIds(Object.keys(next).map((id) => Number(id)));
|
||||||
|
persistAccountRoles(next);
|
||||||
|
setTaskNotice({
|
||||||
|
text: changed
|
||||||
|
? `Лимит инвайтов за цикл обновлен: ${nextLimit} (инвайтеров: ${inviters}).`
|
||||||
|
: `Лимит уже установлен: ${nextLimit}.`,
|
||||||
|
tone: "success",
|
||||||
|
source: "accounts"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const assignAccountsToTask = async (accountIds) => {
|
const assignAccountsToTask = async (accountIds) => {
|
||||||
if (!window.api || selectedTaskId == null) return;
|
if (!window.api || selectedTaskId == null) return;
|
||||||
if (!accountIds.length) return;
|
if (!accountIds.length) return;
|
||||||
const nextRoles = { ...taskAccountRoles };
|
const nextRoles = { ...taskAccountRoles };
|
||||||
accountIds.forEach((accountId) => {
|
accountIds.forEach((accountId) => {
|
||||||
if (!nextRoles[accountId]) {
|
if (!nextRoles[accountId]) {
|
||||||
nextRoles[accountId] = { monitor: true, invite: true, confirm: true };
|
nextRoles[accountId] = { monitor: true, invite: true, confirm: true, inviteLimit: DEFAULT_INVITE_LIMIT };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const rolePayload = Object.entries(nextRoles).map(([accountId, roles]) => ({
|
const rolePayload = Object.entries(nextRoles).map(([accountId, roles]) => ({
|
||||||
@ -289,6 +323,7 @@ export default function useAccountManagement({
|
|||||||
updateAccountInviteLimit,
|
updateAccountInviteLimit,
|
||||||
setAccountRolesAll,
|
setAccountRolesAll,
|
||||||
applyRolePreset,
|
applyRolePreset,
|
||||||
|
setInviteLimitForAllInviters,
|
||||||
assignAccountsToTask,
|
assignAccountsToTask,
|
||||||
moveAccountToTask,
|
moveAccountToTask,
|
||||||
removeAccountFromTask
|
removeAccountFromTask
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export default function useAppTabGroups({
|
|||||||
deleteAccount,
|
deleteAccount,
|
||||||
updateAccountRole,
|
updateAccountRole,
|
||||||
updateAccountInviteLimit,
|
updateAccountInviteLimit,
|
||||||
|
setInviteLimitForAllInviters,
|
||||||
setAccountRolesAll,
|
setAccountRolesAll,
|
||||||
applyRolePreset,
|
applyRolePreset,
|
||||||
removeAccountFromTask,
|
removeAccountFromTask,
|
||||||
@ -174,6 +175,7 @@ export default function useAppTabGroups({
|
|||||||
deleteAccount,
|
deleteAccount,
|
||||||
updateAccountRole,
|
updateAccountRole,
|
||||||
updateAccountInviteLimit,
|
updateAccountInviteLimit,
|
||||||
|
setInviteLimitForAllInviters,
|
||||||
setAccountRolesAll,
|
setAccountRolesAll,
|
||||||
applyRolePreset,
|
applyRolePreset,
|
||||||
removeAccountFromTask,
|
removeAccountFromTask,
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export default function useTabProps(
|
|||||||
deleteAccount,
|
deleteAccount,
|
||||||
updateAccountRole,
|
updateAccountRole,
|
||||||
updateAccountInviteLimit,
|
updateAccountInviteLimit,
|
||||||
|
setInviteLimitForAllInviters,
|
||||||
setAccountRolesAll,
|
setAccountRolesAll,
|
||||||
applyRolePreset,
|
applyRolePreset,
|
||||||
removeAccountFromTask,
|
removeAccountFromTask,
|
||||||
@ -189,6 +190,7 @@ export default function useTabProps(
|
|||||||
deleteAccount,
|
deleteAccount,
|
||||||
updateAccountRole,
|
updateAccountRole,
|
||||||
updateAccountInviteLimit,
|
updateAccountInviteLimit,
|
||||||
|
setInviteLimitForAllInviters,
|
||||||
setAccountRolesAll,
|
setAccountRolesAll,
|
||||||
applyRolePreset,
|
applyRolePreset,
|
||||||
removeAccountFromTask,
|
removeAccountFromTask,
|
||||||
|
|||||||
@ -80,7 +80,8 @@ export default function useTaskActions({
|
|||||||
}
|
}
|
||||||
let accountRolesMap = { ...taskAccountRoles };
|
let accountRolesMap = { ...taskAccountRoles };
|
||||||
let accountIds = Object.keys(accountRolesMap).map((id) => Number(id));
|
let accountIds = Object.keys(accountRolesMap).map((id) => Number(id));
|
||||||
if (nextForm.requireSameBotInBoth) {
|
const autoRoleMode = nextForm.rolesMode === "auto";
|
||||||
|
if (autoRoleMode && nextForm.requireSameBotInBoth) {
|
||||||
const required = Math.max(1, Number(nextForm.maxCompetitorBots || 1));
|
const required = Math.max(1, Number(nextForm.maxCompetitorBots || 1));
|
||||||
const pool = (selectedAccountIds && selectedAccountIds.length ? selectedAccountIds : accounts.map((account) => account.id))
|
const pool = (selectedAccountIds && selectedAccountIds.length ? selectedAccountIds : accounts.map((account) => account.id))
|
||||||
.filter((id) => Number.isFinite(id));
|
.filter((id) => Number.isFinite(id));
|
||||||
@ -88,18 +89,18 @@ export default function useTaskActions({
|
|||||||
accountRolesMap = {};
|
accountRolesMap = {};
|
||||||
chosen.forEach((accountId) => {
|
chosen.forEach((accountId) => {
|
||||||
const existing = taskAccountRoles[accountId] || {};
|
const existing = taskAccountRoles[accountId] || {};
|
||||||
accountRolesMap[accountId] = { monitor: true, invite: true, confirm: true, inviteLimit: existing.inviteLimit || 1 };
|
accountRolesMap[accountId] = { monitor: true, invite: true, confirm: true, inviteLimit: existing.inviteLimit || 7 };
|
||||||
});
|
});
|
||||||
accountIds = chosen;
|
accountIds = chosen;
|
||||||
setTaskAccountRoles(accountRolesMap);
|
setTaskAccountRoles(accountRolesMap);
|
||||||
setSelectedAccountIds(chosen);
|
setSelectedAccountIds(chosen);
|
||||||
}
|
}
|
||||||
if (nextForm.autoAssignAccounts && (!accountIds || accountIds.length === 0)) {
|
if (autoRoleMode && nextForm.autoAssignAccounts && (!accountIds || accountIds.length === 0)) {
|
||||||
accountIds = accounts.map((account) => account.id);
|
accountIds = accounts.map((account) => account.id);
|
||||||
accountRolesMap = {};
|
accountRolesMap = {};
|
||||||
accountIds.forEach((accountId) => {
|
accountIds.forEach((accountId) => {
|
||||||
const existing = taskAccountRoles[accountId] || {};
|
const existing = taskAccountRoles[accountId] || {};
|
||||||
accountRolesMap[accountId] = { monitor: true, invite: true, confirm: true, inviteLimit: existing.inviteLimit || 1 };
|
accountRolesMap[accountId] = { monitor: true, invite: true, confirm: true, inviteLimit: existing.inviteLimit || 7 };
|
||||||
});
|
});
|
||||||
setTaskAccountRoles(accountRolesMap);
|
setTaskAccountRoles(accountRolesMap);
|
||||||
setSelectedAccountIds(accountIds);
|
setSelectedAccountIds(accountIds);
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export default function useTaskLoaders({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
(details.accountIds || []).forEach((accountId) => {
|
(details.accountIds || []).forEach((accountId) => {
|
||||||
roleMap[accountId] = { monitor: true, invite: true, confirm: true, inviteLimit: 1 };
|
roleMap[accountId] = { monitor: true, invite: true, confirm: true, inviteLimit: 7 };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setTaskAccountRoles(roleMap);
|
setTaskAccountRoles(roleMap);
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export default function useTaskPresets({
|
|||||||
if (!roleMap[id]) roleMap[id] = { monitor: false, invite: false, confirm: false, inviteLimit: 0 };
|
if (!roleMap[id]) roleMap[id] = { monitor: false, invite: false, confirm: false, inviteLimit: 0 };
|
||||||
roleMap[id][role] = true;
|
roleMap[id][role] = true;
|
||||||
if (role === "invite" && (!roleMap[id].inviteLimit || roleMap[id].inviteLimit === 0)) {
|
if (role === "invite" && (!roleMap[id].inviteLimit || roleMap[id].inviteLimit === 0)) {
|
||||||
roleMap[id].inviteLimit = 1;
|
roleMap[id].inviteLimit = 7;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const takeFromPool = (count, used) => {
|
const takeFromPool = (count, used) => {
|
||||||
|
|||||||
@ -21,6 +21,13 @@ body {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-footer {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.header-subtitle {
|
.header-subtitle {
|
||||||
color: rgba(226, 232, 240, 0.7);
|
color: rgba(226, 232, 240, 0.7);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@ -22,6 +22,7 @@ function AccountsTab({
|
|||||||
deleteAccount,
|
deleteAccount,
|
||||||
updateAccountRole,
|
updateAccountRole,
|
||||||
updateAccountInviteLimit,
|
updateAccountInviteLimit,
|
||||||
|
setInviteLimitForAllInviters,
|
||||||
setAccountRolesAll,
|
setAccountRolesAll,
|
||||||
applyRolePreset,
|
applyRolePreset,
|
||||||
removeAccountFromTask,
|
removeAccountFromTask,
|
||||||
@ -29,6 +30,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 openMembershipModal = (title, lines) => {
|
const openMembershipModal = (title, lines) => {
|
||||||
setMembershipModal({ title, lines });
|
setMembershipModal({ title, lines });
|
||||||
@ -93,6 +95,25 @@ function AccountsTab({
|
|||||||
<button className="secondary" type="button" onClick={() => applyRolePreset("one")} disabled={rolesMode === "auto"}>Один бот</button>
|
<button className="secondary" type="button" onClick={() => applyRolePreset("one")} disabled={rolesMode === "auto"}>Один бот</button>
|
||||||
<button className="secondary" type="button" onClick={() => applyRolePreset("split")} disabled={rolesMode === "auto"}>Разделить роли</button>
|
<button className="secondary" type="button" onClick={() => applyRolePreset("split")} disabled={rolesMode === "auto"}>Разделить роли</button>
|
||||||
<button className="secondary" type="button" onClick={() => applyRolePreset("all")} disabled={rolesMode === "auto"}>Все роли</button>
|
<button className="secondary" type="button" onClick={() => applyRolePreset("all")} disabled={rolesMode === "auto"}>Все роли</button>
|
||||||
|
<label className="inline-input">
|
||||||
|
Лимит инвайтов для всех инвайтеров
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={bulkInviteLimit}
|
||||||
|
onChange={(event) => {
|
||||||
|
const value = Number(event.target.value || 0);
|
||||||
|
setBulkInviteLimit(Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setInviteLimitForAllInviters(bulkInviteLimit)}
|
||||||
|
>
|
||||||
|
Применить лимит всем
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{filterFreeAccounts && (
|
{filterFreeAccounts && (
|
||||||
|
|||||||
@ -30,6 +30,12 @@ export const explainInviteError = (error) => {
|
|||||||
if (error === "CHAT_MEMBER_ADD_FAILED") {
|
if (error === "CHAT_MEMBER_ADD_FAILED") {
|
||||||
return "Telegram отклонил добавление. Обычно это антиспам-ограничение или недостаток прав.";
|
return "Telegram отклонил добавление. Обычно это антиспам-ограничение или недостаток прав.";
|
||||||
}
|
}
|
||||||
|
if (error === "SOURCE_ADMIN_SKIPPED") {
|
||||||
|
return "Пользователь является администратором в группе конкурента и пропущен по фильтру.";
|
||||||
|
}
|
||||||
|
if (error === "SOURCE_BOT_SKIPPED") {
|
||||||
|
return "Пользователь является ботом в группе конкурента и пропущен по фильтру.";
|
||||||
|
}
|
||||||
if (error === "USER_BLOCKED") {
|
if (error === "USER_BLOCKED") {
|
||||||
return "Пользователь заблокировал аккаунт, который пытается добавить.";
|
return "Пользователь заблокировал аккаунт, который пытается добавить.";
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user