some
This commit is contained in:
parent
d5d2a84a2e
commit
77baea862f
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "telegram-invite-automation",
|
"name": "telegram-invite-automation",
|
||||||
"version": "1.1.0",
|
"version": "1.2.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",
|
||||||
|
|||||||
@ -38,7 +38,8 @@ const filterTaskRolesByAccounts = (taskId, roles, accounts) => {
|
|||||||
filtered.push({
|
filtered.push({
|
||||||
accountId: row.account_id,
|
accountId: row.account_id,
|
||||||
roleMonitor: Boolean(row.role_monitor),
|
roleMonitor: Boolean(row.role_monitor),
|
||||||
roleInvite: Boolean(row.role_invite)
|
roleInvite: Boolean(row.role_invite),
|
||||||
|
roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (removedMissing || removedError) {
|
if (removedMissing || removedError) {
|
||||||
@ -429,7 +430,8 @@ ipcMain.handle("tasks:get", (_event, id) => {
|
|||||||
accountRoles: store.listTaskAccounts(id).map((row) => ({
|
accountRoles: store.listTaskAccounts(id).map((row) => ({
|
||||||
accountId: row.account_id,
|
accountId: row.account_id,
|
||||||
roleMonitor: Boolean(row.role_monitor),
|
roleMonitor: Boolean(row.role_monitor),
|
||||||
roleInvite: Boolean(row.role_invite)
|
roleInvite: Boolean(row.role_invite),
|
||||||
|
roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite)
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -474,6 +476,15 @@ ipcMain.handle("tasks:save", (_event, payload) => {
|
|||||||
if (existing.invite_admin_allow_flood !== (payload.task.inviteAdminAllowFlood ? 1 : 0)) {
|
if (existing.invite_admin_allow_flood !== (payload.task.inviteAdminAllowFlood ? 1 : 0)) {
|
||||||
changes.inviteAdminAllowFlood = [Boolean(existing.invite_admin_allow_flood), Boolean(payload.task.inviteAdminAllowFlood)];
|
changes.inviteAdminAllowFlood = [Boolean(existing.invite_admin_allow_flood), Boolean(payload.task.inviteAdminAllowFlood)];
|
||||||
}
|
}
|
||||||
|
if (existing.invite_admin_anonymous !== (payload.task.inviteAdminAnonymous ? 1 : 0)) {
|
||||||
|
changes.inviteAdminAnonymous = [Boolean(existing.invite_admin_anonymous), Boolean(payload.task.inviteAdminAnonymous)];
|
||||||
|
}
|
||||||
|
if (existing.separate_confirm_roles !== (payload.task.separateConfirmRoles ? 1 : 0)) {
|
||||||
|
changes.separateConfirmRoles = [Boolean(existing.separate_confirm_roles), Boolean(payload.task.separateConfirmRoles)];
|
||||||
|
}
|
||||||
|
if (existing.max_confirm_bots !== Number(payload.task.maxConfirmBots || 0)) {
|
||||||
|
changes.maxConfirmBots = [existing.max_confirm_bots, Number(payload.task.maxConfirmBots || 0)];
|
||||||
|
}
|
||||||
if (existing.warmup_enabled !== (payload.task.warmupEnabled ? 1 : 0)) {
|
if (existing.warmup_enabled !== (payload.task.warmupEnabled ? 1 : 0)) {
|
||||||
changes.warmupEnabled = [Boolean(existing.warmup_enabled), Boolean(payload.task.warmupEnabled)];
|
changes.warmupEnabled = [Boolean(existing.warmup_enabled), Boolean(payload.task.warmupEnabled)];
|
||||||
}
|
}
|
||||||
@ -528,18 +539,25 @@ ipcMain.handle("tasks:appendAccounts", (_event, payload) => {
|
|||||||
const existingRows = store.listTaskAccounts(payload.taskId);
|
const existingRows = store.listTaskAccounts(payload.taskId);
|
||||||
const existing = new Map(existingRows.map((row) => [
|
const existing = new Map(existingRows.map((row) => [
|
||||||
row.account_id,
|
row.account_id,
|
||||||
{ accountId: row.account_id, roleMonitor: Boolean(row.role_monitor), roleInvite: Boolean(row.role_invite) }
|
{
|
||||||
|
accountId: row.account_id,
|
||||||
|
roleMonitor: Boolean(row.role_monitor),
|
||||||
|
roleInvite: Boolean(row.role_invite),
|
||||||
|
roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite)
|
||||||
|
}
|
||||||
]));
|
]));
|
||||||
(payload.accountIds || []).forEach((accountId) => {
|
(payload.accountIds || []).forEach((accountId) => {
|
||||||
if (!existing.has(accountId)) {
|
if (!existing.has(accountId)) {
|
||||||
existing.set(accountId, { accountId, roleMonitor: true, roleInvite: true });
|
existing.set(accountId, { accountId, roleMonitor: true, roleInvite: true, roleConfirm: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
(payload.accountRoles || []).forEach((item) => {
|
(payload.accountRoles || []).forEach((item) => {
|
||||||
|
const roleConfirm = item.roleConfirm != null ? item.roleConfirm : item.roleInvite;
|
||||||
existing.set(item.accountId, {
|
existing.set(item.accountId, {
|
||||||
accountId: item.accountId,
|
accountId: item.accountId,
|
||||||
roleMonitor: Boolean(item.roleMonitor),
|
roleMonitor: Boolean(item.roleMonitor),
|
||||||
roleInvite: Boolean(item.roleInvite)
|
roleInvite: Boolean(item.roleInvite),
|
||||||
|
roleConfirm: Boolean(roleConfirm)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const merged = Array.from(existing.values());
|
const merged = Array.from(existing.values());
|
||||||
@ -557,7 +575,8 @@ ipcMain.handle("tasks:removeAccount", (_event, payload) => {
|
|||||||
.map((row) => ({
|
.map((row) => ({
|
||||||
accountId: row.account_id,
|
accountId: row.account_id,
|
||||||
roleMonitor: Boolean(row.role_monitor),
|
roleMonitor: Boolean(row.role_monitor),
|
||||||
roleInvite: Boolean(row.role_invite)
|
roleInvite: Boolean(row.role_invite),
|
||||||
|
roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite)
|
||||||
}));
|
}));
|
||||||
store.setTaskAccountRoles(payload.taskId, existing);
|
store.setTaskAccountRoles(payload.taskId, existing);
|
||||||
return { ok: true, accountIds: existing.map((item) => item.accountId) };
|
return { ok: true, accountIds: existing.map((item) => item.accountId) };
|
||||||
@ -765,7 +784,8 @@ ipcMain.handle("tasks:checkInviteAccess", async (_event, id) => {
|
|||||||
.map((row) => ({
|
.map((row) => ({
|
||||||
accountId: row.account_id,
|
accountId: row.account_id,
|
||||||
roleMonitor: Boolean(row.role_monitor),
|
roleMonitor: Boolean(row.role_monitor),
|
||||||
roleInvite: Boolean(row.role_invite)
|
roleInvite: Boolean(row.role_invite),
|
||||||
|
roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite)
|
||||||
}));
|
}));
|
||||||
store.setTaskAccountRoles(id, filtered);
|
store.setTaskAccountRoles(id, filtered);
|
||||||
}
|
}
|
||||||
@ -831,11 +851,14 @@ const explainInviteError = (error) => {
|
|||||||
if (error === "CHAT_ADMIN_REQUIRED") {
|
if (error === "CHAT_ADMIN_REQUIRED") {
|
||||||
return "Для добавления участников нужны права администратора.";
|
return "Для добавления участников нужны права администратора.";
|
||||||
}
|
}
|
||||||
|
if (error === "CHANNEL_INVALID") {
|
||||||
|
return "Цель не распознана как группа/канал для этого аккаунта (ссылка недоступна или сущность устарела).";
|
||||||
|
}
|
||||||
if (error === "USER_ALREADY_PARTICIPANT") {
|
if (error === "USER_ALREADY_PARTICIPANT") {
|
||||||
return "Пользователь уже состоит в целевой группе.";
|
return "Пользователь уже состоит в целевой группе.";
|
||||||
}
|
}
|
||||||
if (error === "CHAT_MEMBER_ADD_FAILED") {
|
if (error === "CHAT_MEMBER_ADD_FAILED") {
|
||||||
return "Telegram отклонил добавление участника (ограничения приватности, антиспам или запрет инвайтов).";
|
return "Telegram отказал в добавлении пользователя. Возможные причины: пользователь недоступен для инвайта, неверные данные, ограничения чата или лимиты аккаунта.";
|
||||||
}
|
}
|
||||||
if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") {
|
if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") {
|
||||||
return "Инвайт-ссылка недействительна или истекла.";
|
return "Инвайт-ссылка недействительна или истекла.";
|
||||||
|
|||||||
@ -160,6 +160,9 @@ function initStore(userDataPath) {
|
|||||||
invite_via_admins INTEGER NOT NULL DEFAULT 0,
|
invite_via_admins INTEGER NOT NULL DEFAULT 0,
|
||||||
invite_admin_master_id INTEGER NOT NULL DEFAULT 0,
|
invite_admin_master_id INTEGER NOT NULL DEFAULT 0,
|
||||||
invite_admin_allow_flood INTEGER NOT NULL DEFAULT 0,
|
invite_admin_allow_flood INTEGER NOT NULL DEFAULT 0,
|
||||||
|
invite_admin_anonymous INTEGER NOT NULL DEFAULT 1,
|
||||||
|
separate_confirm_roles INTEGER NOT NULL DEFAULT 0,
|
||||||
|
max_confirm_bots INTEGER NOT NULL DEFAULT 1,
|
||||||
warmup_enabled INTEGER NOT NULL DEFAULT 1,
|
warmup_enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
warmup_start_limit INTEGER NOT NULL DEFAULT 3,
|
warmup_start_limit INTEGER NOT NULL DEFAULT 3,
|
||||||
warmup_daily_increase INTEGER NOT NULL DEFAULT 2,
|
warmup_daily_increase INTEGER NOT NULL DEFAULT 2,
|
||||||
@ -183,7 +186,8 @@ function initStore(userDataPath) {
|
|||||||
task_id INTEGER NOT NULL,
|
task_id INTEGER NOT NULL,
|
||||||
account_id INTEGER NOT NULL,
|
account_id INTEGER NOT NULL,
|
||||||
role_monitor INTEGER NOT NULL DEFAULT 1,
|
role_monitor INTEGER NOT NULL DEFAULT 1,
|
||||||
role_invite INTEGER NOT NULL DEFAULT 1
|
role_invite INTEGER NOT NULL DEFAULT 1,
|
||||||
|
role_confirm INTEGER NOT NULL DEFAULT 1
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS task_audit (
|
CREATE TABLE IF NOT EXISTS task_audit (
|
||||||
@ -251,6 +255,10 @@ function initStore(userDataPath) {
|
|||||||
ensureColumn("tasks", "invite_via_admins", "INTEGER NOT NULL DEFAULT 0");
|
ensureColumn("tasks", "invite_via_admins", "INTEGER NOT NULL DEFAULT 0");
|
||||||
ensureColumn("tasks", "invite_admin_master_id", "INTEGER NOT NULL DEFAULT 0");
|
ensureColumn("tasks", "invite_admin_master_id", "INTEGER NOT NULL DEFAULT 0");
|
||||||
ensureColumn("tasks", "invite_admin_allow_flood", "INTEGER NOT NULL DEFAULT 0");
|
ensureColumn("tasks", "invite_admin_allow_flood", "INTEGER NOT NULL DEFAULT 0");
|
||||||
|
ensureColumn("tasks", "invite_admin_anonymous", "INTEGER NOT NULL DEFAULT 1");
|
||||||
|
ensureColumn("tasks", "separate_confirm_roles", "INTEGER NOT NULL DEFAULT 0");
|
||||||
|
ensureColumn("tasks", "max_confirm_bots", "INTEGER NOT NULL DEFAULT 1");
|
||||||
|
ensureColumn("task_accounts", "role_confirm", "INTEGER NOT NULL DEFAULT 1");
|
||||||
ensureColumn("tasks", "warmup_enabled", "INTEGER NOT NULL DEFAULT 0");
|
ensureColumn("tasks", "warmup_enabled", "INTEGER NOT NULL DEFAULT 0");
|
||||||
ensureColumn("tasks", "warmup_start_limit", "INTEGER NOT NULL DEFAULT 3");
|
ensureColumn("tasks", "warmup_start_limit", "INTEGER NOT NULL DEFAULT 3");
|
||||||
ensureColumn("tasks", "warmup_daily_increase", "INTEGER NOT NULL DEFAULT 2");
|
ensureColumn("tasks", "warmup_daily_increase", "INTEGER NOT NULL DEFAULT 2");
|
||||||
@ -582,7 +590,8 @@ function initStore(userDataPath) {
|
|||||||
retry_on_fail = ?, auto_join_competitors = ?, auto_join_our_group = ?, separate_bot_roles = ?,
|
retry_on_fail = ?, auto_join_competitors = ?, auto_join_our_group = ?, separate_bot_roles = ?,
|
||||||
require_same_bot_in_both = ?, stop_on_blocked = ?, stop_blocked_percent = ?, notes = ?, enabled = ?,
|
require_same_bot_in_both = ?, stop_on_blocked = ?, stop_blocked_percent = ?, notes = ?, enabled = ?,
|
||||||
allow_start_without_invite_rights = ?, parse_participants = ?, invite_via_admins = ?, invite_admin_master_id = ?,
|
allow_start_without_invite_rights = ?, parse_participants = ?, invite_via_admins = ?, invite_admin_master_id = ?,
|
||||||
invite_admin_allow_flood = ?, warmup_enabled = ?, warmup_start_limit = ?, warmup_daily_increase = ?,
|
invite_admin_allow_flood = ?, invite_admin_anonymous = ?, separate_confirm_roles = ?, max_confirm_bots = ?,
|
||||||
|
warmup_enabled = ?, warmup_start_limit = ?, warmup_daily_increase = ?,
|
||||||
cycle_competitors = ?, competitor_cursor = ?, invite_link_on_fail = ?, updated_at = ?
|
cycle_competitors = ?, competitor_cursor = ?, invite_link_on_fail = ?, updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).run(
|
`).run(
|
||||||
@ -611,6 +620,9 @@ function initStore(userDataPath) {
|
|||||||
task.inviteViaAdmins ? 1 : 0,
|
task.inviteViaAdmins ? 1 : 0,
|
||||||
task.inviteAdminMasterId || 0,
|
task.inviteAdminMasterId || 0,
|
||||||
task.inviteAdminAllowFlood ? 1 : 0,
|
task.inviteAdminAllowFlood ? 1 : 0,
|
||||||
|
task.inviteAdminAnonymous ? 1 : 0,
|
||||||
|
task.separateConfirmRoles ? 1 : 0,
|
||||||
|
task.maxConfirmBots || 1,
|
||||||
task.warmupEnabled ? 1 : 0,
|
task.warmupEnabled ? 1 : 0,
|
||||||
task.warmupStartLimit || 3,
|
task.warmupStartLimit || 3,
|
||||||
task.warmupDailyIncrease || 2,
|
task.warmupDailyIncrease || 2,
|
||||||
@ -628,9 +640,10 @@ function initStore(userDataPath) {
|
|||||||
max_invites_per_cycle, max_competitor_bots, max_our_bots, random_accounts, multi_accounts_per_run, retry_on_fail, auto_join_competitors,
|
max_invites_per_cycle, max_competitor_bots, max_our_bots, random_accounts, multi_accounts_per_run, retry_on_fail, auto_join_competitors,
|
||||||
auto_join_our_group, separate_bot_roles, require_same_bot_in_both, stop_on_blocked, stop_blocked_percent, notes, enabled,
|
auto_join_our_group, separate_bot_roles, require_same_bot_in_both, stop_on_blocked, stop_blocked_percent, notes, enabled,
|
||||||
allow_start_without_invite_rights, parse_participants, invite_via_admins, invite_admin_master_id,
|
allow_start_without_invite_rights, parse_participants, invite_via_admins, invite_admin_master_id,
|
||||||
invite_admin_allow_flood, warmup_enabled, warmup_start_limit, warmup_daily_increase, cycle_competitors,
|
invite_admin_allow_flood, invite_admin_anonymous, separate_confirm_roles, max_confirm_bots,
|
||||||
|
warmup_enabled, warmup_start_limit, warmup_daily_increase, cycle_competitors,
|
||||||
competitor_cursor, invite_link_on_fail, created_at, updated_at)
|
competitor_cursor, invite_link_on_fail, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(
|
`).run(
|
||||||
task.name,
|
task.name,
|
||||||
task.ourGroup,
|
task.ourGroup,
|
||||||
@ -657,6 +670,9 @@ function initStore(userDataPath) {
|
|||||||
task.inviteViaAdmins ? 1 : 0,
|
task.inviteViaAdmins ? 1 : 0,
|
||||||
task.inviteAdminMasterId || 0,
|
task.inviteAdminMasterId || 0,
|
||||||
task.inviteAdminAllowFlood ? 1 : 0,
|
task.inviteAdminAllowFlood ? 1 : 0,
|
||||||
|
task.inviteAdminAnonymous ? 1 : 0,
|
||||||
|
task.separateConfirmRoles ? 1 : 0,
|
||||||
|
task.maxConfirmBots || 1,
|
||||||
task.warmupEnabled ? 1 : 0,
|
task.warmupEnabled ? 1 : 0,
|
||||||
task.warmupStartLimit || 3,
|
task.warmupStartLimit || 3,
|
||||||
task.warmupDailyIncrease || 2,
|
task.warmupDailyIncrease || 2,
|
||||||
@ -709,24 +725,26 @@ function initStore(userDataPath) {
|
|||||||
function setTaskAccounts(taskId, accountIds) {
|
function setTaskAccounts(taskId, accountIds) {
|
||||||
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId);
|
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId);
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite)
|
INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite, role_confirm)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
(accountIds || []).forEach((accountId) => stmt.run(taskId, accountId, 1, 1));
|
(accountIds || []).forEach((accountId) => stmt.run(taskId, accountId, 1, 1, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTaskAccountRoles(taskId, roles) {
|
function setTaskAccountRoles(taskId, roles) {
|
||||||
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId);
|
db.prepare("DELETE FROM task_accounts WHERE task_id = ?").run(taskId);
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite)
|
INSERT INTO task_accounts (task_id, account_id, role_monitor, role_invite, role_confirm)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
(roles || []).forEach((item) => {
|
(roles || []).forEach((item) => {
|
||||||
|
const roleConfirm = item.roleConfirm != null ? item.roleConfirm : item.roleInvite;
|
||||||
stmt.run(
|
stmt.run(
|
||||||
taskId,
|
taskId,
|
||||||
item.accountId,
|
item.accountId,
|
||||||
item.roleMonitor ? 1 : 0,
|
item.roleMonitor ? 1 : 0,
|
||||||
item.roleInvite ? 1 : 0
|
item.roleInvite ? 1 : 0,
|
||||||
|
roleConfirm ? 1 : 0
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,9 +60,11 @@ class TaskRunner {
|
|||||||
const accounts = accountRows.map((row) => row.account_id);
|
const accounts = accountRows.map((row) => row.account_id);
|
||||||
const monitorIds = accountRows.filter((row) => row.role_monitor).map((row) => row.account_id);
|
const monitorIds = accountRows.filter((row) => row.role_monitor).map((row) => row.account_id);
|
||||||
const inviteIds = accountRows.filter((row) => row.role_invite).map((row) => row.account_id);
|
const inviteIds = accountRows.filter((row) => row.role_invite).map((row) => row.account_id);
|
||||||
|
const confirmIds = accountRows.filter((row) => row.role_confirm).map((row) => row.account_id);
|
||||||
await this.telegram.joinGroupsForTask(this.task, competitors, accounts, {
|
await this.telegram.joinGroupsForTask(this.task, competitors, accounts, {
|
||||||
monitorIds,
|
monitorIds,
|
||||||
inviteIds
|
inviteIds,
|
||||||
|
confirmIds
|
||||||
});
|
});
|
||||||
await this.telegram.startTaskMonitor(this.task, competitors, accounts, monitorIds);
|
await this.telegram.startTaskMonitor(this.task, competitors, accounts, monitorIds);
|
||||||
}
|
}
|
||||||
@ -96,12 +98,16 @@ class TaskRunner {
|
|||||||
if (ttlHours > 0) {
|
if (ttlHours > 0) {
|
||||||
this.store.clearQueueOlderThan(this.task.id, ttlHours);
|
this.store.clearQueueOlderThan(this.task.id, ttlHours);
|
||||||
}
|
}
|
||||||
const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id);
|
const accountRows = this.store.listTaskAccounts(this.task.id);
|
||||||
|
const accounts = accountRows.map((row) => row.account_id);
|
||||||
|
const explicitInviteIds = accountRows.filter((row) => row.role_invite).map((row) => row.account_id);
|
||||||
let inviteAccounts = accounts;
|
let inviteAccounts = accounts;
|
||||||
const roles = this.telegram.getTaskRoleAssignments(this.task.id);
|
const roles = this.telegram.getTaskRoleAssignments(this.task.id);
|
||||||
const hasExplicitRoles = roles && ((roles.ourIds || []).length || (roles.competitorIds || []).length);
|
const hasExplicitRoles = roles && ((roles.ourIds || []).length || (roles.competitorIds || []).length);
|
||||||
if (hasExplicitRoles) {
|
if (explicitInviteIds.length) {
|
||||||
inviteAccounts = (roles.ourIds || []).length ? roles.ourIds : (roles.competitorIds || []);
|
inviteAccounts = explicitInviteIds.slice();
|
||||||
|
} else if (hasExplicitRoles) {
|
||||||
|
inviteAccounts = (roles.ourIds || []).length ? roles.ourIds : [];
|
||||||
if (!inviteAccounts.length) {
|
if (!inviteAccounts.length) {
|
||||||
errors.push("No invite accounts (role-based)");
|
errors.push("No invite accounts (role-based)");
|
||||||
}
|
}
|
||||||
@ -193,10 +199,7 @@ class TaskRunner {
|
|||||||
this.store.markInviteStatus(item.id, "failed");
|
this.store.markInviteStatus(item.id, "failed");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let accountsForInvite = inviteAccounts;
|
const accountsForInvite = inviteAccounts;
|
||||||
if (item.watcher_account_id && !inviteAccounts.includes(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);
|
||||||
const result = await this.telegram.inviteUserForTask(this.task, item.user_id, accountsForInvite, {
|
const result = await this.telegram.inviteUserForTask(this.task, item.user_id, accountsForInvite, {
|
||||||
randomize: Boolean(this.task.random_accounts),
|
randomize: Boolean(this.task.random_accounts),
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class TelegramManager {
|
|||||||
this.authKeyResetDone = false;
|
this.authKeyResetDone = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildInviteAdminRights() {
|
_buildInviteAdminRights(allowAnonymous = false) {
|
||||||
return new Api.ChatAdminRights({
|
return new Api.ChatAdminRights({
|
||||||
inviteUsers: true,
|
inviteUsers: true,
|
||||||
addUsers: true,
|
addUsers: true,
|
||||||
@ -36,7 +36,7 @@ class TelegramManager {
|
|||||||
manageTopics: false,
|
manageTopics: false,
|
||||||
postMessages: false,
|
postMessages: false,
|
||||||
editMessages: false,
|
editMessages: false,
|
||||||
anonymous: false
|
anonymous: Boolean(allowAnonymous)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,8 +86,8 @@ class TelegramManager {
|
|||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
async _grantTempInviteAdmin(masterClient, targetEntity, account) {
|
async _grantTempInviteAdmin(masterClient, targetEntity, account, allowAnonymous = false) {
|
||||||
const rights = this._buildInviteAdminRights();
|
const rights = this._buildInviteAdminRights(allowAnonymous);
|
||||||
const identifier = account.user_id
|
const identifier = account.user_id
|
||||||
? BigInt(account.user_id)
|
? BigInt(account.user_id)
|
||||||
: (account.username ? `@${account.username}` : "");
|
: (account.username ? `@${account.username}` : "");
|
||||||
@ -555,6 +555,16 @@ class TelegramManager {
|
|||||||
const base = message ? `${code}: ${message}` : code;
|
const base = message ? `${code}: ${message}` : code;
|
||||||
return sourceLabel ? `${base} (${sourceLabel})` : base;
|
return sourceLabel ? `${base} (${sourceLabel})` : base;
|
||||||
};
|
};
|
||||||
|
const formatAccountSource = (label, accountEntry) => {
|
||||||
|
if (label) return label;
|
||||||
|
if (!accountEntry || !accountEntry.account) return "";
|
||||||
|
const phone = accountEntry.account.phone || "";
|
||||||
|
const username = accountEntry.account.username ? `@${accountEntry.account.username}` : "";
|
||||||
|
if (phone && username) return `проверка аккаунтом ${phone} (${username})`;
|
||||||
|
if (phone) return `проверка аккаунтом ${phone}`;
|
||||||
|
if (username) return `проверка аккаунтом ${username}`;
|
||||||
|
return "проверка аккаунтом";
|
||||||
|
};
|
||||||
const confirmMembership = async (user, confirmClient = client, sourceLabel = "") => {
|
const confirmMembership = async (user, confirmClient = client, sourceLabel = "") => {
|
||||||
if (!targetEntity || targetEntity.className !== "Channel") {
|
if (!targetEntity || targetEntity.className !== "Channel") {
|
||||||
return { confirmed: true, error: "", detail: "" };
|
return { confirmed: true, error: "", detail: "" };
|
||||||
@ -564,7 +574,7 @@ class TelegramManager {
|
|||||||
channel: targetEntity,
|
channel: targetEntity,
|
||||||
participant: user
|
participant: user
|
||||||
}));
|
}));
|
||||||
return { confirmed: true, error: "", detail: "" };
|
return { confirmed: true, error: "", detail: sourceLabel ? `OK (${sourceLabel})` : "OK" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error.errorMessage || error.message || String(error);
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
if (errorText.includes("USER_NOT_PARTICIPANT")) {
|
if (errorText.includes("USER_NOT_PARTICIPANT")) {
|
||||||
@ -588,25 +598,57 @@ class TelegramManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const confirmMembershipWithFallback = async (user) => {
|
const confirmMembershipWithFallback = async (user, inviterEntry = null) => {
|
||||||
const attempts = [];
|
const attempts = [];
|
||||||
const direct = await confirmMembership(user, client, "проверка этим аккаунтом");
|
const triedClients = new Set();
|
||||||
|
const directLabel = formatAccountSource("", inviterEntry);
|
||||||
|
const direct = await confirmMembership(user, client, directLabel || "проверка этим аккаунтом");
|
||||||
if (direct.detail) {
|
if (direct.detail) {
|
||||||
attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail });
|
attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail });
|
||||||
}
|
}
|
||||||
|
triedClients.add(client);
|
||||||
if (direct.confirmed !== null) {
|
if (direct.confirmed !== null) {
|
||||||
return { ...direct, attempts };
|
return { ...direct, attempts };
|
||||||
}
|
}
|
||||||
|
let finalResult = direct;
|
||||||
|
const roleAssignments = this.taskRoleAssignments.get(task.id) || {};
|
||||||
|
const confirmIds = Array.isArray(roleAssignments.confirmIds) ? roleAssignments.confirmIds : [];
|
||||||
|
for (const confirmId of confirmIds) {
|
||||||
|
const entry = this.clients.get(confirmId);
|
||||||
|
if (!entry || !entry.client || triedClients.has(entry.client)) continue;
|
||||||
|
triedClients.add(entry.client);
|
||||||
|
const label = formatAccountSource("проверка подтверждающим аккаунтом", entry);
|
||||||
|
const confirmResult = await confirmMembership(user, entry.client, label);
|
||||||
|
if (confirmResult.detail) {
|
||||||
|
attempts.push({ strategy: "confirm_role", ok: confirmResult.confirmed === true, detail: confirmResult.detail });
|
||||||
|
}
|
||||||
|
finalResult = confirmResult;
|
||||||
|
if (confirmResult.confirmed !== null || confirmResult.detail) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (finalResult.confirmed === null && !finalResult.detail) {
|
||||||
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;
|
||||||
if (masterEntry && masterEntry.client && masterEntry.client !== client) {
|
if (masterEntry && masterEntry.client && !triedClients.has(masterEntry.client)) {
|
||||||
const adminConfirm = await confirmMembership(user, masterEntry.client, "проверка админом");
|
const adminLabel = formatAccountSource("проверка админом", masterEntry);
|
||||||
|
const adminConfirm = await confirmMembership(user, masterEntry.client, adminLabel);
|
||||||
if (adminConfirm.detail) {
|
if (adminConfirm.detail) {
|
||||||
attempts.push({ strategy: "confirm_admin", ok: adminConfirm.confirmed === true, detail: adminConfirm.detail });
|
attempts.push({ strategy: "confirm_admin", ok: adminConfirm.confirmed === true, detail: adminConfirm.detail });
|
||||||
}
|
}
|
||||||
return { ...adminConfirm, attempts };
|
finalResult = adminConfirm;
|
||||||
}
|
}
|
||||||
return { ...direct, attempts };
|
}
|
||||||
|
if (finalResult.confirmed === null && !finalResult.detail) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||||
|
const retryLabel = directLabel ? `${directLabel}, повтор через 10с` : "проверка этим аккаунтом, повтор через 10с";
|
||||||
|
const retry = await confirmMembership(user, client, retryLabel);
|
||||||
|
if (retry.detail) {
|
||||||
|
attempts.push({ strategy: "confirm_retry", ok: retry.confirmed === true, detail: retry.detail });
|
||||||
|
}
|
||||||
|
finalResult = retry;
|
||||||
|
}
|
||||||
|
return { ...finalResult, attempts };
|
||||||
};
|
};
|
||||||
const attemptInvite = async (user) => {
|
const attemptInvite = async (user) => {
|
||||||
if (!targetEntity) {
|
if (!targetEntity) {
|
||||||
@ -631,14 +673,50 @@ class TelegramManager {
|
|||||||
throw new Error("Unsupported target chat type");
|
throw new Error("Unsupported target chat type");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const attemptAdminInvite = async (user, adminClient = client) => {
|
const explainWriteForbidden = async () => {
|
||||||
|
if (!targetEntity) return "Цель не определена";
|
||||||
|
try {
|
||||||
|
if (targetEntity.className === "Channel") {
|
||||||
|
const me = await client.getMe();
|
||||||
|
const participant = await client.invoke(new Api.channels.GetParticipant({
|
||||||
|
channel: targetEntity,
|
||||||
|
participant: me
|
||||||
|
}));
|
||||||
|
const part = participant && participant.participant ? participant.participant : participant;
|
||||||
|
const className = part && part.className ? part.className : "";
|
||||||
|
const isCreator = className.includes("Creator");
|
||||||
|
const isAdmin = className.includes("Admin") || isCreator;
|
||||||
|
const rights = part && part.adminRights ? part.adminRights : null;
|
||||||
|
const canInvite = rights ? Boolean(rights.inviteUsers || rights.addUsers) : false;
|
||||||
|
if (!part) return "Аккаунт не состоит в группе назначения.";
|
||||||
|
if (!isAdmin) return "Аккаунт не админ в группе назначения.";
|
||||||
|
if (!canInvite) return "У администратора нет права приглашать.";
|
||||||
|
return "Недостаточно прав для инвайта (проверьте настройки группы).";
|
||||||
|
}
|
||||||
|
if (targetEntity.className === "Chat") {
|
||||||
|
const fullChat = await client.invoke(new Api.messages.GetFullChat({ chatId: targetEntity.id }));
|
||||||
|
const full = fullChat && fullChat.fullChat ? fullChat.fullChat : null;
|
||||||
|
const restricted = Boolean(full && full.defaultBannedRights && full.defaultBannedRights.inviteUsers);
|
||||||
|
if (restricted) return "В группе запрещено приглашать участников.";
|
||||||
|
return "Недостаточно прав для инвайта в группе.";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const text = error.errorMessage || error.message || String(error);
|
||||||
|
if (String(text).includes("USER_NOT_PARTICIPANT")) {
|
||||||
|
return "Аккаунт не состоит в группе назначения.";
|
||||||
|
}
|
||||||
|
return `Не удалось уточнить причину: ${text}`;
|
||||||
|
}
|
||||||
|
return "Недостаточно прав для инвайта.";
|
||||||
|
};
|
||||||
|
const attemptAdminInvite = async (user, adminClient = client, allowAnonymous = false) => {
|
||||||
if (!targetEntity) {
|
if (!targetEntity) {
|
||||||
throw new Error("Target group not resolved");
|
throw new Error("Target group not resolved");
|
||||||
}
|
}
|
||||||
if (targetEntity.className !== "Channel") {
|
if (targetEntity.className !== "Channel") {
|
||||||
throw new Error("ADMIN_INVITE_UNSUPPORTED_TARGET");
|
throw new Error("ADMIN_INVITE_UNSUPPORTED_TARGET");
|
||||||
}
|
}
|
||||||
const rights = this._buildInviteAdminRights();
|
const rights = this._buildInviteAdminRights(allowAnonymous);
|
||||||
await adminClient.invoke(new Api.channels.EditAdmin({
|
await adminClient.invoke(new Api.channels.EditAdmin({
|
||||||
channel: targetEntity,
|
channel: targetEntity,
|
||||||
userId: user,
|
userId: user,
|
||||||
@ -737,10 +815,14 @@ class TelegramManager {
|
|||||||
const masterEntry = masterId ? this.clients.get(masterId) : null;
|
const masterEntry = masterId ? this.clients.get(masterId) : null;
|
||||||
if (masterEntry && masterId !== account.id) {
|
if (masterEntry && masterId !== account.id) {
|
||||||
try {
|
try {
|
||||||
await this._grantTempInviteAdmin(masterEntry.client, targetEntity, account);
|
await this._grantTempInviteAdmin(masterEntry.client, targetEntity, account, Boolean(task.invite_admin_anonymous));
|
||||||
lastAttempts.push({ strategy: "temp_admin", ok: true, detail: "granted" });
|
lastAttempts.push({ strategy: "temp_admin", ok: true, detail: "granted" });
|
||||||
await attemptInvite(user);
|
await attemptInvite(user);
|
||||||
const confirm = await confirmMembershipWithFallback(user);
|
const confirm = await confirmMembershipWithFallback(user, entry);
|
||||||
|
if (confirm.confirmed !== true && !confirm.detail) {
|
||||||
|
const label = formatAccountSource("", entry) || "проверка этим аккаунтом";
|
||||||
|
confirm.detail = buildConfirmDetail("CONFIRM_UNKNOWN", "результат проверки не определен", label);
|
||||||
|
}
|
||||||
if (confirm.attempts && confirm.attempts.length) {
|
if (confirm.attempts && confirm.attempts.length) {
|
||||||
lastAttempts.push(...confirm.attempts);
|
lastAttempts.push(...confirm.attempts);
|
||||||
}
|
}
|
||||||
@ -775,8 +857,12 @@ class TelegramManager {
|
|||||||
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;
|
||||||
const adminClient = masterEntry ? masterEntry.client : client;
|
const adminClient = masterEntry ? masterEntry.client : client;
|
||||||
await attemptAdminInvite(user, adminClient);
|
await attemptAdminInvite(user, adminClient, Boolean(task.invite_admin_anonymous));
|
||||||
const confirm = await confirmMembershipWithFallback(user);
|
const confirm = await confirmMembershipWithFallback(user, entry);
|
||||||
|
if (confirm.confirmed !== true && !confirm.detail) {
|
||||||
|
const label = formatAccountSource("", entry) || "проверка этим аккаунтом";
|
||||||
|
confirm.detail = buildConfirmDetail("CONFIRM_UNKNOWN", "результат проверки не определен", label);
|
||||||
|
}
|
||||||
if (confirm.attempts && confirm.attempts.length) {
|
if (confirm.attempts && confirm.attempts.length) {
|
||||||
lastAttempts.push(...confirm.attempts);
|
lastAttempts.push(...confirm.attempts);
|
||||||
}
|
}
|
||||||
@ -795,6 +881,13 @@ class TelegramManager {
|
|||||||
} catch (adminError) {
|
} catch (adminError) {
|
||||||
const adminText = adminError.errorMessage || adminError.message || String(adminError);
|
const adminText = adminError.errorMessage || adminError.message || String(adminError);
|
||||||
if (adminText.includes("CHANNEL_INVALID")) {
|
if (adminText.includes("CHANNEL_INVALID")) {
|
||||||
|
const targetLabel = task.our_group || "";
|
||||||
|
const entityType = targetEntity && targetEntity.className ? targetEntity.className : "unknown";
|
||||||
|
lastAttempts.push({
|
||||||
|
strategy: "admin_invite",
|
||||||
|
ok: false,
|
||||||
|
detail: `CHANNEL_INVALID; target=${targetLabel}; entity=${entityType}`
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const retryResolved = await this._resolveGroupEntity(client, task.our_group, Boolean(task.auto_join_our_group), account);
|
const retryResolved = await this._resolveGroupEntity(client, task.our_group, Boolean(task.auto_join_our_group), account);
|
||||||
if (retryResolved.ok) {
|
if (retryResolved.ok) {
|
||||||
@ -813,7 +906,11 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await attemptInvite(user);
|
await attemptInvite(user);
|
||||||
const confirm = await confirmMembershipWithFallback(user);
|
const confirm = await confirmMembershipWithFallback(user, entry);
|
||||||
|
if (confirm.confirmed !== true && !confirm.detail) {
|
||||||
|
const label = formatAccountSource("", entry) || "проверка этим аккаунтом";
|
||||||
|
confirm.detail = buildConfirmDetail("CONFIRM_UNKNOWN", "результат проверки не определен", label);
|
||||||
|
}
|
||||||
if (confirm.attempts && confirm.attempts.length) {
|
if (confirm.attempts && confirm.attempts.length) {
|
||||||
lastAttempts.push(...confirm.attempts);
|
lastAttempts.push(...confirm.attempts);
|
||||||
}
|
}
|
||||||
@ -832,6 +929,14 @@ class TelegramManager {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error.errorMessage || error.message || String(error);
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
|
if (errorText.includes("CHAT_WRITE_FORBIDDEN")) {
|
||||||
|
try {
|
||||||
|
const reason = await explainWriteForbidden();
|
||||||
|
lastAttempts.push({ strategy: "invite_access", ok: false, detail: `CHAT_WRITE_FORBIDDEN: ${reason}` });
|
||||||
|
} catch (diagError) {
|
||||||
|
// ignore diagnostics errors
|
||||||
|
}
|
||||||
|
}
|
||||||
this._handleAuthKeyDuplicated(errorText);
|
this._handleAuthKeyDuplicated(errorText);
|
||||||
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") {
|
||||||
@ -1453,7 +1558,7 @@ class TelegramManager {
|
|||||||
return { ok: false, error: "Admin invite поддерживается только для супергрупп" };
|
return { ok: false, error: "Admin invite поддерживается только для супергрупп" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const rights = this._buildInviteAdminRights();
|
const rights = this._buildInviteAdminRights(Boolean(task.invite_admin_anonymous));
|
||||||
const accounts = this.store.listAccounts();
|
const accounts = this.store.listAccounts();
|
||||||
const accountMap = new Map(accounts.map((acc) => [acc.id, acc]));
|
const accountMap = new Map(accounts.map((acc) => [acc.id, acc]));
|
||||||
const results = [];
|
const results = [];
|
||||||
@ -1683,7 +1788,8 @@ class TelegramManager {
|
|||||||
|
|
||||||
const explicitMonitorIds = Array.isArray(roleIds.monitorIds) ? roleIds.monitorIds : [];
|
const explicitMonitorIds = Array.isArray(roleIds.monitorIds) ? roleIds.monitorIds : [];
|
||||||
const explicitInviteIds = Array.isArray(roleIds.inviteIds) ? roleIds.inviteIds : [];
|
const explicitInviteIds = Array.isArray(roleIds.inviteIds) ? roleIds.inviteIds : [];
|
||||||
const hasExplicitRoles = explicitMonitorIds.length || explicitInviteIds.length;
|
const explicitConfirmIds = Array.isArray(roleIds.confirmIds) ? roleIds.confirmIds : [];
|
||||||
|
const hasExplicitRoles = explicitMonitorIds.length || explicitInviteIds.length || explicitConfirmIds.length;
|
||||||
|
|
||||||
const competitors = competitorGroups || [];
|
const competitors = competitorGroups || [];
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
@ -1715,7 +1821,9 @@ class TelegramManager {
|
|||||||
const usedForOur = new Set();
|
const usedForOur = new Set();
|
||||||
if (task.our_group) {
|
if (task.our_group) {
|
||||||
if (hasExplicitRoles) {
|
if (hasExplicitRoles) {
|
||||||
const pool = accounts.filter((entry) => explicitInviteIds.includes(entry.account.id));
|
const pool = accounts.filter((entry) =>
|
||||||
|
explicitInviteIds.includes(entry.account.id) || explicitConfirmIds.includes(entry.account.id)
|
||||||
|
);
|
||||||
for (const entry of pool) {
|
for (const entry of pool) {
|
||||||
usedForOur.add(entry.account.id);
|
usedForOur.add(entry.account.id);
|
||||||
if (task.auto_join_our_group) {
|
if (task.auto_join_our_group) {
|
||||||
@ -1747,7 +1855,8 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
this.taskRoleAssignments.set(task.id, {
|
this.taskRoleAssignments.set(task.id, {
|
||||||
competitorIds: hasExplicitRoles ? explicitMonitorIds : Array.from(usedForCompetitors),
|
competitorIds: hasExplicitRoles ? explicitMonitorIds : Array.from(usedForCompetitors),
|
||||||
ourIds: hasExplicitRoles ? explicitInviteIds : Array.from(usedForOur)
|
ourIds: hasExplicitRoles ? explicitInviteIds : Array.from(usedForOur),
|
||||||
|
confirmIds: hasExplicitRoles ? explicitConfirmIds : Array.from(usedForOur)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1945,6 +2054,12 @@ class TelegramManager {
|
|||||||
accessHash,
|
accessHash,
|
||||||
monitorAccount.account.id
|
monitorAccount.account.id
|
||||||
);
|
);
|
||||||
|
const sender = message && message.sender ? message.sender : null;
|
||||||
|
const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : "";
|
||||||
|
const messageText = message && message.message ? String(message.message).trim() : "";
|
||||||
|
const messagePreview = messageText.length > 160 ? `${messageText.slice(0, 157)}...` : messageText;
|
||||||
|
const messageSuffix = messagePreview ? `\nСообщение: ${messagePreview}` : "";
|
||||||
|
const senderLabel = senderName ? `${senderName} ` : "";
|
||||||
if (enqueued) {
|
if (enqueued) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastEvent = monitorEntry.lastMessageEventAt.get(chatId) || 0;
|
const lastEvent = monitorEntry.lastMessageEventAt.get(chatId) || 0;
|
||||||
@ -1954,7 +2069,7 @@ class TelegramManager {
|
|||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"new_message",
|
"new_message",
|
||||||
`${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}`
|
`${formatGroupLabel(st)}: ${senderLabel}${username ? `@${username}` : senderId}${messageSuffix}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (shouldLogEvent(`${chatId}:dup`, 30000)) {
|
} else if (shouldLogEvent(`${chatId}:dup`, 30000)) {
|
||||||
@ -1964,7 +2079,7 @@ class TelegramManager {
|
|||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"new_message_duplicate",
|
"new_message_duplicate",
|
||||||
`${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}${suffix}`
|
`${formatGroupLabel(st)}: ${senderLabel}${username ? `@${username}` : senderId}${suffix}${messageSuffix}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -2099,6 +2214,12 @@ class TelegramManager {
|
|||||||
monitorEntry.lastSource = st.source;
|
monitorEntry.lastSource = st.source;
|
||||||
if (this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id)) {
|
if (this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id)) {
|
||||||
enqueued += 1;
|
enqueued += 1;
|
||||||
|
const sender = message && message.sender ? message.sender : null;
|
||||||
|
const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : "";
|
||||||
|
const messageText = message && message.message ? String(message.message).trim() : "";
|
||||||
|
const messagePreview = messageText.length > 160 ? `${messageText.slice(0, 157)}...` : messageText;
|
||||||
|
const messageSuffix = messagePreview ? `\nСообщение: ${messagePreview}` : "";
|
||||||
|
const senderLabel = senderName ? `${senderName} ` : "";
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastEvent = monitorEntry.lastMessageEventAt.get(key) || 0;
|
const lastEvent = monitorEntry.lastMessageEventAt.get(key) || 0;
|
||||||
if (now - lastEvent > 15000) {
|
if (now - lastEvent > 15000) {
|
||||||
@ -2107,17 +2228,23 @@ class TelegramManager {
|
|||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"new_message",
|
"new_message",
|
||||||
`${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}`
|
`${formatGroupLabel(st)}: ${senderLabel}${username ? `@${username}` : senderId}${messageSuffix}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (shouldLogEvent(`${key}:dup`, 30000)) {
|
} else if (shouldLogEvent(`${key}:dup`, 30000)) {
|
||||||
const status = this.store.getInviteStatus(task.id, senderId, st.source);
|
const status = this.store.getInviteStatus(task.id, senderId, st.source);
|
||||||
const suffix = status && status !== "pending" ? ` (уже обработан: ${status})` : " (уже в очереди)";
|
const suffix = status && status !== "pending" ? ` (уже обработан: ${status})` : " (уже в очереди)";
|
||||||
|
const sender = message && message.sender ? message.sender : null;
|
||||||
|
const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : "";
|
||||||
|
const messageText = message && message.message ? String(message.message).trim() : "";
|
||||||
|
const messagePreview = messageText.length > 160 ? `${messageText.slice(0, 157)}...` : messageText;
|
||||||
|
const messageSuffix = messagePreview ? `\nСообщение: ${messagePreview}` : "";
|
||||||
|
const senderLabel = senderName ? `${senderName} ` : "";
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"new_message_duplicate",
|
"new_message_duplicate",
|
||||||
`${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}${suffix}`
|
`${formatGroupLabel(st)}: ${senderLabel}${username ? `@${username}` : senderId}${suffix}${messageSuffix}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2521,7 +2648,7 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTaskRoleAssignments(taskId) {
|
getTaskRoleAssignments(taskId) {
|
||||||
return this.taskRoleAssignments.get(taskId) || { competitorIds: [], ourIds: [] };
|
return this.taskRoleAssignments.get(taskId) || { competitorIds: [], ourIds: [], confirmIds: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
getTaskMonitorInfo(taskId) {
|
getTaskMonitorInfo(taskId) {
|
||||||
|
|||||||
@ -40,6 +40,9 @@ const emptySettings = {
|
|||||||
inviteViaAdmins: false,
|
inviteViaAdmins: false,
|
||||||
inviteAdminMasterId: 0,
|
inviteAdminMasterId: 0,
|
||||||
inviteAdminAllowFlood: false,
|
inviteAdminAllowFlood: false,
|
||||||
|
inviteAdminAnonymous: true,
|
||||||
|
separateConfirmRoles: false,
|
||||||
|
maxConfirmBots: 1,
|
||||||
warmupEnabled: true,
|
warmupEnabled: true,
|
||||||
warmupStartLimit: 3,
|
warmupStartLimit: 3,
|
||||||
warmupDailyIncrease: 2,
|
warmupDailyIncrease: 2,
|
||||||
@ -76,6 +79,9 @@ const emptySettings = {
|
|||||||
inviteViaAdmins: Boolean(row.invite_via_admins),
|
inviteViaAdmins: Boolean(row.invite_via_admins),
|
||||||
inviteAdminMasterId: Number(row.invite_admin_master_id || 0),
|
inviteAdminMasterId: Number(row.invite_admin_master_id || 0),
|
||||||
inviteAdminAllowFlood: Boolean(row.invite_admin_allow_flood),
|
inviteAdminAllowFlood: Boolean(row.invite_admin_allow_flood),
|
||||||
|
inviteAdminAnonymous: row.invite_admin_anonymous == null ? true : Boolean(row.invite_admin_anonymous),
|
||||||
|
separateConfirmRoles: Boolean(row.separate_confirm_roles),
|
||||||
|
maxConfirmBots: Number(row.max_confirm_bots || 1),
|
||||||
warmupEnabled: row.warmup_enabled == null ? true : Boolean(row.warmup_enabled),
|
warmupEnabled: row.warmup_enabled == null ? true : Boolean(row.warmup_enabled),
|
||||||
warmupStartLimit: Number(row.warmup_start_limit || 3),
|
warmupStartLimit: Number(row.warmup_start_limit || 3),
|
||||||
warmupDailyIncrease: Number(row.warmup_daily_increase || 2),
|
warmupDailyIncrease: Number(row.warmup_daily_increase || 2),
|
||||||
@ -106,6 +112,9 @@ const sanitizeTaskForm = (form) => {
|
|||||||
normalized.separateBotRoles = false;
|
normalized.separateBotRoles = false;
|
||||||
normalized.maxOurBots = normalized.maxCompetitorBots;
|
normalized.maxOurBots = normalized.maxCompetitorBots;
|
||||||
}
|
}
|
||||||
|
if (!normalized.separateBotRoles) {
|
||||||
|
normalized.separateConfirmRoles = false;
|
||||||
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -124,6 +133,8 @@ export default function App() {
|
|||||||
const [competitorText, setCompetitorText] = useState("");
|
const [competitorText, setCompetitorText] = useState("");
|
||||||
const [selectedAccountIds, setSelectedAccountIds] = useState([]);
|
const [selectedAccountIds, setSelectedAccountIds] = useState([]);
|
||||||
const [taskAccountRoles, setTaskAccountRoles] = useState({});
|
const [taskAccountRoles, setTaskAccountRoles] = useState({});
|
||||||
|
const [activePreset, setActivePreset] = useState("");
|
||||||
|
const presetSignatureRef = useRef("");
|
||||||
const [taskStatus, setTaskStatus] = useState({
|
const [taskStatus, setTaskStatus] = useState({
|
||||||
running: false,
|
running: false,
|
||||||
queueCount: 0,
|
queueCount: 0,
|
||||||
@ -275,12 +286,14 @@ export default function App() {
|
|||||||
const roleSummary = useMemo(() => {
|
const roleSummary = useMemo(() => {
|
||||||
const monitor = [];
|
const monitor = [];
|
||||||
const invite = [];
|
const invite = [];
|
||||||
|
const confirm = [];
|
||||||
Object.entries(taskAccountRoles).forEach(([id, roles]) => {
|
Object.entries(taskAccountRoles).forEach(([id, roles]) => {
|
||||||
const accountId = Number(id);
|
const accountId = Number(id);
|
||||||
if (roles.monitor) monitor.push(accountId);
|
if (roles.monitor) monitor.push(accountId);
|
||||||
if (roles.invite) invite.push(accountId);
|
if (roles.invite) invite.push(accountId);
|
||||||
|
if (roles.confirm) confirm.push(accountId);
|
||||||
});
|
});
|
||||||
return { monitor, invite };
|
return { monitor, invite, confirm };
|
||||||
}, [taskAccountRoles]);
|
}, [taskAccountRoles]);
|
||||||
const roleIntersectionCount = useMemo(() => {
|
const roleIntersectionCount = useMemo(() => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@ -290,7 +303,7 @@ export default function App() {
|
|||||||
return count;
|
return count;
|
||||||
}, [taskAccountRoles]);
|
}, [taskAccountRoles]);
|
||||||
const assignedAccountCount = useMemo(() => {
|
const assignedAccountCount = useMemo(() => {
|
||||||
const ids = new Set([...roleSummary.monitor, ...roleSummary.invite]);
|
const ids = new Set([...roleSummary.monitor, ...roleSummary.invite, ...roleSummary.confirm]);
|
||||||
return ids.size;
|
return ids.size;
|
||||||
}, [roleSummary]);
|
}, [roleSummary]);
|
||||||
const assignedAccountMap = useMemo(() => {
|
const assignedAccountMap = useMemo(() => {
|
||||||
@ -300,7 +313,8 @@ export default function App() {
|
|||||||
list.push({
|
list.push({
|
||||||
taskId: row.task_id,
|
taskId: row.task_id,
|
||||||
roleMonitor: Boolean(row.role_monitor),
|
roleMonitor: Boolean(row.role_monitor),
|
||||||
roleInvite: Boolean(row.role_invite)
|
roleInvite: Boolean(row.role_invite),
|
||||||
|
roleConfirm: row.role_confirm != null ? Boolean(row.role_confirm) : Boolean(row.role_invite)
|
||||||
});
|
});
|
||||||
map.set(row.account_id, list);
|
map.set(row.account_id, list);
|
||||||
});
|
});
|
||||||
@ -400,14 +414,16 @@ export default function App() {
|
|||||||
const roleMap = {};
|
const roleMap = {};
|
||||||
if (details.accountRoles && details.accountRoles.length) {
|
if (details.accountRoles && details.accountRoles.length) {
|
||||||
details.accountRoles.forEach((item) => {
|
details.accountRoles.forEach((item) => {
|
||||||
|
const roleConfirm = item.roleConfirm != null ? item.roleConfirm : item.roleInvite;
|
||||||
roleMap[item.accountId] = {
|
roleMap[item.accountId] = {
|
||||||
monitor: Boolean(item.roleMonitor),
|
monitor: Boolean(item.roleMonitor),
|
||||||
invite: Boolean(item.roleInvite)
|
invite: Boolean(item.roleInvite),
|
||||||
|
confirm: Boolean(roleConfirm)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
(details.accountIds || []).forEach((accountId) => {
|
(details.accountIds || []).forEach((accountId) => {
|
||||||
roleMap[accountId] = { monitor: true, invite: true };
|
roleMap[accountId] = { monitor: true, invite: true, confirm: true };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setTaskAccountRoles(roleMap);
|
setTaskAccountRoles(roleMap);
|
||||||
@ -450,12 +466,49 @@ export default function App() {
|
|||||||
setInviteAccessStatus([]);
|
setInviteAccessStatus([]);
|
||||||
setMembershipStatus({});
|
setMembershipStatus({});
|
||||||
setTaskNotice(null);
|
setTaskNotice(null);
|
||||||
|
setActivePreset("");
|
||||||
if (selectedTaskId != null) {
|
if (selectedTaskId != null) {
|
||||||
checkAccess("auto", true);
|
checkAccess("auto", true);
|
||||||
checkInviteAccess("auto", true);
|
checkInviteAccess("auto", true);
|
||||||
}
|
}
|
||||||
}, [selectedTaskId]);
|
}, [selectedTaskId]);
|
||||||
|
|
||||||
|
const buildPresetSignature = (form, roles) => {
|
||||||
|
const roleEntries = Object.entries(roles || {})
|
||||||
|
.map(([id, value]) => ({
|
||||||
|
id: Number(id),
|
||||||
|
monitor: Boolean(value && value.monitor),
|
||||||
|
invite: Boolean(value && value.invite),
|
||||||
|
confirm: Boolean(value && value.confirm)
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.id - b.id);
|
||||||
|
const snapshot = {
|
||||||
|
form: {
|
||||||
|
warmupEnabled: Boolean(form.warmupEnabled),
|
||||||
|
historyLimit: Number(form.historyLimit || 0),
|
||||||
|
separateBotRoles: Boolean(form.separateBotRoles),
|
||||||
|
requireSameBotInBoth: Boolean(form.requireSameBotInBoth),
|
||||||
|
maxCompetitorBots: Number(form.maxCompetitorBots || 0),
|
||||||
|
maxOurBots: Number(form.maxOurBots || 0),
|
||||||
|
separateConfirmRoles: Boolean(form.separateConfirmRoles),
|
||||||
|
maxConfirmBots: Number(form.maxConfirmBots || 0),
|
||||||
|
inviteViaAdmins: Boolean(form.inviteViaAdmins),
|
||||||
|
inviteAdminAnonymous: Boolean(form.inviteAdminAnonymous),
|
||||||
|
inviteAdminMasterId: Number(form.inviteAdminMasterId || 0)
|
||||||
|
},
|
||||||
|
roles: roleEntries
|
||||||
|
};
|
||||||
|
return JSON.stringify(snapshot);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activePreset) return;
|
||||||
|
const currentSignature = buildPresetSignature(taskForm, taskAccountRoles);
|
||||||
|
if (currentSignature !== presetSignatureRef.current) {
|
||||||
|
setActivePreset("");
|
||||||
|
}
|
||||||
|
}, [taskForm, taskAccountRoles, activePreset]);
|
||||||
|
|
||||||
const taskSummary = useMemo(() => {
|
const taskSummary = useMemo(() => {
|
||||||
const totals = {
|
const totals = {
|
||||||
total: tasks.length,
|
total: tasks.length,
|
||||||
@ -643,11 +696,16 @@ export default function App() {
|
|||||||
if (taskForm.separateBotRoles) {
|
if (taskForm.separateBotRoles) {
|
||||||
const nextCompetitors = Math.max(1, roleSummary.monitor.length || 0);
|
const nextCompetitors = Math.max(1, roleSummary.monitor.length || 0);
|
||||||
const nextOur = Math.max(1, roleSummary.invite.length || 0);
|
const nextOur = Math.max(1, roleSummary.invite.length || 0);
|
||||||
if (taskForm.maxCompetitorBots !== nextCompetitors || taskForm.maxOurBots !== nextOur) {
|
const hasConfirmRoles = roleSummary.confirm.length > 0;
|
||||||
|
const nextConfirm = taskForm.separateConfirmRoles && hasConfirmRoles
|
||||||
|
? Math.max(1, roleSummary.confirm.length)
|
||||||
|
: taskForm.maxConfirmBots;
|
||||||
|
if (taskForm.maxCompetitorBots !== nextCompetitors || taskForm.maxOurBots !== nextOur || (taskForm.separateConfirmRoles && hasConfirmRoles && taskForm.maxConfirmBots !== nextConfirm)) {
|
||||||
setTaskForm((prev) => ({
|
setTaskForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
maxCompetitorBots: nextCompetitors,
|
maxCompetitorBots: nextCompetitors,
|
||||||
maxOurBots: nextOur
|
maxOurBots: nextOur,
|
||||||
|
maxConfirmBots: nextConfirm
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -656,8 +714,11 @@ export default function App() {
|
|||||||
roleIntersectionCount,
|
roleIntersectionCount,
|
||||||
roleSummary.monitor.length,
|
roleSummary.monitor.length,
|
||||||
roleSummary.invite.length,
|
roleSummary.invite.length,
|
||||||
|
roleSummary.confirm.length,
|
||||||
taskForm.requireSameBotInBoth,
|
taskForm.requireSameBotInBoth,
|
||||||
taskForm.separateBotRoles
|
taskForm.separateBotRoles,
|
||||||
|
taskForm.separateConfirmRoles,
|
||||||
|
taskForm.maxConfirmBots
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const formatAccountStatus = (status) => {
|
const formatAccountStatus = (status) => {
|
||||||
@ -720,6 +781,9 @@ export default function App() {
|
|||||||
if (error === "CHAT_ADMIN_REQUIRED") {
|
if (error === "CHAT_ADMIN_REQUIRED") {
|
||||||
return "Для добавления участников нужны права администратора.";
|
return "Для добавления участников нужны права администратора.";
|
||||||
}
|
}
|
||||||
|
if (error === "CHANNEL_INVALID") {
|
||||||
|
return "Цель не распознана как группа/канал для этого аккаунта (ссылка недоступна или сущность устарела).";
|
||||||
|
}
|
||||||
if (error === "USER_ALREADY_PARTICIPANT") {
|
if (error === "USER_ALREADY_PARTICIPANT") {
|
||||||
return "Пользователь уже состоит в целевой группе.";
|
return "Пользователь уже состоит в целевой группе.";
|
||||||
}
|
}
|
||||||
@ -1159,7 +1223,7 @@ export default function App() {
|
|||||||
const chosen = pool.slice(0, required);
|
const chosen = pool.slice(0, required);
|
||||||
accountRolesMap = {};
|
accountRolesMap = {};
|
||||||
chosen.forEach((accountId) => {
|
chosen.forEach((accountId) => {
|
||||||
accountRolesMap[accountId] = { monitor: true, invite: true };
|
accountRolesMap[accountId] = { monitor: true, invite: true, confirm: true };
|
||||||
});
|
});
|
||||||
accountIds = chosen;
|
accountIds = chosen;
|
||||||
setTaskAccountRoles(accountRolesMap);
|
setTaskAccountRoles(accountRolesMap);
|
||||||
@ -1169,7 +1233,7 @@ export default function App() {
|
|||||||
accountIds = accounts.map((account) => account.id);
|
accountIds = accounts.map((account) => account.id);
|
||||||
accountRolesMap = {};
|
accountRolesMap = {};
|
||||||
accountIds.forEach((accountId) => {
|
accountIds.forEach((accountId) => {
|
||||||
accountRolesMap[accountId] = { monitor: true, invite: true };
|
accountRolesMap[accountId] = { monitor: true, invite: true, confirm: true };
|
||||||
});
|
});
|
||||||
setTaskAccountRoles(accountRolesMap);
|
setTaskAccountRoles(accountRolesMap);
|
||||||
setSelectedAccountIds(accountIds);
|
setSelectedAccountIds(accountIds);
|
||||||
@ -1187,6 +1251,7 @@ export default function App() {
|
|||||||
if (roleEntries.length) {
|
if (roleEntries.length) {
|
||||||
const hasMonitor = roleEntries.some((item) => item.monitor);
|
const hasMonitor = roleEntries.some((item) => item.monitor);
|
||||||
const hasInvite = roleEntries.some((item) => item.invite);
|
const hasInvite = roleEntries.some((item) => item.invite);
|
||||||
|
const hasConfirm = roleEntries.some((item) => item.confirm);
|
||||||
if (!hasMonitor) {
|
if (!hasMonitor) {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
showNotification("Нужен хотя бы один аккаунт с ролью мониторинга.", "error");
|
showNotification("Нужен хотя бы один аккаунт с ролью мониторинга.", "error");
|
||||||
@ -1199,11 +1264,19 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (nextForm.separateConfirmRoles && !hasConfirm) {
|
||||||
|
if (!silent) {
|
||||||
|
showNotification("Нужен хотя бы один аккаунт с ролью подтверждения.", "error");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const requiredAccounts = nextForm.requireSameBotInBoth
|
const requiredAccounts = nextForm.requireSameBotInBoth
|
||||||
? Math.max(1, Number(nextForm.maxCompetitorBots || 1))
|
? Math.max(1, Number(nextForm.maxCompetitorBots || 1))
|
||||||
: nextForm.separateBotRoles
|
: nextForm.separateBotRoles
|
||||||
? Math.max(1, Number(nextForm.maxCompetitorBots || 1)) + Math.max(1, Number(nextForm.maxOurBots || 1))
|
? Math.max(1, Number(nextForm.maxCompetitorBots || 1))
|
||||||
|
+ Math.max(1, Number(nextForm.maxOurBots || 1))
|
||||||
|
+ (nextForm.separateConfirmRoles ? Math.max(1, Number(nextForm.maxConfirmBots || 1)) : 0)
|
||||||
: 1;
|
: 1;
|
||||||
if (accountIds.length < requiredAccounts) {
|
if (accountIds.length < requiredAccounts) {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
@ -1215,7 +1288,8 @@ export default function App() {
|
|||||||
const accountRoles = Object.entries(accountRolesMap).map(([id, roles]) => ({
|
const accountRoles = Object.entries(accountRolesMap).map(([id, roles]) => ({
|
||||||
accountId: Number(id),
|
accountId: Number(id),
|
||||||
roleMonitor: Boolean(roles.monitor),
|
roleMonitor: Boolean(roles.monitor),
|
||||||
roleInvite: Boolean(roles.invite)
|
roleInvite: Boolean(roles.invite),
|
||||||
|
roleConfirm: Boolean(roles.confirm != null ? roles.confirm : roles.invite)
|
||||||
}));
|
}));
|
||||||
const result = await window.api.saveTask({
|
const result = await window.api.saveTask({
|
||||||
task: nextForm,
|
task: nextForm,
|
||||||
@ -1641,7 +1715,8 @@ export default function App() {
|
|||||||
const rolePayload = Object.entries(next).map(([id, roles]) => ({
|
const rolePayload = Object.entries(next).map(([id, roles]) => ({
|
||||||
accountId: Number(id),
|
accountId: Number(id),
|
||||||
roleMonitor: Boolean(roles.monitor),
|
roleMonitor: Boolean(roles.monitor),
|
||||||
roleInvite: Boolean(roles.invite)
|
roleInvite: Boolean(roles.invite),
|
||||||
|
roleConfirm: Boolean(roles.confirm != null ? roles.confirm : roles.invite)
|
||||||
}));
|
}));
|
||||||
await window.api.appendTaskAccounts({
|
await window.api.appendTaskAccounts({
|
||||||
taskId: selectedTaskId,
|
taskId: selectedTaskId,
|
||||||
@ -1652,9 +1727,9 @@ export default function App() {
|
|||||||
|
|
||||||
const updateAccountRole = (accountId, role, value) => {
|
const updateAccountRole = (accountId, role, value) => {
|
||||||
const next = { ...taskAccountRoles };
|
const next = { ...taskAccountRoles };
|
||||||
const existing = next[accountId] || { monitor: false, invite: false };
|
const existing = next[accountId] || { monitor: false, invite: false, confirm: false };
|
||||||
next[accountId] = { ...existing, [role]: value };
|
next[accountId] = { ...existing, [role]: value };
|
||||||
if (!next[accountId].monitor && !next[accountId].invite) {
|
if (!next[accountId].monitor && !next[accountId].invite && !next[accountId].confirm) {
|
||||||
delete next[accountId];
|
delete next[accountId];
|
||||||
}
|
}
|
||||||
const ids = Object.keys(next).map((id) => Number(id));
|
const ids = Object.keys(next).map((id) => Number(id));
|
||||||
@ -1666,7 +1741,7 @@ export default function App() {
|
|||||||
const setAccountRolesAll = (accountId, value) => {
|
const setAccountRolesAll = (accountId, value) => {
|
||||||
const next = { ...taskAccountRoles };
|
const next = { ...taskAccountRoles };
|
||||||
if (value) {
|
if (value) {
|
||||||
next[accountId] = { monitor: true, invite: true };
|
next[accountId] = { monitor: true, invite: true, confirm: true };
|
||||||
} else {
|
} else {
|
||||||
delete next[accountId];
|
delete next[accountId];
|
||||||
}
|
}
|
||||||
@ -1688,25 +1763,35 @@ export default function App() {
|
|||||||
const next = {};
|
const next = {};
|
||||||
if (type === "all") {
|
if (type === "all") {
|
||||||
availableIds.forEach((id) => {
|
availableIds.forEach((id) => {
|
||||||
next[id] = { monitor: true, invite: true };
|
next[id] = { monitor: true, invite: true, confirm: true };
|
||||||
});
|
});
|
||||||
} else if (type === "one") {
|
} else if (type === "one") {
|
||||||
const id = availableIds[0];
|
const id = availableIds[0];
|
||||||
next[id] = { monitor: true, invite: true };
|
next[id] = { monitor: true, invite: true, confirm: true };
|
||||||
} 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));
|
||||||
const monitorIds = availableIds.slice(0, monitorCount);
|
const monitorIds = availableIds.slice(0, monitorCount);
|
||||||
const inviteIds = availableIds.slice(monitorCount, monitorCount + inviteCount);
|
const inviteIds = availableIds.slice(monitorCount, monitorCount + inviteCount);
|
||||||
|
const confirmCount = taskForm.separateConfirmRoles ? Math.max(1, Number(taskForm.maxConfirmBots || 1)) : 0;
|
||||||
|
const confirmIds = taskForm.separateConfirmRoles
|
||||||
|
? availableIds.slice(monitorCount + inviteCount, monitorCount + inviteCount + confirmCount)
|
||||||
|
: [];
|
||||||
monitorIds.forEach((id) => {
|
monitorIds.forEach((id) => {
|
||||||
next[id] = { monitor: true, invite: false };
|
next[id] = { monitor: true, invite: false, confirm: false };
|
||||||
});
|
});
|
||||||
inviteIds.forEach((id) => {
|
inviteIds.forEach((id) => {
|
||||||
next[id] = { monitor: false, invite: true };
|
next[id] = { monitor: false, invite: true, confirm: true };
|
||||||
|
});
|
||||||
|
confirmIds.forEach((id) => {
|
||||||
|
next[id] = { monitor: false, invite: false, confirm: true };
|
||||||
});
|
});
|
||||||
if (inviteIds.length < inviteCount) {
|
if (inviteIds.length < inviteCount) {
|
||||||
showNotification("Не хватает аккаунтов для роли инвайта.", "error");
|
showNotification("Не хватает аккаунтов для роли инвайта.", "error");
|
||||||
}
|
}
|
||||||
|
if (taskForm.separateConfirmRoles && confirmIds.length < confirmCount) {
|
||||||
|
showNotification("Не хватает аккаунтов для роли подтверждения.", "error");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const ids = Object.keys(next).map((id) => Number(id));
|
const ids = Object.keys(next).map((id) => Number(id));
|
||||||
setTaskAccountRoles(next);
|
setTaskAccountRoles(next);
|
||||||
@ -1716,19 +1801,95 @@ export default function App() {
|
|||||||
setTaskNotice({ text: `Пресет: ${label}`, tone: "success", source: "accounts" });
|
setTaskNotice({ text: `Пресет: ${label}`, tone: "success", source: "accounts" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyTaskPreset = (type) => {
|
||||||
|
if (!hasSelectedTask) {
|
||||||
|
showNotification("Сначала выберите задачу.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!accounts.length) {
|
||||||
|
showNotification("Нет доступных аккаунтов.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const masterId = accounts[0].id;
|
||||||
|
const requiredCount = 3;
|
||||||
|
const baseIds = selectedAccountIds.length >= requiredCount
|
||||||
|
? selectedAccountIds.slice()
|
||||||
|
: accounts.map((account) => account.id);
|
||||||
|
if (baseIds.length < 3) {
|
||||||
|
showNotification("Для раздельных ролей желательно минимум 3 аккаунта (мониторинг/инвайт/подтверждение).", "info");
|
||||||
|
}
|
||||||
|
if (!baseIds.includes(masterId)) {
|
||||||
|
baseIds.unshift(masterId);
|
||||||
|
}
|
||||||
|
const pool = baseIds.filter((id) => id !== masterId);
|
||||||
|
const roleMap = {};
|
||||||
|
const addRole = (id, role) => {
|
||||||
|
if (!id) return;
|
||||||
|
if (!roleMap[id]) roleMap[id] = { monitor: false, invite: false, confirm: false };
|
||||||
|
roleMap[id][role] = true;
|
||||||
|
};
|
||||||
|
const takeFromPool = (count, used) => {
|
||||||
|
const result = [];
|
||||||
|
for (const id of pool) {
|
||||||
|
if (result.length >= count) break;
|
||||||
|
if (used.has(id)) continue;
|
||||||
|
used.add(id);
|
||||||
|
result.push(id);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const used = new Set();
|
||||||
|
const monitorCount = 1;
|
||||||
|
const inviteCount = 1;
|
||||||
|
const confirmCount = 1;
|
||||||
|
const monitorIds = takeFromPool(monitorCount, used);
|
||||||
|
const confirmIds = takeFromPool(confirmCount, used);
|
||||||
|
const inviteIds = masterId ? [masterId] : takeFromPool(inviteCount, used);
|
||||||
|
if (monitorIds.length < monitorCount) addRole(masterId, "monitor");
|
||||||
|
if (confirmIds.length < confirmCount) addRole(masterId, "confirm");
|
||||||
|
monitorIds.forEach((id) => addRole(id, "monitor"));
|
||||||
|
confirmIds.forEach((id) => addRole(id, "confirm"));
|
||||||
|
inviteIds.forEach((id) => addRole(id, "invite"));
|
||||||
|
|
||||||
|
const nextForm = sanitizeTaskForm({
|
||||||
|
...taskForm,
|
||||||
|
warmupEnabled: true,
|
||||||
|
historyLimit: 35,
|
||||||
|
separateBotRoles: true,
|
||||||
|
requireSameBotInBoth: false,
|
||||||
|
maxCompetitorBots: 1,
|
||||||
|
maxOurBots: 1,
|
||||||
|
separateConfirmRoles: true,
|
||||||
|
maxConfirmBots: 1,
|
||||||
|
inviteViaAdmins: type === "admin",
|
||||||
|
inviteAdminAnonymous: true,
|
||||||
|
inviteAdminMasterId: masterId
|
||||||
|
});
|
||||||
|
const signature = buildPresetSignature(nextForm, roleMap);
|
||||||
|
presetSignatureRef.current = signature;
|
||||||
|
setTaskForm(nextForm);
|
||||||
|
setTaskAccountRoles(roleMap);
|
||||||
|
setSelectedAccountIds(Object.keys(roleMap).map((id) => Number(id)));
|
||||||
|
persistAccountRoles(roleMap);
|
||||||
|
const label = type === "admin" ? "Автораспределение + Инвайт через админа" : "Автораспределение + Без админки";
|
||||||
|
setTaskNotice({ text: `Пресет: ${label}`, tone: "success", source: "task" });
|
||||||
|
setActivePreset(type);
|
||||||
|
};
|
||||||
|
|
||||||
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 };
|
nextRoles[accountId] = { monitor: true, invite: true, confirm: true };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const rolePayload = Object.entries(nextRoles).map(([accountId, roles]) => ({
|
const rolePayload = Object.entries(nextRoles).map(([accountId, roles]) => ({
|
||||||
accountId: Number(accountId),
|
accountId: Number(accountId),
|
||||||
roleMonitor: Boolean(roles.monitor),
|
roleMonitor: Boolean(roles.monitor),
|
||||||
roleInvite: Boolean(roles.invite)
|
roleInvite: Boolean(roles.invite),
|
||||||
|
roleConfirm: Boolean(roles.confirm != null ? roles.confirm : roles.invite)
|
||||||
}));
|
}));
|
||||||
const result = await window.api.appendTaskAccounts({
|
const result = await window.api.appendTaskAccounts({
|
||||||
taskId: selectedTaskId,
|
taskId: selectedTaskId,
|
||||||
@ -2524,6 +2685,26 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="section-title">Основное</div>
|
<div className="section-title">Основное</div>
|
||||||
|
<div className="row-inline">
|
||||||
|
<div className="status-caption">Пресеты:</div>
|
||||||
|
<button
|
||||||
|
className={`secondary ${activePreset === "admin" ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => applyTaskPreset("admin")}
|
||||||
|
>
|
||||||
|
Автораспределение + Инвайт через админа
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`secondary ${activePreset === "no_admin" ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => applyTaskPreset("no_admin")}
|
||||||
|
>
|
||||||
|
Автораспределение + Без админки
|
||||||
|
</button>
|
||||||
|
<span className="status-caption">
|
||||||
|
Мастер-админ: {taskForm.inviteAdminMasterId ? formatAccountLabel(accountById.get(taskForm.inviteAdminMasterId)) : "не выбран"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<label>
|
<label>
|
||||||
<span className="label-line">Название задачи <span className="required">*</span></span>
|
<span className="label-line">Название задачи <span className="required">*</span></span>
|
||||||
@ -2554,6 +2735,8 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="task-editor-grid">
|
<div className="task-editor-grid">
|
||||||
|
<details className="section" open>
|
||||||
|
<summary className="section-title">Базовые настройки</summary>
|
||||||
<details className="section" open>
|
<details className="section" open>
|
||||||
<summary className="section-title">Роли ботов и вступление</summary>
|
<summary className="section-title">Роли ботов и вступление</summary>
|
||||||
<div className="toggle-row">
|
<div className="toggle-row">
|
||||||
@ -2607,82 +2790,6 @@ export default function App() {
|
|||||||
Режим “один и тот же бот” нужен, когда аккаунт должен быть в конкурентах и в нашей группе для инвайта.
|
Режим “один и тот же бот” нужен, когда аккаунт должен быть в конкурентах и в нашей группе для инвайта.
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details className="section">
|
|
||||||
<summary className="section-title">Инвайт через админов</summary>
|
|
||||||
<div className="admin-invite-grid">
|
|
||||||
<label className="checkbox admin-invite-toggle">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={Boolean(taskForm.inviteViaAdmins)}
|
|
||||||
onChange={(event) => setTaskForm({ ...taskForm, inviteViaAdmins: event.target.checked })}
|
|
||||||
/>
|
|
||||||
Инвайтить через админов
|
|
||||||
<span className="hint">
|
|
||||||
Временно назначаем пользователя админом с правом “Приглашать”, затем снимаем права.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<label className="checkbox admin-invite-toggle">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={Boolean(taskForm.inviteLinkOnFail)}
|
|
||||||
onChange={(event) => setTaskForm({ ...taskForm, inviteLinkOnFail: event.target.checked })}
|
|
||||||
/>
|
|
||||||
Отправлять ссылку при USER_NOT_MUTUAL_CONTACT
|
|
||||||
<span className="hint">
|
|
||||||
Отправляет пользователю ссылку из поля “Наша группа”.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<label className="admin-invite-master">
|
|
||||||
<span className="label-line">Главный аккаунт</span>
|
|
||||||
<div className="input-row">
|
|
||||||
<select
|
|
||||||
value={taskForm.inviteAdminMasterId || ""}
|
|
||||||
onChange={(event) => setTaskForm({ ...taskForm, inviteAdminMasterId: Number(event.target.value) || 0 })}
|
|
||||||
disabled={!taskForm.inviteViaAdmins}
|
|
||||||
>
|
|
||||||
<option value="">Не выбран</option>
|
|
||||||
{accounts.map((account) => (
|
|
||||||
<option key={`master-${account.id}`} value={account.id}>
|
|
||||||
{formatAccountLabel(account)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="secondary"
|
|
||||||
disabled={!taskForm.inviteViaAdmins || !taskForm.inviteAdminMasterId}
|
|
||||||
onClick={async () => {
|
|
||||||
const account = accountById.get(taskForm.inviteAdminMasterId);
|
|
||||||
const username = account && account.username ? `@${account.username}` : "";
|
|
||||||
if (!username) {
|
|
||||||
showNotification("У выбранного аккаунта нет username.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ok = await copyToClipboard(username);
|
|
||||||
showNotification(ok ? `Скопировано: ${username}` : "Не удалось скопировать.", ok ? "success" : "error");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Копировать username
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span className="hint">
|
|
||||||
Этот аккаунт должен быть админом в целевой группе и уметь выдавать права другим.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<label className="checkbox admin-invite-flood">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={Boolean(taskForm.inviteAdminAllowFlood)}
|
|
||||||
onChange={(event) => setTaskForm({ ...taskForm, inviteAdminAllowFlood: event.target.checked })}
|
|
||||||
disabled={!taskForm.inviteViaAdmins}
|
|
||||||
/>
|
|
||||||
Инвайтить в чаты с флудом
|
|
||||||
<span className="hint">
|
|
||||||
Использует выдачу прав между аккаунтами, если Telegram ограничивает инвайтинг.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details className="section" open>
|
<details className="section" open>
|
||||||
<summary className="section-title">Интервалы и лимиты</summary>
|
<summary className="section-title">Интервалы и лимиты</summary>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@ -2814,72 +2921,96 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
</details>
|
||||||
<details className="section">
|
<details className="section">
|
||||||
<summary className="section-title">Импорт аудитории</summary>
|
<summary className="section-title">Расширенные настройки</summary>
|
||||||
<div className="row">
|
<details className="section">
|
||||||
<label className="checkbox">
|
<summary className="section-title">Инвайт через админов</summary>
|
||||||
|
<div className="admin-invite-grid">
|
||||||
|
<label className="checkbox admin-invite-toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={Boolean(taskForm.parseParticipants)}
|
checked={Boolean(taskForm.inviteViaAdmins)}
|
||||||
onChange={(event) => setTaskForm({ ...taskForm, parseParticipants: event.target.checked })}
|
onChange={(event) => setTaskForm({ ...taskForm, inviteViaAdmins: event.target.checked })}
|
||||||
/>
|
/>
|
||||||
Собирать участников чатов конкурентов
|
Инвайтить через админов
|
||||||
<span className="hint">
|
<span className="hint">
|
||||||
Используется для закрытых участников и полного списка аудитории.
|
Временно назначаем пользователя админом с правом “Приглашать”, затем снимаем права.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="checkbox">
|
<label className="checkbox admin-invite-toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={Boolean(taskForm.cycleCompetitors)}
|
checked={Boolean(taskForm.inviteLinkOnFail)}
|
||||||
onChange={(event) => setTaskForm({ ...taskForm, cycleCompetitors: event.target.checked })}
|
onChange={(event) => setTaskForm({ ...taskForm, inviteLinkOnFail: event.target.checked })}
|
||||||
/>
|
/>
|
||||||
Циклически обходить конкурентов
|
Отправлять ссылку при USER_NOT_MUTUAL_CONTACT
|
||||||
<span className="hint">
|
<span className="hint">
|
||||||
Мониторинг и сбор будут переключаться по группам по очереди.
|
Отправляет пользователю ссылку из поля “Наша группа”.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="checkbox">
|
<label className="admin-invite-master">
|
||||||
<input
|
<span className="label-line">Главный аккаунт</span>
|
||||||
type="checkbox"
|
<div className="input-row">
|
||||||
checked={Boolean(fileImportForm.onlyIds)}
|
<select
|
||||||
onChange={(event) => setFileImportForm({ ...fileImportForm, onlyIds: event.target.checked })}
|
value={taskForm.inviteAdminMasterId || ""}
|
||||||
/>
|
onChange={(event) => setTaskForm({ ...taskForm, inviteAdminMasterId: Number(event.target.value) || 0 })}
|
||||||
В файле только ID
|
disabled={!taskForm.inviteViaAdmins}
|
||||||
<span className="hint">Если включено — нужен источник (чат), из которого брались ID.</span>
|
>
|
||||||
</label>
|
<option value="">Не выбран</option>
|
||||||
<label>
|
{accounts.map((account) => (
|
||||||
<span className="label-line">Источник для ID</span>
|
<option key={`master-${account.id}`} value={account.id}>
|
||||||
<input
|
{formatAccountLabel(account)}
|
||||||
type="text"
|
</option>
|
||||||
placeholder="https://t.me/чат"
|
|
||||||
value={fileImportForm.sourceChat}
|
|
||||||
onChange={(event) => setFileImportForm({ ...fileImportForm, sourceChat: event.target.value })}
|
|
||||||
disabled={!fileImportForm.onlyIds}
|
|
||||||
/>
|
|
||||||
<span className="hint">Используется для резолва ID при инвайте.</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="row-inline">
|
|
||||||
<button className="secondary" type="button" onClick={importInviteFile}>
|
|
||||||
Импортировать файл
|
|
||||||
</button>
|
|
||||||
{fileImportResult && (
|
|
||||||
<div className="status-text compact">
|
|
||||||
Импортировано: {fileImportResult.importedCount} · Пропущено: {fileImportResult.skippedCount} · Ошибок: {fileImportResult.failed.length}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{fileImportResult && fileImportResult.failed.length > 0 && (
|
|
||||||
<div className="access-list">
|
|
||||||
{fileImportResult.failed.map((item, index) => (
|
|
||||||
<div key={`${item.path}-${index}`} className="access-row fail">
|
|
||||||
<div className="access-title">{item.path}</div>
|
|
||||||
<div className="access-error">{item.error}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="secondary"
|
||||||
|
disabled={!taskForm.inviteViaAdmins || !taskForm.inviteAdminMasterId}
|
||||||
|
onClick={async () => {
|
||||||
|
const account = accountById.get(taskForm.inviteAdminMasterId);
|
||||||
|
const username = account && account.username ? `@${account.username}` : "";
|
||||||
|
if (!username) {
|
||||||
|
showNotification("У выбранного аккаунта нет username.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ok = await copyToClipboard(username);
|
||||||
|
showNotification(ok ? `Скопировано: ${username}` : "Не удалось скопировать.", ok ? "success" : "error");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Копировать username
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span className="hint">
|
||||||
|
Этот аккаунт должен быть админом в целевой группе и уметь выдавать права другим.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label className="checkbox admin-invite-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(taskForm.inviteAdminAnonymous)}
|
||||||
|
onChange={(event) => setTaskForm({ ...taskForm, inviteAdminAnonymous: event.target.checked })}
|
||||||
|
disabled={!taskForm.inviteViaAdmins}
|
||||||
|
/>
|
||||||
|
Делать админов анонимными
|
||||||
|
<span className="hint">
|
||||||
|
Мастер-админ назначает остальных админами с анонимностью и минимальными правами.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label className="checkbox admin-invite-flood">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(taskForm.inviteAdminAllowFlood)}
|
||||||
|
onChange={(event) => setTaskForm({ ...taskForm, inviteAdminAllowFlood: event.target.checked })}
|
||||||
|
disabled={!taskForm.inviteViaAdmins}
|
||||||
|
/>
|
||||||
|
Инвайтить в чаты с флудом
|
||||||
|
<span className="hint">
|
||||||
|
Использует выдачу прав между аккаунтами, если Telegram ограничивает инвайтинг.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</details>
|
</details>
|
||||||
<details className="section">
|
<details className="section">
|
||||||
<summary className="section-title">Распределение ботов</summary>
|
<summary className="section-title">Распределение ботов</summary>
|
||||||
@ -2953,23 +3084,111 @@ export default function App() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(taskForm.separateConfirmRoles)}
|
||||||
|
onChange={(event) => setTaskForm({ ...taskForm, separateConfirmRoles: event.target.checked })}
|
||||||
|
disabled={!taskForm.separateBotRoles}
|
||||||
|
/>
|
||||||
|
Подтверждение отдельными аккаунтами
|
||||||
|
<span className="hint">
|
||||||
|
Если включено, проверку участия выполняют отдельные аккаунты, не совпадающие с инвайтерами.
|
||||||
|
Ручные чекбоксы ролей в разделе “Аккаунты” имеют приоритет над авто‑распределением.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span className="label-line">Ботов для подтверждения</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={taskForm.maxConfirmBots === "" ? "" : taskForm.maxConfirmBots}
|
||||||
|
onChange={(event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setTaskForm({ ...taskForm, maxConfirmBots: value === "" ? "" : Number(value) });
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const value = Number(taskForm.maxConfirmBots);
|
||||||
|
const normalized = Number.isFinite(value) && value > 0 ? value : 1;
|
||||||
|
setTaskForm({ ...taskForm, maxConfirmBots: normalized });
|
||||||
|
}}
|
||||||
|
disabled={!taskForm.separateBotRoles || !taskForm.separateConfirmRoles}
|
||||||
|
/>
|
||||||
|
<span className="hint">Используется при авто-разделении ролей.</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details className="section">
|
<details className="section">
|
||||||
<summary className="section-title">Ошибки аккаунтов</summary>
|
<summary className="section-title">Импорт аудитории</summary>
|
||||||
{criticalErrorAccounts.length === 0 && (
|
<div className="row">
|
||||||
<div className="status-text compact">Ошибок нет.</div>
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(taskForm.parseParticipants)}
|
||||||
|
onChange={(event) => setTaskForm({ ...taskForm, parseParticipants: event.target.checked })}
|
||||||
|
/>
|
||||||
|
Собирать участников чатов конкурентов
|
||||||
|
<span className="hint">
|
||||||
|
Используется для закрытых участников и полного списка аудитории.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(taskForm.cycleCompetitors)}
|
||||||
|
onChange={(event) => setTaskForm({ ...taskForm, cycleCompetitors: event.target.checked })}
|
||||||
|
/>
|
||||||
|
Циклически обходить конкурентов
|
||||||
|
<span className="hint">
|
||||||
|
Мониторинг и сбор будут переключаться по группам по очереди.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(fileImportForm.onlyIds)}
|
||||||
|
onChange={(event) => setFileImportForm({ ...fileImportForm, onlyIds: event.target.checked })}
|
||||||
|
/>
|
||||||
|
В файле только ID
|
||||||
|
<span className="hint">Если включено — нужен источник (чат), из которого брались ID.</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span className="label-line">Источник для ID</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="https://t.me/чат"
|
||||||
|
value={fileImportForm.sourceChat}
|
||||||
|
onChange={(event) => setFileImportForm({ ...fileImportForm, sourceChat: event.target.value })}
|
||||||
|
disabled={!fileImportForm.onlyIds}
|
||||||
|
/>
|
||||||
|
<span className="hint">Используется для резолва ID при инвайте.</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="row-inline">
|
||||||
|
<button className="secondary" type="button" onClick={importInviteFile}>
|
||||||
|
Импортировать файл
|
||||||
|
</button>
|
||||||
|
{fileImportResult && (
|
||||||
|
<div className="status-text compact">
|
||||||
|
Импортировано: {fileImportResult.importedCount} · Пропущено: {fileImportResult.skippedCount} · Ошибок: {fileImportResult.failed.length}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{criticalErrorAccounts.length > 0 && (
|
</div>
|
||||||
|
{fileImportResult && fileImportResult.failed.length > 0 && (
|
||||||
<div className="access-list">
|
<div className="access-list">
|
||||||
{criticalErrorAccounts.map((account) => (
|
{fileImportResult.failed.map((item, index) => (
|
||||||
<div key={account.id} className="access-row fail">
|
<div key={`${item.path}-${index}`} className="access-row fail">
|
||||||
<div className="access-title">{formatAccountLabel(account)}</div>
|
<div className="access-title">{item.path}</div>
|
||||||
<div className="access-error">{account.last_error || "Ошибка сессии"}</div>
|
<div className="access-error">{item.error}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</details>
|
</details>
|
||||||
|
</details>
|
||||||
|
<details className="section">
|
||||||
|
<summary className="section-title">Экспертные настройки</summary>
|
||||||
<details className="section">
|
<details className="section">
|
||||||
<summary className="section-title">Безопасность</summary>
|
<summary className="section-title">Безопасность</summary>
|
||||||
<div className="toggle-row">
|
<div className="toggle-row">
|
||||||
@ -3038,6 +3257,23 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
|
<details className="section">
|
||||||
|
<summary className="section-title">Ошибки аккаунтов</summary>
|
||||||
|
{criticalErrorAccounts.length === 0 && (
|
||||||
|
<div className="status-text compact">Ошибок нет.</div>
|
||||||
|
)}
|
||||||
|
{criticalErrorAccounts.length > 0 && (
|
||||||
|
<div className="access-list">
|
||||||
|
{criticalErrorAccounts.map((account) => (
|
||||||
|
<div key={account.id} className="access-row fail">
|
||||||
|
<div className="access-title">{formatAccountLabel(account)}</div>
|
||||||
|
<div className="access-error">{account.last_error || "Ошибка сессии"}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
<label>
|
<label>
|
||||||
<span className="label-line">Заметки</span>
|
<span className="label-line">Заметки</span>
|
||||||
<textarea
|
<textarea
|
||||||
@ -3200,6 +3436,7 @@ export default function App() {
|
|||||||
selectedAccountIds={selectedAccountIds}
|
selectedAccountIds={selectedAccountIds}
|
||||||
taskAccountRoles={taskAccountRoles}
|
taskAccountRoles={taskAccountRoles}
|
||||||
hasSelectedTask={hasSelectedTask}
|
hasSelectedTask={hasSelectedTask}
|
||||||
|
inviteAdminMasterId={taskForm.inviteAdminMasterId}
|
||||||
refreshMembership={refreshMembership}
|
refreshMembership={refreshMembership}
|
||||||
refreshIdentity={refreshIdentity}
|
refreshIdentity={refreshIdentity}
|
||||||
formatAccountStatus={formatAccountStatus}
|
formatAccountStatus={formatAccountStatus}
|
||||||
|
|||||||
@ -1040,6 +1040,11 @@ button.secondary {
|
|||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.secondary.active {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
button.danger {
|
button.danger {
|
||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ function AccountsTab({
|
|||||||
selectedAccountIds,
|
selectedAccountIds,
|
||||||
taskAccountRoles,
|
taskAccountRoles,
|
||||||
hasSelectedTask,
|
hasSelectedTask,
|
||||||
|
inviteAdminMasterId,
|
||||||
refreshMembership,
|
refreshMembership,
|
||||||
refreshIdentity,
|
refreshIdentity,
|
||||||
formatAccountStatus,
|
formatAccountStatus,
|
||||||
@ -39,15 +40,17 @@ function AccountsTab({
|
|||||||
const knownIds = new Set((accounts || []).map((account) => account.id));
|
const knownIds = new Set((accounts || []).map((account) => account.id));
|
||||||
let monitor = 0;
|
let monitor = 0;
|
||||||
let invite = 0;
|
let invite = 0;
|
||||||
|
let confirm = 0;
|
||||||
let total = 0;
|
let total = 0;
|
||||||
Object.entries(taskAccountRoles || {}).forEach(([id, roles]) => {
|
Object.entries(taskAccountRoles || {}).forEach(([id, roles]) => {
|
||||||
const accountId = Number(id);
|
const accountId = Number(id);
|
||||||
if (!knownIds.has(accountId)) return;
|
if (!knownIds.has(accountId)) return;
|
||||||
if (roles.monitor) monitor += 1;
|
if (roles.monitor) monitor += 1;
|
||||||
if (roles.invite) invite += 1;
|
if (roles.invite) invite += 1;
|
||||||
if (roles.monitor || roles.invite) total += 1;
|
if (roles.confirm) confirm += 1;
|
||||||
|
if (roles.monitor || roles.invite || roles.confirm) total += 1;
|
||||||
});
|
});
|
||||||
return { monitor, invite, total };
|
return { monitor, invite, confirm, total };
|
||||||
}, [taskAccountRoles, accounts]);
|
}, [taskAccountRoles, accounts]);
|
||||||
return (
|
return (
|
||||||
<section className="card">
|
<section className="card">
|
||||||
@ -66,7 +69,12 @@ function AccountsTab({
|
|||||||
)}
|
)}
|
||||||
{hasSelectedTask && (
|
{hasSelectedTask && (
|
||||||
<div className="account-summary">
|
<div className="account-summary">
|
||||||
Мониторят: {roleStats.monitor} · Инвайтят: {roleStats.invite} · Всего: {roleStats.total}
|
Мониторят: {roleStats.monitor} · Инвайтят: {roleStats.invite} · Подтверждают: {roleStats.confirm} · Всего: {roleStats.total}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasSelectedTask && (
|
||||||
|
<div className="hint">
|
||||||
|
Ручные чекбоксы ролей имеют приоритет над авто‑распределением в настройках.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasSelectedTask && (
|
{hasSelectedTask && (
|
||||||
@ -104,13 +112,15 @@ function AccountsTab({
|
|||||||
? `В нашей: ${membership.ourGroupMember ? "да" : "нет"}`
|
? `В нашей: ${membership.ourGroupMember ? "да" : "нет"}`
|
||||||
: "В нашей: —";
|
: "В нашей: —";
|
||||||
const selected = selectedAccountIds.includes(account.id);
|
const selected = selectedAccountIds.includes(account.id);
|
||||||
const roles = taskAccountRoles[account.id] || { monitor: false, invite: false };
|
const roles = taskAccountRoles[account.id] || { monitor: false, invite: false, confirm: false };
|
||||||
|
const isMasterAdmin = hasSelectedTask && inviteAdminMasterId && Number(inviteAdminMasterId) === account.id;
|
||||||
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}`;
|
||||||
const roles = [
|
const roles = [
|
||||||
item.roleMonitor ? "М" : null,
|
item.roleMonitor ? "М" : null,
|
||||||
item.roleInvite ? "И" : null
|
item.roleInvite ? "И" : null,
|
||||||
|
item.roleConfirm ? "П" : null
|
||||||
].filter(Boolean).join("/");
|
].filter(Boolean).join("/");
|
||||||
return roles ? `${name} (${roles})` : name;
|
return roles ? `${name} (${roles})` : name;
|
||||||
})
|
})
|
||||||
@ -124,6 +134,8 @@ function AccountsTab({
|
|||||||
<div className="role-badges">
|
<div className="role-badges">
|
||||||
{roles.monitor && <span className="role-pill">Мониторинг</span>}
|
{roles.monitor && <span className="role-pill">Мониторинг</span>}
|
||||||
{roles.invite && <span className="role-pill">Инвайт</span>}
|
{roles.invite && <span className="role-pill">Инвайт</span>}
|
||||||
|
{roles.confirm && <span className="role-pill">Подтверждение</span>}
|
||||||
|
{isMasterAdmin && <span className="role-pill accent">Мастер-админ</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="account-meta">User ID: {account.user_id || "—"}</div>
|
<div className="account-meta">User ID: {account.user_id || "—"}</div>
|
||||||
<div className="account-meta membership-row">
|
<div className="account-meta membership-row">
|
||||||
@ -187,12 +199,20 @@ function AccountsTab({
|
|||||||
/>
|
/>
|
||||||
Инвайт
|
Инвайт
|
||||||
</label>
|
</label>
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(roles.confirm)}
|
||||||
|
onChange={(event) => updateAccountRole(account.id, "confirm", event.target.checked)}
|
||||||
|
/>
|
||||||
|
Подтверждение
|
||||||
|
</label>
|
||||||
<button
|
<button
|
||||||
className="ghost tiny"
|
className="ghost tiny"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAccountRolesAll(account.id, !selected)}
|
onClick={() => setAccountRolesAll(account.id, !selected)}
|
||||||
>
|
>
|
||||||
{selected ? "Снять роли" : "Оба"}
|
{selected ? "Снять роли" : "Все роли"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -226,14 +246,16 @@ function AccountsTab({
|
|||||||
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),
|
||||||
invite: assignedTasks.some((item) => item.roleInvite)
|
invite: assignedTasks.some((item) => item.roleInvite),
|
||||||
|
confirm: assignedTasks.some((item) => item.roleConfirm)
|
||||||
};
|
};
|
||||||
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}`;
|
||||||
const roles = [
|
const roles = [
|
||||||
item.roleMonitor ? "М" : null,
|
item.roleMonitor ? "М" : null,
|
||||||
item.roleInvite ? "И" : null
|
item.roleInvite ? "И" : null,
|
||||||
|
item.roleConfirm ? "П" : null
|
||||||
].filter(Boolean).join("/");
|
].filter(Boolean).join("/");
|
||||||
return roles ? `${name} (${roles})` : name;
|
return roles ? `${name} (${roles})` : name;
|
||||||
})
|
})
|
||||||
@ -270,6 +292,7 @@ function AccountsTab({
|
|||||||
<div className="role-badges">
|
<div className="role-badges">
|
||||||
{roles.monitor && <span className="role-pill">Мониторинг</span>}
|
{roles.monitor && <span className="role-pill">Мониторинг</span>}
|
||||||
{roles.invite && <span className="role-pill">Инвайт</span>}
|
{roles.invite && <span className="role-pill">Инвайт</span>}
|
||||||
|
{roles.confirm && <span className="role-pill">Подтверждение</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="account-meta">User ID: {account.user_id || "—"}</div>
|
<div className="account-meta">User ID: {account.user_id || "—"}</div>
|
||||||
<div className="account-meta membership-row">
|
<div className="account-meta membership-row">
|
||||||
|
|||||||
@ -432,7 +432,7 @@ function LogsTab({
|
|||||||
<span className={`match-badge ${invite.watcherAccountId === invite.accountId ? "ok" : "warn"}`}>
|
<span className={`match-badge ${invite.watcherAccountId === invite.accountId ? "ok" : "warn"}`}>
|
||||||
{invite.watcherAccountId === invite.accountId
|
{invite.watcherAccountId === invite.accountId
|
||||||
? "Инвайт тем же аккаунтом, что наблюдал"
|
? "Инвайт тем же аккаунтом, что наблюдал"
|
||||||
: "Инвайт другим аккаунтом (наблюдатель отличается)"}
|
: ""}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -452,9 +452,14 @@ function LogsTab({
|
|||||||
{invite.error && invite.error !== "" && (
|
{invite.error && invite.error !== "" && (
|
||||||
<div className="log-errors">Ошибка: {formatErrorWithExplain(invite.error)}</div>
|
<div className="log-errors">Ошибка: {formatErrorWithExplain(invite.error)}</div>
|
||||||
)}
|
)}
|
||||||
{invite.confirmError && (
|
|
||||||
<div className="log-errors">
|
<div className="log-errors">
|
||||||
Проверка участия: {formatErrorWithExplain(invite.confirmError)}
|
Проверка участия: {invite.confirmError
|
||||||
|
? formatErrorWithExplain(invite.confirmError)
|
||||||
|
: (invite.confirmed ? "OK" : "Не подтверждено")}
|
||||||
|
</div>
|
||||||
|
{invite.confirmError && invite.confirmError.includes("(") && (
|
||||||
|
<div className="log-users">
|
||||||
|
Проверял: {invite.confirmError.slice(invite.confirmError.indexOf("(") + 1, invite.confirmError.lastIndexOf(")"))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{invite.strategy && (
|
{invite.strategy && (
|
||||||
@ -488,7 +493,12 @@ function LogsTab({
|
|||||||
<div>Статус: {formatInviteStatus(invite.status)}</div>
|
<div>Статус: {formatInviteStatus(invite.status)}</div>
|
||||||
<div>Результат: {formatErrorWithExplain(invite.skippedReason)}</div>
|
<div>Результат: {formatErrorWithExplain(invite.skippedReason)}</div>
|
||||||
<div>Ошибка: {formatErrorWithExplain(invite.error)}</div>
|
<div>Ошибка: {formatErrorWithExplain(invite.error)}</div>
|
||||||
<div>Проверка участия: {formatErrorWithExplain(invite.confirmError)}</div>
|
<div>Проверка участия: {invite.confirmError
|
||||||
|
? formatErrorWithExplain(invite.confirmError)
|
||||||
|
: (invite.confirmed ? "OK" : "Не подтверждено")}</div>
|
||||||
|
{invite.confirmError && invite.confirmError.includes("(") && (
|
||||||
|
<div>Проверял: {invite.confirmError.slice(invite.confirmError.indexOf("(") + 1, invite.confirmError.lastIndexOf(")"))}</div>
|
||||||
|
)}
|
||||||
{invite.watcherAccountId && invite.accountId && invite.watcherAccountId !== invite.accountId
|
{invite.watcherAccountId && invite.accountId && invite.watcherAccountId !== invite.accountId
|
||||||
&& selectedTask && selectedTask.randomAccounts && hasBothRoles(invite.watcherAccountId) && (
|
&& selectedTask && selectedTask.randomAccounts && hasBothRoles(invite.watcherAccountId) && (
|
||||||
<div>Примечание: у наблюдателя стоят обе роли, но включен случайный выбор — инвайт выполнен другим аккаунтом.</div>
|
<div>Примечание: у наблюдателя стоят обе роли, но включен случайный выбор — инвайт выполнен другим аккаунтом.</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user