Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6481605ca | |||
| 6de9a1289e |
+10
-15
@@ -1,27 +1,22 @@
|
|||||||
# v0.2.6 (2026-04-22)
|
# v0.2.7 (2026-04-22)
|
||||||
|
|
||||||
Bug-fix release. Notably: saving settings was silently overwriting the
|
Follow-up to v0.2.6: unifies the Telegram send routine across notifications
|
||||||
Telegram webhook secret with its own display mask, invalidating HMAC on
|
and bot commands so both sides share the same aiohttp session, the same
|
||||||
every webhook-mode bot after any settings save. Also fixes template-editor
|
`file_id` caches, and the same rules for video / thumbnail URL construction.
|
||||||
variable discovery for provider-specific command slots (`/search`, `/status`,
|
Eliminates repeat uploads that happened because single-asset sends and media
|
||||||
`/repos`, `/issues`, `/boards`), asset enrichment (city / country / favorite)
|
groups were keyed in different caches.
|
||||||
for Immich `/search` / `/find` / `/person` / `/place`, and video rendering
|
|
||||||
in command media groups.
|
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
- **Don't clobber the Telegram webhook secret with its mask on save** — `GET /settings` returns the secret masked as `***<last4>`; the frontend bound that masked value into state and shipped it back on any Save, so the PUT handler persisted the mask as the new secret. The next GET re-masked the mask to itself, so the UI showed no corruption while HMAC verification silently broke for every webhook-mode bot. Incoming values that begin with `***` are now treated as *unchanged*; empty strings still clear the secret explicitly. **Operators running webhook-mode bots should save the page once with a known-good secret after upgrading.** ([8531168](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/8531168))
|
- **Single-asset sends now hit the asset cache** — `TelegramClient._get_cache_and_key` treats `cache_key` values that look like asset UUIDs as asset-cache entries. Single-asset sends were storing `file_id`s in `url_cache` while the media-group path stored them in `asset_cache`, so repeat sends of the same asset never hit the cache and re-uploaded. ([6de9a12](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6de9a12))
|
||||||
- **Surface Variables button / autocomplete for provider-specific command slots** — the command-template-configs UI only resolved slot variables against the shared catalog, so Immich's `/search` and `/status`, Gitea's `/repos` / `/issues`, and Planka's `/boards` offered no autocomplete. It now resolves against the active provider (`varsRef[provider_type][slot]`) first, falling back to shared entries. ([fab6169](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/fab6169))
|
- **Notifications and commands now share the Telegram client factory** — new `services/telegram_send.py` is the single construction path for `TelegramClient`: always wires the shared aiohttp session and both `file_id` caches. `send_reply` and `send_media_group` in `commands/handler.py` now delegate to the factory instead of constructing their own uncached clients, so commands reuse `file_id`s populated by notification dispatches (and vice versa) instead of re-uploading the same bytes. ([6de9a12](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6de9a12))
|
||||||
- **Enrich raw Immich search results through `build_asset_dict`** — `/search`, `/find`, `/person`, `/place` previously handed raw API rows to templates, so `city` / `country` (from `exifInfo`) and `is_favorite` (mapped from `isFavorite`) were missing and templates couldn't render location or favorite indicators. Now normalised the same way as notification events. ([fab6169](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/fab6169))
|
- **Single rule for `/video/playback` vs thumbnail URLs** — extracted `build_asset_media_urls` so the notification dispatcher's `asset_to_media` and the bot command handlers' `common._format_assets` agree on when to use the playback URL and when to use the thumbnail. Removes a subtle drift that could show stills in one path and video in the other for the same asset. ([6de9a12](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6de9a12))
|
||||||
- **Videos render correctly in command media groups** — `/latest`, `/random`, `/favorites` were sending videos as still thumbnails because the media-group path duplicated asset-typing logic. Extracted `build_telegram_asset_entry` into a shared helper so the notification dispatcher and command groups agree on video typing and `/video/playback` URLs. ([fab6169](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/fab6169))
|
|
||||||
- **Command media groups reuse the Telegram `file_id` cache** — `send_media_group` was re-uploading assets on every repeat command instead of honoring the cache the notification dispatcher already populates. Now shares the cache, avoiding re-upload churn. ([fab6169](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/fab6169))
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>All Commits</summary>
|
<summary>All Commits</summary>
|
||||||
|
|
||||||
- [fab6169](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/fab6169) — fix(commands): enrich search assets, surface variables for all command slots *(alexei.dolgolyov)*
|
- [6de9a12](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6de9a12) — fix(telegram): unify send routine across notifications and commands *(alexei.dolgolyov)*
|
||||||
- [8531168](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/8531168) — fix(settings): don't clobber webhook secret with its mask on save *(alexei.dolgolyov)*
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "notify-bridge-frontend",
|
"name": "notify-bridge-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.6",
|
"version": "0.2.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "notify-bridge-core"
|
name = "notify-bridge-core"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
description = "Core library for Notify Bridge — service provider abstractions, models, notifications, and templates"
|
description = "Core library for Notify Bridge — service provider abstractions, models, notifications, and templates"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -89,6 +89,18 @@ class TelegramClient:
|
|||||||
self, url: str | None, cache_key: str | None = None,
|
self, url: str | None, cache_key: str | None = None,
|
||||||
) -> tuple[TelegramFileCache | None, str | None, str | None]:
|
) -> tuple[TelegramFileCache | None, str | None, str | None]:
|
||||||
if cache_key:
|
if cache_key:
|
||||||
|
# Route asset-UUID cache keys to the asset cache so single-item
|
||||||
|
# sends hit the same cache the media-group path uses. Without
|
||||||
|
# this, a command returning one photo stored file_ids in the
|
||||||
|
# URL cache and a command returning multiple stored them in
|
||||||
|
# the asset cache — repeated sends never hit.
|
||||||
|
if is_asset_cache_key(cache_key):
|
||||||
|
bare_id = asset_id_from_cache_key(cache_key)
|
||||||
|
thumbhash = (
|
||||||
|
self._thumbhash_resolver(bare_id)
|
||||||
|
if self._thumbhash_resolver else None
|
||||||
|
)
|
||||||
|
return self._asset_cache, cache_key, thumbhash
|
||||||
return self._url_cache, cache_key, None
|
return self._url_cache, cache_key, None
|
||||||
if url:
|
if url:
|
||||||
if is_asset_id(url):
|
if is_asset_id(url):
|
||||||
|
|||||||
@@ -193,6 +193,27 @@ def get_asset_video_url(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_asset_media_urls(
|
||||||
|
external_url: str, asset_id: str, asset_type: str,
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
"""Return ``(preview_url, full_url)`` for an Immich asset.
|
||||||
|
|
||||||
|
Single source of truth for the photo-vs-video endpoint rule. Used by
|
||||||
|
``asset_to_media`` (notification path) and the bot command handlers
|
||||||
|
(command path) so both always pick the transcoded ``/video/playback``
|
||||||
|
for videos and the preview-sized thumbnail for photos — if they
|
||||||
|
diverge, Telegram ends up delivering a still JPEG for videos in a
|
||||||
|
media group.
|
||||||
|
"""
|
||||||
|
is_video = asset_type == ASSET_TYPE_VIDEO
|
||||||
|
if is_video:
|
||||||
|
preview_url = f"{external_url}/api/assets/{asset_id}/video/playback"
|
||||||
|
else:
|
||||||
|
preview_url = f"{external_url}/api/assets/{asset_id}/thumbnail?size=preview"
|
||||||
|
full_url = f"{external_url}/api/assets/{asset_id}/original"
|
||||||
|
return preview_url, full_url
|
||||||
|
|
||||||
|
|
||||||
def build_asset_detail(
|
def build_asset_detail(
|
||||||
asset: ImmichAssetInfo,
|
asset: ImmichAssetInfo,
|
||||||
external_url: str,
|
external_url: str,
|
||||||
@@ -246,12 +267,7 @@ def asset_to_media(asset: ImmichAssetInfo, external_url: str) -> MediaAsset:
|
|||||||
# preview_url is what the notification dispatcher feeds to Telegram as the
|
# preview_url is what the notification dispatcher feeds to Telegram as the
|
||||||
# actual media bytes — for videos it must be the transcoded playback (mp4),
|
# actual media bytes — for videos it must be the transcoded playback (mp4),
|
||||||
# not the JPEG thumbnail, or Telegram receives a JPEG labeled as video/mp4.
|
# not the JPEG thumbnail, or Telegram receives a JPEG labeled as video/mp4.
|
||||||
if asset.type == ASSET_TYPE_VIDEO:
|
preview_url, full_url = build_asset_media_urls(external_url, asset.id, asset.type)
|
||||||
preview_url = f"{external_url}/api/assets/{asset.id}/video/playback"
|
|
||||||
full_url = f"{external_url}/api/assets/{asset.id}/original"
|
|
||||||
else:
|
|
||||||
preview_url = f"{external_url}/api/assets/{asset.id}/thumbnail?size=preview"
|
|
||||||
full_url = f"{external_url}/api/assets/{asset.id}/original"
|
|
||||||
|
|
||||||
return MediaAsset(
|
return MediaAsset(
|
||||||
id=asset.id,
|
id=asset.id,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "notify-bridge-server"
|
name = "notify-bridge-server"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
description = "Standalone Notify Bridge server — FastAPI REST API with SQLite database"
|
description = "Standalone Notify Bridge server — FastAPI REST API with SQLite database"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -367,20 +367,23 @@ async def send_reply(
|
|||||||
bot_token: str, chat_id: str, text: str, reply_to_message_id: int | None = None,
|
bot_token: str, chat_id: str, text: str, reply_to_message_id: int | None = None,
|
||||||
session: aiohttp.ClientSession | None = None,
|
session: aiohttp.ClientSession | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send a text reply via TelegramClient.
|
"""Send a text reply to a chat.
|
||||||
|
|
||||||
Command responses are listings (albums, people, events, ...) that embed
|
Thin wrapper that goes through the single ``services.telegram_send``
|
||||||
multiple links; Telegram's default behavior of rendering a preview of
|
entry point so commands and notifications share one routine — same
|
||||||
the first URL is almost never what the user wants and clashes with the
|
HTTP session pool, same file_id caches.
|
||||||
"Disable link previews" toggle operators set on their Telegram target.
|
|
||||||
We always pass ``disable_web_page_preview=True`` here.
|
Command responses are listings (albums, people, events, ...) that
|
||||||
|
embed multiple links; Telegram's default behavior of rendering a
|
||||||
|
preview of the first URL is almost never what the user wants and
|
||||||
|
clashes with the "Disable link previews" toggle operators set on
|
||||||
|
their Telegram target. We always pass
|
||||||
|
``disable_web_page_preview=True`` here.
|
||||||
"""
|
"""
|
||||||
if session is None:
|
from ..services.telegram_send import send_telegram_message
|
||||||
from ..services.http_session import get_http_session
|
|
||||||
session = await get_http_session()
|
result = await send_telegram_message(
|
||||||
client = TelegramClient(session, bot_token)
|
bot_token, chat_id, text,
|
||||||
result = await client.send_message(
|
|
||||||
chat_id, text,
|
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
disable_web_page_preview=True,
|
disable_web_page_preview=True,
|
||||||
)
|
)
|
||||||
@@ -393,38 +396,28 @@ async def send_media_group(
|
|||||||
reply_to_message_id: int | None = None,
|
reply_to_message_id: int | None = None,
|
||||||
session: aiohttp.ClientSession | None = None,
|
session: aiohttp.ClientSession | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send media items via TelegramClient.send_notification.
|
"""Send media items via the shared Telegram routine.
|
||||||
|
|
||||||
``media_items`` must already be in TelegramClient asset format — each
|
``media_items`` must already be in TelegramClient asset format — each
|
||||||
entry contains ``type`` (``"photo"``/``"video"``/``"document"``),
|
entry contains ``type`` (``"photo"``/``"video"``/``"document"``),
|
||||||
``url``, optional ``cache_key``, and optional ``headers``. Provider
|
``url``, optional ``cache_key``, and optional ``headers``. Provider
|
||||||
command handlers build this format directly (via
|
command handlers build this format via
|
||||||
``build_telegram_asset_entry``) so videos keep their ``"video"`` type
|
``build_telegram_asset_entry`` — the same helper the notification
|
||||||
and point at a real video URL instead of a still thumbnail.
|
dispatcher uses — so videos keep their ``"video"`` type and point at
|
||||||
|
a real video URL instead of a still thumbnail.
|
||||||
|
|
||||||
Reuses the same Telegram file_id caches as the notification dispatcher
|
Uses ``services.telegram_send.send_telegram_media`` so the URL cache
|
||||||
so repeated ``/latest`` / ``/random`` commands don't re-upload bytes
|
and asset cache are wired in exactly like the notification path.
|
||||||
for assets Telegram has already seen. If the cache hasn't been
|
Repeated ``/latest`` / ``/random`` commands that match previously-sent
|
||||||
initialized (no data dir configured) we fall through to a plain
|
assets hit the cache and skip the re-upload.
|
||||||
upload — identical behavior to the notification path.
|
|
||||||
"""
|
"""
|
||||||
if not media_items:
|
if not media_items:
|
||||||
return
|
return
|
||||||
|
|
||||||
if session is None:
|
from ..services.telegram_send import send_telegram_media
|
||||||
from ..services.http_session import get_http_session
|
|
||||||
session = await get_http_session()
|
|
||||||
|
|
||||||
from ..services.watcher import _get_telegram_caches
|
result = await send_telegram_media(
|
||||||
url_cache, asset_cache = await _get_telegram_caches()
|
bot_token, chat_id, media_items,
|
||||||
|
|
||||||
client = TelegramClient(
|
|
||||||
session, bot_token,
|
|
||||||
url_cache=url_cache,
|
|
||||||
asset_cache=asset_cache,
|
|
||||||
)
|
|
||||||
result = await client.send_notification(
|
|
||||||
chat_id, assets=media_items,
|
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
chat_action=None,
|
chat_action=None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from notify_bridge_core.notifications.telegram.media import build_telegram_asset_entry
|
from notify_bridge_core.notifications.telegram.media import build_telegram_asset_entry
|
||||||
from notify_bridge_core.providers.immich.asset_utils import get_public_url
|
from notify_bridge_core.providers.immich.asset_utils import (
|
||||||
|
build_asset_media_urls,
|
||||||
|
get_public_url,
|
||||||
|
)
|
||||||
|
|
||||||
from ..handler import _render_cmd_template
|
from ..handler import _render_cmd_template
|
||||||
|
|
||||||
@@ -127,21 +130,19 @@ def _format_assets(
|
|||||||
})
|
})
|
||||||
|
|
||||||
if response_mode == "media":
|
if response_mode == "media":
|
||||||
# Reuse the same entry-building helper as the notification dispatcher
|
# Reuse the same URL rule (build_asset_media_urls) and entry builder
|
||||||
# so videos keep their "video" type and point at /video/playback —
|
# (build_telegram_asset_entry) as the notification dispatcher so both
|
||||||
# typing them as "photo" made Telegram render the still poster
|
# paths agree on video → /video/playback and photo → thumbnail. When
|
||||||
# thumbnail in media groups instead of the real clip.
|
# these diverged, Telegram rendered a still JPEG for each video in
|
||||||
|
# the media group instead of the real clip.
|
||||||
media_items: list[dict[str, Any]] = []
|
media_items: list[dict[str, Any]] = []
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
asset_id = asset.get("id", "")
|
asset_id = asset.get("id", "")
|
||||||
is_video = (asset.get("type") or "").upper() == "VIDEO"
|
asset_type = (asset.get("type") or "").upper()
|
||||||
if is_video:
|
preview_url, _ = build_asset_media_urls(client.url, asset_id, asset_type)
|
||||||
url = f"{client.url}/api/assets/{asset_id}/video/playback"
|
|
||||||
else:
|
|
||||||
url = f"{client.url}/api/assets/{asset_id}/thumbnail?size=preview"
|
|
||||||
entry = build_telegram_asset_entry(
|
entry = build_telegram_asset_entry(
|
||||||
url=url,
|
url=preview_url,
|
||||||
media_type="video" if is_video else "image",
|
media_type="video" if asset_type == "VIDEO" else "image",
|
||||||
api_key=client.api_key,
|
api_key=client.api_key,
|
||||||
internal_url=client.url,
|
internal_url=client.url,
|
||||||
cache_key=asset_id,
|
cache_key=asset_id,
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"""Single entry point for all Telegram send operations.
|
||||||
|
|
||||||
|
Both the notification dispatcher (event-driven) and the bot command
|
||||||
|
handlers (user-driven) funnel their Telegram API calls through this
|
||||||
|
module. Keeping construction in one place means:
|
||||||
|
|
||||||
|
* The shared aiohttp session is always reused (one TCP pool for the
|
||||||
|
whole process).
|
||||||
|
* The Telegram file_id caches (URL cache + asset cache) are always
|
||||||
|
wired in, so repeated sends — whether from a scheduled tracker or
|
||||||
|
a ``/latest`` command — reuse cached file_ids instead of re-uploading
|
||||||
|
the same bytes.
|
||||||
|
* Future cross-cutting concerns (rate limiting, telemetry, retries)
|
||||||
|
have exactly one place to live.
|
||||||
|
|
||||||
|
The actual Telegram API routine is still ``TelegramClient`` in core —
|
||||||
|
this module just guarantees every caller gets a properly-wired client.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from notify_bridge_core.notifications.telegram.client import (
|
||||||
|
NotificationResult,
|
||||||
|
TelegramClient,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .http_session import get_http_session
|
||||||
|
from .watcher import _get_telegram_caches
|
||||||
|
|
||||||
|
|
||||||
|
async def get_telegram_client(
|
||||||
|
bot_token: str,
|
||||||
|
*,
|
||||||
|
session: aiohttp.ClientSession | None = None,
|
||||||
|
thumbhash_resolver: Callable[[str], str | None] | None = None,
|
||||||
|
) -> TelegramClient:
|
||||||
|
"""Return a ``TelegramClient`` wired to shared session + shared caches.
|
||||||
|
|
||||||
|
Every Telegram send in the process should acquire its client from
|
||||||
|
here — constructing ``TelegramClient`` directly skips the caches and
|
||||||
|
silently halves cache hit rate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_token: The bot's API token.
|
||||||
|
session: Optional explicit aiohttp session. Defaults to the
|
||||||
|
process-wide shared session.
|
||||||
|
thumbhash_resolver: Optional asset-id → thumbhash lookup. The
|
||||||
|
notification dispatcher passes one so asset-cache entries
|
||||||
|
invalidate on visual change; the command path doesn't need it
|
||||||
|
(commands always ask for a fresh result).
|
||||||
|
"""
|
||||||
|
if session is None:
|
||||||
|
session = await get_http_session()
|
||||||
|
url_cache, asset_cache = await _get_telegram_caches()
|
||||||
|
return TelegramClient(
|
||||||
|
session, bot_token,
|
||||||
|
url_cache=url_cache,
|
||||||
|
asset_cache=asset_cache,
|
||||||
|
thumbhash_resolver=thumbhash_resolver,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_telegram_message(
|
||||||
|
bot_token: str,
|
||||||
|
chat_id: str,
|
||||||
|
text: str,
|
||||||
|
*,
|
||||||
|
reply_to_message_id: int | None = None,
|
||||||
|
disable_web_page_preview: bool = True,
|
||||||
|
parse_mode: str = "HTML",
|
||||||
|
) -> NotificationResult:
|
||||||
|
"""Send a plain-text Telegram message with caches wired in."""
|
||||||
|
client = await get_telegram_client(bot_token)
|
||||||
|
return await client.send_message(
|
||||||
|
chat_id, text,
|
||||||
|
reply_to_message_id=reply_to_message_id,
|
||||||
|
disable_web_page_preview=disable_web_page_preview,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_telegram_media(
|
||||||
|
bot_token: str,
|
||||||
|
chat_id: str,
|
||||||
|
assets: list[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
caption: str | None = None,
|
||||||
|
reply_to_message_id: int | None = None,
|
||||||
|
max_group_size: int = 10,
|
||||||
|
chunk_delay: int = 0,
|
||||||
|
max_asset_data_size: int | None = None,
|
||||||
|
send_large_photos_as_documents: bool = False,
|
||||||
|
chat_action: str | None = "typing",
|
||||||
|
thumbhash_resolver: Callable[[str], str | None] | None = None,
|
||||||
|
) -> NotificationResult:
|
||||||
|
"""Send a Telegram media group (or single asset) with caches wired in.
|
||||||
|
|
||||||
|
``assets`` must be in ``TelegramClient`` format — see
|
||||||
|
``notify_bridge_core.notifications.telegram.media.build_telegram_asset_entry``
|
||||||
|
for the canonical builder.
|
||||||
|
"""
|
||||||
|
client = await get_telegram_client(
|
||||||
|
bot_token, thumbhash_resolver=thumbhash_resolver,
|
||||||
|
)
|
||||||
|
return await client.send_notification(
|
||||||
|
chat_id,
|
||||||
|
assets=assets,
|
||||||
|
caption=caption,
|
||||||
|
reply_to_message_id=reply_to_message_id,
|
||||||
|
max_group_size=max_group_size,
|
||||||
|
chunk_delay=chunk_delay,
|
||||||
|
max_asset_data_size=max_asset_data_size,
|
||||||
|
send_large_photos_as_documents=send_large_photos_as_documents,
|
||||||
|
chat_action=chat_action,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user