mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-02 21:55:22 +08:00
fix(notification): send JSON bodies as JSON, gate poster URLs on a public base_url
- Discord/Webhook/Gotify/Bark POST real JSON via RequestURL.post_json (was form-encoded and always rejected) - Route through a distinct _post_json to avoid the form-encoding post_json shadowed on RequestContent - Poster fields use settings.notification.base_url when set, otherwise omitted (local paths were never reachable) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
This commit is contained in:
@@ -2,8 +2,10 @@
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from module.conf import settings
|
||||
from module.models.bangumi import Notification
|
||||
from module.network import RequestContent
|
||||
from module.network.request_url import RequestURL
|
||||
|
||||
|
||||
class NotificationProvider(RequestContent, ABC):
|
||||
@@ -49,3 +51,40 @@ class NotificationProvider(RequestContent, ABC):
|
||||
f"季度: 第{notify.season}季\n"
|
||||
f"更新集数: 第{notify.episode}集"
|
||||
)
|
||||
|
||||
def _poster_url(self, notify: Notification) -> str | None:
|
||||
"""Build a public poster URL from a locally-stored poster path.
|
||||
|
||||
``Notification.poster_path`` is a local relative path (e.g.
|
||||
``posters/<hash>.jpg``), not reachable by external services. It can
|
||||
only be exposed to providers when a public ``base_url`` is
|
||||
configured; otherwise callers must omit the poster field entirely.
|
||||
|
||||
Args:
|
||||
notify: The notification data.
|
||||
|
||||
Returns:
|
||||
The public poster URL, or None if it cannot be resolved.
|
||||
"""
|
||||
if not notify.poster_path or notify.poster_path == "https://mikanani.me":
|
||||
return None
|
||||
base_url = settings.notification.base_url
|
||||
if not base_url:
|
||||
return None
|
||||
return f"{base_url.rstrip('/')}/{notify.poster_path}"
|
||||
|
||||
async def _post_json(self, url: str, json_data: dict):
|
||||
"""POST a JSON payload via ``RequestURL.post_json`` (httpx ``json=``).
|
||||
|
||||
Called explicitly on ``RequestURL`` rather than ``self.post_json``
|
||||
because ``RequestContent`` (a parent of this class) defines its own
|
||||
unrelated, form-encoded ``post_json`` that would otherwise shadow it.
|
||||
|
||||
Args:
|
||||
url: The target URL.
|
||||
json_data: The JSON-serializable payload.
|
||||
|
||||
Returns:
|
||||
The httpx response.
|
||||
"""
|
||||
return await RequestURL.post_json(self, url, json_data)
|
||||
|
||||
@@ -9,8 +9,8 @@ from module.database import Database
|
||||
from module.models.bangumi import Notification
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from module.notification.base import NotificationProvider
|
||||
from module.models.config import NotificationProvider as ProviderConfig
|
||||
from module.notification.base import NotificationProvider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from module.notification.providers.telegram import TelegramProvider
|
||||
from module.notification.providers.discord import DiscordProvider
|
||||
from module.notification.providers.bark import BarkProvider
|
||||
from module.notification.providers.server_chan import ServerChanProvider
|
||||
from module.notification.providers.wecom import WecomProvider
|
||||
from module.notification.providers.discord import DiscordProvider
|
||||
from module.notification.providers.gotify import GotifyProvider
|
||||
from module.notification.providers.pushover import PushoverProvider
|
||||
from module.notification.providers.server_chan import ServerChanProvider
|
||||
from module.notification.providers.telegram import TelegramProvider
|
||||
from module.notification.providers.webhook import WebhookProvider
|
||||
from module.notification.providers.wecom import WecomProvider
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from module.notification.base import NotificationProvider
|
||||
|
||||
@@ -30,11 +30,13 @@ class BarkProvider(NotificationProvider):
|
||||
data = {
|
||||
"title": notification.official_title,
|
||||
"body": text,
|
||||
"icon": notification.poster_path or "",
|
||||
"device_key": self.device_key,
|
||||
}
|
||||
poster_url = self._poster_url(notification)
|
||||
if poster_url:
|
||||
data["icon"] = poster_url
|
||||
|
||||
resp = await self.post_data(self.notification_url, data)
|
||||
resp = await self._post_json(self.notification_url, data)
|
||||
logger.debug("Bark notification: %s", resp.status_code)
|
||||
return resp.status_code == 200
|
||||
|
||||
@@ -46,7 +48,7 @@ class BarkProvider(NotificationProvider):
|
||||
"device_key": self.device_key,
|
||||
}
|
||||
try:
|
||||
resp = await self.post_data(self.notification_url, data)
|
||||
resp = await self._post_json(self.notification_url, data)
|
||||
if resp.status_code == 200:
|
||||
return True, "Bark test notification sent successfully"
|
||||
else:
|
||||
|
||||
@@ -30,15 +30,16 @@ class DiscordProvider(NotificationProvider):
|
||||
"color": 0x00BFFF, # Deep Sky Blue
|
||||
}
|
||||
|
||||
# Add poster as thumbnail if available
|
||||
if notification.poster_path and notification.poster_path != "https://mikanani.me":
|
||||
embed["thumbnail"] = {"url": notification.poster_path}
|
||||
# Add poster as thumbnail if a public poster URL is available
|
||||
poster_url = self._poster_url(notification)
|
||||
if poster_url:
|
||||
embed["thumbnail"] = {"url": poster_url}
|
||||
|
||||
data = {
|
||||
"embeds": [embed],
|
||||
}
|
||||
|
||||
resp = await self.post_data(self.webhook_url, data)
|
||||
resp = await self._post_json(self.webhook_url, data)
|
||||
logger.debug("Discord notification: %s", resp.status_code)
|
||||
return resp.status_code in (200, 204)
|
||||
|
||||
@@ -52,7 +53,7 @@ class DiscordProvider(NotificationProvider):
|
||||
data = {"embeds": [embed]}
|
||||
|
||||
try:
|
||||
resp = await self.post_data(self.webhook_url, data)
|
||||
resp = await self._post_json(self.webhook_url, data)
|
||||
if resp.status_code in (200, 204):
|
||||
return True, "Discord test message sent successfully"
|
||||
else:
|
||||
|
||||
@@ -30,9 +30,10 @@ class GotifyProvider(NotificationProvider):
|
||||
"client::display": {"contentType": "text/markdown"},
|
||||
}
|
||||
|
||||
if notification.poster_path and notification.poster_path != "https://mikanani.me":
|
||||
poster_url = self._poster_url(notification)
|
||||
if poster_url:
|
||||
extras["client::notification"] = {
|
||||
"bigImageUrl": notification.poster_path,
|
||||
"bigImageUrl": poster_url,
|
||||
}
|
||||
|
||||
data = {
|
||||
@@ -42,7 +43,7 @@ class GotifyProvider(NotificationProvider):
|
||||
"extras": extras,
|
||||
}
|
||||
|
||||
resp = await self.post_data(self.notification_url, data)
|
||||
resp = await self._post_json(self.notification_url, data)
|
||||
logger.debug("Gotify notification: %s", resp.status_code)
|
||||
return resp.status_code == 200
|
||||
|
||||
@@ -54,7 +55,7 @@ class GotifyProvider(NotificationProvider):
|
||||
"priority": 5,
|
||||
}
|
||||
try:
|
||||
resp = await self.post_data(self.notification_url, data)
|
||||
resp = await self._post_json(self.notification_url, data)
|
||||
if resp.status_code == 200:
|
||||
return True, "Gotify test message sent successfully"
|
||||
else:
|
||||
|
||||
@@ -49,7 +49,7 @@ class WebhookProvider(NotificationProvider):
|
||||
"{{title}}": notification.official_title,
|
||||
"{{season}}": str(notification.season),
|
||||
"{{episode}}": str(notification.episode),
|
||||
"{{poster_url}}": notification.poster_path or "",
|
||||
"{{poster_url}}": self._poster_url(notification) or "",
|
||||
}
|
||||
|
||||
for pattern, value in replacements.items():
|
||||
@@ -66,14 +66,14 @@ class WebhookProvider(NotificationProvider):
|
||||
"title": notification.official_title,
|
||||
"season": notification.season,
|
||||
"episode": notification.episode,
|
||||
"poster_url": notification.poster_path or "",
|
||||
"poster_url": self._poster_url(notification) or "",
|
||||
}
|
||||
|
||||
async def send(self, notification: Notification) -> bool:
|
||||
"""Send notification via generic webhook."""
|
||||
data = self._render_template(notification)
|
||||
|
||||
resp = await self.post_data(self.url, data)
|
||||
resp = await self._post_json(self.url, data)
|
||||
logger.debug("Webhook notification: %s", resp.status_code)
|
||||
# Accept any 2xx status code as success
|
||||
return 200 <= resp.status_code < 300
|
||||
@@ -89,7 +89,7 @@ class WebhookProvider(NotificationProvider):
|
||||
data = self._render_template(test_notification)
|
||||
|
||||
try:
|
||||
resp = await self.post_data(self.url, data)
|
||||
resp = await self._post_json(self.url, data)
|
||||
if 200 <= resp.status_code < 300:
|
||||
return True, "Webhook test request sent successfully"
|
||||
else:
|
||||
|
||||
@@ -11,12 +11,6 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Default fallback image for when no poster is available
|
||||
DEFAULT_POSTER = (
|
||||
"https://article.biliimg.com/bfs/article/"
|
||||
"d8bcd0408bf32594fd82f27de7d2c685829d1b2e.png"
|
||||
)
|
||||
|
||||
|
||||
class WecomProvider(NotificationProvider):
|
||||
"""WeChat Work (企业微信) notification provider using news message format."""
|
||||
@@ -32,18 +26,15 @@ class WecomProvider(NotificationProvider):
|
||||
title = f"【番剧更新】{notification.official_title}"
|
||||
msg = self._format_message(notification)
|
||||
|
||||
# Use default poster if none available or if it's just the base Mikan URL
|
||||
picurl = notification.poster_path
|
||||
if not picurl or picurl == "https://mikanani.me":
|
||||
picurl = DEFAULT_POSTER
|
||||
|
||||
data = {
|
||||
"key": self.token,
|
||||
"type": "news",
|
||||
"title": title,
|
||||
"msg": msg,
|
||||
"picurl": picurl,
|
||||
}
|
||||
poster_url = self._poster_url(notification)
|
||||
if poster_url:
|
||||
data["picurl"] = poster_url
|
||||
|
||||
resp = await self.post_data(self.notification_url, data)
|
||||
logger.debug("Wecom notification: %s", resp.status_code)
|
||||
@@ -56,7 +47,6 @@ class WecomProvider(NotificationProvider):
|
||||
"type": "news",
|
||||
"title": "AutoBangumi 通知测试",
|
||||
"msg": "通知测试成功!\nNotification test successful!",
|
||||
"picurl": DEFAULT_POSTER,
|
||||
}
|
||||
try:
|
||||
resp = await self.post_data(self.notification_url, data)
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
"""Tests for notification: provider registry, manager, and provider implementations."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
|
||||
from module.models import Notification
|
||||
from module.models.config import NotificationProvider as ProviderConfig
|
||||
from module.notification import PROVIDER_REGISTRY, NotificationManager
|
||||
from module.notification.providers import (
|
||||
TelegramProvider,
|
||||
DiscordProvider,
|
||||
BarkProvider,
|
||||
ServerChanProvider,
|
||||
WecomProvider,
|
||||
DiscordProvider,
|
||||
GotifyProvider,
|
||||
PushoverProvider,
|
||||
ServerChanProvider,
|
||||
TelegramProvider,
|
||||
WebhookProvider,
|
||||
WecomProvider,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provider Registry
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -237,7 +237,7 @@ class TestDiscordProvider:
|
||||
official_title="Test Anime", season=1, episode=5, poster_path="https://example.com/poster.jpg"
|
||||
)
|
||||
|
||||
with patch.object(provider, "post_data", new_callable=AsyncMock) as mock_post:
|
||||
with patch.object(provider, "_post_json", new_callable=AsyncMock) as mock_post:
|
||||
mock_post.return_value = MagicMock(status_code=204)
|
||||
result = await provider.send(notify)
|
||||
|
||||
@@ -256,7 +256,7 @@ class TestBarkProvider:
|
||||
"""Sends push notification."""
|
||||
notify = Notification(official_title="Test Anime", season=1, episode=5)
|
||||
|
||||
with patch.object(provider, "post_data", new_callable=AsyncMock) as mock_post:
|
||||
with patch.object(provider, "_post_json", new_callable=AsyncMock) as mock_post:
|
||||
mock_post.return_value = MagicMock(status_code=200)
|
||||
result = await provider.send(notify)
|
||||
|
||||
@@ -288,7 +288,7 @@ class TestWebhookProvider:
|
||||
"""Sends custom payload."""
|
||||
notify = Notification(official_title="Test Anime", season=1, episode=5)
|
||||
|
||||
with patch.object(provider, "post_data", new_callable=AsyncMock) as mock_post:
|
||||
with patch.object(provider, "_post_json", new_callable=AsyncMock) as mock_post:
|
||||
mock_post.return_value = MagicMock(status_code=200)
|
||||
result = await provider.send(notify)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user