feat(watcher,api,frontend): watched bots become accounts and drain the update backlog
This commit is contained in:
1 parent
4bd031f21e
commit
b0ed041c2e
21 files changed
+414
-1155
No files matched your search
@@ -4,8 +4,6 @@ import type {
|
||||
Alert,
|
||||
Annotation,
|
||||
AvatarHistoryView,
|
||||
BotStats,
|
||||
BotUpdate,
|
||||
CallbackView,
|
||||
CaptureToggles,
|
||||
Chat,
|
||||
@@ -583,20 +581,3 @@ export function addWatchedBot(token: string): Promise<WatchedBot> {
|
||||
export function removeWatchedBot(botId: number): Promise<void> {
|
||||
return request<void>(`/bots/${botId}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export function botStats(botId: number, days = 90): Promise<BotStats> {
|
||||
return request<BotStats>(`/bots/${botId}/stats`, { query: { days } });
|
||||
}
|
||||
|
||||
export function botUpdates(
|
||||
botId: number,
|
||||
options: { limit?: number; offset?: number; senderId?: number } = {}
|
||||
): Promise<BotUpdate[]> {
|
||||
return request<BotUpdate[]>(`/bots/${botId}/updates`, {
|
||||
query: {
|
||||
limit: options.limit,
|
||||
offset: options.offset,
|
||||
sender_id: options.senderId,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export interface Account {
|
||||
account_id: number;
|
||||
device_model: string | null;
|
||||
is_active: boolean;
|
||||
is_bot: boolean;
|
||||
label: string | null;
|
||||
phone: string | null;
|
||||
tg_user_id: number | null;
|
||||
@@ -548,80 +549,14 @@ export interface ShareSubject {
|
||||
}
|
||||
|
||||
export interface WatchedBot {
|
||||
account_id: number | null;
|
||||
added_at: string;
|
||||
backfilled_at: string | null;
|
||||
bot_id: number;
|
||||
checked_at: string | null;
|
||||
enabled: boolean;
|
||||
incoming: number;
|
||||
last_ts: string | null;
|
||||
senders: number;
|
||||
status: string;
|
||||
status_detail: string | null;
|
||||
title: string | null;
|
||||
username: string | null;
|
||||
}
|
||||
|
||||
export interface BotSummary {
|
||||
chats: number;
|
||||
first_ts: string | null;
|
||||
incoming: number;
|
||||
last_ts: string | null;
|
||||
outgoing: number;
|
||||
senders: number;
|
||||
}
|
||||
|
||||
export interface BotSender {
|
||||
chat_id: number | null;
|
||||
first_ts: string;
|
||||
incoming: number;
|
||||
is_bot: boolean | null;
|
||||
last_ts: string;
|
||||
name: string | null;
|
||||
sender_id: number;
|
||||
username: string | null;
|
||||
}
|
||||
|
||||
export interface BotBucket {
|
||||
bucket: string;
|
||||
incoming: number;
|
||||
senders: number;
|
||||
}
|
||||
|
||||
export interface BotCount {
|
||||
incoming: number;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface BotHour {
|
||||
hour: number;
|
||||
incoming: number;
|
||||
}
|
||||
|
||||
export interface BotStats {
|
||||
bot: WatchedBot;
|
||||
hours: BotHour[];
|
||||
kinds: BotCount[];
|
||||
media: BotCount[];
|
||||
senders: BotSender[];
|
||||
summary: BotSummary;
|
||||
timeline: BotBucket[];
|
||||
}
|
||||
|
||||
export interface BotUpdate {
|
||||
chat_id: number;
|
||||
chat_title: string | null;
|
||||
chat_type: string | null;
|
||||
edited_at: string | null;
|
||||
item_id: number;
|
||||
kind: string;
|
||||
media_kind: string | null;
|
||||
outgoing: boolean;
|
||||
sender_id: number | null;
|
||||
sender_is_bot: boolean | null;
|
||||
sender_name: string | null;
|
||||
sender_username: string | null;
|
||||
source: string;
|
||||
text: string | null;
|
||||
ts: string;
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
watches: "Отслеживания",
|
||||
alerts: "Алерты",
|
||||
shares: "Файлы по ссылке",
|
||||
bots: "Боты под наблюдением",
|
||||
bots: "Боты",
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { botStats, botUpdates } from "$lib/api/endpoints";
|
||||
import type { BotSender, BotStats, BotUpdate } from "$lib/api/types";
|
||||
import BotTimeline from "$lib/components/bots/BotTimeline.svelte";
|
||||
import BotUpdateList from "$lib/components/bots/BotUpdateList.svelte";
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import Spinner from "$lib/components/ui/Spinner.svelte";
|
||||
import { formatListDate } from "$lib/format/datetime";
|
||||
|
||||
interface Props {
|
||||
botId: number;
|
||||
}
|
||||
|
||||
const { botId }: Props = $props();
|
||||
|
||||
let stats = $state<BotStats | null>(null);
|
||||
let updates = $state<BotUpdate[]>([]);
|
||||
let sender = $state<number | null>(null);
|
||||
let loading = $state(true);
|
||||
|
||||
$effect(() => {
|
||||
loading = true;
|
||||
botStats(botId)
|
||||
.then((result) => {
|
||||
stats = result;
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false;
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
botUpdates(botId, { limit: 50, senderId: sender ?? undefined }).then(
|
||||
(result) => {
|
||||
updates = result;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
function senderLabel(item: BotSender): string {
|
||||
return (
|
||||
item.name ??
|
||||
(item.username ? `@${item.username}` : String(item.sender_id))
|
||||
);
|
||||
}
|
||||
|
||||
function toggleSender(id: number): void {
|
||||
sender = sender === id ? null : id;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loading && stats === null}
|
||||
<Spinner />
|
||||
{:else if stats}
|
||||
<div class="stats">
|
||||
<div class="tiles">
|
||||
<div class="tile">
|
||||
<span class="value">{stats.summary.incoming}</span>
|
||||
<span class="label">входящих</span>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<span class="value">{stats.summary.senders}</span>
|
||||
<span class="label">отправителей</span>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<span class="value">{stats.summary.chats}</span>
|
||||
<span class="label">чатов</span>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<span class="value">{stats.summary.outgoing}</span>
|
||||
<span class="label">ответов бота</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if stats.timeline.length > 0}
|
||||
<section>
|
||||
<h3>По дням</h3>
|
||||
<BotTimeline timeline={stats.timeline} />
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if stats.media.length > 0}
|
||||
<section>
|
||||
<h3>Вложения</h3>
|
||||
<ul class="counts">
|
||||
{#each stats.media as item (item.key)}
|
||||
<li><span>{item.key}</span><b>{item.incoming}</b></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section>
|
||||
<h3>Кто пишет</h3>
|
||||
{#if stats.senders.length === 0}
|
||||
<p class="empty">Пока никто.</p>
|
||||
{:else}
|
||||
<ul class="senders">
|
||||
{#each stats.senders as item (item.sender_id)}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="sender"
|
||||
class:active={sender === item.sender_id}
|
||||
onclick={() => toggleSender(item.sender_id)}
|
||||
>
|
||||
<span class="name">{senderLabel(item)}</span>
|
||||
<span class="meta">
|
||||
{item.incoming}
|
||||
· {formatListDate(item.last_ts)}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<h3>Лента</h3>
|
||||
{#if sender !== null}
|
||||
<Button variant="translucent" smaller onclick={() => (sender = null)}>
|
||||
Все
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if updates.length === 0}
|
||||
<p class="empty">Ничего не пришло.</p>
|
||||
{:else}
|
||||
<BotUpdateList {updates} />
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
align-items: center;
|
||||
|
||||
padding: 0.5rem 0.25rem;
|
||||
border-radius: var(--border-radius-default);
|
||||
background-color: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 1.125rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.counts,
|
||||
.senders {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.counts li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.sender {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: none;
|
||||
border-radius: var(--border-radius-default);
|
||||
|
||||
font-size: 0.875rem;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--color-background-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { BotBucket } from "$lib/api/types";
|
||||
|
||||
interface Props {
|
||||
timeline: BotBucket[];
|
||||
}
|
||||
|
||||
const { timeline }: Props = $props();
|
||||
|
||||
const max = $derived(
|
||||
Math.max(1, ...timeline.map((bucket) => bucket.incoming))
|
||||
);
|
||||
|
||||
function barTitle(bucket: BotBucket): string {
|
||||
return `${bucket.bucket.slice(0, 10)} — ${bucket.incoming} входящих от ${bucket.senders}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bars">
|
||||
{#each timeline as bucket (bucket.bucket)}
|
||||
<div
|
||||
class="bar"
|
||||
style:height={`${(bucket.incoming / max) * 100}%`}
|
||||
title={barTitle(bucket)}
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 1px;
|
||||
height: 7.5rem;
|
||||
}
|
||||
|
||||
.bar {
|
||||
flex: 1;
|
||||
min-height: 2px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,106 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { BotUpdate } from "$lib/api/types";
|
||||
import { formatFull } from "$lib/format/datetime";
|
||||
import { mediaKindLabel } from "$lib/format/media";
|
||||
|
||||
interface Props {
|
||||
updates: BotUpdate[];
|
||||
}
|
||||
|
||||
const { updates }: Props = $props();
|
||||
|
||||
const kindLabels: Record<string, string> = {
|
||||
message: "сообщение",
|
||||
callback_query: "кнопка",
|
||||
member: "статус",
|
||||
};
|
||||
|
||||
function senderName(update: BotUpdate): string {
|
||||
if (update.sender_name) {
|
||||
return update.sender_name;
|
||||
}
|
||||
if (update.sender_username) {
|
||||
return `@${update.sender_username}`;
|
||||
}
|
||||
return update.sender_id === null ? "неизвестно" : String(update.sender_id);
|
||||
}
|
||||
|
||||
function body(update: BotUpdate): string {
|
||||
const media = mediaKindLabel(update.media_kind);
|
||||
const text = update.text ?? "";
|
||||
if (media && text) {
|
||||
return `[${media}] ${text}`;
|
||||
}
|
||||
return media ? `[${media}]` : text;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="updates">
|
||||
{#each updates as update (`${update.kind}:${update.chat_id}:${update.item_id}:${update.ts}`)}
|
||||
<li class="update">
|
||||
<div class="head">
|
||||
<span class="sender">{senderName(update)}</span>
|
||||
{#if update.sender_username && update.sender_name}
|
||||
<span class="username">@{update.sender_username}</span>
|
||||
{/if}
|
||||
<span class="kind">{kindLabels[update.kind] ?? update.kind}</span>
|
||||
{#if update.source === "history"}
|
||||
<span class="kind">история</span>
|
||||
{/if}
|
||||
<time>{formatFull(update.ts)}</time>
|
||||
</div>
|
||||
{#if body(update)}
|
||||
<p class="body">{body(update)}</p>
|
||||
{/if}
|
||||
{#if update.chat_title && update.chat_type !== "private"}
|
||||
<div class="chat">{update.chat_title}</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style lang="scss">
|
||||
.updates {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.update {
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.sender {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.username,
|
||||
.kind,
|
||||
time {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow-wrap: anywhere;
|
||||
margin: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.chat {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -5,18 +5,17 @@
|
||||
removeWatchedBot,
|
||||
} from "$lib/api/endpoints";
|
||||
import type { WatchedBot } from "$lib/api/types";
|
||||
import BotStatsView from "$lib/components/bots/BotStatsView.svelte";
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import Spinner from "$lib/components/ui/Spinner.svelte";
|
||||
import { formatListDate } from "$lib/format/datetime";
|
||||
import { accounts } from "$lib/stores/accounts.svelte";
|
||||
import { toasts } from "$lib/stores/toasts.svelte";
|
||||
import { ui } from "$lib/stores/ui.svelte";
|
||||
|
||||
let bots = $state<WatchedBot[]>([]);
|
||||
let loading = $state(true);
|
||||
let token = $state("");
|
||||
let adding = $state(false);
|
||||
let selected = $state<number | null>(null);
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
pending: "подключается",
|
||||
@@ -48,6 +47,7 @@
|
||||
await addWatchedBot(token.trim());
|
||||
token = "";
|
||||
await load();
|
||||
await accounts.load();
|
||||
} catch {
|
||||
toasts.error("Не похоже на токен бота");
|
||||
} finally {
|
||||
@@ -57,106 +57,87 @@
|
||||
|
||||
async function remove(bot: WatchedBot) {
|
||||
await removeWatchedBot(bot.bot_id);
|
||||
if (selected === bot.bot_id) {
|
||||
selected = null;
|
||||
}
|
||||
await load();
|
||||
await accounts.load();
|
||||
}
|
||||
|
||||
function botName(bot: WatchedBot): string {
|
||||
return bot.username
|
||||
? `@${bot.username}`
|
||||
: (bot.title ?? String(bot.bot_id));
|
||||
return bot.username ? `@${bot.username}` : (bot.title ?? String(bot.bot_id));
|
||||
}
|
||||
|
||||
const current = $derived(bots.find((bot) => bot.bot_id === selected) ?? null);
|
||||
function open(bot: WatchedBot) {
|
||||
if (bot.account_id === null) {
|
||||
return;
|
||||
}
|
||||
accounts.select(bot.account_id);
|
||||
ui.closeSettings();
|
||||
ui.closePanel();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="panel-content">
|
||||
{#if current}
|
||||
<header class="bot-header">
|
||||
<Button
|
||||
variant="translucent"
|
||||
round
|
||||
smaller
|
||||
onclick={() => (selected = null)}
|
||||
aria-label="Назад"
|
||||
>
|
||||
<Icon name="arrow-left" />
|
||||
</Button>
|
||||
<div>
|
||||
<div class="name">{botName(current)}</div>
|
||||
<div class="meta">
|
||||
{statusLabels[current.status] ?? current.status}
|
||||
{#if current.backfilled_at === null && current.status === "ok"}
|
||||
· история загружается
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{#if current.status_detail}
|
||||
<p class="detail">{current.status_detail}</p>
|
||||
{/if}
|
||||
<BotStatsView botId={current.bot_id} />
|
||||
{#if loading}
|
||||
<Spinner />
|
||||
{:else if bots.length === 0}
|
||||
<p class="empty">Ботов пока нет.</p>
|
||||
{:else}
|
||||
{#if loading}
|
||||
<Spinner />
|
||||
{:else if bots.length === 0}
|
||||
<p class="empty">Ботов пока нет.</p>
|
||||
{:else}
|
||||
<ul class="bots">
|
||||
{#each bots as bot (bot.bot_id)}
|
||||
<li class="bot">
|
||||
<button
|
||||
type="button"
|
||||
class="open"
|
||||
onclick={() => (selected = bot.bot_id)}
|
||||
>
|
||||
<span class="name">{botName(bot)}</span>
|
||||
<span class="meta">
|
||||
{statusLabels[bot.status] ?? bot.status}
|
||||
· {bot.incoming} от {bot.senders}
|
||||
{#if bot.last_ts}
|
||||
· {formatListDate(bot.last_ts)}
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
{#if bot.enabled}
|
||||
<Button
|
||||
variant="translucent"
|
||||
round
|
||||
smaller
|
||||
onclick={() => remove(bot)}
|
||||
aria-label="Отключить"
|
||||
>
|
||||
<Icon name="delete" />
|
||||
</Button>
|
||||
<ul class="bots">
|
||||
{#each bots as bot (bot.bot_id)}
|
||||
<li class="bot">
|
||||
<button
|
||||
type="button"
|
||||
class="open"
|
||||
disabled={bot.account_id === null}
|
||||
onclick={() => open(bot)}
|
||||
>
|
||||
<span class="name">{botName(bot)}</span>
|
||||
<span class="meta">
|
||||
{statusLabels[bot.status] ?? bot.status}
|
||||
{#if bot.account_id !== null}
|
||||
· аккаунт {bot.account_id}
|
||||
{/if}
|
||||
</span>
|
||||
{#if bot.status_detail}
|
||||
<span class="detail">{bot.status_detail}</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<div class="add">
|
||||
<label class="field">
|
||||
<span>Токен бота от @BotFather</span>
|
||||
<input type="password" bind:value={token} placeholder="123456:AA…">
|
||||
</label>
|
||||
<Button fluid loading={adding} onclick={add}>Добавить</Button>
|
||||
</div>
|
||||
|
||||
<p class="note">
|
||||
Бот логинится по MTProto, отдельной авторизацией. Это не забирает
|
||||
getUpdates у того, кто держит того же бота через Bot API, и не требует
|
||||
вебхука. Сколько прошлой переписки удастся вытащить, решает сервер
|
||||
Telegram: watcher просит историю диалогов бота и берёт, что дают.
|
||||
</p>
|
||||
<p class="note">
|
||||
В личке бот видит каждого, кто ему написал. В группах ему доставляют всё
|
||||
только при выключенном privacy mode, иначе — команды и ответы на его
|
||||
сообщения. Отключение бота убирает токен и сессию, но архив остаётся.
|
||||
</p>
|
||||
</button>
|
||||
{#if bot.enabled}
|
||||
<Button
|
||||
variant="translucent"
|
||||
round
|
||||
smaller
|
||||
onclick={() => remove(bot)}
|
||||
aria-label="Отключить"
|
||||
>
|
||||
<Icon name="delete" />
|
||||
</Button>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<div class="add">
|
||||
<label class="field">
|
||||
<span>Токен бота от @BotFather</span>
|
||||
<input type="password" bind:value={token} placeholder="123456:AA…">
|
||||
</label>
|
||||
<Button fluid loading={adding} onclick={add}>Добавить</Button>
|
||||
</div>
|
||||
|
||||
<p class="note">
|
||||
Бот входит по MTProto отдельной авторизацией — это не отнимает getUpdates у
|
||||
того, кто держит того же бота через Bot API, и не требует вебхука. Дальше он
|
||||
становится обычным аккаунтом архива: чаты, сообщения, медиа и участники
|
||||
групп видны там же, где переписка остальных аккаунтов.
|
||||
</p>
|
||||
<p class="note">
|
||||
Прошлое подтягивается из очереди недоставленных апдейтов, которую Telegram
|
||||
держит для бота; история диалогов боту запрещена, глубже этой очереди
|
||||
заглянуть нельзя. В личке бот видит каждого, кто ему написал; в группах всё
|
||||
— только при выключенном privacy mode. Отключение убирает токен и сессию,
|
||||
архив остаётся.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -167,12 +148,6 @@
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.bot-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.bots {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -205,24 +180,27 @@
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.meta {
|
||||
.meta,
|
||||
.detail {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.detail {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.add {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
/>
|
||||
<SettingsItem
|
||||
icon="bots"
|
||||
label="Боты под наблюдением"
|
||||
label="Боты"
|
||||
onclick={() => ui.openPanel("bots")}
|
||||
/>
|
||||
<SettingsItem
|
||||
|
||||
Reference in new issue
Block a user