Smail/backend/controllers/plugin_telegram.go
smail 754a2578fb
Some checks are pending
Security Checks / security (push) Waiting to run
完善各角色独立通知绑定的隔离检查和回归用例
2026-09-28 22:09:15 +08:00

220 lines
7.9 KiB
Go

package controllers
import (
"encoding/json"
"io"
"net/http"
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/dto/response"
"git.pcux.cn/xuans/Smail/hooks"
"git.pcux.cn/xuans/Smail/hooks/framework"
"git.pcux.cn/xuans/Smail/services/access"
pluginservice "git.pcux.cn/xuans/Smail/services/plugin"
appcontext "git.pcux.cn/xuans/Smail/utils/context"
)
func telegramRequestService(ctx *appcontext.Context, w http.ResponseWriter, recent bool) *pluginservice.ConfigService {
if !authorizedPluginUser(ctx) {
writeConfigAccessError(w, &access.Denial{Code: access.CodeAccessDenied})
return nil
}
if recent && !ctx.RecentAuth {
writeConfigAccessError(w, &access.Denial{Code: access.CodeRecentAuthRequired})
return nil
}
service := hooks.PluginConfigService()
if service == nil {
response.NewErrorResponse(response.ServerError, "通知配置服务不可用", "").FPrint(w)
}
return service
}
func telegramPublicSettings(service *pluginservice.ConfigService, status pluginservice.UserSettingsStatus) map[string]any {
settings := status.Settings
return map[string]any{
"version": status.Version, "bound": settings.ChatID != "", "displayName": settings.DisplayName,
"needsRebind": service.TelegramBindingNeedsRebind(settings),
"enabled": settings.ChatID != "" && settings.Verified && !settings.Disabled,
"showContent": settings.ShowContent, "spoilerContent": settings.SpoilerContent,
"sendAttachments": settings.SendAttachments, "disableLinkPreview": settings.DisableLinkPreview,
}
}
func writeTelegramUserSettings(w http.ResponseWriter, service *pluginservice.ConfigService, userID int, status pluginservice.UserSettingsStatus) {
data := telegramPublicSettings(service, status)
data["synced"] = hooks.SyncTelegramUserSettings(userID, status.Settings) == nil
response.NewSuccessResponse(data).FPrint(w)
}
// 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 req.Method == http.MethodGet {
status, err := service.GetOwnTelegramSettings(ctx.UserID)
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 推送插件未运行,请联系超级管理员"
}
data["configured"], data["running"], data["available"], data["message"] = configured, running, configured && running, message
response.NewSuccessResponse(data).FPrint(w)
return
}
if req.Method != http.MethodPut && req.Method != http.MethodDelete {
w.Header().Set("Allow", "GET, PUT, DELETE")
http.Error(w, "请求方法不支持", http.StatusMethodNotAllowed)
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"`
}
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
}
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
}
service := telegramRequestService(ctx, w, input.Action == "begin" || input.Action == "confirm")
if service == nil {
return
}
actor, err := notificationBindingActor(ctx)
if err != nil {
writeTelegramSettingsError(w, pluginservice.ErrTelegramBinding)
return
}
var result any
switch input.Action {
case "begin":
if _, running := hooks.GetHook("telegram_push"); !running {
writeTelegramSettingsError(w, pluginservice.TelegramNotice("Telegram 推送插件未运行,请联系超级管理员"))
return
}
result, err = service.BeginTelegramBinding(req.Context(), actor)
case "status":
result, err = service.GetTelegramBinding(actor, input.Ticket)
case "cancel":
err = service.CancelTelegramBinding(actor, input.Ticket)
case "confirm":
var status pluginservice.UserSettingsStatus
status, err = service.ConfirmTelegramBinding(actor, input.Ticket)
if err == nil {
writeTelegramUserSettings(w, service, ctx.UserID, status)
return
}
default:
err = pluginservice.TelegramNotice("不支持的绑定操作")
}
if err != nil {
writeTelegramSettingsError(w, err)
return
}
response.NewSuccessResponse(result).FPrint(w)
}
func TelegramCallbackSettings(ctx *appcontext.Context, w http.ResponseWriter, req *http.Request) {
if ctx == nil || ctx.UserID <= 0 || ctx.EffectiveRole() != access.RoleSuperAdmin {
writeConfigAccessError(w, &access.Denial{Code: access.CodeAccessDenied})
return
}
service := telegramRequestService(ctx, w, req.Method == http.MethodPost)
if service == nil {
return
}
var status pluginservice.TelegramCallbackStatus
var err error
switch req.Method {
case http.MethodGet:
status, err = service.TelegramCallbackState(req.Context())
case http.MethodPost:
status, err = service.ConfigureTelegramCallback(req.Context(), ctx.UserID)
default:
w.Header().Set("Allow", "GET, POST")
http.Error(w, "请求方法不支持", http.StatusMethodNotAllowed)
return
}
if err != nil {
writeTelegramSettingsError(w, err)
return
}
response.NewSuccessResponse(status).FPrint(w)
}
// TelegramBindingCallback 不使用网页登录会话,只接受带独立回调密钥的 Telegram 请求。
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
}
var update pluginservice.TelegramBindingUpdate
decoder := json.NewDecoder(http.MaxBytesReader(w, req.Body, 64<<10))
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
}
w.WriteHeader(http.StatusNoContent)
}
func TestBoundTelegramMessage(ctx *appcontext.Context, w http.ResponseWriter, req *http.Request) {
service := telegramRequestService(ctx, w, true)
if service == nil {
return
}
status, err := service.GetOwnTelegramSettings(ctx.UserID)
if err != nil || !service.TelegramCanNotify(status.Settings) {
writeTelegramSettingsError(w, pluginservice.TelegramNotice("请先绑定本人 Telegram 并启用通知"))
return
}
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 provider.TestMessage(pluginservice.TelegramDeliverySettings(status.Settings)) != nil {
writeTelegramSettingsError(w, pluginservice.TelegramNotice("测试消息发送失败,请确认没有屏蔽机器人"))
return
}
response.NewSuccessResponse(nil).FPrint(w)
}