mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-03 14:16:03 +08:00
feat(notification): Bark 支持铃声、分组、中断级别、图标与点击地址;插件权限新增「运行外部命令」
- Bark 渠道新增可选字段 sound、group、level、icon,并复用 url 作为点击打开的地址; 只发送已设置的项,旧配置的请求体不变;配置了 icon 时优先于海报地址(#792) - 测试接口 /notification/test-config 透传新字段;通知设置表单加上对应输入框 与 level 下拉框,并更新三语言的通知配置文档 - 插件权限标签新增 subprocess(运行外部命令),仅用于展示
This commit is contained in:
@@ -48,6 +48,10 @@ class TestProviderConfigRequest(BaseModel):
|
||||
api_token: Optional[str] = Field(default=None, description="API token")
|
||||
template: Optional[str] = Field(default=None, description="Custom template")
|
||||
url: Optional[str] = Field(default=None, description="URL for generic webhook")
|
||||
sound: Optional[str] = Field(default=None, description="Bark sound")
|
||||
group: Optional[str] = Field(default=None, description="Bark group")
|
||||
level: Optional[str] = Field(default=None, description="Bark level")
|
||||
icon: Optional[str] = Field(default=None, description="Bark icon URL")
|
||||
|
||||
|
||||
class TestResponse(BaseModel):
|
||||
@@ -108,6 +112,10 @@ async def test_provider_config(request: TestProviderConfigRequest):
|
||||
api_token=request.api_token or "",
|
||||
template=request.template,
|
||||
url=request.url or "",
|
||||
sound=request.sound,
|
||||
group=request.group,
|
||||
level=request.level,
|
||||
icon=request.icon,
|
||||
)
|
||||
|
||||
success, message = await NotificationManager.test_provider_config(config)
|
||||
|
||||
@@ -181,7 +181,19 @@ class NotificationProvider(BaseModel):
|
||||
),
|
||||
)
|
||||
url_: Optional[str] = Field(
|
||||
default=None, alias="url", description="URL for generic webhook provider"
|
||||
default=None,
|
||||
alias="url",
|
||||
description="URL for generic webhook provider; tap-to-open URL for bark",
|
||||
)
|
||||
# Bark 的可选推送参数,留空则不发送
|
||||
sound: Optional[str] = Field(default=None, description="Bark sound name")
|
||||
group: Optional[str] = Field(default=None, description="Bark message group")
|
||||
level: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Bark interruption level: active/timeSensitive/passive/critical",
|
||||
)
|
||||
icon: Optional[str] = Field(
|
||||
default=None, description="Bark icon URL; defaults to the poster URL"
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -22,30 +22,42 @@ class BarkProvider(NotificationProvider):
|
||||
self.device_key = config.device_key
|
||||
server_url = config.server_url or self.DEFAULT_SERVER
|
||||
self.notification_url = f"{server_url.rstrip('/')}/push"
|
||||
# 可选推送参数:只发送已设置的项,旧配置的请求体不变
|
||||
self.options = {
|
||||
"sound": config.sound,
|
||||
"group": config.group,
|
||||
"level": config.level,
|
||||
"url": config.url,
|
||||
}
|
||||
self.icon = config.icon
|
||||
|
||||
def _payload(self, title: str, body: str, icon: str | None = None) -> dict:
|
||||
data: dict[str, str | None] = {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"device_key": self.device_key,
|
||||
}
|
||||
data.update({k: v for k, v in self.options.items() if v})
|
||||
# 配置的图标优先,其次是调用方给的(单集通知的海报)
|
||||
if self.icon or icon:
|
||||
data["icon"] = self.icon or icon
|
||||
return data
|
||||
|
||||
async def send(self, notification: Notification) -> bool:
|
||||
"""Send notification via Bark."""
|
||||
text = self._format_message(notification)
|
||||
data = {
|
||||
"title": notification.official_title,
|
||||
"body": text,
|
||||
"device_key": self.device_key,
|
||||
}
|
||||
poster_url = self._poster_url(notification)
|
||||
if poster_url:
|
||||
data["icon"] = poster_url
|
||||
|
||||
data = self._payload(
|
||||
notification.official_title, text, self._poster_url(notification)
|
||||
)
|
||||
resp = await self._post_json(self.notification_url, data)
|
||||
logger.debug("Bark notification: %s", resp.status_code)
|
||||
return resp.status_code == 200
|
||||
|
||||
async def test(self) -> tuple[bool, str]:
|
||||
"""Test Bark configuration by sending a test notification."""
|
||||
data = {
|
||||
"title": "AutoBangumi",
|
||||
"body": "通知测试成功!\nNotification test successful!",
|
||||
"device_key": self.device_key,
|
||||
}
|
||||
data = self._payload(
|
||||
"AutoBangumi", "通知测试成功!\nNotification test successful!"
|
||||
)
|
||||
try:
|
||||
resp = await self._post_json(self.notification_url, data)
|
||||
if resp.status_code == 200:
|
||||
@@ -57,6 +69,5 @@ class BarkProvider(NotificationProvider):
|
||||
|
||||
async def _deliver_text(self, title: str, body: str) -> bool:
|
||||
"""Deliver a system event via Bark."""
|
||||
data = {"title": title, "body": body, "device_key": self.device_key}
|
||||
resp = await self._post_json(self.notification_url, data)
|
||||
resp = await self._post_json(self.notification_url, self._payload(title, body))
|
||||
return resp.status_code == 200
|
||||
|
||||
@@ -354,6 +354,67 @@ class TestBarkProvider:
|
||||
call_args = mock_post.call_args[0]
|
||||
assert "device_key" in call_args[1]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("options", "expected"),
|
||||
[
|
||||
({}, {}),
|
||||
({"sound": "", "level": ""}, {}),
|
||||
(
|
||||
{
|
||||
"sound": "minuet",
|
||||
"group": "AB",
|
||||
"level": "timeSensitive",
|
||||
"url": "https://ab.local",
|
||||
},
|
||||
{
|
||||
"sound": "minuet",
|
||||
"group": "AB",
|
||||
"level": "timeSensitive",
|
||||
"url": "https://ab.local",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_deliver_text_sends_only_set_options(self, options, expected):
|
||||
"""#792:sound/group/level/url 只在设置时发送,旧配置的请求体不变。"""
|
||||
config = ProviderConfig(type="bark", device_key="k", **options)
|
||||
provider = BarkProvider(config)
|
||||
|
||||
with patch.object(provider, "_post_json", new_callable=AsyncMock) as mock_post:
|
||||
mock_post.return_value = MagicMock(status_code=200)
|
||||
assert await provider._deliver_text("T", "B") is True
|
||||
|
||||
assert mock_post.call_args[0][1] == {
|
||||
"title": "T",
|
||||
"body": "B",
|
||||
"device_key": "k",
|
||||
**expected,
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("icon", "poster", "expected"),
|
||||
[
|
||||
(None, "https://ab/p.jpg", "https://ab/p.jpg"),
|
||||
("https://ab/i.png", "https://ab/p.jpg", "https://ab/i.png"),
|
||||
(None, None, None),
|
||||
],
|
||||
)
|
||||
async def test_send_icon_prefers_configured_icon_over_poster(
|
||||
self, icon, poster, expected
|
||||
):
|
||||
"""配置的 icon 优先;未配置时沿用海报地址;都没有则不发 icon。"""
|
||||
provider = BarkProvider(ProviderConfig(type="bark", device_key="k", icon=icon))
|
||||
notify = Notification(official_title="Test Anime", season=1, episode=5)
|
||||
|
||||
with (
|
||||
patch.object(provider, "_poster_url", return_value=poster),
|
||||
patch.object(provider, "_post_json", new_callable=AsyncMock) as mock_post,
|
||||
):
|
||||
mock_post.return_value = MagicMock(status_code=200)
|
||||
await provider.send(notify)
|
||||
|
||||
assert mock_post.call_args[0][1].get("icon") == expected
|
||||
|
||||
|
||||
class TestWebhookProvider:
|
||||
@pytest.fixture
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
- Telegram:`Bot Token`、`Chat ID`
|
||||
- Discord:Webhook URL
|
||||
- WeCom:Webhook URL、Key
|
||||
- Bark:Device Key,可选 Server URL
|
||||
- Bark:Device Key,可选 Server URL、铃声、分组、中断级别、图标地址、点击打开的地址
|
||||
- Server Chan:SendKey
|
||||
- Gotify:Server URL、App Token
|
||||
- Pushover:User Key、API Token
|
||||
@@ -53,9 +53,10 @@ Webhook 模板可以使用 `{{title}}`、`{{season}}`、`{{episode}}`、`{{poste
|
||||
| `token` | Telegram(Bot Token)、Server Chan(SendKey)、WeCom(Key)、Gotify(App Token)使用 |
|
||||
| `chat_id` | Telegram 使用 |
|
||||
| `webhook_url` | Discord、WeCom 使用 |
|
||||
| `url` | Webhook 使用 |
|
||||
| `url` | Webhook 使用;Bark 中为点击通知后打开的地址(可选) |
|
||||
| `server_url` | Bark(可选)、Gotify 使用 |
|
||||
| `device_key` | Bark 使用 |
|
||||
| `sound` / `group` / `level` / `icon` | Bark 可选:铃声、分组、中断级别(`active`、`timeSensitive`、`passive`、`critical`)、图标地址。留空则不发送;`icon` 留空时单集通知用海报地址 |
|
||||
| `user_key` / `api_token` | Pushover 使用 |
|
||||
| `template` | 自定义通知模板 |
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Provider-specific fields include:
|
||||
- Telegram: `Bot Token`, `Chat ID`
|
||||
- Discord: Webhook URL
|
||||
- WeCom: Webhook URL and Key
|
||||
- Bark: Device Key and optional Server URL
|
||||
- Bark: Device Key. Optional: Server URL, sound, group, interruption level, icon URL, and the URL to open on tap
|
||||
- Server Chan: SendKey
|
||||
- Gotify: Server URL and App Token
|
||||
- Pushover: User Key and API Token
|
||||
@@ -53,9 +53,10 @@ Each object in `providers` usually contains:
|
||||
| `token` | Used by Telegram (Bot Token), Server Chan (SendKey), WeCom (Key) and Gotify (App Token) |
|
||||
| `chat_id` | Used by Telegram |
|
||||
| `webhook_url` | Used by Discord and WeCom |
|
||||
| `url` | Used by Webhook |
|
||||
| `url` | Used by Webhook. For Bark, the URL to open on tap (optional) |
|
||||
| `server_url` | Used by Bark (optional) and Gotify |
|
||||
| `device_key` | Used by Bark |
|
||||
| `sound` / `group` / `level` / `icon` | Optional for Bark: sound, group, interruption level (`active`, `timeSensitive`, `passive`, `critical`), and icon URL. AB does not send an empty field. When `icon` is empty, episode notifications use the poster URL |
|
||||
| `user_key` / `api_token` | Used by Pushover |
|
||||
| `template` | Custom message template |
|
||||
|
||||
|
||||
@@ -42,9 +42,10 @@ Webhookテンプレートでは `{{title}}`、`{{season}}`、`{{episode}}`、`{{
|
||||
| `token` | Telegram(Bot Token)、Server Chan(SendKey)、WeCom(Key)、Gotify(App Token)で使用 |
|
||||
| `chat_id` | Telegram で使用 |
|
||||
| `webhook_url` | Discord、WeCom で使用 |
|
||||
| `url` | Webhook で使用 |
|
||||
| `url` | Webhook で使用。Bark ではタップで開く URL(任意) |
|
||||
| `server_url` | Bark(任意)、Gotify で使用 |
|
||||
| `device_key` | Bark で使用 |
|
||||
| `sound` / `group` / `level` / `icon` | Bark(任意):通知音、グループ、割り込みレベル(`active`、`timeSensitive`、`passive`、`critical`)、アイコン URL。空の項目は送信しません。`icon` が空のとき、エピソード通知はポスターの URL を使います |
|
||||
| `user_key` / `api_token` | Pushover で使用 |
|
||||
| `template` | カスタム通知テンプレート |
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ export interface TestProviderConfigRequest {
|
||||
api_token?: string;
|
||||
template?: string;
|
||||
url?: string;
|
||||
sound?: string;
|
||||
group?: string;
|
||||
level?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface TestResponse {
|
||||
|
||||
@@ -42,8 +42,10 @@ const providerFields: Record<
|
||||
string,
|
||||
{
|
||||
key: keyof NotificationProviderConfig;
|
||||
label: string;
|
||||
label: string | (() => string);
|
||||
placeholder: string;
|
||||
// 有选项时渲染为可清空的下拉框
|
||||
options?: string[];
|
||||
}[]
|
||||
> = {
|
||||
telegram: [
|
||||
@@ -64,6 +66,32 @@ const providerFields: Record<
|
||||
label: 'Server URL (optional)',
|
||||
placeholder: 'https://api.day.app',
|
||||
},
|
||||
{
|
||||
key: 'sound',
|
||||
label: () => t('config.notification_set.bark_sound'),
|
||||
placeholder: 'minuet',
|
||||
},
|
||||
{
|
||||
key: 'group',
|
||||
label: () => t('config.notification_set.bark_group'),
|
||||
placeholder: 'AutoBangumi',
|
||||
},
|
||||
{
|
||||
key: 'level',
|
||||
label: () => t('config.notification_set.bark_level'),
|
||||
placeholder: 'active',
|
||||
options: ['active', 'timeSensitive', 'passive', 'critical'],
|
||||
},
|
||||
{
|
||||
key: 'icon',
|
||||
label: () => t('config.notification_set.bark_icon'),
|
||||
placeholder: 'https://example.com/icon.png',
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
label: () => t('config.notification_set.bark_url'),
|
||||
placeholder: 'https://example.com',
|
||||
},
|
||||
],
|
||||
'server-chan': [{ key: 'token', label: 'SendKey', placeholder: 'sendkey' }],
|
||||
wecom: [
|
||||
@@ -385,8 +413,15 @@ function getFieldsForType(type: string) {
|
||||
:key="field.key"
|
||||
:label="field.label"
|
||||
>
|
||||
<NSelect
|
||||
v-if="field.options"
|
||||
v-model:value="(newProvider as any)[field.key]"
|
||||
:options="field.options.map((value) => ({ label: value, value }))"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
/>
|
||||
<ab-input
|
||||
v-if="field.key !== 'template'"
|
||||
v-else-if="field.key !== 'template'"
|
||||
v-model="(newProvider as any)[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
@@ -457,8 +492,15 @@ function getFieldsForType(type: string) {
|
||||
:key="field.key"
|
||||
:label="field.label"
|
||||
>
|
||||
<NSelect
|
||||
v-if="field.options"
|
||||
v-model:value="(newProvider as any)[field.key]"
|
||||
:options="field.options.map((value) => ({ label: value, value }))"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
/>
|
||||
<ab-input
|
||||
v-if="field.key !== 'template'"
|
||||
v-else-if="field.key !== 'template'"
|
||||
v-model="(newProvider as any)[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
|
||||
@@ -119,7 +119,12 @@
|
||||
"test_success": "Test successful",
|
||||
"test_failed": "Test failed",
|
||||
"plugin_label": "{id} (plugin)",
|
||||
"plugin_hint": "This channel comes from a plugin and has no fields here. Enter its credentials in the plugin's options in the Plugins section."
|
||||
"plugin_hint": "This channel comes from a plugin and has no fields here. Enter its credentials in the plugin's options in the Plugins section.",
|
||||
"bark_sound": "Sound (optional)",
|
||||
"bark_group": "Group (optional)",
|
||||
"bark_level": "Interruption level (optional)",
|
||||
"bark_icon": "Icon URL (optional, defaults to the poster)",
|
||||
"bark_url": "URL to open on tap (optional)"
|
||||
},
|
||||
"parser_set": {
|
||||
"enable": "Enable",
|
||||
@@ -245,7 +250,8 @@
|
||||
"permission_labels": {
|
||||
"fs_write": "Write files",
|
||||
"network": "Network access",
|
||||
"filesystem": "File system access"
|
||||
"filesystem": "File system access",
|
||||
"subprocess": "Run external commands"
|
||||
},
|
||||
"disable": "Disable",
|
||||
"disable_confirm_title": "Disable {name}?",
|
||||
|
||||
@@ -119,7 +119,12 @@
|
||||
"test_success": "测试成功",
|
||||
"test_failed": "测试失败",
|
||||
"plugin_label": "{id}(插件)",
|
||||
"plugin_hint": "此渠道由插件提供,这里没有可填字段。请在插件设置中该插件的配置里填写凭据。"
|
||||
"plugin_hint": "此渠道由插件提供,这里没有可填字段。请在插件设置中该插件的配置里填写凭据。",
|
||||
"bark_sound": "铃声(可选)",
|
||||
"bark_group": "分组(可选)",
|
||||
"bark_level": "中断级别(可选)",
|
||||
"bark_icon": "图标地址(可选,默认用海报)",
|
||||
"bark_url": "点击打开的地址(可选)"
|
||||
},
|
||||
"parser_set": {
|
||||
"enable": "启用",
|
||||
@@ -245,7 +250,8 @@
|
||||
"permission_labels": {
|
||||
"fs_write": "写入文件",
|
||||
"network": "访问网络",
|
||||
"filesystem": "访问文件系统"
|
||||
"filesystem": "访问文件系统",
|
||||
"subprocess": "运行外部命令"
|
||||
},
|
||||
"disable": "停用",
|
||||
"disable_confirm_title": "停用 {name}?",
|
||||
|
||||
@@ -95,6 +95,11 @@ export interface NotificationProviderConfig {
|
||||
api_token?: string;
|
||||
template?: string;
|
||||
url?: string;
|
||||
// Bark 的可选推送参数
|
||||
sound?: string;
|
||||
group?: string;
|
||||
level?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
|
||||
Reference in New Issue
Block a user