mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-08 22:36:37 +08:00
fix: tighten telegram channel binding
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.21
|
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
||||||
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.21
|
# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
||||||
#
|
#
|
||||||
# 路径映射总览:
|
# 路径映射总览:
|
||||||
# /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.21
|
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
||||||
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.21-linux-amd64.tar.gz` |
|
| Linux x86_64 | `MediaStationGo-v0.0.22-linux-amd64.tar.gz` |
|
||||||
| Linux ARM64 | `MediaStationGo-v0.0.21-linux-arm64.tar.gz` |
|
| Linux ARM64 | `MediaStationGo-v0.0.22-linux-arm64.tar.gz` |
|
||||||
| Windows x86_64 | `MediaStationGo-v0.0.21-windows-amd64.zip` |
|
| Windows x86_64 | `MediaStationGo-v0.0.22-windows-amd64.zip` |
|
||||||
| macOS Intel | `MediaStationGo-v0.0.21-darwin-amd64.tar.gz` |
|
| macOS Intel | `MediaStationGo-v0.0.22-darwin-amd64.tar.gz` |
|
||||||
| macOS Apple Silicon | `MediaStationGo-v0.0.21-darwin-arm64.tar.gz` |
|
| macOS Apple Silicon | `MediaStationGo-v0.0.22-darwin-arm64.tar.gz` |
|
||||||
|
|
||||||
部署步骤:
|
部署步骤:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux 示例
|
# Linux 示例
|
||||||
tar -xzf MediaStationGo-v0.0.21-linux-amd64.tar.gz
|
tar -xzf MediaStationGo-v0.0.22-linux-amd64.tar.gz
|
||||||
cd MediaStationGo-v0.0.21-linux-amd64
|
cd MediaStationGo-v0.0.22-linux-amd64
|
||||||
MEDIASTATION_APP_PORT=18080 ./mediastation-go
|
MEDIASTATION_APP_PORT=18080 ./mediastation-go
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows:
|
Windows:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Expand-Archive .\MediaStationGo-v0.0.21-windows-amd64.zip
|
Expand-Archive .\MediaStationGo-v0.0.22-windows-amd64.zip
|
||||||
cd .\MediaStationGo-v0.0.21-windows-amd64
|
cd .\MediaStationGo-v0.0.22-windows-amd64
|
||||||
$env:MEDIASTATION_APP_PORT = "18080"
|
$env:MEDIASTATION_APP_PORT = "18080"
|
||||||
.\mediastation-go.exe
|
.\mediastation-go.exe
|
||||||
```
|
```
|
||||||
|
|||||||
+11
-11
@@ -235,7 +235,7 @@ mkdir -p data cache media downloads
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat > .env <<'EOF'
|
cat > .env <<'EOF'
|
||||||
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.21
|
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
||||||
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.21
|
MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
||||||
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.21-linux-amd64.tar.gz` |
|
| Linux x86_64 | `MediaStationGo-v0.0.22-linux-amd64.tar.gz` |
|
||||||
| Linux ARM64 | `MediaStationGo-v0.0.21-linux-arm64.tar.gz` |
|
| Linux ARM64 | `MediaStationGo-v0.0.22-linux-arm64.tar.gz` |
|
||||||
| Windows x86_64 | `MediaStationGo-v0.0.21-windows-amd64.zip` |
|
| Windows x86_64 | `MediaStationGo-v0.0.22-windows-amd64.zip` |
|
||||||
| macOS Intel | `MediaStationGo-v0.0.21-darwin-amd64.tar.gz` |
|
| macOS Intel | `MediaStationGo-v0.0.22-darwin-amd64.tar.gz` |
|
||||||
| macOS Apple Silicon | `MediaStationGo-v0.0.21-darwin-arm64.tar.gz` |
|
| macOS Apple Silicon | `MediaStationGo-v0.0.22-darwin-arm64.tar.gz` |
|
||||||
|
|
||||||
Linux example:
|
Linux example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar -xzf MediaStationGo-v0.0.21-linux-amd64.tar.gz
|
tar -xzf MediaStationGo-v0.0.22-linux-amd64.tar.gz
|
||||||
cd MediaStationGo-v0.0.21-linux-amd64
|
cd MediaStationGo-v0.0.22-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.21-windows-amd64.zip
|
Expand-Archive .\MediaStationGo-v0.0.22-windows-amd64.zip
|
||||||
cd .\MediaStationGo-v0.0.21-windows-amd64
|
cd .\MediaStationGo-v0.0.22-windows-amd64
|
||||||
$env:MEDIASTATION_APP_PORT = "18080"
|
$env:MEDIASTATION_APP_PORT = "18080"
|
||||||
.\mediastation-go.exe
|
.\mediastation-go.exe
|
||||||
```
|
```
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
#
|
#
|
||||||
# 镜像版本:
|
# 镜像版本:
|
||||||
# 默认拉取 latest;如需固定版本,创建 .env 并写入:
|
# 默认拉取 latest;如需固定版本,创建 .env 并写入:
|
||||||
# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.21
|
# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.22
|
||||||
#
|
#
|
||||||
# 路径映射总览:
|
# 路径映射总览:
|
||||||
# /data 程序数据目录。保存 SQLite 数据库、JWT secret、系统配置等,必须持久化。
|
# /data 程序数据目录。保存 SQLite 数据库、JWT secret、系统配置等,必须持久化。
|
||||||
|
|||||||
@@ -95,11 +95,11 @@ func (s *NotifyChannelService) Create(ctx context.Context, in ChannelInput) (*ch
|
|||||||
cfgBlob, _ := json.Marshal(in.Config)
|
cfgBlob, _ := json.Marshal(in.Config)
|
||||||
evBlob, _ := json.Marshal(in.Events)
|
evBlob, _ := json.Marshal(in.Events)
|
||||||
n := &model.NotifyChannel{
|
n := &model.NotifyChannel{
|
||||||
Name: strings.TrimSpace(in.Name),
|
Name: strings.TrimSpace(in.Name),
|
||||||
Type: in.Type,
|
Type: in.Type,
|
||||||
Config: string(cfgBlob),
|
Config: string(cfgBlob),
|
||||||
Events: string(evBlob),
|
Events: string(evBlob),
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
if in.Enabled != nil {
|
if in.Enabled != nil {
|
||||||
n.Enabled = *in.Enabled
|
n.Enabled = *in.Enabled
|
||||||
@@ -119,10 +119,10 @@ func (s *NotifyChannelService) Update(ctx context.Context, id string, in Channel
|
|||||||
cfgBlob, _ := json.Marshal(in.Config)
|
cfgBlob, _ := json.Marshal(in.Config)
|
||||||
evBlob, _ := json.Marshal(in.Events)
|
evBlob, _ := json.Marshal(in.Events)
|
||||||
patch := map[string]any{
|
patch := map[string]any{
|
||||||
"name": strings.TrimSpace(in.Name),
|
"name": strings.TrimSpace(in.Name),
|
||||||
"type": in.Type,
|
"type": in.Type,
|
||||||
"config": string(cfgBlob),
|
"config": string(cfgBlob),
|
||||||
"events": string(evBlob),
|
"events": string(evBlob),
|
||||||
}
|
}
|
||||||
if in.Enabled != nil {
|
if in.Enabled != nil {
|
||||||
patch["enabled"] = *in.Enabled
|
patch["enabled"] = *in.Enabled
|
||||||
@@ -320,6 +320,21 @@ func validateChannel(in ChannelInput) error {
|
|||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported channel type %q", in.Type)
|
return fmt.Errorf("unsupported channel type %q", in.Type)
|
||||||
}
|
}
|
||||||
|
if in.Type == "telegram" {
|
||||||
|
cfg := in.Config
|
||||||
|
if str(cfg["bot_token"]) == "" {
|
||||||
|
return errors.New("telegram bot_token required")
|
||||||
|
}
|
||||||
|
if str(cfg["chat_id"]) == "" {
|
||||||
|
return errors.New("telegram notification chat_id required")
|
||||||
|
}
|
||||||
|
if str(cfg["admin_user_ids"]) == "" {
|
||||||
|
return errors.New("telegram admin_user_ids required")
|
||||||
|
}
|
||||||
|
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 nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,8 +114,17 @@ func (s *TelegramBotService) HandleWebhook(ctx context.Context, body []byte) err
|
|||||||
zap.String("text", text),
|
zap.String("text", text),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 获取该 chat_id 对应的通知渠道配置
|
// 获取该消息可使用的 Telegram 通知渠道配置。群组/频道消息必须来自
|
||||||
channel := s.findChannelByChatID(ctx, msg.Chat.ID)
|
// 已配置的群组/频道;私聊消息会选择一个可验证该用户成员身份的 Bot。
|
||||||
|
channel := s.findChannelForMessage(ctx, msg)
|
||||||
|
if channel == nil {
|
||||||
|
s.log.Warn("telegram channel not allowed or not configured",
|
||||||
|
zap.Int("chat_id", msg.Chat.ID),
|
||||||
|
zap.String("chat_type", msg.Chat.Type),
|
||||||
|
zap.Int("telegram_user_id", msg.From.ID),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 解析并执行命令
|
// 解析并执行命令
|
||||||
reply, err := s.executeCommand(ctx, channel, msg, text)
|
reply, err := s.executeCommand(ctx, channel, msg, text)
|
||||||
@@ -155,22 +164,22 @@ func (s *TelegramBotService) executeCommand(ctx context.Context, channel *model.
|
|||||||
case "/hideadult", "/hide_adult", "/adult":
|
case "/hideadult", "/hide_adult", "/adult":
|
||||||
return s.cmdHideAdult(ctx, msg, args), nil
|
return s.cmdHideAdult(ctx, msg, args), nil
|
||||||
case "/status":
|
case "/status":
|
||||||
if !s.telegramUserIsAdmin(ctx, msg.From.ID) {
|
if !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) {
|
||||||
return telegramCommandReply{Text: "此命令仅管理员可用。普通用户只能使用 /start 绑定账号,并通过按钮隐藏成人目录。"}, nil
|
return telegramCommandReply{Text: "此命令仅管理员可用。普通用户只能使用 /start 绑定账号,并通过按钮隐藏成人目录。"}, nil
|
||||||
}
|
}
|
||||||
return s.cmdStatus(ctx)
|
return s.cmdStatus(ctx)
|
||||||
case "/search":
|
case "/search":
|
||||||
if !s.telegramUserIsAdmin(ctx, msg.From.ID) {
|
if !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) {
|
||||||
return telegramCommandReply{Text: "此命令仅管理员可用。"}, nil
|
return telegramCommandReply{Text: "此命令仅管理员可用。"}, nil
|
||||||
}
|
}
|
||||||
return s.cmdSearch(ctx, args)
|
return s.cmdSearch(ctx, args)
|
||||||
case "/downloads":
|
case "/downloads":
|
||||||
if !s.telegramUserIsAdmin(ctx, msg.From.ID) {
|
if !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) {
|
||||||
return telegramCommandReply{Text: "此命令仅管理员可用。"}, nil
|
return telegramCommandReply{Text: "此命令仅管理员可用。"}, nil
|
||||||
}
|
}
|
||||||
return s.cmdDownloads(ctx)
|
return s.cmdDownloads(ctx)
|
||||||
case "/stats":
|
case "/stats":
|
||||||
if !s.telegramUserIsAdmin(ctx, msg.From.ID) {
|
if !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) {
|
||||||
return telegramCommandReply{Text: "此命令仅管理员可用。"}, nil
|
return telegramCommandReply{Text: "此命令仅管理员可用。"}, nil
|
||||||
}
|
}
|
||||||
return s.cmdStats(ctx)
|
return s.cmdStats(ctx)
|
||||||
@@ -202,6 +211,10 @@ func (s *TelegramBotService) cmdStart(ctx context.Context, msg *TelegramMessage,
|
|||||||
}
|
}
|
||||||
return telegramCommandReply{Text: "<b>欢迎使用 MediaStationGo</b>\n\n普通用户请先绑定账号:\n<code>/start 用户名 密码</code>\n或:<code>/start 用户名-密码</code>\n\n如果没有账号,请联系管理员注册。"}
|
return telegramCommandReply{Text: "<b>欢迎使用 MediaStationGo</b>\n\n普通用户请先绑定账号:\n<code>/start 用户名 密码</code>\n或:<code>/start 用户名-密码</code>\n\n如果没有账号,请联系管理员注册。"}
|
||||||
}
|
}
|
||||||
|
channel := s.findChannelForMessage(ctx, msg)
|
||||||
|
if !s.telegramUserCanBind(ctx, channel, msg.From.ID) {
|
||||||
|
return telegramCommandReply{Text: "当前 Telegram 账号不在已绑定的群组/频道中,无法绑定媒体中心账号。请先加入管理员配置的群组或频道。"}
|
||||||
|
}
|
||||||
username, password := parseStartCredentials(args)
|
username, password := parseStartCredentials(args)
|
||||||
if username == "" || password == "" {
|
if username == "" || password == "" {
|
||||||
return telegramCommandReply{Text: "绑定格式不正确,请使用:\n<code>/start 用户名 密码</code>\n或:<code>/start 用户名-密码</code>"}
|
return telegramCommandReply{Text: "绑定格式不正确,请使用:\n<code>/start 用户名 密码</code>\n或:<code>/start 用户名-密码</code>"}
|
||||||
@@ -230,7 +243,8 @@ func (s *TelegramBotService) cmdStart(ctx context.Context, msg *TelegramMessage,
|
|||||||
|
|
||||||
// cmdHelp 处理 /help 命令。
|
// cmdHelp 处理 /help 命令。
|
||||||
func (s *TelegramBotService) cmdHelp(ctx context.Context, msg *TelegramMessage) string {
|
func (s *TelegramBotService) cmdHelp(ctx context.Context, msg *TelegramMessage) string {
|
||||||
if !s.telegramUserIsAdmin(ctx, msg.From.ID) {
|
channel := s.findChannelForMessage(ctx, msg)
|
||||||
|
if !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) {
|
||||||
return "<b>MediaStationGo 用户命令</b>\n\n" +
|
return "<b>MediaStationGo 用户命令</b>\n\n" +
|
||||||
"<b>/start 用户名 密码</b> — 绑定账号\n" +
|
"<b>/start 用户名 密码</b> — 绑定账号\n" +
|
||||||
"<b>/hideadult on|off</b> — 隐藏或显示成人目录\n\n" +
|
"<b>/hideadult on|off</b> — 隐藏或显示成人目录\n\n" +
|
||||||
@@ -253,6 +267,10 @@ func (s *TelegramBotService) cmdHelp(ctx context.Context, msg *TelegramMessage)
|
|||||||
|
|
||||||
// cmdStatus 处理 /status 命令。
|
// cmdStatus 处理 /status 命令。
|
||||||
func (s *TelegramBotService) cmdHideAdult(ctx context.Context, msg *TelegramMessage, args []string) telegramCommandReply {
|
func (s *TelegramBotService) cmdHideAdult(ctx context.Context, msg *TelegramMessage, args []string) telegramCommandReply {
|
||||||
|
channel := s.findChannelForMessage(ctx, msg)
|
||||||
|
if !s.telegramUserCanBind(ctx, channel, msg.From.ID) {
|
||||||
|
return telegramCommandReply{Text: "当前 Telegram 账号不在已绑定的群组/频道中,无法使用成人目录隐藏开关。"}
|
||||||
|
}
|
||||||
binding := s.telegramBinding(ctx, msg.From.ID)
|
binding := s.telegramBinding(ctx, msg.From.ID)
|
||||||
if binding == nil {
|
if binding == nil {
|
||||||
return telegramCommandReply{Text: "请先绑定账号:<code>/start 用户名 密码</code>"}
|
return telegramCommandReply{Text: "请先绑定账号:<code>/start 用户名 密码</code>"}
|
||||||
@@ -610,7 +628,8 @@ func (s *TelegramBotService) findChannelByChatID(ctx context.Context, chatID int
|
|||||||
}
|
}
|
||||||
var cfg map[string]string
|
var cfg map[string]string
|
||||||
json.Unmarshal([]byte(configStr), &cfg)
|
json.Unmarshal([]byte(configStr), &cfg)
|
||||||
if cfg["chat_id"] == target || cfg["command_chat_id"] == target {
|
if cfg["chat_id"] == target || cfg["command_chat_id"] == target ||
|
||||||
|
cfg["group_chat_id"] == target || cfg["channel_chat_id"] == target {
|
||||||
return &ch
|
return &ch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -620,6 +639,33 @@ func (s *TelegramBotService) findChannelByChatID(ctx context.Context, chatID int
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) findChannelForMessage(ctx context.Context, msg *TelegramMessage) *model.NotifyChannel {
|
||||||
|
if msg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if msg.Chat.Type != "" && msg.Chat.Type != "private" {
|
||||||
|
return s.findChannelByChatID(ctx, msg.Chat.ID)
|
||||||
|
}
|
||||||
|
channels, err := s.repo.NotifyChannel.ListByType(ctx, "telegram")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var first *model.NotifyChannel
|
||||||
|
for i := range channels {
|
||||||
|
ch := channels[i]
|
||||||
|
if !ch.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if first == nil {
|
||||||
|
first = &ch
|
||||||
|
}
|
||||||
|
if s.telegramUserIsAdmin(ctx, &ch, msg.From.ID) || s.telegramUserCanBind(ctx, &ch, msg.From.ID) {
|
||||||
|
return &ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TelegramBotService) handleCallback(ctx context.Context, cb *TelegramCallbackQuery) error {
|
func (s *TelegramBotService) handleCallback(ctx context.Context, cb *TelegramCallbackQuery) error {
|
||||||
if cb == nil || cb.Message == nil {
|
if cb == nil || cb.Message == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -649,7 +695,10 @@ func (s *TelegramBotService) telegramBinding(ctx context.Context, telegramUserID
|
|||||||
return &binding
|
return &binding
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TelegramBotService) telegramUserIsAdmin(ctx context.Context, telegramUserID int) bool {
|
func (s *TelegramBotService) telegramUserIsAdmin(ctx context.Context, channel *model.NotifyChannel, telegramUserID int) bool {
|
||||||
|
if s.telegramUserIDConfigured(channel, telegramUserID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
binding := s.telegramBinding(ctx, telegramUserID)
|
binding := s.telegramBinding(ctx, telegramUserID)
|
||||||
if binding == nil {
|
if binding == nil {
|
||||||
return false
|
return false
|
||||||
@@ -671,13 +720,115 @@ func (s *TelegramBotService) telegramChatAllowed(channel *model.NotifyChannel, c
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
target := strconv.Itoa(chatID)
|
target := strconv.Itoa(chatID)
|
||||||
commandChatID := strings.TrimSpace(cfg["command_chat_id"])
|
for _, key := range []string{"group_chat_id", "channel_chat_id", "command_chat_id"} {
|
||||||
if commandChatID != "" {
|
if configured := strings.TrimSpace(cfg[key]); configured != "" && configured == target {
|
||||||
return commandChatID == target
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg["group_chat_id"]) != "" || strings.TrimSpace(cfg["channel_chat_id"]) != "" || strings.TrimSpace(cfg["command_chat_id"]) != "" {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(cfg["chat_id"]) == target
|
return strings.TrimSpace(cfg["chat_id"]) == target
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) telegramUserCanBind(ctx context.Context, channel *model.NotifyChannel, telegramUserID int) bool {
|
||||||
|
if telegramUserID == 0 || channel == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s.telegramUserIDConfigured(channel, telegramUserID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
cfg := s.telegramChannelConfig(channel)
|
||||||
|
groupID := strings.TrimSpace(cfg["group_chat_id"])
|
||||||
|
channelID := strings.TrimSpace(cfg["channel_chat_id"])
|
||||||
|
if groupID == "" && channelID == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, chatID := range []string{groupID, channelID} {
|
||||||
|
if chatID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.telegramUserIsChatMember(ctx, channel, chatID, telegramUserID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) telegramUserIsChatMember(ctx context.Context, channel *model.NotifyChannel, chatID string, telegramUserID int) bool {
|
||||||
|
token := strings.TrimSpace(s.telegramChannelConfig(channel)["bot_token"])
|
||||||
|
if token == "" || chatID == "" || telegramUserID == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
payload, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"chat_id": chatID,
|
||||||
|
"user_id": telegramUserID,
|
||||||
|
})
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||||
|
fmt.Sprintf("https://api.telegram.org/bot%s/getChatMember", token),
|
||||||
|
bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := (&http.Client{Timeout: 8 * time.Second}).Do(req)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("telegram getChatMember failed", zap.String("chat_id", chatID), zap.Int("telegram_user_id", telegramUserID), zap.Error(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var result struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Result struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || !result.OK {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch strings.ToLower(result.Result.Status) {
|
||||||
|
case "creator", "administrator", "member", "restricted":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) telegramUserIDConfigured(channel *model.NotifyChannel, telegramUserID int) bool {
|
||||||
|
if channel == nil || telegramUserID == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cfg := s.telegramChannelConfig(channel)
|
||||||
|
target := strconv.Itoa(telegramUserID)
|
||||||
|
for _, value := range strings.FieldsFunc(cfg["admin_user_ids"], func(r rune) bool {
|
||||||
|
return r == ',' || r == ';' || r == ',' || r == ' ' || r == '\n' || r == '\t'
|
||||||
|
}) {
|
||||||
|
if strings.TrimSpace(value) == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) telegramChannelConfig(channel *model.NotifyChannel) map[string]string {
|
||||||
|
return telegramConfigFromChannel(s.crypto, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func telegramConfigFromChannel(crypto *CryptoService, channel *model.NotifyChannel) map[string]string {
|
||||||
|
if channel == nil {
|
||||||
|
return map[string]string{}
|
||||||
|
}
|
||||||
|
configStr := channel.Config
|
||||||
|
if crypto != nil && configStr != "" {
|
||||||
|
configStr = crypto.Decrypt(configStr)
|
||||||
|
}
|
||||||
|
var cfg map[string]string
|
||||||
|
if err := json.Unmarshal([]byte(configStr), &cfg); err != nil || cfg == nil {
|
||||||
|
return map[string]string{}
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TelegramBotService) upsertTelegramBinding(ctx context.Context, msg *TelegramMessage, userID string) error {
|
func (s *TelegramBotService) upsertTelegramBinding(ctx context.Context, msg *TelegramMessage, userID string) error {
|
||||||
name := strings.TrimSpace(msg.From.FirstName)
|
name := strings.TrimSpace(msg.From.FirstName)
|
||||||
if msg.From.Username != "" {
|
if msg.From.Username != "" {
|
||||||
|
|||||||
@@ -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.command_chat_id ?? cfg.chat_id ?? '-'}`
|
return `Bot ${String(cfg.bot_token ?? '').slice(0, 10)}… → 通知 ${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,7 @@ 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: '', command_chat_id: '' },
|
telegram: { bot_token: '', chat_id: '', admin_user_ids: '', group_chat_id: '', channel_chat_id: '' },
|
||||||
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: '' },
|
||||||
@@ -311,16 +311,33 @@ function ChannelFormModal({
|
|||||||
onChange={(e) => updateConfig('chat_id', e.target.value)}
|
onChange={(e) => updateConfig('chat_id', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="命令群组/频道 Chat ID (可选)">
|
<Field label="管理员 Telegram ID">
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
className="input-base"
|
||||||
|
placeholder="多个用逗号分隔,如 123456789,987654321"
|
||||||
|
value={config.admin_user_ids ?? ''}
|
||||||
|
onChange={(e) => updateConfig('admin_user_ids', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="绑定群组 ID">
|
||||||
<input
|
<input
|
||||||
className="input-base"
|
className="input-base"
|
||||||
placeholder="留空则使用上方 Chat ID;填写后只有该群组/频道可唤醒 Bot"
|
placeholder="如 -1001234567890;群组成员才允许唤醒/绑定"
|
||||||
value={config.command_chat_id ?? ''}
|
value={config.group_chat_id ?? ''}
|
||||||
onChange={(e) => updateConfig('command_chat_id', e.target.value)}
|
onChange={(e) => updateConfig('group_chat_id', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="绑定频道 ID">
|
||||||
|
<input
|
||||||
|
className="input-base"
|
||||||
|
placeholder="如 -1009876543210;频道成员才允许唤醒/绑定"
|
||||||
|
value={config.channel_chat_id ?? ''}
|
||||||
|
onChange={(e) => updateConfig('channel_chat_id', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</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">
|
||||||
普通用户只能通过 <code>/start 用户名 密码</code> 绑定账号,并使用隐藏成人目录按钮;<code>/status</code>、<code>/search</code>、<code>/downloads</code>、<code>/stats</code> 仅管理员可用。
|
必须至少填写群组 ID 或频道 ID。只有配置群组/频道中的成员可以唤醒 Bot、使用 <code>/start 用户名 密码</code> 绑定账号和隐藏成人目录;<code>/status</code>、<code>/search</code>、<code>/downloads</code>、<code>/stats</code> 仅管理员 Telegram ID 或已绑定的本地管理员可用。
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user