feat: telegram commands, app settings, bot polling, webhook handling, UI improvements
Adds telegram bot command system with 13 commands (search, latest, random, etc.), webhook/polling handlers, rate limiting, app settings page, and various UI/UX improvements across all entity pages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,26 +8,47 @@
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import IconPicker from '$lib/components/IconPicker.svelte';
|
||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import Hint from '$lib/components/Hint.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import JinjaEditor from '$lib/components/JinjaEditor.svelte';
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
import type { TemplateConfig } from '$lib/types';
|
||||
|
||||
let configs = $state<any[]>([]);
|
||||
let configs = $state<TemplateConfig[]>([]);
|
||||
let loaded = $state(false);
|
||||
let varsRef = $state<Record<string, any>>({});
|
||||
let showVarsFor = $state<string | null>(null);
|
||||
let showForm = $state(false);
|
||||
let editing = $state<number | null>(null);
|
||||
let error = $state('');
|
||||
let confirmDelete = $state<any>(null);
|
||||
let confirmDelete = $state<{ id: number; onconfirm: () => Promise<void> } | null>(null);
|
||||
let slotPreview = $state<Record<string, string>>({});
|
||||
let slotErrors = $state<Record<string, string>>({});
|
||||
let slotErrorLines = $state<Record<string, number | null>>({});
|
||||
let slotErrorTypes = $state<Record<string, string>>({});
|
||||
let validateTimers: Record<string, ReturnType<typeof setTimeout>> = {};
|
||||
let dateFormatPreview = $state<Record<string, string | null>>({});
|
||||
|
||||
function refreshDateFormatPreview() {
|
||||
clearTimeout(validateTimers['_dateFmt']);
|
||||
validateTimers['_dateFmt'] = setTimeout(async () => {
|
||||
try {
|
||||
const res = await api('/template-configs/preview-date-format', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
date_format: (form as any).date_format,
|
||||
date_only_format: (form as any).date_only_format,
|
||||
}),
|
||||
});
|
||||
dateFormatPreview = res;
|
||||
} catch {
|
||||
dateFormatPreview = {};
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
|
||||
function validateSlot(slotKey: string, template: string, immediate = false) {
|
||||
if (validateTimers[slotKey]) clearTimeout(validateTimers[slotKey]);
|
||||
@@ -71,6 +92,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
refreshDateFormatPreview();
|
||||
}
|
||||
|
||||
const defaultForm = () => ({
|
||||
@@ -119,10 +141,10 @@
|
||||
finally { loaded = true; }
|
||||
}
|
||||
|
||||
function openNew() { form = defaultForm(); editing = null; showForm = true; slotPreview = {}; slotErrors = {}; }
|
||||
function openNew() { form = defaultForm(); editing = null; showForm = true; slotPreview = {}; slotErrors = {}; dateFormatPreview = {}; refreshDateFormatPreview(); }
|
||||
function edit(c: any) {
|
||||
form = { ...defaultForm(), ...c }; editing = c.id; showForm = true;
|
||||
slotPreview = {}; slotErrors = {};
|
||||
slotPreview = {}; slotErrors = {}; dateFormatPreview = {};
|
||||
setTimeout(() => refreshAllPreviews(), 100);
|
||||
}
|
||||
|
||||
@@ -154,8 +176,8 @@
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
// Restore allowed tags
|
||||
.replace(/<a href="([^"]*)">/g, '<a href="$1" target="_blank" rel="noopener">')
|
||||
// Restore allowed tags — only http(s) URLs for <a> to prevent javascript: XSS
|
||||
.replace(/<a href="(https?:\/\/[^&]*)">/g, '<a href="$1" target="_blank" rel="noopener noreferrer">')
|
||||
.replace(/<\/a>/g, '</a>')
|
||||
.replace(/<b>/g, '<b>').replace(/<\/b>/g, '</b>')
|
||||
.replace(/<i>/g, '<i>').replace(/<\/i>/g, '</i>')
|
||||
@@ -229,8 +251,13 @@
|
||||
</div>
|
||||
{#if slot.key === 'date_format' || slot.key === 'date_only_format'}
|
||||
<input bind:value={(form as any)[slot.key]}
|
||||
oninput={() => { clearTimeout(validateTimers['_fmt']); validateTimers['_fmt'] = setTimeout(refreshAllPreviews, 600); }}
|
||||
oninput={() => { clearTimeout(validateTimers['_fmt']); validateTimers['_fmt'] = setTimeout(refreshAllPreviews, 600); refreshDateFormatPreview(); }}
|
||||
class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)] font-mono" />
|
||||
{#if dateFormatPreview[slot.key]}
|
||||
<p class="mt-1 text-xs font-mono" style="color: var(--color-muted-foreground);">{t('templateConfig.preview')}: <span style="color: var(--color-foreground);">{dateFormatPreview[slot.key]}</span></p>
|
||||
{:else if dateFormatPreview[slot.key] === null}
|
||||
<p class="mt-1 text-xs" style="color: var(--color-error-fg);">{t('templateConfig.invalidFormat')}</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<JinjaEditor value={(form as any)[slot.key] || ''} onchange={(v: string) => { (form as any)[slot.key] = v; validateSlot(slot.key, v); }} rows={slot.rows || 3} errorLine={slotErrorLines[slot.key] || null} />
|
||||
{#if slotErrors[slot.key]}
|
||||
@@ -262,10 +289,7 @@
|
||||
|
||||
{#if configs.length === 0 && !showForm}
|
||||
<Card>
|
||||
<div class="flex flex-col items-center py-8 gap-3" style="color: var(--color-muted-foreground);">
|
||||
<div style="opacity: 0.4;"><MdiIcon name="mdiFileDocumentEdit" size={40} /></div>
|
||||
<p class="text-sm">{t('templateConfig.noConfigs')}</p>
|
||||
</div>
|
||||
<EmptyState icon="mdiFileDocumentEdit" message={t('templateConfig.noConfigs')} />
|
||||
</Card>
|
||||
{:else}
|
||||
<div class="space-y-3 stagger-children">
|
||||
|
||||
Reference in New Issue
Block a user