some
This commit is contained in:
parent
f09f412fd9
commit
7ed57048da
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "telegram-invite-automation",
|
||||
"version": "1.9.4",
|
||||
"version": "1.9.5",
|
||||
"private": true,
|
||||
"description": "Automated user parsing and invites for Telegram groups",
|
||||
"main": "src/main/index.js",
|
||||
|
||||
@ -715,6 +715,7 @@ ipcMain.handle("test:inviteOnce", async (_event, payload) => {
|
||||
userAccessHash: item.user_access_hash,
|
||||
username: item.username,
|
||||
sourceChat: item.source_chat,
|
||||
sourceMessageId: Number(item.source_message_id || 0),
|
||||
watcherAccountId: watcherAccount ? watcherAccount.id : 0,
|
||||
watcherPhone: watcherAccount ? watcherAccount.phone : ""
|
||||
});
|
||||
|
||||
@ -60,11 +60,12 @@ function initStore(userDataPath) {
|
||||
user_access_hash TEXT DEFAULT '',
|
||||
watcher_account_id INTEGER DEFAULT 0,
|
||||
source_chat TEXT NOT NULL,
|
||||
source_message_id INTEGER NOT NULL DEFAULT 0,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(user_id, source_chat)
|
||||
UNIQUE(task_id, user_id, source_chat)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
@ -255,6 +256,7 @@ function initStore(userDataPath) {
|
||||
ensureColumn("invite_queue", "username", "TEXT DEFAULT ''");
|
||||
ensureColumn("invite_queue", "user_access_hash", "TEXT DEFAULT ''");
|
||||
ensureColumn("invite_queue", "watcher_account_id", "INTEGER DEFAULT 0");
|
||||
ensureColumn("invite_queue", "source_message_id", "INTEGER NOT NULL DEFAULT 0");
|
||||
ensureColumn("invites", "username", "TEXT DEFAULT ''");
|
||||
ensureColumn("invites", "user_access_hash", "TEXT DEFAULT ''");
|
||||
ensureColumn("invites", "confirmed", "INTEGER NOT NULL DEFAULT 1");
|
||||
@ -342,6 +344,71 @@ function initStore(userDataPath) {
|
||||
ensureColumn("task_accounts", "role_invite", "INTEGER NOT NULL DEFAULT 1");
|
||||
ensureColumn("task_accounts", "invite_limit", "INTEGER NOT NULL DEFAULT 0");
|
||||
|
||||
const hasInviteQueueScopedUnique = () => {
|
||||
const indexes = db.prepare("PRAGMA index_list(invite_queue)").all();
|
||||
for (const idx of indexes) {
|
||||
if (!idx || !idx.unique) continue;
|
||||
const cols = db.prepare(`PRAGMA index_info(${idx.name})`).all().map((row) => row.name);
|
||||
if (cols.length === 3 && cols[0] === "task_id" && cols[1] === "user_id" && cols[2] === "source_chat") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const migrateInviteQueueUniqueness = () => {
|
||||
if (hasInviteQueueScopedUnique()) return;
|
||||
const migrate = db.transaction(() => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS invite_queue_migrated (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER DEFAULT 0,
|
||||
user_id TEXT NOT NULL,
|
||||
username TEXT DEFAULT '',
|
||||
user_access_hash TEXT DEFAULT '',
|
||||
watcher_account_id INTEGER DEFAULT 0,
|
||||
source_chat TEXT NOT NULL,
|
||||
source_message_id INTEGER NOT NULL DEFAULT 0,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(task_id, user_id, source_chat)
|
||||
);
|
||||
`);
|
||||
db.exec(`
|
||||
INSERT INTO invite_queue_migrated (
|
||||
id, task_id, user_id, username, user_access_hash, watcher_account_id, source_chat,
|
||||
source_message_id, attempts, status, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
iq.id,
|
||||
COALESCE(iq.task_id, 0),
|
||||
iq.user_id,
|
||||
iq.username,
|
||||
iq.user_access_hash,
|
||||
COALESCE(iq.watcher_account_id, 0),
|
||||
iq.source_chat,
|
||||
COALESCE(iq.source_message_id, 0),
|
||||
COALESCE(iq.attempts, 0),
|
||||
iq.status,
|
||||
iq.created_at,
|
||||
iq.updated_at
|
||||
FROM invite_queue iq
|
||||
INNER JOIN (
|
||||
SELECT MAX(id) AS id
|
||||
FROM invite_queue
|
||||
GROUP BY COALESCE(task_id, 0), user_id, source_chat
|
||||
) latest ON latest.id = iq.id;
|
||||
`);
|
||||
db.exec("DROP TABLE invite_queue;");
|
||||
db.exec("ALTER TABLE invite_queue_migrated RENAME TO invite_queue;");
|
||||
});
|
||||
migrate();
|
||||
};
|
||||
|
||||
migrateInviteQueueUniqueness();
|
||||
|
||||
const settingsRow = db.prepare("SELECT value FROM settings WHERE key = ?").get("settings");
|
||||
if (!settingsRow) {
|
||||
db.prepare("INSERT INTO settings (key, value) VALUES (?, ?)")
|
||||
@ -646,7 +713,7 @@ function initStore(userDataPath) {
|
||||
db.prepare("DELETE FROM task_audit WHERE task_id = ?").run(taskId || 0);
|
||||
}
|
||||
|
||||
function enqueueInvite(taskId, userId, username, sourceChat, accessHash, watcherAccountId) {
|
||||
function enqueueInvite(taskId, userId, username, sourceChat, accessHash, watcherAccountId, sourceMessageId = 0) {
|
||||
const now = dayjs().toISOString();
|
||||
try {
|
||||
if (taskId) {
|
||||
@ -658,9 +725,11 @@ function initStore(userDataPath) {
|
||||
}
|
||||
}
|
||||
const result = db.prepare(`
|
||||
INSERT OR IGNORE INTO invite_queue (task_id, user_id, username, user_access_hash, watcher_account_id, source_chat, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)
|
||||
`).run(taskId || 0, userId, username || "", accessHash || "", watcherAccountId || 0, sourceChat, now, now);
|
||||
INSERT OR IGNORE INTO invite_queue (
|
||||
task_id, user_id, username, user_access_hash, watcher_account_id, source_chat, source_message_id, status, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)
|
||||
`).run(taskId || 0, userId, username || "", accessHash || "", watcherAccountId || 0, sourceChat, Number(sourceMessageId || 0), now, now);
|
||||
return result.changes > 0;
|
||||
} catch (error) {
|
||||
return false;
|
||||
|
||||
@ -353,6 +353,7 @@ class TaskRunner {
|
||||
userAccessHash: item.user_access_hash,
|
||||
username: item.username,
|
||||
sourceChat: item.source_chat,
|
||||
sourceMessageId: Number(item.source_message_id || 0),
|
||||
watcherAccountId: watcherAccount ? watcherAccount.id : 0,
|
||||
watcherPhone: watcherAccount ? watcherAccount.phone : ""
|
||||
});
|
||||
|
||||
@ -870,7 +870,7 @@ class TelegramManager {
|
||||
const accessHash = senderEntity && senderEntity.accessHash ? senderEntity.accessHash.toString() : "";
|
||||
const sourceChat = event.message.chatId ? event.message.chatId.toString() : "unknown";
|
||||
if (!this.monitorState.has(sourceChat)) return;
|
||||
this.store.enqueueInvite(0, userId, username, sourceChat, accessHash, this.monitorClientId || 0);
|
||||
this.store.enqueueInvite(0, userId, username, sourceChat, accessHash, this.monitorClientId || 0, 0);
|
||||
this.lastMonitorMessageAt = new Date().toISOString();
|
||||
this.lastMonitorSource = sourceChat;
|
||||
};
|
||||
@ -1479,6 +1479,7 @@ class TelegramManager {
|
||||
const accessHash = options.userAccessHash || "";
|
||||
const providedUsername = options.username || "";
|
||||
const sourceChat = options.sourceChat || "";
|
||||
const sourceMessageId = Number(options.sourceMessageId || 0);
|
||||
const attempts = [];
|
||||
let user = null;
|
||||
const resolveEvents = [];
|
||||
@ -1539,6 +1540,32 @@ class TelegramManager {
|
||||
attempts.push({ strategy: "participants", ok: false, detail: "source chat not provided" });
|
||||
resolveEvents.push("participants: skip (no source chat)");
|
||||
}
|
||||
if (!user && sourceChat && sourceMessageId > 0) {
|
||||
try {
|
||||
const resolvedSource = await this._resolveGroupEntity(client, sourceChat, false, account);
|
||||
if (resolvedSource && resolvedSource.ok && resolvedSource.entity) {
|
||||
const peer = await client.getInputEntity(resolvedSource.entity);
|
||||
user = new Api.InputUserFromMessage({
|
||||
peer,
|
||||
msgId: sourceMessageId,
|
||||
userId: BigInt(userId)
|
||||
});
|
||||
attempts.push({ strategy: "from_message", ok: true, detail: `msgId=${sourceMessageId}` });
|
||||
resolveEvents.push(`from_message: ok (msgId=${sourceMessageId})`);
|
||||
} else {
|
||||
const detail = resolvedSource ? resolvedSource.error : "resolve failed";
|
||||
attempts.push({ strategy: "from_message", ok: false, detail });
|
||||
resolveEvents.push(`from_message: fail (${detail})`);
|
||||
}
|
||||
} catch (error) {
|
||||
const detail = error.errorMessage || error.message || String(error);
|
||||
attempts.push({ strategy: "from_message", ok: false, detail });
|
||||
resolveEvents.push(`from_message: fail (${detail})`);
|
||||
}
|
||||
} else if (!user && sourceChat && sourceMessageId <= 0) {
|
||||
attempts.push({ strategy: "from_message", ok: false, detail: "source message not provided" });
|
||||
resolveEvents.push("from_message: skip (no source message)");
|
||||
}
|
||||
// username already attempted above
|
||||
if (!user) {
|
||||
const resolvedUser = await client.getEntity(userId);
|
||||
@ -2190,7 +2217,7 @@ class TelegramManager {
|
||||
username = "";
|
||||
accessHash = "";
|
||||
}
|
||||
this.store.enqueueInvite(0, senderId.toString(), username, group, accessHash, this.monitorClientId || 0);
|
||||
this.store.enqueueInvite(0, senderId.toString(), username, group, accessHash, this.monitorClientId || 0, Number(message && message.id ? message.id : 0));
|
||||
}
|
||||
}
|
||||
|
||||
@ -2423,7 +2450,9 @@ class TelegramManager {
|
||||
if (!sourceChat) {
|
||||
return { accessHash: "", detail: "no source chat" };
|
||||
}
|
||||
const cacheEntry = this.participantCache.get(sourceChat);
|
||||
const accountId = Number(client && client.__traceContext && client.__traceContext.accountId ? client.__traceContext.accountId : 0);
|
||||
const cacheKey = `${accountId}:${sourceChat}`;
|
||||
const cacheEntry = this.participantCache.get(cacheKey);
|
||||
const now = Date.now();
|
||||
if (cacheEntry && now - cacheEntry.at < 5 * 60 * 1000) {
|
||||
const cached = cacheEntry.map.get(userId.toString());
|
||||
@ -2440,7 +2469,7 @@ class TelegramManager {
|
||||
return { accessHash: "", detail: `resolve failed (${resolved ? resolved.error : "unknown"})` };
|
||||
}
|
||||
const map = await this._loadParticipantCache(client, resolved.entity, 400);
|
||||
this.participantCache.set(sourceChat, { at: now, map });
|
||||
this.participantCache.set(cacheKey, { at: now, map });
|
||||
if (!map.size) {
|
||||
return { accessHash: "", detail: "participants hidden" };
|
||||
}
|
||||
@ -3539,7 +3568,7 @@ class TelegramManager {
|
||||
username = "";
|
||||
accessHash = "";
|
||||
}
|
||||
this.store.enqueueInvite(0, senderId, username, state.source, accessHash, this.monitorClientId || 0);
|
||||
this.store.enqueueInvite(0, senderId, username, state.source, accessHash, this.monitorClientId || 0, Number(message && message.id ? message.id : 0));
|
||||
this.lastMonitorMessageAt = new Date().toISOString();
|
||||
this.lastMonitorSource = state.source;
|
||||
}
|
||||
@ -3594,6 +3623,25 @@ class TelegramManager {
|
||||
: explicitConfirmIdsRaw;
|
||||
const hasExplicitRoles = explicitMonitorIds.length || explicitInviteIds.length || explicitConfirmIds.length;
|
||||
|
||||
if (forceJoin) {
|
||||
const competitors = competitorGroups || [];
|
||||
for (const entry of accounts) {
|
||||
this._instrumentClientInvoke(entry.client, entry.account.id, entry.account.phone || "", Number(task && task.id ? task.id : 0));
|
||||
if (competitors.length) {
|
||||
await this._autoJoinGroups(entry.client, competitors, true, entry.account);
|
||||
}
|
||||
if (task.our_group) {
|
||||
await this._autoJoinGroups(entry.client, [task.our_group], true, entry.account);
|
||||
}
|
||||
}
|
||||
this.taskRoleAssignments.set(task.id, {
|
||||
competitorIds: hasExplicitRoles ? explicitMonitorIds : accounts.map((entry) => entry.account.id),
|
||||
ourIds: hasExplicitRoles ? explicitInviteIds : accounts.map((entry) => entry.account.id),
|
||||
confirmIds: hasExplicitRoles ? explicitConfirmIds : accounts.map((entry) => entry.account.id)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const competitors = competitorGroups || [];
|
||||
let cursor = 0;
|
||||
const usedForCompetitors = new Set();
|
||||
@ -3808,7 +3856,7 @@ class TelegramManager {
|
||||
}
|
||||
monitorEntry.lastMessageAt = new Date().toISOString();
|
||||
monitorEntry.lastSource = st.source;
|
||||
this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id);
|
||||
this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id, Number(message && message.id ? message.id : 0));
|
||||
return;
|
||||
}
|
||||
const senderPayload = { ...senderInfo.info };
|
||||
@ -3866,7 +3914,8 @@ class TelegramManager {
|
||||
username,
|
||||
st.source,
|
||||
accessHash,
|
||||
monitorAccount.account.id
|
||||
monitorAccount.account.id,
|
||||
Number(message && message.id ? message.id : 0)
|
||||
);
|
||||
const sender = message && message.sender ? message.sender : null;
|
||||
const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : "";
|
||||
@ -3974,7 +4023,7 @@ class TelegramManager {
|
||||
}
|
||||
monitorEntry.lastMessageAt = new Date().toISOString();
|
||||
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, Number(message && message.id ? message.id : 0))) {
|
||||
enqueued += 1;
|
||||
}
|
||||
continue;
|
||||
@ -4031,7 +4080,7 @@ class TelegramManager {
|
||||
}
|
||||
monitorEntry.lastMessageAt = messageDate.toISOString();
|
||||
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, Number(message && message.id ? message.id : 0))) {
|
||||
enqueued += 1;
|
||||
const sender = message && message.sender ? message.sender : null;
|
||||
const senderName = sender ? [sender.firstName, sender.lastName].filter(Boolean).join(" ") : "";
|
||||
@ -4154,7 +4203,17 @@ class TelegramManager {
|
||||
const targetGroups = task.cycle_competitors
|
||||
? [groups[Math.max(0, Number(task.competitor_cursor || 0)) % groups.length]]
|
||||
: groups;
|
||||
const entry = this._pickClientFromAllowed(accountIds);
|
||||
const monitorIds = this.store.listTaskAccounts(task.id)
|
||||
.filter((row) => row.role_monitor)
|
||||
.map((row) => Number(row.account_id))
|
||||
.filter(Boolean);
|
||||
const preferredMonitorIds = monitorIds.length
|
||||
? monitorIds.filter((id) => (accountIds || []).includes(id))
|
||||
: [];
|
||||
let entry = this._pickClientFromAllowed(preferredMonitorIds);
|
||||
if (!entry) {
|
||||
entry = this._pickClientFromAllowed(accountIds);
|
||||
}
|
||||
if (!entry) {
|
||||
const ids = Array.isArray(accountIds) ? accountIds.filter(Boolean) : [];
|
||||
const allAccounts = this.store.listAccounts();
|
||||
@ -4243,7 +4302,7 @@ class TelegramManager {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (this.store.enqueueInvite(task.id, rawSenderId, "", group, resolved.accessHash, entry.account.id)) {
|
||||
if (this.store.enqueueInvite(task.id, rawSenderId, "", group, resolved.accessHash, entry.account.id, Number(message && message.id ? message.id : 0))) {
|
||||
enqueued += 1;
|
||||
totalEnqueued += 1;
|
||||
}
|
||||
@ -4277,7 +4336,7 @@ class TelegramManager {
|
||||
}
|
||||
const { userId: senderId, username, accessHash } = senderPayload;
|
||||
if (this._isOwnAccount(senderId)) continue;
|
||||
if (this.store.enqueueInvite(task.id, senderId, username, group, accessHash, entry.account.id)) {
|
||||
if (this.store.enqueueInvite(task.id, senderId, username, group, accessHash, entry.account.id, Number(message && message.id ? message.id : 0))) {
|
||||
enqueued += 1;
|
||||
totalEnqueued += 1;
|
||||
}
|
||||
@ -4437,6 +4496,8 @@ class TelegramManager {
|
||||
return "поиск по username";
|
||||
case "entity":
|
||||
return "getEntity по userId";
|
||||
case "from_message":
|
||||
return "InputUserFromMessage";
|
||||
case "retry":
|
||||
return "повторная попытка";
|
||||
default:
|
||||
@ -4455,6 +4516,7 @@ class TelegramManager {
|
||||
if (normalized === "no result") return "нет результата";
|
||||
if (normalized === "resolve failed") return "не удалось найти пользователя по username";
|
||||
if (normalized === "getentity(userid)") return "получен через getEntity";
|
||||
if (normalized.startsWith("msgid=")) return `из сообщения (${raw})`;
|
||||
if (normalized === "no access_hash") return "нет access_hash";
|
||||
if (normalized === "no sender_id") return "в сообщении нет sender_id";
|
||||
if (normalized.includes("could not find the input entity")) {
|
||||
|
||||
@ -96,7 +96,7 @@ export default function InfoModal({ open, onClose, infoTab, setInfoTab }) {
|
||||
<li>Сохранить — сохраняет настройки задачи.</li>
|
||||
<li>Экспорт логов — выгружает логи/очередь/ошибки по задаче.</li>
|
||||
<li>Собрать историю — добавляет авторов последних сообщений конкурентов в очередь.</li>
|
||||
<li>Добавить ботов в Telegram группы — вводит аккаунты в конкурентов/нашу группу.</li>
|
||||
<li>Отправить заявки/вступить в группы — отправляет join (в приватных группах будет INVITE_REQUEST_SENT до ручного одобрения).</li>
|
||||
<li>Проверить всё — проверяет доступы и права инвайта у аккаунтов.</li>
|
||||
<li>Тестовый прогон — один реальный инвайт из очереди для проверки логики.</li>
|
||||
<li>Проверить участие — обновляет статусы участия аккаунтов в группах.</li>
|
||||
|
||||
@ -54,7 +54,7 @@ export default function QuickActionsBar({
|
||||
{taskActionLoading ? "Собираем..." : "Собрать историю"}
|
||||
</button>
|
||||
<button className="secondary" onClick={() => joinGroupsForTask("bar")} disabled={!hasSelectedTask}>
|
||||
Добавить ботов в Telegram группы
|
||||
Отправить заявки/вступить в группы
|
||||
</button>
|
||||
<button className="secondary" onClick={() => refreshMembership("bar")} disabled={!hasSelectedTask}>
|
||||
Проверить участие
|
||||
|
||||
@ -128,6 +128,7 @@ export default function useLogsView({
|
||||
item.user_id,
|
||||
item.username,
|
||||
item.source_chat,
|
||||
item.source_message_id,
|
||||
item.watcher_account_id,
|
||||
item.attempts,
|
||||
item.created_at
|
||||
|
||||
@ -112,12 +112,25 @@ function LogsTab({
|
||||
return "username (никнейм)";
|
||||
case "entity":
|
||||
return "entity по ID";
|
||||
case "from_message":
|
||||
return "InputUserFromMessage";
|
||||
case "retry":
|
||||
return "повторная попытка";
|
||||
default:
|
||||
return strategy || "—";
|
||||
}
|
||||
};
|
||||
const usedInputUserFromMessage = (invite) => {
|
||||
if (!invite) return false;
|
||||
if (invite.strategy === "from_message") return true;
|
||||
if (!invite.strategyMeta) return false;
|
||||
try {
|
||||
const parsed = JSON.parse(invite.strategyMeta);
|
||||
return Array.isArray(parsed) && parsed.some((item) => item && item.strategy === "from_message" && item.ok);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatStrategies = (meta) => {
|
||||
if (!meta) return "";
|
||||
@ -618,6 +631,9 @@ function LogsTab({
|
||||
{isMissingInvitee(invite) && (
|
||||
<span className="inline-flag warn">Не доставлен Telegram (INVITE_MISSING_INVITEE)</span>
|
||||
)}
|
||||
{usedInputUserFromMessage(invite) && (
|
||||
<span className="inline-flag">Через InputUserFromMessage</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="log-details">
|
||||
@ -708,7 +724,7 @@ function LogsTab({
|
||||
<div>Примечание: у наблюдателя стоят обе роли, но включен случайный выбор — инвайт выполнен другим аккаунтом.</div>
|
||||
)}
|
||||
<div>Пояснение: {buildDetailedExplanation(invite)}</div>
|
||||
<div>Стратегия: {invite.strategy || "—"}</div>
|
||||
<div>Стратегия: {strategyLabel(invite.strategy)}</div>
|
||||
{invite.strategyMeta && !hasStrategySuccess(invite.strategyMeta) && (
|
||||
<div>Результат: все стратегии не сработали</div>
|
||||
)}
|
||||
|
||||
@ -167,6 +167,7 @@ export default function QueueTab({
|
||||
<div className="log-head">
|
||||
<span>Пользователь</span>
|
||||
<span>Источник</span>
|
||||
<span>msg_id</span>
|
||||
<span>Наблюдатель</span>
|
||||
<span>Попытки</span>
|
||||
<span>Добавлен</span>
|
||||
@ -182,6 +183,7 @@ export default function QueueTab({
|
||||
<div className={`log-row ${issue ? "queue-row-warn" : ""}`} key={item.id}>
|
||||
<div>{formatUserWithUsername(item)}</div>
|
||||
<div>{item.source_chat || "—"}</div>
|
||||
<div>{item.source_message_id ? String(item.source_message_id) : "—"}</div>
|
||||
<div>{watcherLabel}</div>
|
||||
<div>{item.attempts ?? 0}</div>
|
||||
<div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user