some
This commit is contained in:
parent
e43eed7f9c
commit
6af9c974be
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "telegram-invite-automation",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"private": true,
|
||||
"description": "Automated user parsing and invites for Telegram groups",
|
||||
"main": "src/main/index.js",
|
||||
|
||||
@ -1092,6 +1092,12 @@ const explainInviteError = (error) => {
|
||||
if (error === "CHAT_MEMBER_ADD_FAILED") {
|
||||
return "Telegram отказал в добавлении пользователя. Возможные причины: пользователь недоступен для инвайта, неверные данные, ограничения чата или лимиты аккаунта.";
|
||||
}
|
||||
if (error === "SOURCE_ADMIN_SKIPPED") {
|
||||
return "Пользователь является администратором в группе конкурента и пропущен по фильтру.";
|
||||
}
|
||||
if (error === "SOURCE_BOT_SKIPPED") {
|
||||
return "Пользователь является ботом в группе конкурента и пропущен по фильтру.";
|
||||
}
|
||||
if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") {
|
||||
return "Инвайт-ссылка недействительна или истекла.";
|
||||
}
|
||||
|
||||
@ -6,8 +6,8 @@ const dayjs = require("dayjs");
|
||||
const DEFAULT_SETTINGS = {
|
||||
competitorGroups: [""],
|
||||
ourGroup: "",
|
||||
minIntervalMinutes: 5,
|
||||
maxIntervalMinutes: 10,
|
||||
minIntervalMinutes: 10,
|
||||
maxIntervalMinutes: 20,
|
||||
dailyLimit: 100,
|
||||
historyLimit: 200,
|
||||
accountMaxGroups: 10,
|
||||
@ -153,8 +153,8 @@ function initStore(userDataPath) {
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
our_group TEXT NOT NULL,
|
||||
min_interval_minutes INTEGER NOT NULL DEFAULT 1,
|
||||
max_interval_minutes INTEGER NOT NULL DEFAULT 3,
|
||||
min_interval_minutes INTEGER NOT NULL DEFAULT 10,
|
||||
max_interval_minutes INTEGER NOT NULL DEFAULT 20,
|
||||
daily_limit INTEGER NOT NULL DEFAULT 15,
|
||||
history_limit INTEGER NOT NULL DEFAULT 35,
|
||||
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)
|
||||
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) {
|
||||
|
||||
@ -363,6 +363,38 @@ class TaskRunner {
|
||||
false,
|
||||
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 {
|
||||
errors.push(`${item.user_id}: ${result.error}`);
|
||||
if (this.task.retry_on_fail) {
|
||||
|
||||
@ -866,6 +866,40 @@ class TelegramManager {
|
||||
lastAttempts = resolved.attempts || [];
|
||||
const user = resolved.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") {
|
||||
const masterId = Number(task.invite_admin_master_id || 0);
|
||||
const masterEntry = masterId ? this.clients.get(masterId) : null;
|
||||
@ -1335,6 +1369,78 @@ class TelegramManager {
|
||||
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) {
|
||||
const groups = (competitorGroups || []).filter(Boolean);
|
||||
if (!groups.length) return [];
|
||||
|
||||
@ -31,6 +31,7 @@ import useAppState from "./hooks/useAppState.js";
|
||||
import useAppTaskDerived from "./hooks/useAppTaskDerived.js";
|
||||
import useOpenLogsTabListener from "./hooks/useOpenLogsTabListener.js";
|
||||
import useAppOutsideClicks from "./hooks/useAppOutsideClicks.js";
|
||||
import { APP_VERSION } from "./constants/consts.js";
|
||||
|
||||
|
||||
export default function App() {
|
||||
@ -536,6 +537,7 @@ export default function App() {
|
||||
refreshIdentity,
|
||||
updateAccountRole,
|
||||
updateAccountInviteLimit,
|
||||
setInviteLimitForAllInviters,
|
||||
setAccountRolesAll,
|
||||
applyRolePreset,
|
||||
assignAccountsToTask,
|
||||
@ -834,6 +836,7 @@ export default function App() {
|
||||
deleteAccount,
|
||||
updateAccountRole,
|
||||
updateAccountInviteLimit,
|
||||
setInviteLimitForAllInviters,
|
||||
setAccountRolesAll,
|
||||
applyRolePreset,
|
||||
removeAccountFromTask,
|
||||
@ -1009,6 +1012,7 @@ export default function App() {
|
||||
settingsTab={settingsTab}
|
||||
/>
|
||||
</div>
|
||||
<div className="app-footer">Версия: {APP_VERSION}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -15,8 +15,8 @@ export const emptyTaskForm = {
|
||||
id: null,
|
||||
name: "",
|
||||
ourGroup: "",
|
||||
minIntervalMinutes: 1,
|
||||
maxIntervalMinutes: 3,
|
||||
minIntervalMinutes: 10,
|
||||
maxIntervalMinutes: 20,
|
||||
dailyLimit: 15,
|
||||
historyLimit: 35,
|
||||
maxInvitesPerCycle: 1,
|
||||
@ -80,8 +80,8 @@ export const normalizeTask = (row) => ({
|
||||
id: row.id,
|
||||
name: row.name || "",
|
||||
ourGroup: row.our_group || "",
|
||||
minIntervalMinutes: Number(row.min_interval_minutes || 1),
|
||||
maxIntervalMinutes: Number(row.max_interval_minutes || 3),
|
||||
minIntervalMinutes: Number(row.min_interval_minutes || 10),
|
||||
maxIntervalMinutes: Number(row.max_interval_minutes || 20),
|
||||
dailyLimit: Number(row.daily_limit || 15),
|
||||
historyLimit: Number(row.history_limit || 35),
|
||||
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,
|
||||
refreshMembership
|
||||
}) {
|
||||
const DEFAULT_INVITE_LIMIT = 7;
|
||||
|
||||
const persistAccountRoles = async (next) => {
|
||||
if (!window.api || selectedTaskId == null) return;
|
||||
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 };
|
||||
let inviteLimit = existing.inviteLimit || 0;
|
||||
if (role === "invite" && value && inviteLimit === 0) {
|
||||
inviteLimit = 1;
|
||||
inviteLimit = DEFAULT_INVITE_LIMIT;
|
||||
}
|
||||
next[accountId] = { ...existing, [role]: value, inviteLimit };
|
||||
if (!next[accountId].monitor && !next[accountId].invite && !next[accountId].confirm) {
|
||||
@ -68,7 +70,7 @@ export default function useAccountManagement({
|
||||
const next = { ...taskAccountRoles };
|
||||
if (value) {
|
||||
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 {
|
||||
delete next[accountId];
|
||||
}
|
||||
@ -91,12 +93,12 @@ export default function useAccountManagement({
|
||||
if (type === "all") {
|
||||
availableIds.forEach((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") {
|
||||
const id = availableIds[0];
|
||||
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") {
|
||||
const monitorCount = Math.max(1, Number(taskForm.maxCompetitorBots || 1));
|
||||
const inviteCount = Math.max(1, Number(taskForm.maxOurBots || 1));
|
||||
@ -112,7 +114,7 @@ export default function useAccountManagement({
|
||||
});
|
||||
inviteIds.forEach((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) => {
|
||||
const existing = taskAccountRoles[id] || {};
|
||||
@ -184,13 +186,45 @@ export default function useAccountManagement({
|
||||
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) => {
|
||||
if (!window.api || selectedTaskId == null) return;
|
||||
if (!accountIds.length) return;
|
||||
const nextRoles = { ...taskAccountRoles };
|
||||
accountIds.forEach((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]) => ({
|
||||
@ -289,6 +323,7 @@ export default function useAccountManagement({
|
||||
updateAccountInviteLimit,
|
||||
setAccountRolesAll,
|
||||
applyRolePreset,
|
||||
setInviteLimitForAllInviters,
|
||||
assignAccountsToTask,
|
||||
moveAccountToTask,
|
||||
removeAccountFromTask
|
||||
|
||||
@ -45,6 +45,7 @@ export default function useAppTabGroups({
|
||||
deleteAccount,
|
||||
updateAccountRole,
|
||||
updateAccountInviteLimit,
|
||||
setInviteLimitForAllInviters,
|
||||
setAccountRolesAll,
|
||||
applyRolePreset,
|
||||
removeAccountFromTask,
|
||||
@ -174,6 +175,7 @@ export default function useAppTabGroups({
|
||||
deleteAccount,
|
||||
updateAccountRole,
|
||||
updateAccountInviteLimit,
|
||||
setInviteLimitForAllInviters,
|
||||
setAccountRolesAll,
|
||||
applyRolePreset,
|
||||
removeAccountFromTask,
|
||||
|
||||
@ -56,6 +56,7 @@ export default function useTabProps(
|
||||
deleteAccount,
|
||||
updateAccountRole,
|
||||
updateAccountInviteLimit,
|
||||
setInviteLimitForAllInviters,
|
||||
setAccountRolesAll,
|
||||
applyRolePreset,
|
||||
removeAccountFromTask,
|
||||
@ -189,6 +190,7 @@ export default function useTabProps(
|
||||
deleteAccount,
|
||||
updateAccountRole,
|
||||
updateAccountInviteLimit,
|
||||
setInviteLimitForAllInviters,
|
||||
setAccountRolesAll,
|
||||
applyRolePreset,
|
||||
removeAccountFromTask,
|
||||
|
||||
@ -80,7 +80,8 @@ export default function useTaskActions({
|
||||
}
|
||||
let accountRolesMap = { ...taskAccountRoles };
|
||||
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 pool = (selectedAccountIds && selectedAccountIds.length ? selectedAccountIds : accounts.map((account) => account.id))
|
||||
.filter((id) => Number.isFinite(id));
|
||||
@ -88,18 +89,18 @@ export default function useTaskActions({
|
||||
accountRolesMap = {};
|
||||
chosen.forEach((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;
|
||||
setTaskAccountRoles(accountRolesMap);
|
||||
setSelectedAccountIds(chosen);
|
||||
}
|
||||
if (nextForm.autoAssignAccounts && (!accountIds || accountIds.length === 0)) {
|
||||
if (autoRoleMode && nextForm.autoAssignAccounts && (!accountIds || accountIds.length === 0)) {
|
||||
accountIds = accounts.map((account) => account.id);
|
||||
accountRolesMap = {};
|
||||
accountIds.forEach((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);
|
||||
setSelectedAccountIds(accountIds);
|
||||
|
||||
@ -62,7 +62,7 @@ export default function useTaskLoaders({
|
||||
});
|
||||
} else {
|
||||
(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);
|
||||
|
||||
@ -44,7 +44,7 @@ export default function useTaskPresets({
|
||||
if (!roleMap[id]) roleMap[id] = { monitor: false, invite: false, confirm: false, inviteLimit: 0 };
|
||||
roleMap[id][role] = true;
|
||||
if (role === "invite" && (!roleMap[id].inviteLimit || roleMap[id].inviteLimit === 0)) {
|
||||
roleMap[id].inviteLimit = 1;
|
||||
roleMap[id].inviteLimit = 7;
|
||||
}
|
||||
};
|
||||
const takeFromPool = (count, used) => {
|
||||
|
||||
@ -21,6 +21,13 @@ body {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
margin-top: 8px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
color: rgba(226, 232, 240, 0.7);
|
||||
font-size: 13px;
|
||||
|
||||
@ -22,6 +22,7 @@ function AccountsTab({
|
||||
deleteAccount,
|
||||
updateAccountRole,
|
||||
updateAccountInviteLimit,
|
||||
setInviteLimitForAllInviters,
|
||||
setAccountRolesAll,
|
||||
applyRolePreset,
|
||||
removeAccountFromTask,
|
||||
@ -29,6 +30,7 @@ function AccountsTab({
|
||||
}) {
|
||||
const [membershipModal, setMembershipModal] = useState(null);
|
||||
const [usageModal, setUsageModal] = useState(null);
|
||||
const [bulkInviteLimit, setBulkInviteLimit] = useState(7);
|
||||
|
||||
const openMembershipModal = (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("split")} 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>
|
||||
)}
|
||||
{filterFreeAccounts && (
|
||||
|
||||
@ -30,6 +30,12 @@ export const explainInviteError = (error) => {
|
||||
if (error === "CHAT_MEMBER_ADD_FAILED") {
|
||||
return "Telegram отклонил добавление. Обычно это антиспам-ограничение или недостаток прав.";
|
||||
}
|
||||
if (error === "SOURCE_ADMIN_SKIPPED") {
|
||||
return "Пользователь является администратором в группе конкурента и пропущен по фильтру.";
|
||||
}
|
||||
if (error === "SOURCE_BOT_SKIPPED") {
|
||||
return "Пользователь является ботом в группе конкурента и пропущен по фильтру.";
|
||||
}
|
||||
if (error === "USER_BLOCKED") {
|
||||
return "Пользователь заблокировал аккаунт, который пытается добавить.";
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user