This commit is contained in:
Ivan Neplokhov 2026-02-04 14:21:07 +04:00
parent e43eed7f9c
commit 6af9c974be
17 changed files with 247 additions and 22 deletions

View File

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

View File

@ -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 "Инвайт-ссылка недействительна или истекла.";
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import packageJson from "../../../package.json";
export const APP_VERSION = packageJson.version || "dev";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

@ -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 "Пользователь заблокировал аккаунт, который пытается добавить.";
} }