This commit is contained in:
parent
e8c2e7867e
commit
754a2578fb
@ -2,6 +2,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
@ -23,7 +24,9 @@ func telegramRequestService(ctx *appcontext.Context, w http.ResponseWriter, rece
|
||||
return nil
|
||||
}
|
||||
service := hooks.PluginConfigService()
|
||||
if service == nil { response.NewErrorResponse(response.ServerError, "通知配置服务不可用", "").FPrint(w) }
|
||||
if service == nil {
|
||||
response.NewErrorResponse(response.ServerError, "通知配置服务不可用", "").FPrint(w)
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
@ -32,7 +35,7 @@ func telegramPublicSettings(service *pluginservice.ConfigService, status plugins
|
||||
return map[string]any{
|
||||
"version": status.Version, "bound": settings.ChatID != "", "displayName": settings.DisplayName,
|
||||
"needsRebind": service.TelegramBindingNeedsRebind(settings),
|
||||
"enabled": settings.ChatID != "" && settings.Verified && !settings.Disabled,
|
||||
"enabled": settings.ChatID != "" && settings.Verified && !settings.Disabled,
|
||||
"showContent": settings.ShowContent, "spoilerContent": settings.SpoilerContent,
|
||||
"sendAttachments": settings.SendAttachments, "disableLinkPreview": settings.DisableLinkPreview,
|
||||
}
|
||||
@ -47,14 +50,21 @@ func writeTelegramUserSettings(w http.ResponseWriter, service *pluginservice.Con
|
||||
// TelegramUserSettings 的接收身份只能来自授权流程,页面仅维护本人偏好。
|
||||
func TelegramUserSettings(ctx *appcontext.Context, w http.ResponseWriter, req *http.Request) {
|
||||
service := telegramRequestService(ctx, w, req.Method != http.MethodGet)
|
||||
if service == nil { return }
|
||||
if service == nil {
|
||||
return
|
||||
}
|
||||
if req.Method == http.MethodGet {
|
||||
status, err := service.GetOwnTelegramSettings(ctx.UserID)
|
||||
if err != nil { writeTelegramSettingsError(w, err); return }
|
||||
if err != nil {
|
||||
writeTelegramSettingsError(w, err)
|
||||
return
|
||||
}
|
||||
data := telegramPublicSettings(service, status)
|
||||
configured, message := service.TelegramBindingAvailability()
|
||||
_, running := hooks.GetHook("telegram_push")
|
||||
if configured && !running { message = "Telegram 推送插件未运行,请联系超级管理员" }
|
||||
if configured && !running {
|
||||
message = "Telegram 推送插件未运行,请联系超级管理员"
|
||||
}
|
||||
data["configured"], data["running"], data["available"], data["message"] = configured, running, configured && running, message
|
||||
response.NewSuccessResponse(data).FPrint(w)
|
||||
return
|
||||
@ -65,28 +75,43 @@ func TelegramUserSettings(ctx *appcontext.Context, w http.ResponseWriter, req *h
|
||||
return
|
||||
}
|
||||
var input struct {
|
||||
Version uint64 `json:"version"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ShowContent bool `json:"showContent"`
|
||||
SpoilerContent bool `json:"spoilerContent"`
|
||||
SendAttachments bool `json:"sendAttachments"`
|
||||
DisableLinkPreview bool `json:"disableLinkPreview"`
|
||||
Version uint64 `json:"version"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ShowContent bool `json:"showContent"`
|
||||
SpoilerContent bool `json:"spoilerContent"`
|
||||
SendAttachments bool `json:"sendAttachments"`
|
||||
DisableLinkPreview bool `json:"disableLinkPreview"`
|
||||
}
|
||||
if !decodePluginJSONBody(w, req, &input) {
|
||||
return
|
||||
}
|
||||
if !decodePluginJSONBody(w, req, &input) { return }
|
||||
preferences := pluginservice.TelegramUserSettings{Disabled: !input.Enabled, ShowContent: input.ShowContent,
|
||||
SpoilerContent: input.SpoilerContent, SendAttachments: input.SendAttachments, DisableLinkPreview: input.DisableLinkPreview}
|
||||
status, err := service.UpdateOwnTelegramPreferences(ctx.UserID, preferences, req.Method == http.MethodDelete, input.Version)
|
||||
if err != nil { writeTelegramSettingsError(w, err); return }
|
||||
if err != nil {
|
||||
writeTelegramSettingsError(w, err)
|
||||
return
|
||||
}
|
||||
writeTelegramUserSettings(w, service, ctx.UserID, status)
|
||||
}
|
||||
|
||||
func TelegramBinding(ctx *appcontext.Context, w http.ResponseWriter, req *http.Request) {
|
||||
var input struct { Action string `json:"action"`; Ticket string `json:"ticket"` }
|
||||
if !decodePluginJSONBody(w, req, &input) { return }
|
||||
var input struct {
|
||||
Action string `json:"action"`
|
||||
Ticket string `json:"ticket"`
|
||||
}
|
||||
if !decodePluginJSONBody(w, req, &input) {
|
||||
return
|
||||
}
|
||||
service := telegramRequestService(ctx, w, input.Action == "begin" || input.Action == "confirm")
|
||||
if service == nil { return }
|
||||
if service == nil {
|
||||
return
|
||||
}
|
||||
actor, err := notificationBindingActor(ctx)
|
||||
if err != nil { writeTelegramSettingsError(w, pluginservice.ErrTelegramBinding); return }
|
||||
if err != nil {
|
||||
writeTelegramSettingsError(w, pluginservice.ErrTelegramBinding)
|
||||
return
|
||||
}
|
||||
var result any
|
||||
switch input.Action {
|
||||
case "begin":
|
||||
@ -102,11 +127,17 @@ func TelegramBinding(ctx *appcontext.Context, w http.ResponseWriter, req *http.R
|
||||
case "confirm":
|
||||
var status pluginservice.UserSettingsStatus
|
||||
status, err = service.ConfirmTelegramBinding(actor, input.Ticket)
|
||||
if err == nil { writeTelegramUserSettings(w, service, ctx.UserID, status); return }
|
||||
if err == nil {
|
||||
writeTelegramUserSettings(w, service, ctx.UserID, status)
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = pluginservice.TelegramNotice("不支持的绑定操作")
|
||||
}
|
||||
if err != nil { writeTelegramSettingsError(w, err); return }
|
||||
if err != nil {
|
||||
writeTelegramSettingsError(w, err)
|
||||
return
|
||||
}
|
||||
response.NewSuccessResponse(result).FPrint(w)
|
||||
}
|
||||
|
||||
@ -116,7 +147,9 @@ func TelegramCallbackSettings(ctx *appcontext.Context, w http.ResponseWriter, re
|
||||
return
|
||||
}
|
||||
service := telegramRequestService(ctx, w, req.Method == http.MethodPost)
|
||||
if service == nil { return }
|
||||
if service == nil {
|
||||
return
|
||||
}
|
||||
var status pluginservice.TelegramCallbackStatus
|
||||
var err error
|
||||
switch req.Method {
|
||||
@ -129,7 +162,10 @@ func TelegramCallbackSettings(ctx *appcontext.Context, w http.ResponseWriter, re
|
||||
http.Error(w, "请求方法不支持", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err != nil { writeTelegramSettingsError(w, err); return }
|
||||
if err != nil {
|
||||
writeTelegramSettingsError(w, err)
|
||||
return
|
||||
}
|
||||
response.NewSuccessResponse(status).FPrint(w)
|
||||
}
|
||||
|
||||
@ -138,10 +174,16 @@ func TelegramBindingCallback(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
service := hooks.PluginConfigService()
|
||||
if !config.IsSetupReady() || service == nil { http.Error(w, "服务未就绪", http.StatusServiceUnavailable); return }
|
||||
if !config.IsSetupReady() || service == nil {
|
||||
http.Error(w, "服务未就绪", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
var update pluginservice.TelegramBindingUpdate
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, req.Body, 64<<10))
|
||||
if decoder.Decode(&update) != nil { http.Error(w, "回调格式无效", http.StatusBadRequest); return }
|
||||
if decoder.Decode(&update) != nil || decoder.Decode(new(any)) != io.EOF {
|
||||
http.Error(w, "回调格式无效", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := service.ClaimTelegramBinding(req.Header.Get("X-Telegram-Bot-Api-Secret-Token"), update); err != nil {
|
||||
http.Error(w, "回调认证失败", http.StatusForbidden)
|
||||
return
|
||||
@ -151,7 +193,9 @@ func TelegramBindingCallback(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
func TestBoundTelegramMessage(ctx *appcontext.Context, w http.ResponseWriter, req *http.Request) {
|
||||
service := telegramRequestService(ctx, w, true)
|
||||
if service == nil { return }
|
||||
if service == nil {
|
||||
return
|
||||
}
|
||||
status, err := service.GetOwnTelegramSettings(ctx.UserID)
|
||||
if err != nil || !service.TelegramCanNotify(status.Settings) {
|
||||
writeTelegramSettingsError(w, pluginservice.TelegramNotice("请先绑定本人 Telegram 并启用通知"))
|
||||
@ -159,8 +203,14 @@ func TestBoundTelegramMessage(ctx *appcontext.Context, w http.ResponseWriter, re
|
||||
}
|
||||
hook, _ := hooks.GetHook("telegram_push")
|
||||
provider, ok := hook.(framework.TestMessageProvider)
|
||||
if !ok { writeTelegramSettingsError(w, pluginservice.TelegramNotice("Telegram 推送插件未运行,请联系超级管理员")); return }
|
||||
if err := service.ReserveTelegramTest(ctx.UserID); err != nil { writeTelegramSettingsError(w, err); return }
|
||||
if !ok {
|
||||
writeTelegramSettingsError(w, pluginservice.TelegramNotice("Telegram 推送插件未运行,请联系超级管理员"))
|
||||
return
|
||||
}
|
||||
if err := service.ReserveTelegramTest(ctx.UserID); err != nil {
|
||||
writeTelegramSettingsError(w, err)
|
||||
return
|
||||
}
|
||||
if provider.TestMessage(pluginservice.TelegramDeliverySettings(status.Settings)) != nil {
|
||||
writeTelegramSettingsError(w, pluginservice.TelegramNotice("测试消息发送失败,请确认没有屏蔽机器人"))
|
||||
return
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -381,6 +382,118 @@ func TestConfigServiceBulkTicketIsBoundOneTimeAndVersioned(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEveryRoleKeepsIndependentOwnTelegramBinding(t *testing.T) {
|
||||
service, engine := newPluginConfigService(t)
|
||||
var users []models.User
|
||||
for _, role := range []access.Role{access.RoleSuperAdmin, access.RoleAdmin, access.RoleUser} {
|
||||
user := insertPluginTestUser(t, engine, "telegram-own-"+string(role), role)
|
||||
users = append(users, user)
|
||||
status, err := service.GetOwnTelegramSettings(user.ID)
|
||||
if err != nil || status.Settings.ChatID != "" || status.Version != 0 {
|
||||
t.Fatalf("角色 %s 不应自动继承系统接收人:%v", role, err)
|
||||
}
|
||||
if _, err := service.UpdateOwnTelegramPreferences(user.ID, TelegramUserSettings{}, false, 0); !errors.Is(err, ErrTelegramBinding) {
|
||||
t.Fatal("未完成绑定时不应直接启用通知")
|
||||
}
|
||||
identity := TelegramUserSettings{ChatID: strconv.Itoa(10000 + user.ID), BotID: "123456", Verified: true, DisableLinkPreview: true}
|
||||
if _, err := service.UpdateOwnTelegramSettings(user.ID, identity, 0); !errors.Is(err, ErrTelegramBinding) {
|
||||
t.Fatal("任何角色都不能通过偏好设置伪造已授权身份")
|
||||
}
|
||||
if _, err := service.updateTelegramSettings(user.ID, identity, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status, err = service.UpdateOwnTelegramPreferences(user.ID, TelegramUserSettings{Disabled: true}, false, 1)
|
||||
if err != nil || status.Settings.ChatID != identity.ChatID || !status.Settings.Verified || !status.Settings.Disabled || status.Version != 2 {
|
||||
t.Fatalf("角色 %s 停用通知时应保留本人绑定:%v", role, err)
|
||||
}
|
||||
}
|
||||
if _, err := service.UpdateOwnTelegramPreferences(users[0].ID, TelegramUserSettings{}, true, 2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for index, user := range users {
|
||||
status, err := service.GetOwnTelegramSettings(user.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if index == 0 {
|
||||
if status.Settings.ChatID != "" || status.Settings.Verified || status.Version != 3 {
|
||||
t.Fatal("超级管理员应能单独解绑本人身份")
|
||||
}
|
||||
} else if status.Settings.ChatID != strconv.Itoa(10000+user.ID) || status.Version != 2 {
|
||||
t.Fatal("超级管理员解绑不得改动其他账户的身份")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramBindingTracksBotIdentityNotRuntimeAvailability(t *testing.T) {
|
||||
service, _ := newPluginConfigService(t)
|
||||
settings := TelegramUserSettings{ChatID: "456789", BotID: "123456", Verified: true}
|
||||
for index, botID := range []string{"123456", "123456", "987654"} {
|
||||
_, err := service.ChangeSecret(telegramPluginID, "botToken", config.SecretChange{
|
||||
Operation: config.SecretReplace,
|
||||
Value: &config.SecretInput{Plaintext: botID + ":" + strings.Repeat(string(rune('a'+index)), 32)},
|
||||
}, uint64(index))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := service.TelegramBindingNeedsRebind(settings); got != (index == 2) {
|
||||
t.Fatal("同一机器人轮换令牌或临时停用不应强制重绑,更换机器人必须重绑")
|
||||
}
|
||||
}
|
||||
settings.Verified = false
|
||||
if !service.TelegramBindingNeedsRebind(settings) {
|
||||
t.Fatal("旧版手填身份必须重新授权")
|
||||
}
|
||||
if service.TelegramBindingNeedsRebind(TelegramUserSettings{}) {
|
||||
t.Fatal("尚未绑定不应显示为需要重新绑定")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramBindingCancellationRemainsAccountAndSessionScoped(t *testing.T) {
|
||||
service, engine := newPluginConfigService(t)
|
||||
super := insertPluginTestUser(t, engine, "telegram-cancel-super", access.RoleSuperAdmin)
|
||||
user := insertPluginTestUser(t, engine, "telegram-cancel-user", access.RoleUser)
|
||||
actor := NotificationBindingActor{UserID: super.ID, SessionHash: strings.Repeat("a", 64),
|
||||
SessionVersion: super.SessionVersion, SecurityVersion: super.SecurityVersion}
|
||||
now := time.Now().UTC()
|
||||
service.now = func() time.Time { return now }
|
||||
service.telegramBindings = map[string]telegramBinding{"ticket": {Actor: actor,
|
||||
Status: TelegramBindingStatus{Phase: "waiting", ExpiresAt: now.Add(time.Minute)}}}
|
||||
if _, err := service.GetTelegramBinding(actor, "ticket"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
other := actor
|
||||
other.SessionHash = strings.Repeat("b", 64)
|
||||
if err := service.CancelTelegramBinding(other, "ticket"); !errors.Is(err, ErrTelegramBinding) {
|
||||
t.Fatal("同一账户的其他会话不能取消绑定")
|
||||
}
|
||||
other = NotificationBindingActor{UserID: user.ID, SessionHash: strings.Repeat("c", 64),
|
||||
SessionVersion: user.SessionVersion, SecurityVersion: user.SecurityVersion}
|
||||
if _, err := service.GetTelegramBinding(other, "ticket"); !errors.Is(err, ErrTelegramBinding) {
|
||||
t.Fatal("其他账户不能读取超级管理员的绑定请求")
|
||||
}
|
||||
if err := service.CancelTelegramBinding(other, "ticket"); !errors.Is(err, ErrTelegramBinding) {
|
||||
t.Fatal("其他账户不能取消超级管理员的绑定请求")
|
||||
}
|
||||
now = now.Add(2 * time.Minute)
|
||||
if _, err := service.GetTelegramBinding(actor, "ticket"); !errors.Is(err, ErrTelegramBinding) {
|
||||
t.Fatal("过期请求不能继续确认")
|
||||
}
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if err := service.CancelTelegramBinding(actor, "ticket"); err != nil {
|
||||
t.Fatal("本人的过期请求应能幂等取消", err)
|
||||
}
|
||||
}
|
||||
service.telegramBindings["revoked"] = telegramBinding{Actor: actor,
|
||||
Status: TelegramBindingStatus{Phase: "waiting", ExpiresAt: now.Add(time.Minute)}}
|
||||
if _, err := engine.ID(super.ID).Cols("security_version").Update(&models.User{SecurityVersion: super.SecurityVersion + 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := service.GetTelegramBinding(actor, "revoked"); !errors.Is(err, ErrTelegramBinding) {
|
||||
t.Fatal("安全版本变化必须撤销旧会话的绑定权限")
|
||||
}
|
||||
}
|
||||
|
||||
func newPluginConfigService(t *testing.T) (*ConfigService, *xorm.Engine) {
|
||||
t.Helper()
|
||||
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "plugin-config.db"))
|
||||
|
||||
@ -134,7 +134,9 @@ type TelegramCallbackStatus struct {
|
||||
}
|
||||
|
||||
func (s *ConfigService) TelegramBindingAvailability() (bool, string) {
|
||||
if _, err := s.telegramConfiguration(); err != nil { return false, err.Error() }
|
||||
if _, err := s.telegramConfiguration(); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
@ -388,12 +390,17 @@ func (s *ConfigService) TelegramBindingNeedsRebind(settings TelegramUserSettings
|
||||
if settings.ChatID == "" {
|
||||
return false
|
||||
}
|
||||
configuration, err := s.telegramConfiguration()
|
||||
return err != nil || !settings.Verified || settings.BotID != configuration.BotID
|
||||
if s == nil || s.vault == nil || s.engine == nil {
|
||||
return true
|
||||
}
|
||||
token, err := s.openSecretForPlugin(telegramPluginID, "botToken")
|
||||
parts := telegramTokenPattern.FindStringSubmatch(token)
|
||||
return err != nil || len(parts) != 2 || !settings.Verified || settings.BotID != parts[1]
|
||||
}
|
||||
|
||||
func (s *ConfigService) TelegramCanNotify(settings TelegramUserSettings) bool {
|
||||
return settings.ChatID != "" && !settings.Disabled && !s.TelegramBindingNeedsRebind(settings)
|
||||
configuration, err := s.telegramConfiguration()
|
||||
return err == nil && settings.ChatID != "" && settings.Verified && !settings.Disabled && settings.BotID == configuration.BotID
|
||||
}
|
||||
|
||||
func (s *ConfigService) UpdateOwnTelegramPreferences(userID int, preferences TelegramUserSettings, unbind bool, expectedVersion uint64) (UserSettingsStatus, error) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user