Persist Telegram chats in DB, auto-save from webhooks, click-to-copy
All checks were successful
Validate / Hassfest (push) Successful in 4s

- Add TelegramChat model (bot_id, chat_id, title, type, username)
- Chats auto-saved when bot receives webhook messages
- New API: GET/DELETE chats, POST discover (merges from getUpdates)
- Cascade delete chats when bot is deleted
- Frontend: click chat row to copy chat ID to clipboard
- Frontend: delete button per chat, "Discover chats" sync button
- Add 4 i18n keys (EN/RU) for chat management

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 21:45:20 +03:00
parent e6ff0a423a
commit 482f54d620
6 changed files with 206 additions and 30 deletions

View File

@@ -194,7 +194,11 @@
"rateSearch": "Search cooldown",
"rateFind": "Find cooldown",
"rateDefault": "Default cooldown",
"syncCommands": "Sync to Telegram"
"syncCommands": "Sync to Telegram",
"discoverChats": "Discover chats from Telegram",
"clickToCopy": "Click to copy chat ID",
"chatsDiscovered": "Chats discovered",
"chatDeleted": "Chat removed"
},
"trackingConfig": {
"title": "Tracking Configs",

View File

@@ -194,7 +194,11 @@
"rateSearch": "Кулдаун поиска",
"rateFind": "Кулдаун поиска файлов",
"rateDefault": "Кулдаун по умолчанию",
"syncCommands": "Синхронизировать с Telegram"
"syncCommands": "Синхронизировать с Telegram",
"discoverChats": "Обнаружить чаты из Telegram",
"clickToCopy": "Нажмите, чтобы скопировать ID чата",
"chatsDiscovered": "Чаты обнаружены",
"chatDeleted": "Чат удалён"
},
"trackingConfig": {
"title": "Конфигурации отслеживания",

View File

@@ -11,7 +11,7 @@
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import Hint from '$lib/components/Hint.svelte';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
import { snackSuccess, snackError, snackInfo } from '$lib/stores/snackbar.svelte';
const ALL_COMMANDS = [
'status', 'albums', 'events', 'summary', 'latest',
@@ -82,11 +82,7 @@
expandedSection[botId] = section;
if (section === 'chats') {
chatsLoading[botId] = true;
api(`/telegram-bots/${botId}/chats`)
.then((data: any) => chats[botId] = data)
.catch(() => chats[botId] = [])
.finally(() => chatsLoading[botId] = false);
loadChats(botId);
}
if (section === 'commands') {
@@ -95,6 +91,35 @@
}
}
async function loadChats(botId: number) {
chatsLoading[botId] = true;
try { chats[botId] = await api(`/telegram-bots/${botId}/chats`); }
catch { chats[botId] = []; }
chatsLoading[botId] = false;
}
async function discoverChats(botId: number) {
chatsLoading[botId] = true;
try {
chats[botId] = await api(`/telegram-bots/${botId}/chats/discover`, { method: 'POST' });
snackSuccess(t('telegramBot.chatsDiscovered'));
} catch (err: any) { snackError(err.message); }
chatsLoading[botId] = false;
}
async function deleteChat(botId: number, chatDbId: number) {
try {
await api(`/telegram-bots/${botId}/chats/${chatDbId}`, { method: 'DELETE' });
chats[botId] = (chats[botId] || []).filter((c: any) => c.id !== chatDbId);
snackSuccess(t('telegramBot.chatDeleted'));
} catch (err: any) { snackError(err.message); }
}
function copyChatId(chatId: string) {
navigator.clipboard.writeText(chatId);
snackInfo(t('snack.copied') + ': ' + chatId);
}
function toggleCommand(botId: number, cmd: string) {
const cfg = editingConfig[botId];
if (!cfg) return;
@@ -211,19 +236,24 @@
{:else}
<div class="space-y-1">
{#each chats[bot.id] as chat}
<div class="flex items-center justify-between text-sm px-2 py-1 rounded hover:bg-[var(--color-muted)]">
<div>
<div class="flex items-center justify-between text-sm px-2 py-1 rounded hover:bg-[var(--color-muted)] group">
<button class="flex items-center gap-2 text-left cursor-pointer"
onclick={() => copyChatId(chat.chat_id)}
title={t('telegramBot.clickToCopy')}>
<span class="font-medium">{chat.title || chat.username || 'Unknown'}</span>
<span class="text-xs ml-2 px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{chatTypeLabel(chat.type)}</span>
</div>
<span class="text-xs text-[var(--color-muted-foreground)] font-mono">{chat.id}</span>
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{chatTypeLabel(chat.type)}</span>
<span class="text-xs text-[var(--color-muted-foreground)] font-mono">{chat.chat_id}</span>
</button>
<IconButton icon="mdiDelete" title={t('common.delete')} size={14}
onclick={() => deleteChat(bot.id, chat.id)} variant="danger" />
</div>
{/each}
</div>
{/if}
<button onclick={() => toggleSection(bot.id, 'chats')}
class="text-xs text-[var(--color-muted-foreground)] hover:underline mt-2">
{t('telegramBot.refreshChats')}
<button onclick={() => discoverChats(bot.id)}
class="text-xs text-[var(--color-primary)] hover:underline mt-2 flex items-center gap-1">
<MdiIcon name="mdiSync" size={14} />
{t('telegramBot.discoverChats')}
</button>
</div>
{/if}