mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
fix: improve telegram channel connectivity
This commit is contained in:
@@ -238,7 +238,7 @@ mkdir -p data cache media downloads
|
|||||||
```bash
|
```bash
|
||||||
cat > .env <<'EOF'
|
cat > .env <<'EOF'
|
||||||
# 固定版本;需要升级时改成新的 MediaStationGo-vX.Y.Z 后执行 docker compose pull && docker compose up -d
|
# 固定版本;需要升级时改成新的 MediaStationGo-vX.Y.Z 后执行 docker compose pull && docker compose up -d
|
||||||
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23
|
||||||
MEDIASTATION_HTTP_PORT=18080
|
MEDIASTATION_HTTP_PORT=18080
|
||||||
|
|
||||||
# 程序数据和缓存建议放在 MediaStationGo 部署目录下,便于备份和迁移。
|
# 程序数据和缓存建议放在 MediaStationGo 部署目录下,便于备份和迁移。
|
||||||
@@ -307,7 +307,7 @@ vim docker-compose.yml
|
|||||||
#
|
#
|
||||||
# 镜像版本:
|
# 镜像版本:
|
||||||
# 默认拉取 latest;如需固定版本,创建 .env 并写入:
|
# 默认拉取 latest;如需固定版本,创建 .env 并写入:
|
||||||
# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23
|
||||||
#
|
#
|
||||||
# 路径映射总览:
|
# 路径映射总览:
|
||||||
# /data 程序数据目录。保存 SQLite 数据库、JWT secret、系统配置等,必须持久化。
|
# /data 程序数据目录。保存 SQLite 数据库、JWT secret、系统配置等,必须持久化。
|
||||||
@@ -516,7 +516,7 @@ docker compose up -d
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat > .env <<'EOF'
|
cat > .env <<'EOF'
|
||||||
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23
|
||||||
MEDIASTATION_HTTP_PORT=18080
|
MEDIASTATION_HTTP_PORT=18080
|
||||||
MEDIASTATION_DATA_DIR=./data
|
MEDIASTATION_DATA_DIR=./data
|
||||||
MEDIASTATION_CACHE_DIR=./cache
|
MEDIASTATION_CACHE_DIR=./cache
|
||||||
@@ -779,26 +779,26 @@ cd MediaStationGo
|
|||||||
|
|
||||||
| 平台 | 包名示例 |
|
| 平台 | 包名示例 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Linux x86_64 | `MediaStationGo-v0.0.22-linux-amd64.tar.gz` |
|
| Linux x86_64 | `MediaStationGo-v0.0.23-linux-amd64.tar.gz` |
|
||||||
| Linux ARM64 | `MediaStationGo-v0.0.22-linux-arm64.tar.gz` |
|
| Linux ARM64 | `MediaStationGo-v0.0.23-linux-arm64.tar.gz` |
|
||||||
| Windows x86_64 | `MediaStationGo-v0.0.22-windows-amd64.zip` |
|
| Windows x86_64 | `MediaStationGo-v0.0.23-windows-amd64.zip` |
|
||||||
| macOS Intel | `MediaStationGo-v0.0.22-darwin-amd64.tar.gz` |
|
| macOS Intel | `MediaStationGo-v0.0.23-darwin-amd64.tar.gz` |
|
||||||
| macOS Apple Silicon | `MediaStationGo-v0.0.22-darwin-arm64.tar.gz` |
|
| macOS Apple Silicon | `MediaStationGo-v0.0.23-darwin-arm64.tar.gz` |
|
||||||
|
|
||||||
部署步骤:
|
部署步骤:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux 示例
|
# Linux 示例
|
||||||
tar -xzf MediaStationGo-v0.0.22-linux-amd64.tar.gz
|
tar -xzf MediaStationGo-v0.0.23-linux-amd64.tar.gz
|
||||||
cd MediaStationGo-v0.0.22-linux-amd64
|
cd MediaStationGo-v0.0.23-linux-amd64
|
||||||
MEDIASTATION_APP_PORT=18080 ./mediastation-go
|
MEDIASTATION_APP_PORT=18080 ./mediastation-go
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows:
|
Windows:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Expand-Archive .\MediaStationGo-v0.0.22-windows-amd64.zip
|
Expand-Archive .\MediaStationGo-v0.0.23-windows-amd64.zip
|
||||||
cd .\MediaStationGo-v0.0.22-windows-amd64
|
cd .\MediaStationGo-v0.0.23-windows-amd64
|
||||||
$env:MEDIASTATION_APP_PORT = "18080"
|
$env:MEDIASTATION_APP_PORT = "18080"
|
||||||
.\mediastation-go.exe
|
.\mediastation-go.exe
|
||||||
```
|
```
|
||||||
@@ -905,13 +905,15 @@ MediaStationGo/
|
|||||||
| `MEDIASTATION_CACHE_CACHE_DIR` | `./cache` | 图片/转码缓存目录 |
|
| `MEDIASTATION_CACHE_CACHE_DIR` | `./cache` | 图片/转码缓存目录 |
|
||||||
| `MEDIASTATION_SECRETS_JWT_SECRET` | 自动生成 | JWT 和敏感配置加密种子 |
|
| `MEDIASTATION_SECRETS_JWT_SECRET` | 自动生成 | JWT 和敏感配置加密种子 |
|
||||||
| `MEDIASTATION_APP_CORS_ORIGINS` | 空 | 额外允许的跨域来源 |
|
| `MEDIASTATION_APP_CORS_ORIGINS` | 空 | 额外允许的跨域来源 |
|
||||||
|
| `MEDIASTATION_TELEGRAM_API_BASE_URL` | `https://api.telegram.org` | Telegram Bot API 地址;网络受限时可填写反代地址 |
|
||||||
|
| `MEDIASTATION_TELEGRAM_PROXY_URL` | 空 | Telegram 出站代理,例如 `http://172.17.0.1:7890` 或 `socks5://172.17.0.1:1080` |
|
||||||
|
|
||||||
后台可运行时配置:
|
后台可运行时配置:
|
||||||
|
|
||||||
- API Key:TMDb、Bangumi、TheTVDB、Fanart、OpenAI Compatible 等。
|
- API Key:TMDb、Bangumi、TheTVDB、Fanart、OpenAI Compatible 等。
|
||||||
- 站点:M-Team、NexusPHP、Unit3D、自定义 RSS 等。
|
- 站点:M-Team、NexusPHP、Unit3D、自定义 RSS 等。
|
||||||
- 下载器:qBittorrent、Transmission、Aria2。
|
- 下载器:qBittorrent、Transmission、Aria2。
|
||||||
- 通知渠道:Telegram、Bark、Webhook、Email 等。
|
- 通知渠道:Telegram、Bark、Webhook、Email 等。Telegram 渠道支持单独配置 API 反代与代理地址,测试通知失败时错误信息会自动隐藏 Bot Token。
|
||||||
- 播放配置、权限配置、调度任务、存储配置。
|
- 播放配置、权限配置、调度任务、存储配置。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+14
-12
@@ -235,7 +235,7 @@ mkdir -p data cache media downloads
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat > .env <<'EOF'
|
cat > .env <<'EOF'
|
||||||
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23
|
||||||
MEDIASTATION_HTTP_PORT=18080
|
MEDIASTATION_HTTP_PORT=18080
|
||||||
MEDIASTATION_DATA_DIR=./data
|
MEDIASTATION_DATA_DIR=./data
|
||||||
MEDIASTATION_CACHE_DIR=./cache
|
MEDIASTATION_CACHE_DIR=./cache
|
||||||
@@ -344,7 +344,7 @@ For production, pin a specific release tag instead of using `latest`. Recommende
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat > .env <<'EOF'
|
cat > .env <<'EOF'
|
||||||
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23
|
||||||
MEDIASTATION_HTTP_PORT=18080
|
MEDIASTATION_HTTP_PORT=18080
|
||||||
MEDIASTATION_DATA_DIR=./data
|
MEDIASTATION_DATA_DIR=./data
|
||||||
MEDIASTATION_CACHE_DIR=./cache
|
MEDIASTATION_CACHE_DIR=./cache
|
||||||
@@ -593,25 +593,25 @@ Each release provides multi-platform archives:
|
|||||||
|
|
||||||
| Platform | Package example |
|
| Platform | Package example |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Linux x86_64 | `MediaStationGo-v0.0.22-linux-amd64.tar.gz` |
|
| Linux x86_64 | `MediaStationGo-v0.0.23-linux-amd64.tar.gz` |
|
||||||
| Linux ARM64 | `MediaStationGo-v0.0.22-linux-arm64.tar.gz` |
|
| Linux ARM64 | `MediaStationGo-v0.0.23-linux-arm64.tar.gz` |
|
||||||
| Windows x86_64 | `MediaStationGo-v0.0.22-windows-amd64.zip` |
|
| Windows x86_64 | `MediaStationGo-v0.0.23-windows-amd64.zip` |
|
||||||
| macOS Intel | `MediaStationGo-v0.0.22-darwin-amd64.tar.gz` |
|
| macOS Intel | `MediaStationGo-v0.0.23-darwin-amd64.tar.gz` |
|
||||||
| macOS Apple Silicon | `MediaStationGo-v0.0.22-darwin-arm64.tar.gz` |
|
| macOS Apple Silicon | `MediaStationGo-v0.0.23-darwin-arm64.tar.gz` |
|
||||||
|
|
||||||
Linux example:
|
Linux example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar -xzf MediaStationGo-v0.0.22-linux-amd64.tar.gz
|
tar -xzf MediaStationGo-v0.0.23-linux-amd64.tar.gz
|
||||||
cd MediaStationGo-v0.0.22-linux-amd64
|
cd MediaStationGo-v0.0.23-linux-amd64
|
||||||
MEDIASTATION_APP_PORT=18080 ./mediastation-go
|
MEDIASTATION_APP_PORT=18080 ./mediastation-go
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows example:
|
Windows example:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Expand-Archive .\MediaStationGo-v0.0.22-windows-amd64.zip
|
Expand-Archive .\MediaStationGo-v0.0.23-windows-amd64.zip
|
||||||
cd .\MediaStationGo-v0.0.22-windows-amd64
|
cd .\MediaStationGo-v0.0.23-windows-amd64
|
||||||
$env:MEDIASTATION_APP_PORT = "18080"
|
$env:MEDIASTATION_APP_PORT = "18080"
|
||||||
.\mediastation-go.exe
|
.\mediastation-go.exe
|
||||||
```
|
```
|
||||||
@@ -718,13 +718,15 @@ Common variables:
|
|||||||
| `MEDIASTATION_CACHE_CACHE_DIR` | `./cache` | Image/transcode cache |
|
| `MEDIASTATION_CACHE_CACHE_DIR` | `./cache` | Image/transcode cache |
|
||||||
| `MEDIASTATION_SECRETS_JWT_SECRET` | Auto-generated | JWT and encrypted settings seed |
|
| `MEDIASTATION_SECRETS_JWT_SECRET` | Auto-generated | JWT and encrypted settings seed |
|
||||||
| `MEDIASTATION_APP_CORS_ORIGINS` | empty | Extra CORS origins |
|
| `MEDIASTATION_APP_CORS_ORIGINS` | empty | Extra CORS origins |
|
||||||
|
| `MEDIASTATION_TELEGRAM_API_BASE_URL` | `https://api.telegram.org` | Telegram Bot API base URL; use a reverse proxy when Telegram is blocked or slow |
|
||||||
|
| `MEDIASTATION_TELEGRAM_PROXY_URL` | empty | Telegram outbound proxy, e.g. `http://172.17.0.1:7890` or `socks5://172.17.0.1:1080` |
|
||||||
|
|
||||||
Runtime settings from the admin UI:
|
Runtime settings from the admin UI:
|
||||||
|
|
||||||
- API keys: TMDb, Bangumi, TheTVDB, Fanart, OpenAI Compatible.
|
- API keys: TMDb, Bangumi, TheTVDB, Fanart, OpenAI Compatible.
|
||||||
- Sites: M-Team, NexusPHP, Unit3D, custom RSS.
|
- Sites: M-Team, NexusPHP, Unit3D, custom RSS.
|
||||||
- Download clients: qBittorrent, Transmission, Aria2.
|
- Download clients: qBittorrent, Transmission, Aria2.
|
||||||
- Notifications: Telegram, Bark, Webhook, Email.
|
- Notifications: Telegram, Bark, Webhook, Email. Telegram channels can use a per-channel API base URL or proxy, and test errors redact Bot Tokens automatically.
|
||||||
- Playback profiles, permissions, scheduler tasks, storage settings.
|
- Playback profiles, permissions, scheduler tasks, storage settings.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+8
-1
@@ -17,7 +17,7 @@
|
|||||||
#
|
#
|
||||||
# 镜像版本:
|
# 镜像版本:
|
||||||
# 默认拉取 latest;如需固定版本,创建 .env 并写入:
|
# 默认拉取 latest;如需固定版本,创建 .env 并写入:
|
||||||
# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23
|
||||||
#
|
#
|
||||||
# 路径映射总览:
|
# 路径映射总览:
|
||||||
# /data 程序数据目录。保存 SQLite 数据库、JWT secret、系统配置等,必须持久化。
|
# /data 程序数据目录。保存 SQLite 数据库、JWT secret、系统配置等,必须持久化。
|
||||||
@@ -141,6 +141,13 @@ services:
|
|||||||
# 跨域来源。通常无需设置;反向代理或三端客户端异常时再按需填写。
|
# 跨域来源。通常无需设置;反向代理或三端客户端异常时再按需填写。
|
||||||
MEDIASTATION_APP_CORS_ORIGINS: ${MEDIASTATION_APP_CORS_ORIGINS:-}
|
MEDIASTATION_APP_CORS_ORIGINS: ${MEDIASTATION_APP_CORS_ORIGINS:-}
|
||||||
|
|
||||||
|
# Telegram 通知/Bot 出站网络。通常留空即可;如 NAS 访问 Telegram 超时,
|
||||||
|
# 可在 .env 中填写反代 API 或代理地址:
|
||||||
|
# MEDIASTATION_TELEGRAM_API_BASE_URL=https://api.telegram.org
|
||||||
|
# MEDIASTATION_TELEGRAM_PROXY_URL=http://172.17.0.1:7890
|
||||||
|
MEDIASTATION_TELEGRAM_API_BASE_URL: ${MEDIASTATION_TELEGRAM_API_BASE_URL:-}
|
||||||
|
MEDIASTATION_TELEGRAM_PROXY_URL: ${MEDIASTATION_TELEGRAM_PROXY_URL:-}
|
||||||
|
|
||||||
# 宿主机文件权限映射。Linux/NAS 常用 1000:1000,可用 id 命令查看。
|
# 宿主机文件权限映射。Linux/NAS 常用 1000:1000,可用 id 命令查看。
|
||||||
PUID: ${PUID:-1000}
|
PUID: ${PUID:-1000}
|
||||||
PGID: ${PGID:-1000}
|
PGID: ${PGID:-1000}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
// NotifierService dispatches structured messages to one or more channels
|
// NotifierService dispatches structured messages to one or more channels
|
||||||
// configured in the system settings table:
|
// configured in the system settings table:
|
||||||
//
|
//
|
||||||
// notify.telegram.bot_token + notify.telegram.chat_id
|
// notify.telegram.bot_token + notify.telegram.chat_id
|
||||||
// notify.bark.server + notify.bark.key
|
// notify.bark.server + notify.bark.key
|
||||||
// notify.wechat.sendkey
|
// notify.wechat.sendkey
|
||||||
// notify.webhook.url + notify.webhook.method
|
// notify.webhook.url + notify.webhook.method
|
||||||
//
|
//
|
||||||
// Notifications are triggered by the subscription poller, the download
|
// Notifications are triggered by the subscription poller, the download
|
||||||
// poller, the scan / scrape completions, and any future event worth
|
// poller, the scan / scrape completions, and any future event worth
|
||||||
@@ -38,7 +38,7 @@ func NewNotifierService(log *zap.Logger, repo *repository.Container) *NotifierSe
|
|||||||
return &NotifierService{
|
return &NotifierService{
|
||||||
log: log,
|
log: log,
|
||||||
repo: repo,
|
repo: repo,
|
||||||
client: &http.Client{Timeout: 10 * time.Second},
|
client: NewExternalHTTPClient(10 * time.Second),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,17 +63,20 @@ func (n *NotifierService) sendTelegram(ctx context.Context, title, body string)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
text := fmt.Sprintf("<b>%s</b>\n\n%s", escapeHTML(title), escapeHTML(body))
|
text := fmt.Sprintf("<b>%s</b>\n\n%s", escapeHTML(title), escapeHTML(body))
|
||||||
u := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", token)
|
|
||||||
form := url.Values{}
|
form := url.Values{}
|
||||||
form.Set("chat_id", chatID)
|
form.Set("chat_id", chatID)
|
||||||
form.Set("text", text)
|
form.Set("text", text)
|
||||||
form.Set("parse_mode", "HTML")
|
form.Set("parse_mode", "HTML")
|
||||||
resp, err := n.client.PostForm(u, form)
|
cfg := map[string]string{"bot_token": token}
|
||||||
if err != nil {
|
if apiBase := n.get(ctx, "notify.telegram.api_base_url"); apiBase != "" {
|
||||||
n.log.Debug("telegram notify failed", zap.Error(err))
|
cfg["api_base_url"] = apiBase
|
||||||
return
|
}
|
||||||
|
if proxyURL := n.get(ctx, "notify.telegram.proxy_url"); proxyURL != "" {
|
||||||
|
cfg["proxy_url"] = proxyURL
|
||||||
|
}
|
||||||
|
if err := telegramPostForm(ctx, cfg, "sendMessage", form, 15*time.Second); err != nil {
|
||||||
|
n.log.Debug("telegram notify failed", zap.Error(err))
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NotifierService) sendBark(ctx context.Context, title, body string) {
|
func (n *NotifierService) sendBark(ctx context.Context, title, body string) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func NewNotifyChannelService(log *zap.Logger, repo *repository.Container) *Notif
|
|||||||
return &NotifyChannelService{
|
return &NotifyChannelService{
|
||||||
log: log,
|
log: log,
|
||||||
repo: repo,
|
repo: repo,
|
||||||
client: &http.Client{Timeout: 10 * time.Second},
|
client: NewExternalHTTPClient(10 * time.Second),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +89,7 @@ func (s *NotifyChannelService) List(ctx context.Context) ([]channelView, error)
|
|||||||
|
|
||||||
// Create persists a new channel.
|
// Create persists a new channel.
|
||||||
func (s *NotifyChannelService) Create(ctx context.Context, in ChannelInput) (*channelView, error) {
|
func (s *NotifyChannelService) Create(ctx context.Context, in ChannelInput) (*channelView, error) {
|
||||||
|
normalizeChannelInput(&in)
|
||||||
if err := validateChannel(in); err != nil {
|
if err := validateChannel(in); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -113,6 +114,7 @@ func (s *NotifyChannelService) Create(ctx context.Context, in ChannelInput) (*ch
|
|||||||
|
|
||||||
// Update applies a partial patch to an existing channel.
|
// Update applies a partial patch to an existing channel.
|
||||||
func (s *NotifyChannelService) Update(ctx context.Context, id string, in ChannelInput) (*channelView, error) {
|
func (s *NotifyChannelService) Update(ctx context.Context, id string, in ChannelInput) (*channelView, error) {
|
||||||
|
normalizeChannelInput(&in)
|
||||||
if err := validateChannel(in); err != nil {
|
if err := validateChannel(in); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -218,20 +220,18 @@ func (s *NotifyChannelService) dispatchOne(ctx context.Context, n model.NotifyCh
|
|||||||
|
|
||||||
switch n.Type {
|
switch n.Type {
|
||||||
case "telegram":
|
case "telegram":
|
||||||
token := str(cfg["bot_token"])
|
telegramCfg := telegramStringConfigFromAny(cfg)
|
||||||
chat := str(cfg["chat_id"])
|
token := telegramCfg["bot_token"]
|
||||||
|
chat := telegramCfg["chat_id"]
|
||||||
if token == "" || chat == "" {
|
if token == "" || chat == "" {
|
||||||
return errors.New("telegram missing bot_token / chat_id")
|
return errors.New("telegram missing bot_token / chat_id")
|
||||||
}
|
}
|
||||||
text := fmt.Sprintf("<b>%s</b>\n\n%s", escapeHTML(title), escapeHTML(body))
|
text := fmt.Sprintf("<b>%s</b>\n\n%s", escapeHTML(title), escapeHTML(body))
|
||||||
u := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", token)
|
|
||||||
form := url.Values{}
|
form := url.Values{}
|
||||||
form.Set("chat_id", chat)
|
form.Set("chat_id", chat)
|
||||||
form.Set("text", text)
|
form.Set("text", text)
|
||||||
form.Set("parse_mode", "HTML")
|
form.Set("parse_mode", "HTML")
|
||||||
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, u, strings.NewReader(form.Encode()))
|
return telegramPostForm(ctx, telegramCfg, "sendMessage", form, 15*time.Second)
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
return s.do(req)
|
|
||||||
|
|
||||||
case "bark":
|
case "bark":
|
||||||
key := str(cfg["device_key"])
|
key := str(cfg["device_key"])
|
||||||
@@ -332,12 +332,28 @@ func validateChannel(in ChannelInput) error {
|
|||||||
return errors.New("telegram admin_user_ids required")
|
return errors.New("telegram admin_user_ids required")
|
||||||
}
|
}
|
||||||
if str(cfg["group_chat_id"]) == "" && str(cfg["channel_chat_id"]) == "" && str(cfg["command_chat_id"]) == "" {
|
if str(cfg["group_chat_id"]) == "" && str(cfg["channel_chat_id"]) == "" && str(cfg["command_chat_id"]) == "" {
|
||||||
return errors.New("telegram group_chat_id or channel_chat_id required")
|
return errors.New("telegram group_chat_id or channel_chat_id required: 请填写绑定群组 ID 或绑定频道 ID;如果通知 Chat ID 是群组/频道负数 ID,也可以直接填在 Chat ID")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeChannelInput(in *ChannelInput) {
|
||||||
|
if in == nil || in.Type != "telegram" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if in.Config == nil {
|
||||||
|
in.Config = map[string]any{}
|
||||||
|
}
|
||||||
|
chatID := str(in.Config["chat_id"])
|
||||||
|
if chatID == "" || !strings.HasPrefix(chatID, "-") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if str(in.Config["group_chat_id"]) == "" && str(in.Config["channel_chat_id"]) == "" && str(in.Config["command_chat_id"]) == "" {
|
||||||
|
in.Config["group_chat_id"] = chatID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// str safely extracts a string from an interface{} loaded from JSON.
|
// str safely extracts a string from an interface{} loaded from JSON.
|
||||||
func str(v any) string {
|
func str(v any) string {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
|
|||||||
@@ -2,12 +2,8 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -35,30 +31,7 @@ func (p *TelegramProvider) Send(ctx context.Context, cfg map[string]string, even
|
|||||||
"text": text,
|
"text": text,
|
||||||
"parse_mode": parseMode,
|
"parse_mode": parseMode,
|
||||||
}
|
}
|
||||||
body, err := json.Marshal(payload)
|
return telegramPostJSON(ctx, cfg, "sendMessage", payload, 15*time.Second)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", botToken)
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 15 * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
return fmt.Errorf("telegram api error %d: %s", resp.StatusCode, string(respBody))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateConfig 验证 Telegram 配置。
|
// ValidateConfig 验证 Telegram 配置。
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultTelegramAPIBaseURL = "https://api.telegram.org"
|
||||||
|
|
||||||
|
var telegramTokenPattern = regexp.MustCompile(`bot[0-9]+:[^/\s"'?]+`)
|
||||||
|
|
||||||
|
func telegramAPIBaseURL(cfg map[string]string) string {
|
||||||
|
base := strings.TrimSpace(cfg["api_base_url"])
|
||||||
|
if base == "" {
|
||||||
|
base = strings.TrimSpace(os.Getenv("MEDIASTATION_TELEGRAM_API_BASE_URL"))
|
||||||
|
}
|
||||||
|
if base == "" {
|
||||||
|
base = defaultTelegramAPIBaseURL
|
||||||
|
}
|
||||||
|
return strings.TrimRight(base, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func telegramMethodURL(cfg map[string]string, botToken, method string) (string, error) {
|
||||||
|
botToken = strings.TrimSpace(botToken)
|
||||||
|
method = strings.TrimSpace(method)
|
||||||
|
if botToken == "" {
|
||||||
|
return "", errors.New("telegram bot_token required")
|
||||||
|
}
|
||||||
|
if method == "" {
|
||||||
|
return "", errors.New("telegram method required")
|
||||||
|
}
|
||||||
|
base := telegramAPIBaseURL(cfg)
|
||||||
|
if _, err := url.ParseRequestURI(base); err != nil {
|
||||||
|
return "", fmt.Errorf("telegram api_base_url invalid")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/bot%s/%s", base, botToken, method), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func telegramHTTPClient(timeout time.Duration, cfg map[string]string) *http.Client {
|
||||||
|
transport := NewExternalTransport()
|
||||||
|
proxyRaw := strings.TrimSpace(cfg["proxy_url"])
|
||||||
|
if proxyRaw == "" {
|
||||||
|
proxyRaw = strings.TrimSpace(os.Getenv("MEDIASTATION_TELEGRAM_PROXY_URL"))
|
||||||
|
}
|
||||||
|
if proxyRaw != "" {
|
||||||
|
if proxyURL, err := normalizeProxyURL(proxyRaw, "http"); err == nil {
|
||||||
|
transport.Proxy = http.ProxyURL(proxyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &http.Client{Timeout: timeout, Transport: transport}
|
||||||
|
}
|
||||||
|
|
||||||
|
func telegramPostForm(ctx context.Context, cfg map[string]string, method string, form url.Values, timeout time.Duration) error {
|
||||||
|
apiURL, err := telegramMethodURL(cfg, cfg["bot_token"], method)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
return telegramDo(telegramHTTPClient(timeout, cfg), req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func telegramPostJSON(ctx context.Context, cfg map[string]string, method string, payload any, timeout time.Duration) error {
|
||||||
|
apiURL, err := telegramMethodURL(cfg, cfg["bot_token"], method)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(string(body)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
return telegramDo(telegramHTTPClient(timeout, cfg), req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func telegramDo(client *http.Client, req *http.Request) error {
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return sanitizeTelegramError(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||||
|
return fmt.Errorf("telegram api error %d: %s", resp.StatusCode, sanitizeTelegramText(string(body)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func telegramStringConfigFromAny(cfg map[string]any) map[string]string {
|
||||||
|
out := make(map[string]string, len(cfg))
|
||||||
|
for key, value := range cfg {
|
||||||
|
out[key] = str(value)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeTelegramError(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
msg := sanitizeTelegramText(err.Error())
|
||||||
|
if strings.Contains(msg, "Client.Timeout exceeded") || strings.Contains(msg, "context deadline exceeded") {
|
||||||
|
return errors.New("telegram request timeout: 请检查 NAS/Docker 到 Telegram API 的代理、反代或网络连通性")
|
||||||
|
}
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeTelegramText(text string) string {
|
||||||
|
return telegramTokenPattern.ReplaceAllString(text, "bot<redacted>")
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTelegramMethodURLUsesCustomAPIBase(t *testing.T) {
|
||||||
|
got, err := telegramMethodURL(map[string]string{
|
||||||
|
"api_base_url": "https://tg.example.com/",
|
||||||
|
}, "123456:ABC-def", "sendMessage")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("telegramMethodURL returned error: %v", err)
|
||||||
|
}
|
||||||
|
want := "https://tg.example.com/bot123456:ABC-def/sendMessage"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeTelegramErrorRedactsBotToken(t *testing.T) {
|
||||||
|
err := sanitizeTelegramError(errors.New(`Post "https://api.telegram.org/bot123456:SECRET/sendMessage": context deadline exceeded`))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
if strings.Contains(msg, "SECRET") || strings.Contains(msg, "123456:") {
|
||||||
|
t.Fatalf("telegram token leaked in error: %s", msg)
|
||||||
|
}
|
||||||
|
if !strings.Contains(msg, "timeout") {
|
||||||
|
t.Fatalf("expected timeout hint, got: %s", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -478,7 +477,7 @@ func (s *TelegramBotService) StartPolling(ctx context.Context) {
|
|||||||
s.pollingCancel[botToken] = cancel
|
s.pollingCancel[botToken] = cancel
|
||||||
s.pollingMu.Unlock()
|
s.pollingMu.Unlock()
|
||||||
|
|
||||||
go s.pollLoop(pollCtx, botToken)
|
go s.pollLoop(pollCtx, cfg)
|
||||||
s.log.Info("started telegram polling", zap.String("channel", ch.Name))
|
s.log.Info("started telegram polling", zap.String("channel", ch.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,10 +494,14 @@ func (s *TelegramBotService) StopPolling() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pollLoop 对单个 Bot Token 执行长轮询。
|
// pollLoop 对单个 Bot Token 执行长轮询。
|
||||||
func (s *TelegramBotService) pollLoop(ctx context.Context, botToken string) {
|
func (s *TelegramBotService) pollLoop(ctx context.Context, cfg map[string]string) {
|
||||||
var offset int64 = 0
|
var offset int64 = 0
|
||||||
pollURL := fmt.Sprintf("https://api.telegram.org/bot%s/getUpdates", botToken)
|
pollURL, err := telegramMethodURL(cfg, cfg["bot_token"], "getUpdates")
|
||||||
client := &http.Client{Timeout: 45 * time.Second}
|
if err != nil {
|
||||||
|
s.log.Warn("telegram polling config invalid", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := telegramHTTPClient(45*time.Second, cfg)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -511,7 +514,7 @@ func (s *TelegramBotService) pollLoop(ctx context.Context, botToken string) {
|
|||||||
"offset": offset,
|
"offset": offset,
|
||||||
"timeout": 30,
|
"timeout": 30,
|
||||||
})
|
})
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, pollURL, bytes.NewReader(reqBody))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, pollURL, strings.NewReader(string(reqBody)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
continue
|
continue
|
||||||
@@ -520,6 +523,7 @@ func (s *TelegramBotService) pollLoop(ctx context.Context, botToken string) {
|
|||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.log.Debug("telegram polling failed", zap.Error(sanitizeTelegramError(err)))
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -554,18 +558,15 @@ func (s *TelegramBotService) pollLoop(ctx context.Context, botToken string) {
|
|||||||
|
|
||||||
// reply 通过 Telegram Bot API 发送回复消息。
|
// reply 通过 Telegram Bot API 发送回复消息。
|
||||||
func (s *TelegramBotService) reply(ctx context.Context, channel *model.NotifyChannel, chatID int, reply telegramCommandReply) error {
|
func (s *TelegramBotService) reply(ctx context.Context, channel *model.NotifyChannel, chatID int, reply telegramCommandReply) error {
|
||||||
botToken := ""
|
cfg := map[string]string{}
|
||||||
if channel != nil {
|
if channel != nil {
|
||||||
configStr := channel.Config
|
configStr := channel.Config
|
||||||
if s.crypto != nil && configStr != "" {
|
if s.crypto != nil && configStr != "" {
|
||||||
configStr = s.crypto.Decrypt(configStr)
|
configStr = s.crypto.Decrypt(configStr)
|
||||||
}
|
}
|
||||||
var cfg map[string]string
|
_ = json.Unmarshal([]byte(configStr), &cfg)
|
||||||
if err := json.Unmarshal([]byte(configStr), &cfg); err == nil {
|
|
||||||
botToken = cfg["bot_token"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if botToken == "" {
|
if strings.TrimSpace(cfg["bot_token"]) == "" {
|
||||||
return fmt.Errorf("bot_token not configured")
|
return fmt.Errorf("bot_token not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,27 +589,7 @@ func (s *TelegramBotService) reply(ctx context.Context, channel *model.NotifyCha
|
|||||||
}
|
}
|
||||||
payload["reply_markup"] = map[string]interface{}{"inline_keyboard": keyboard}
|
payload["reply_markup"] = map[string]interface{}{"inline_keyboard": keyboard}
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
return telegramPostJSON(ctx, cfg, "sendMessage", payload, 15*time.Second)
|
||||||
|
|
||||||
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", botToken)
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 15 * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
|
||||||
return fmt.Errorf("telegram api error %d: %s", resp.StatusCode, string(respBody))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// findChannelByChatID 根据 chat_id 查找已配置的通知渠道。
|
// findChannelByChatID 根据 chat_id 查找已配置的通知渠道。
|
||||||
@@ -756,24 +737,27 @@ func (s *TelegramBotService) telegramUserCanBind(ctx context.Context, channel *m
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *TelegramBotService) telegramUserIsChatMember(ctx context.Context, channel *model.NotifyChannel, chatID string, telegramUserID int) bool {
|
func (s *TelegramBotService) telegramUserIsChatMember(ctx context.Context, channel *model.NotifyChannel, chatID string, telegramUserID int) bool {
|
||||||
token := strings.TrimSpace(s.telegramChannelConfig(channel)["bot_token"])
|
cfg := s.telegramChannelConfig(channel)
|
||||||
if token == "" || chatID == "" || telegramUserID == 0 {
|
if strings.TrimSpace(cfg["bot_token"]) == "" || chatID == "" || telegramUserID == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
payload, _ := json.Marshal(map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
"user_id": telegramUserID,
|
"user_id": telegramUserID,
|
||||||
})
|
}
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
apiURL, err := telegramMethodURL(cfg, cfg["bot_token"], "getChatMember")
|
||||||
fmt.Sprintf("https://api.telegram.org/bot%s/getChatMember", token),
|
if err != nil {
|
||||||
bytes.NewReader(payload))
|
return false
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(payload)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(string(body)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
resp, err := (&http.Client{Timeout: 8 * time.Second}).Do(req)
|
resp, err := telegramHTTPClient(8*time.Second, cfg).Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Warn("telegram getChatMember failed", zap.String("chat_id", chatID), zap.Int("telegram_user_id", telegramUserID), zap.Error(err))
|
s.log.Warn("telegram getChatMember failed", zap.String("chat_id", chatID), zap.Int("telegram_user_id", telegramUserID), zap.Error(sanitizeTelegramError(err)))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -884,33 +868,40 @@ func (s *TelegramBotService) SetWebhook(ctx context.Context, botToken, webhookUR
|
|||||||
"url": webhookURL,
|
"url": webhookURL,
|
||||||
"allowed_updates": []string{"message"},
|
"allowed_updates": []string{"message"},
|
||||||
})
|
})
|
||||||
req, _ := http.NewRequestWithContext(ctx, http.MethodPost,
|
cfg := map[string]string{"bot_token": botToken}
|
||||||
fmt.Sprintf("https://api.telegram.org/bot%s/setWebhook", botToken),
|
apiURL, err := telegramMethodURL(cfg, botToken, "setWebhook")
|
||||||
bytes.NewReader(payload))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 15 * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(string(payload)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := telegramHTTPClient(15*time.Second, cfg)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return sanitizeTelegramError(err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
return fmt.Errorf("setWebhook failed: %s", string(body))
|
return fmt.Errorf("setWebhook failed: %s", sanitizeTelegramText(string(body)))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebhookInfo 获取 Webhook 配置信息。
|
// GetWebhookInfo 获取 Webhook 配置信息。
|
||||||
func (s *TelegramBotService) GetWebhookInfo(ctx context.Context, botToken string) (map[string]interface{}, error) {
|
func (s *TelegramBotService) GetWebhookInfo(ctx context.Context, botToken string) (map[string]interface{}, error) {
|
||||||
resp, err := (&http.Client{Timeout: 10 * time.Second}).Get(
|
cfg := map[string]string{"bot_token": botToken}
|
||||||
fmt.Sprintf("https://api.telegram.org/bot%s/getWebhookInfo", botToken),
|
apiURL, err := telegramMethodURL(cfg, botToken, "getWebhookInfo")
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
|
||||||
|
resp, err := telegramHTTPClient(10*time.Second, cfg).Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sanitizeTelegramError(err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ function channelSummary(ch: NotifyChannel): string {
|
|||||||
const cfg = ch.config ?? {}
|
const cfg = ch.config ?? {}
|
||||||
switch (ch.type) {
|
switch (ch.type) {
|
||||||
case 'telegram':
|
case 'telegram':
|
||||||
return `Bot ${String(cfg.bot_token ?? '').slice(0, 10)}… → 通知 ${cfg.chat_id ?? '-'} · 管理员 ${cfg.admin_user_ids ?? '-'} · 群组 ${cfg.group_chat_id ?? '-'} · 频道 ${cfg.channel_chat_id ?? '-'}`
|
return `Bot ${cfg.bot_token ? '已配置' : '未配置'} → 通知 ${cfg.chat_id ?? '-'} · 管理员 ${cfg.admin_user_ids ?? '-'} · 群组 ${cfg.group_chat_id ?? '-'} · 频道 ${cfg.channel_chat_id ?? '-'}`
|
||||||
case 'wechat':
|
case 'wechat':
|
||||||
return `SendKey ${String(cfg.sendkey ?? '').slice(0, 10)}…`
|
return `SendKey ${String(cfg.sendkey ?? '').slice(0, 10)}…`
|
||||||
case 'bark':
|
case 'bark':
|
||||||
@@ -200,7 +200,15 @@ function channelSummary(ch: NotifyChannel): string {
|
|||||||
// ─── Form Modal ─────────────────────────────────────────────────────────────
|
// ─── Form Modal ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const EMPTY_CONFIG: Record<NotifyChannel['type'], Record<string, string>> = {
|
const EMPTY_CONFIG: Record<NotifyChannel['type'], Record<string, string>> = {
|
||||||
telegram: { bot_token: '', chat_id: '', admin_user_ids: '', group_chat_id: '', channel_chat_id: '' },
|
telegram: {
|
||||||
|
bot_token: '',
|
||||||
|
chat_id: '',
|
||||||
|
admin_user_ids: '',
|
||||||
|
group_chat_id: '',
|
||||||
|
channel_chat_id: '',
|
||||||
|
api_base_url: '',
|
||||||
|
proxy_url: '',
|
||||||
|
},
|
||||||
wechat: { sendkey: '' },
|
wechat: { sendkey: '' },
|
||||||
bark: { device_key: '', server: '' },
|
bark: { device_key: '', server: '' },
|
||||||
webhook: { url: '', method: 'POST', headers: '', body_template: '' },
|
webhook: { url: '', method: 'POST', headers: '', body_template: '' },
|
||||||
@@ -221,7 +229,7 @@ function ChannelFormModal({
|
|||||||
editing?.type ?? 'telegram',
|
editing?.type ?? 'telegram',
|
||||||
)
|
)
|
||||||
const [config, setConfig] = useState<Record<string, string>>(
|
const [config, setConfig] = useState<Record<string, string>>(
|
||||||
editing?.config ?? EMPTY_CONFIG.telegram,
|
{ ...EMPTY_CONFIG[editing?.type ?? 'telegram'], ...(editing?.config ?? {}) },
|
||||||
)
|
)
|
||||||
const [enabled, setEnabled] = useState(editing?.enabled ?? true)
|
const [enabled, setEnabled] = useState(editing?.enabled ?? true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
@@ -233,12 +241,38 @@ function ChannelFormModal({
|
|||||||
|
|
||||||
const onSubmit = async (e: FormEvent) => {
|
const onSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (type === 'telegram') {
|
||||||
|
if (!String(config.bot_token ?? '').trim()) {
|
||||||
|
toast.error('请填写 Telegram Bot Token')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!String(config.chat_id ?? '').trim()) {
|
||||||
|
toast.error('请填写通知 Chat ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!String(config.admin_user_ids ?? '').trim()) {
|
||||||
|
toast.error('请填写管理员 Telegram ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const chatID = String(config.chat_id ?? '').trim()
|
||||||
|
const groupChatID = String(config.group_chat_id ?? '').trim()
|
||||||
|
const channelChatID = String(config.channel_chat_id ?? '').trim()
|
||||||
|
if (!groupChatID && !channelChatID && !chatID.startsWith('-')) {
|
||||||
|
toast.error('请至少填写绑定群组 ID 或绑定频道 ID;或把群组/频道负数 ID 填到 Chat ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!groupChatID && !channelChatID && chatID.startsWith('-')) {
|
||||||
|
config.group_chat_id = chatID
|
||||||
|
}
|
||||||
|
}
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
const input: NotifyChannelInput = {
|
const input: NotifyChannelInput = {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
type: type,
|
type: type,
|
||||||
config,
|
config: Object.fromEntries(
|
||||||
|
Object.entries(config).map(([key, value]) => [key, String(value ?? '').trim()]),
|
||||||
|
),
|
||||||
enabled,
|
enabled,
|
||||||
}
|
}
|
||||||
if (editing) {
|
if (editing) {
|
||||||
@@ -336,8 +370,24 @@ function ChannelFormModal({
|
|||||||
onChange={(e) => updateConfig('channel_chat_id', e.target.value)}
|
onChange={(e) => updateConfig('channel_chat_id', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field label="Telegram API 地址 (可选)">
|
||||||
|
<input
|
||||||
|
className="input-base"
|
||||||
|
placeholder="默认 https://api.telegram.org;反代可填 https://tg.example.com"
|
||||||
|
value={config.api_base_url ?? ''}
|
||||||
|
onChange={(e) => updateConfig('api_base_url', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Telegram 代理 (可选)">
|
||||||
|
<input
|
||||||
|
className="input-base"
|
||||||
|
placeholder="如 http://172.17.0.1:7890 或 socks5://172.17.0.1:1080"
|
||||||
|
value={config.proxy_url ?? ''}
|
||||||
|
onChange={(e) => updateConfig('proxy_url', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
<div className="rounded-2xl border border-primary-400/15 bg-primary-400/5 px-4 py-3 text-xs leading-6 text-ink-50">
|
<div className="rounded-2xl border border-primary-400/15 bg-primary-400/5 px-4 py-3 text-xs leading-6 text-ink-50">
|
||||||
必须至少填写群组 ID 或频道 ID。只有配置群组/频道中的成员可以唤醒 Bot、使用 <code>/start 用户名 密码</code> 绑定账号和隐藏成人目录;<code>/status</code>、<code>/search</code>、<code>/downloads</code>、<code>/stats</code> 仅管理员 Telegram ID 或已绑定的本地管理员可用。
|
必须至少填写群组 ID 或频道 ID。只有配置群组/频道中的成员可以唤醒 Bot、使用 <code>/start 用户名 密码</code> 绑定账号和隐藏成人目录;<code>/status</code>、<code>/search</code>、<code>/downloads</code>、<code>/stats</code> 仅管理员 Telegram ID 或已绑定的本地管理员可用。若测试通知超时,可填写反代 API 地址或代理地址。
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user