feat: fix template preview links, default chat action, update default templates

- Fix sanitizePreview regex to match literal quotes instead of " entities
- Default telegram chat_action to "typing" in model and frontend
- Change "photo(s)" to "file(s)" in default templates (EN/RU)
- Remove redundant album URL line from assets_added templates
- Auto-refresh system-owned templates from files on server startup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 01:37:58 +03:00
parent 1d445f3980
commit 371ea70756
8 changed files with 37 additions and 29 deletions
+2 -2
View File
@@ -23,7 +23,7 @@
let formType = $state<'telegram' | 'webhook'>('telegram'); let formType = $state<'telegram' | 'webhook'>('telegram');
const defaultForm = () => ({ name: '', icon: '', bot_id: 0, chat_id: '', bot_token: '', url: '', headers: '', const defaultForm = () => ({ name: '', icon: '', bot_id: 0, chat_id: '', bot_token: '', url: '', headers: '',
max_media_to_send: 50, max_media_per_group: 10, media_delay: 500, max_asset_size: 50, max_media_to_send: 50, max_media_per_group: 10, media_delay: 500, max_asset_size: 50,
disable_url_preview: false, send_large_photos_as_documents: false, ai_captions: false, chat_action: '' }); disable_url_preview: false, send_large_photos_as_documents: false, ai_captions: false, chat_action: 'typing' });
let form = $state(defaultForm()); let form = $state(defaultForm());
let error = $state(''); let error = $state('');
let headersError = $state(''); let headersError = $state('');
@@ -55,7 +55,7 @@
max_media_to_send: c.max_media_to_send ?? 50, max_media_per_group: c.max_media_per_group ?? 10, max_media_to_send: c.max_media_to_send ?? 50, max_media_per_group: c.max_media_per_group ?? 10,
media_delay: c.media_delay ?? 500, max_asset_size: c.max_asset_size ?? 50, media_delay: c.media_delay ?? 500, max_asset_size: c.max_asset_size ?? 50,
disable_url_preview: c.disable_url_preview ?? false, send_large_photos_as_documents: c.send_large_photos_as_documents ?? false, disable_url_preview: c.disable_url_preview ?? false, send_large_photos_as_documents: c.send_large_photos_as_documents ?? false,
ai_captions: c.ai_captions ?? false, chat_action: c.chat_action ?? '', ai_captions: c.ai_captions ?? false, chat_action: c.chat_action ?? 'typing',
}; };
editing = tgt.id; showTelegramSettings = false; showForm = true; editing = tgt.id; showTelegramSettings = false; showForm = true;
if (form.bot_id) await loadBotChats(); if (form.bot_id) await loadBotChats();
@@ -177,7 +177,7 @@
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
// Restore allowed tags — only http(s) URLs for <a> to prevent javascript: XSS // Restore allowed tags — only http(s) URLs for <a> to prevent javascript: XSS
.replace(/&lt;a href=&quot;(https?:\/\/[^&]*)&quot;&gt;/g, '<a href="$1" target="_blank" rel="noopener noreferrer">') .replace(/&lt;a href="(https?:\/\/[^"]*)"&gt;/g, '<a href="$1" target="_blank" rel="noopener noreferrer">')
.replace(/&lt;\/a&gt;/g, '</a>') .replace(/&lt;\/a&gt;/g, '</a>')
.replace(/&lt;b&gt;/g, '<b>').replace(/&lt;\/b&gt;/g, '</b>') .replace(/&lt;b&gt;/g, '<b>').replace(/&lt;\/b&gt;/g, '</b>')
.replace(/&lt;i&gt;/g, '<i>').replace(/&lt;\/i&gt;/g, '</i>') .replace(/&lt;i&gt;/g, '<i>').replace(/&lt;\/i&gt;/g, '</i>')
@@ -1,12 +1,9 @@
📷 {{ added_count }} new photo(s) added to album {% if public_url %}<a href="{{ public_url }}">{{ album_name }}</a>{% else %}"{{ album_name }}"{% endif %}. 📷 {{ added_count }} new file(s) added to album {% if public_url %}<a href="{{ public_url }}">{{ album_name }}</a>{% else %}"{{ album_name }}"{% endif %}.
{%- if common_date %} 📅 {{ common_date }}{% endif %} {%- if common_date %} 📅 {{ common_date }}{% endif %}
{%- if common_location %} 📍 {{ common_location }}{% endif %} {%- if common_location %} 📍 {{ common_location }}{% endif %}
{%- if people %} {%- if people %}
👤 {{ people | join(", ") }} 👤 {{ people | join(", ") }}
{%- endif %} {%- endif %}
{%- if public_url %}
🔗 <a href="{{ public_url }}">Album URL</a>
{%- endif %}
{%- if added_assets %} {%- if added_assets %}
{%- for asset in added_assets %} {%- for asset in added_assets %}
• {%- if asset.type == "VIDEO" %} 🎬{% else %} 🖼️{% endif %} {% if asset.public_url %}<a href="{{ asset.public_url }}">{{ asset.filename }}</a>{% else %}{{ asset.filename }}{% endif %} • {%- if asset.type == "VIDEO" %} 🎬{% else %} 🖼️{% endif %} {% if asset.public_url %}<a href="{{ asset.public_url }}">{{ asset.filename }}</a>{% else %}{{ asset.filename }}{% endif %}
@@ -1 +1 @@
🗑️ {{ removed_count }} photo(s) removed from album {% if public_url %}<a href="{{ public_url }}">{{ album_name }}</a>{% else %}"{{ album_name }}"{% endif %}. 🗑️ {{ removed_count }} file(s) removed from album {% if public_url %}<a href="{{ public_url }}">{{ album_name }}</a>{% else %}"{{ album_name }}"{% endif %}.
@@ -1,12 +1,9 @@
📷 {{ added_count }} новых фото добавлено в альбом {% if public_url %}<a href="{{ public_url }}">{{ album_name }}</a>{% else %}"{{ album_name }}"{% endif %}. 📷 {{ added_count }} новых файл(ов) добавлено в альбом {% if public_url %}<a href="{{ public_url }}">{{ album_name }}</a>{% else %}"{{ album_name }}"{% endif %}.
{%- if common_date %} 📅 {{ common_date }}{% endif %} {%- if common_date %} 📅 {{ common_date }}{% endif %}
{%- if common_location %} 📍 {{ common_location }}{% endif %} {%- if common_location %} 📍 {{ common_location }}{% endif %}
{%- if people %} {%- if people %}
👤 {{ people | join(", ") }} 👤 {{ people | join(", ") }}
{%- endif %} {%- endif %}
{%- if public_url %}
🔗 <a href="{{ public_url }}">Ссылка на альбом</a>
{%- endif %}
{%- if added_assets %} {%- if added_assets %}
{%- for asset in added_assets %} {%- for asset in added_assets %}
• {%- if asset.type == "VIDEO" %} 🎬{% else %} 🖼️{% endif %} {% if asset.public_url %}<a href="{{ asset.public_url }}">{{ asset.filename }}</a>{% else %}{{ asset.filename }}{% endif %} • {%- if asset.type == "VIDEO" %} 🎬{% else %} 🖼️{% endif %} {% if asset.public_url %}<a href="{{ asset.public_url }}">{{ asset.filename }}</a>{% else %}{{ asset.filename }}{% endif %}
@@ -1 +1 @@
🗑️ {{ removed_count }} фото удалено из альбома {% if public_url %}<a href="{{ public_url }}">{{ album_name }}</a>{% else %}"{{ album_name }}"{% endif %}. 🗑️ {{ removed_count }} файл(ов) удалено из альбома {% if public_url %}<a href="{{ public_url }}">{{ album_name }}</a>{% else %}"{{ album_name }}"{% endif %}.
@@ -164,7 +164,7 @@ class NotificationTarget(SQLModel, table=True):
name: str name: str
icon: str = Field(default="") icon: str = Field(default="")
config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON)) config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
chat_action: str | None = Field(default=None) # e.g. "typing", "upload_photo" chat_action: str | None = Field(default="typing") # e.g. "typing", "upload_photo"
created_at: datetime = Field(default_factory=_utcnow) created_at: datetime = Field(default_factory=_utcnow)
@@ -78,7 +78,7 @@ async def health():
async def _seed_default_templates(): async def _seed_default_templates():
"""Seed default templates on first startup if none exist.""" """Seed or update default (system-owned) templates on startup."""
from sqlmodel import func, select from sqlmodel import func, select
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from .database.engine import get_engine from .database.engine import get_engine
@@ -89,22 +89,36 @@ async def _seed_default_templates():
async with AsyncSession(engine) as session: async with AsyncSession(engine) as session:
result = await session.exec(select(func.count()).select_from(TemplateConfig)) result = await session.exec(select(func.count()).select_from(TemplateConfig))
count = result.one() count = result.one()
if count > 0:
return
for locale in ("en", "ru"): if count == 0:
slots = load_default_templates(locale) # First startup — seed all defaults
if not slots: for locale in ("en", "ru"):
continue slots = load_default_templates(locale)
name = f"Default ({locale.upper()})" if not slots:
config = TemplateConfig( continue
user_id=0, name = f"Default ({locale.upper()})"
provider_type="immich", config = TemplateConfig(
name=name, user_id=0,
description=f"Default Immich templates ({locale.upper()})", provider_type="immich",
**slots, name=name,
description=f"Default Immich templates ({locale.upper()})",
**slots,
)
session.add(config)
else:
# Update existing system-owned templates from files
result = await session.exec(
select(TemplateConfig).where(TemplateConfig.user_id == 0)
) )
session.add(config) system_configs = result.all()
for config in system_configs:
locale = "ru" if "(RU)" in config.name else "en"
slots = load_default_templates(locale)
if not slots:
continue
for key, value in slots.items():
setattr(config, key, value)
session.add(config)
await session.commit() await session.commit()