diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index 4219788..e1b9642 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -207,6 +207,8 @@ "quietHoursStart": "Quiet hours start", "quietHoursEnd": "Quiet hours end", "batchDuration": "Batch duration (seconds)", + "defaultTrackingConfig": "Default tracking config", + "defaultTemplateConfig": "Default template config", "linkedTargets": "targets", "noLinkedTargets": "No targets linked. Add a target below.", "addTarget": "Add target", @@ -540,7 +542,8 @@ "albumFields": "Album fields (in {% for album in albums %})", "confirmDelete": "Delete this template config?", "invalidFormat": "Invalid format string", - "filterSlots": "Filter slots..." + "filterSlots": "Filter slots...", + "slots": "slots" }, "templateVars": { "message_assets_added": { @@ -645,6 +648,8 @@ "templateConfig": "Controls the message format. Uses default templates if not set.", "scanInterval": "How often to poll the provider for changes, in seconds. Lower = faster detection but more API calls.", "batchDuration": "Time to accumulate changes before dispatching notifications. 0 = send immediately.", + "defaultTrackingConfig": "Applied to all linked targets unless overridden per target.", + "defaultTemplateConfig": "Applied to all linked targets unless overridden per target.", "defaultCount": "How many results to return when the user doesn't specify a count (1-20).", "responseMode": "Media: send actual photos. Text: send filenames/links only. Media mode uses more bandwidth.", "botLocale": "Language for command descriptions in Telegram's menu and bot response messages.", diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json index 7d82532..211659e 100644 --- a/frontend/src/lib/i18n/ru.json +++ b/frontend/src/lib/i18n/ru.json @@ -207,6 +207,8 @@ "quietHoursStart": "Тихие часы начало", "quietHoursEnd": "Тихие часы конец", "batchDuration": "Длительность пакета (секунды)", + "defaultTrackingConfig": "Конфигурация отслеживания по умолчанию", + "defaultTemplateConfig": "Шаблон уведомлений по умолчанию", "linkedTargets": "получатели", "noLinkedTargets": "Нет привязанных получателей. Добавьте получателя ниже.", "addTarget": "Добавить получателя", @@ -540,7 +542,8 @@ "albumFields": "Поля альбома (в {% for album in albums %})", "confirmDelete": "Удалить эту конфигурацию шаблона?", "invalidFormat": "Некорректная строка формата", - "filterSlots": "Фильтр слотов..." + "filterSlots": "Фильтр слотов...", + "slots": "слотов" }, "templateVars": { "message_assets_added": { @@ -645,6 +648,8 @@ "templateConfig": "Управляет форматом сообщений. Используются шаблоны по умолчанию, если не задано.", "scanInterval": "Как часто опрашивать провайдер на предмет изменений (в секундах). Меньше = быстрее обнаружение, но больше запросов к API.", "batchDuration": "Время накопления изменений перед отправкой уведомлений. 0 = отправлять сразу.", + "defaultTrackingConfig": "Применяется ко всем привязанным получателям, если не переопределено.", + "defaultTemplateConfig": "Применяется ко всем привязанным получателям, если не переопределено.", "defaultCount": "Сколько результатов возвращать, если пользователь не указал количество (1-20).", "responseMode": "Медиа: отправка фото. Текст: только имена файлов/ссылки. Медиа-режим использует больше трафика.", "botLocale": "Язык описаний команд в меню Telegram и ответов бота.", diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index bfc292b..f2a3740 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -80,6 +80,8 @@ export interface Tracker { collection_ids: string[]; scan_interval: number; batch_duration: number; + default_tracking_config_id: number | null; + default_template_config_id: number | null; enabled: boolean; filters?: Record; tracker_targets: TrackerTarget[]; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 2a20224..d273954 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -35,10 +35,12 @@ ...allProviders.map(p => ({ value: p.id, icon: providerDefaultIcon(p), label: p.name, desc: p.type })), ]); let providerFilterValue = $state(globalProviderFilter.id ?? 0); + let _syncingFilter = false; // Sync filter value → store $effect(() => { const v = providerFilterValue; + if (_syncingFilter) return; globalProviderFilter.set(v === 0 ? null : v); }); @@ -46,7 +48,9 @@ $effect(() => { const storeId = globalProviderFilter.id; if (storeId === null && providerFilterValue !== 0) { + _syncingFilter = true; providerFilterValue = 0; + _syncingFilter = false; } }); @@ -85,6 +89,11 @@ ptype ? items.filter(i => i.provider_type === ptype) : items; const targets = targetsCache.items; + // Single pass to count targets by type + const targetsByType = new Map(); + for (const t of targets) { + targetsByType.set(t.type, (targetsByType.get(t.type) || 0) + 1); + } return { providers: pid ? 1 : providersCache.items.length, notification_trackers: filterById(notificationTrackersCache.items as any[]).length, @@ -97,14 +106,14 @@ telegram_bots: telegramBotsCache.items.length, email_bots: emailBotsCache.items.length, matrix_bots: matrixBotsCache.items.length, - targets_telegram: targets.filter(t => t.type === 'telegram').length, - targets_webhook: targets.filter(t => t.type === 'webhook').length, - targets_email: targets.filter(t => t.type === 'email').length, - targets_discord: targets.filter(t => t.type === 'discord').length, - targets_slack: targets.filter(t => t.type === 'slack').length, - targets_ntfy: targets.filter(t => t.type === 'ntfy').length, - targets_matrix: targets.filter(t => t.type === 'matrix').length, - targets_broadcast: targets.filter(t => t.type === 'broadcast').length, + targets_telegram: targetsByType.get('telegram') || 0, + targets_webhook: targetsByType.get('webhook') || 0, + targets_email: targetsByType.get('email') || 0, + targets_discord: targetsByType.get('discord') || 0, + targets_slack: targetsByType.get('slack') || 0, + targets_ntfy: targetsByType.get('ntfy') || 0, + targets_matrix: targetsByType.get('matrix') || 0, + targets_broadcast: targetsByType.get('broadcast') || 0, } as Record; }); diff --git a/frontend/src/routes/bots/EmailBotTab.svelte b/frontend/src/routes/bots/EmailBotTab.svelte index 88ddfb6..d1175b9 100644 --- a/frontend/src/routes/bots/EmailBotTab.svelte +++ b/frontend/src/routes/bots/EmailBotTab.svelte @@ -161,7 +161,7 @@
- testEmailBot(bot.id)} disabled={emailTesting[bot.id]} /> + testEmailBot(bot.id)} disabled={emailTesting[bot.id]} /> editEmailBot(bot)} /> removeEmail(bot.id)} variant="danger" />
diff --git a/frontend/src/routes/notification-trackers/+page.svelte b/frontend/src/routes/notification-trackers/+page.svelte index e5c07e3..cc5e29b 100644 --- a/frontend/src/routes/notification-trackers/+page.svelte +++ b/frontend/src/routes/notification-trackers/+page.svelte @@ -61,6 +61,7 @@ const defaultForm = () => ({ name: '', icon: '', provider_id: 0, collection_ids: [] as string[], scan_interval: 60, batch_duration: 0, + default_tracking_config_id: 0, default_template_config_id: 0, filters: {} as Record, }); let form = $state(defaultForm()); @@ -143,6 +144,8 @@ name: trk.name, icon: trk.icon || '', provider_id: trk.provider_id, collection_ids: [...(trk.collection_ids || [])], scan_interval: trk.scan_interval, batch_duration: trk.batch_duration ?? 0, + default_tracking_config_id: (trk as any).default_tracking_config_id || 0, + default_template_config_id: (trk as any).default_template_config_id || 0, filters: trk.filters || {}, }; previousCollectionIds = [...(trk.collection_ids || [])]; @@ -179,11 +182,16 @@ async function doSave() { submitting = true; try { + const payload = { + ...form, + default_tracking_config_id: form.default_tracking_config_id || null, + default_template_config_id: form.default_template_config_id || null, + }; if (editing) { - await api(`/notification-trackers/${editing}`, { method: 'PUT', body: JSON.stringify(form) }); + await api(`/notification-trackers/${editing}`, { method: 'PUT', body: JSON.stringify(payload) }); snackSuccess(t('snack.trackerUpdated')); } else { - await api('/notification-trackers', { method: 'POST', body: JSON.stringify(form) }); + await api('/notification-trackers', { method: 'POST', body: JSON.stringify(payload) }); snackSuccess(t('snack.trackerCreated')); } showForm = false; editing = null; linkWarning = null; await load(); @@ -371,6 +379,8 @@ {providerItems} {collections} bind:collectionFilter + trackingConfigItems={trackingConfigs.filter(c => !selectedProviderType || c.provider_type === selectedProviderType).map(c => ({ value: c.id, label: c.name, icon: (c as any).icon || 'mdiCog' }))} + templateConfigItems={templateConfigs.filter(c => !selectedProviderType || c.provider_type === selectedProviderType).map(c => ({ value: c.id, label: c.name, icon: (c as any).icon || 'mdiFileDocumentEdit' }))} {editing} {submitting} {linkCheckLoading} @@ -406,7 +416,7 @@ {:else if !showForm}
- {#each notificationTrackers as tracker} + {#each notificationTrackers as tracker (tracker.id)}
diff --git a/frontend/src/routes/notification-trackers/LinkedTargetsSection.svelte b/frontend/src/routes/notification-trackers/LinkedTargetsSection.svelte index 10c475a..f5c0b63 100644 --- a/frontend/src/routes/notification-trackers/LinkedTargetsSection.svelte +++ b/frontend/src/routes/notification-trackers/LinkedTargetsSection.svelte @@ -3,10 +3,16 @@ import { t } from '$lib/i18n'; import MdiIcon from '$lib/components/MdiIcon.svelte'; import IconButton from '$lib/components/IconButton.svelte'; + import CrossLink from '$lib/components/CrossLink.svelte'; import EntitySelect from '$lib/components/EntitySelect.svelte'; import type { EntityItem } from '$lib/components/EntitySelect.svelte'; import type { Tracker, NotificationTarget, TrackingConfig, TemplateConfig } from '$lib/types'; + const TARGET_TYPE_ICONS: Record = { + telegram: 'mdiSend', webhook: 'mdiWebhook', email: 'mdiEmailOutline', + discord: 'mdiChat', slack: 'mdiSlack', ntfy: 'mdiBell', matrix: 'mdiMatrix', broadcast: 'mdiBullhorn', + }; + interface Props { tracker: Tracker; trackingConfigs: TrackingConfig[]; @@ -47,6 +53,8 @@ onchangeNewTemplateConfig, }: Props = $props(); + let expandedTt = $state(null); + function toItems(configs: any[]): EntityItem[] { return configsForTracker(configs).map(c => ({ value: c.id, @@ -55,55 +63,86 @@ })); } + function configName(configs: any[], id: number | null): string { + if (!id) return ''; + const c = configs.find((x: any) => x.id === id); + return c?.name || ''; + } + const trackingConfigItems = $derived(toItems(trackingConfigs)); const templateConfigItems = $derived(toItems(templateConfigs)); const linkedTargetIds = $derived(new Set((tracker.tracker_targets || []).map((tt: any) => tt.target_id))); const targetItems = $derived(unlinkedTargets.map(tgt => ({ value: tgt.id, label: tgt.name, - icon: tgt.icon || (tgt.type === 'telegram' ? 'mdiSend' : 'mdiWebhook'), + icon: tgt.icon || TARGET_TYPE_ICONS[tgt.type] || 'mdiTarget', desc: tgt.type, disabled: linkedTargetIds.has(tgt.id), disabledHint: linkedTargetIds.has(tgt.id) ? t('notificationTracker.alreadyLinked') : undefined, }))); -
+
{#if (tracker.tracker_targets || []).length === 0}

{t('notificationTracker.noLinkedTargets')}

{:else} - {#each tracker.tracker_targets as tt} -
-
- - {tt.target_name || `Target #${tt.target_id}`} - {tt.target_type} - {#if !tt.enabled} - {t('notificationTracker.paused')} - {/if} -
-
-
- onupdateLink(tt, 'tracking_config_id', Number(v) || null)} /> + {#each tracker.tracker_targets as tt (tt.id)} + {@const isExpanded = expandedTt === tt.id} +
+ +
+
+ + + {tt.target_type} + {#if !tt.enabled} + {t('notificationTracker.paused')} + {/if} + + {#if !isExpanded} + {#if tt.tracking_config_id} + + {/if} + {#if tt.template_config_id} + + {/if} + {/if}
-
- onupdateLink(tt, 'template_config_id', Number(v) || null)} /> -
-
+
onopenTestMenu(tt.id, e)} disabled={Object.keys(ttTesting).some(k => k.startsWith(`${tt.id}_`) && ttTesting[k])} /> + onupdateLink(tt, 'enabled', !tt.enabled)} /> + onremoveLink(tt.id)} variant="danger" />
- onupdateLink(tt, 'enabled', !tt.enabled)} /> - onremoveLink(tt.id)} variant="danger" />
+ + + {#if isExpanded} +
+
+
+ + onupdateLink(tt, 'tracking_config_id', Number(v) || null)} /> +
+
+ + onupdateLink(tt, 'template_config_id', Number(v) || null)} /> +
+
+
+ {/if}
{/each} {/if} @@ -116,16 +155,6 @@ placeholder={t('notificationTracker.addTarget')} size="sm" onselect={(v) => onchangeNewTarget(Number(v) || 0)} />
-
- onchangeNewTrackingConfig(Number(v) || 0)} /> -
-
- onchangeNewTemplateConfig(Number(v) || 0)} /> -
{/if} + + {#if trackingConfigItems.length > 0 || templateConfigItems.length > 0} +
+ {#if trackingConfigItems.length > 0} +
+ + +
+ {/if} + {#if templateConfigItems.length > 0} +
+ + +
+ {/if} +
+ {/if} + diff --git a/frontend/src/routes/targets/+page.svelte b/frontend/src/routes/targets/+page.svelte index d670728..979a9dd 100644 --- a/frontend/src/routes/targets/+page.svelte +++ b/frontend/src/routes/targets/+page.svelte @@ -475,7 +475,7 @@ {#if target.type === 'broadcast' && target.child_targets?.length} {target.child_targets.length} {t('targets.childTargets')} {:else if target.type !== 'broadcast' && (target.receivers || []).length > 0} - {(target.receivers || []).length} receiver(s) + {(target.receivers || []).length} {t('targets.receivers')} {/if} {#if getBotName(target)}{/if}
diff --git a/packages/core/src/notify_bridge_core/notifications/dispatcher.py b/packages/core/src/notify_bridge_core/notifications/dispatcher.py index aa1cf56..d7ce5f7 100644 --- a/packages/core/src/notify_bridge_core/notifications/dispatcher.py +++ b/packages/core/src/notify_bridge_core/notifications/dispatcher.py @@ -274,6 +274,7 @@ class NotificationDispatcher: to_email=receiver.email, subject=subject, body_text=message, + body_html=message, to_name=receiver.name, ) results.append(result) diff --git a/packages/core/src/notify_bridge_core/notifications/email/client.py b/packages/core/src/notify_bridge_core/notifications/email/client.py index 30a40a3..cd99113 100644 --- a/packages/core/src/notify_bridge_core/notifications/email/client.py +++ b/packages/core/src/notify_bridge_core/notifications/email/client.py @@ -30,6 +30,33 @@ class EmailClient: def __init__(self, smtp_config: SmtpConfig) -> None: self._config = smtp_config + async def verify_connection(self) -> dict[str, Any]: + """Test SMTP connection and authentication without sending an email.""" + try: + import aiosmtplib + except ImportError: + return {"success": False, "error": "aiosmtplib not installed"} + + cfg = self._config + if not cfg.host: + return {"success": False, "error": "SMTP host not configured"} + + try: + smtp = aiosmtplib.SMTP( + hostname=cfg.host, + port=cfg.port, + use_tls=cfg.use_tls, + start_tls=not cfg.use_tls and cfg.port != 25, + ) + await smtp.connect() + if cfg.username and cfg.password: + await smtp.login(cfg.username, cfg.password) + await smtp.quit() + return {"success": True} + except Exception as e: + _LOGGER.warning("SMTP verification failed for %s:%d: %s", cfg.host, cfg.port, e) + return {"success": False, "error": str(e)} + async def send( self, to_email: str, diff --git a/packages/server/src/notify_bridge_server/api/email_bots.py b/packages/server/src/notify_bridge_server/api/email_bots.py index 8fc61e2..4bf3c8a 100644 --- a/packages/server/src/notify_bridge_server/api/email_bots.py +++ b/packages/server/src/notify_bridge_server/api/email_bots.py @@ -55,6 +55,18 @@ async def create_email_bot( user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): + # Verify SMTP connection before saving + from notify_bridge_core.notifications.email.client import EmailClient, SmtpConfig + client = EmailClient(SmtpConfig( + host=body.smtp_host, port=body.smtp_port, + username=body.smtp_username, password=body.smtp_password, + from_address=body.email, from_name=body.name, + use_tls=body.smtp_use_tls, + )) + result = await client.verify_connection() + if not result.get("success"): + raise HTTPException(status_code=400, detail=f"SMTP connection failed: {result.get('error', 'Unknown error')}") + bot = EmailBot(user_id=user.id, **body.model_dump()) session.add(bot) await session.commit() diff --git a/packages/server/src/notify_bridge_server/api/notification_trackers.py b/packages/server/src/notify_bridge_server/api/notification_trackers.py index 9eb58d1..2edd383 100644 --- a/packages/server/src/notify_bridge_server/api/notification_trackers.py +++ b/packages/server/src/notify_bridge_server/api/notification_trackers.py @@ -32,6 +32,8 @@ class NotificationTrackerCreate(BaseModel): collection_ids: list[str] = [] scan_interval: int = 60 batch_duration: int = 0 + default_tracking_config_id: int | None = None + default_template_config_id: int | None = None enabled: bool = True @@ -41,6 +43,8 @@ class NotificationTrackerUpdate(BaseModel): collection_ids: list[str] | None = None scan_interval: int | None = None batch_duration: int | None = None + default_tracking_config_id: int | None = None + default_template_config_id: int | None = None enabled: bool | None = None @@ -190,6 +194,8 @@ async def _tracker_response(session: AsyncSession, t: NotificationTracker) -> di "collection_ids": t.collection_ids, "scan_interval": t.scan_interval, "batch_duration": t.batch_duration, + "default_tracking_config_id": t.default_tracking_config_id, + "default_template_config_id": t.default_template_config_id, "enabled": t.enabled, "tracker_targets": tracker_targets, "created_at": t.created_at.isoformat(), diff --git a/packages/server/src/notify_bridge_server/database/models.py b/packages/server/src/notify_bridge_server/database/models.py index 2c1205f..bf7ad30 100644 --- a/packages/server/src/notify_bridge_server/database/models.py +++ b/packages/server/src/notify_bridge_server/database/models.py @@ -309,6 +309,8 @@ class NotificationTracker(SQLModel, table=True): filters: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON)) scan_interval: int = Field(default=60) batch_duration: int = Field(default=0) # seconds to accumulate events before dispatch (0=immediate) + default_tracking_config_id: int | None = Field(default=None, foreign_key="tracking_config.id") + default_template_config_id: int | None = Field(default=None, foreign_key="template_config.id") enabled: bool = Field(default=True) created_at: datetime = Field(default_factory=_utcnow) diff --git a/packages/server/src/notify_bridge_server/services/dispatch_helpers.py b/packages/server/src/notify_bridge_server/services/dispatch_helpers.py index d6817da..c6a3664 100644 --- a/packages/server/src/notify_bridge_server/services/dispatch_helpers.py +++ b/packages/server/src/notify_bridge_server/services/dispatch_helpers.py @@ -16,6 +16,7 @@ from ..database.models import ( EmailBot, MatrixBot, NotificationTarget, + NotificationTracker, NotificationTrackerTarget, TargetReceiver, TelegramBot, @@ -191,6 +192,11 @@ async def load_link_data( tracker_id: ID of the tracker whose links to load. check_quiet_hours: If True, skip links currently in quiet hours. """ + # Load the tracker itself for default config IDs + tracker = await session.get(NotificationTracker, tracker_id) + default_tc_id = getattr(tracker, "default_tracking_config_id", None) if tracker else None + default_tmpl_id = getattr(tracker, "default_template_config_id", None) if tracker else None + tt_result = await session.exec( select(NotificationTrackerTarget).where( NotificationTrackerTarget.tracker_id == tracker_id @@ -198,35 +204,61 @@ async def load_link_data( ) tracker_targets = tt_result.all() - link_data: list[dict[str, Any]] = [] - for tt in tracker_targets: - if not tt.enabled: - continue - if check_quiet_hours and in_quiet_hours(tt.quiet_hours_start, tt.quiet_hours_end): - continue + # Filter enabled links and quiet hours upfront + active_links = [ + tt for tt in tracker_targets + if tt.enabled and not (check_quiet_hours and in_quiet_hours(tt.quiet_hours_start, tt.quiet_hours_end)) + ] + if not active_links: + return [] - target = await session.get(NotificationTarget, tt.target_id) + # Batch-load targets + target_ids = list({tt.target_id for tt in active_links}) + target_result = await session.exec( + select(NotificationTarget).where(NotificationTarget.id.in_(target_ids)) + ) + target_map = {t.id: t for t in target_result.all()} + + # Batch-load tracking configs (per-link + tracker default) + tc_ids = list({tid for tid in + [tt.tracking_config_id for tt in active_links] + [default_tc_id] + if tid}) + tc_map: dict[int, TrackingConfig] = {} + if tc_ids: + tc_result = await session.exec(select(TrackingConfig).where(TrackingConfig.id.in_(tc_ids))) + tc_map = {tc.id: tc for tc in tc_result.all()} + + # Batch-load template configs (per-link + tracker default) + tmpl_ids = list({tid for tid in + [tt.template_config_id for tt in active_links] + [default_tmpl_id] + if tid}) + tmpl_map: dict[int, TemplateConfig] = {} + if tmpl_ids: + tmpl_result = await session.exec(select(TemplateConfig).where(TemplateConfig.id.in_(tmpl_ids))) + tmpl_map = {tc.id: tc for tc in tmpl_result.all()} + + # Batch-load template slots for all template configs + slots_by_config: dict[int, dict[str, dict[str, str]]] = {} + if tmpl_ids: + slot_result = await session.exec( + select(TemplateSlot).where(TemplateSlot.config_id.in_(tmpl_ids)) + ) + for s in slot_result.all(): + event_key = s.slot_name.removeprefix("message_") if s.slot_name.startswith("message_") else s.slot_name + slots_by_config.setdefault(s.config_id, {}).setdefault(event_key, {})[s.locale] = s.template + + link_data: list[dict[str, Any]] = [] + for tt in active_links: + target = target_map.get(tt.target_id) if not target: continue - # Load tracking config and template slots (shared across broadcast children) - tracking_config = None - if tt.tracking_config_id: - tracking_config = await session.get(TrackingConfig, tt.tracking_config_id) - - template_config = None - template_slots: dict[str, dict[str, str]] | None = None - if tt.template_config_id: - template_config = await session.get(TemplateConfig, tt.template_config_id) - if template_config: - slot_result = await session.exec( - select(TemplateSlot).where(TemplateSlot.config_id == template_config.id) - ) - raw_slots: dict[str, dict[str, str]] = {} - for s in slot_result.all(): - event_key = s.slot_name.removeprefix("message_") if s.slot_name.startswith("message_") else s.slot_name - raw_slots.setdefault(event_key, {})[s.locale] = s.template - template_slots = raw_slots + # Per-link config overrides tracker defaults + tc_id = tt.tracking_config_id or default_tc_id + tmpl_id = tt.template_config_id or default_tmpl_id + tracking_config = tc_map.get(tc_id) if tc_id else None + template_config = tmpl_map.get(tmpl_id) if tmpl_id else None + template_slots = slots_by_config.get(template_config.id) if template_config else None # Broadcast target: expand into child targets if target.type == "broadcast": diff --git a/packages/server/src/notify_bridge_server/services/notifier.py b/packages/server/src/notify_bridge_server/services/notifier.py index 93c9d61..557cdae 100644 --- a/packages/server/src/notify_bridge_server/services/notifier.py +++ b/packages/server/src/notify_bridge_server/services/notifier.py @@ -104,14 +104,7 @@ async def _send_telegram_broadcast(target: NotificationTarget, message: str, rec ) results.append(result) - successes = sum(1 for r in results if r.get("success")) - if successes == len(results) and results: - return {"success": True, "receivers": len(results)} - elif successes > 0: - return {"success": True, "receivers": len(results), "partial_failures": len(results) - successes} - elif results: - return results[0] - return {"success": False, "error": "No valid receivers"} + return _aggregate(results) async def _send_webhook_broadcast(target: NotificationTarget, message: str, receivers: list[dict]) -> dict: @@ -130,14 +123,7 @@ async def _send_webhook_broadcast(target: NotificationTarget, message: str, rece client = WebhookClient(session, url, headers) results.append(await client.send({"message": message, "event_type": "notification"})) - successes = sum(1 for r in results if r.get("success")) - if successes == len(results) and results: - return {"success": True, "receivers": len(results)} - elif successes > 0: - return {"success": True, "receivers": len(results), "partial_failures": len(results) - successes} - elif results: - return results[0] - return {"success": False, "error": "No valid receivers"} + return _aggregate(results) async def _send_email_broadcast(target: NotificationTarget, message: str, receivers: list[dict]) -> dict: @@ -179,18 +165,12 @@ async def _send_email_broadcast(target: NotificationTarget, message: str, receiv to_email=email, subject="Notification from Notify Bridge", body_text=message, + body_html=message, to_name=recv.get("name", ""), ) results.append(result) - successes = sum(1 for r in results if r.get("success")) - if successes == len(results) and results: - return {"success": True, "receivers": len(results)} - elif successes > 0: - return {"success": True, "receivers": len(results), "partial_failures": len(results) - successes} - elif results: - return results[0] - return {"success": False, "error": "No valid email receivers"} + return _aggregate(results) async def _send_webhook_like_broadcast(target: NotificationTarget, message: str, receivers: list[dict]) -> dict: