Smail/backend/controllers/plugin_wechat.go
smail 4a927a32a3
Some checks are pending
Security Checks / security (push) Waiting to run
优化微信授权结果页的移动端布局与安全提示
2026-10-03 21:07:19 +08:00

211 lines
7.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package controllers
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"net/http"
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/controllers/wechatcallback"
"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"
"git.pcux.cn/xuans/Smail/session"
appcontext "git.pcux.cn/xuans/Smail/utils/context"
)
func weChatHook() framework.EmailHook {
for _, name := range []string{"wechat_push", "WeChatPushHook", "WechatPush"} {
if hook, found := hooks.GetHook(name); found {
return hook
}
}
return nil
}
func notificationBindingActor(ctx *appcontext.Context) (pluginservice.NotificationBindingActor, error) {
if !authorizedPluginUser(ctx) || session.Instance == nil || ctx.Context == nil {
return pluginservice.NotificationBindingActor{}, pluginservice.ErrWeChatBinding
}
token := session.Instance.Token(ctx)
if token == "" {
return pluginservice.NotificationBindingActor{}, pluginservice.ErrWeChatBinding
}
hash := sha256.Sum256([]byte(token))
return pluginservice.NotificationBindingActor{UserID: ctx.UserID, SessionVersion: ctx.SessionVersion,
SecurityVersion: ctx.SecurityVersion, SessionHash: hex.EncodeToString(hash[:])}, nil
}
func weChatRequestService(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 weChatPublicSettings(status pluginservice.WeChatUserSettingsStatus) map[string]any {
return map[string]any{"version": status.Version, "bound": status.Settings.OpenID != "", "enabled": status.Settings.Enabled,
"showContent": status.Settings.ShowContent, "displayName": status.Settings.DisplayName,
"needsRebind": status.NeedsRebind}
}
func writeWeChatSettings(w http.ResponseWriter, userID int, status pluginservice.WeChatUserSettingsStatus) {
data := weChatPublicSettings(status)
data["synced"] = hooks.SyncWeChatUserSettings(userID, status.Settings) == nil
response.NewSuccessResponse(data).FPrint(w)
}
func WeChatUserSettings(ctx *appcontext.Context, w http.ResponseWriter, req *http.Request) {
service := weChatRequestService(ctx, w, req.Method != http.MethodGet)
if service == nil {
return
}
if req.Method == http.MethodGet {
status, err := service.GetOwnWeChatSettings(ctx.UserID)
if err != nil {
writeWeChatError(w, err)
return
}
available, message := service.WeChatBindingAvailability()
running := weChatHook() != nil
if available && !running {
message = "微信推送插件未运行,请联系超级管理员"
}
data := weChatPublicSettings(status)
data["available"], data["configured"], data["running"], data["message"] = available && running, available, 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 {
Enabled bool `json:"enabled"`
ShowContent bool `json:"showContent"`
Version uint64 `json:"version"`
}
if !decodePluginJSONBody(w, req, &input) {
return
}
status, err := service.UpdateOwnWeChatPreferences(ctx.UserID, input.Enabled, input.ShowContent, req.Method == http.MethodDelete, input.Version)
if err != nil {
writeWeChatError(w, err)
return
}
writeWeChatSettings(w, ctx.UserID, status)
}
func WeChatBinding(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 := weChatRequestService(ctx, w, input.Action == "begin" || input.Action == "confirm")
if service == nil {
return
}
actor, err := notificationBindingActor(ctx)
if err != nil {
writeWeChatError(w, err)
return
}
var result any
switch input.Action {
case "begin":
if weChatHook() == nil {
writeWeChatError(w, pluginservice.WeChatNotice("微信推送插件未运行,请联系超级管理员"))
return
}
result, err = service.BeginWeChatBinding(actor)
case "status":
result, err = service.GetWeChatBinding(actor, input.Ticket)
case "cancel":
err = service.CancelWeChatBinding(actor, input.Ticket)
case "confirm":
var status pluginservice.WeChatUserSettingsStatus
status, err = service.ConfirmWeChatBinding(actor, input.Ticket)
if err == nil {
writeWeChatSettings(w, ctx.UserID, status)
return
}
default:
err = pluginservice.WeChatNotice("绑定操作无效")
}
if err != nil {
writeWeChatError(w, err)
return
}
response.NewSuccessResponse(result).FPrint(w)
}
// WeChatBindingCallback 不依赖手机浏览器的登录 Cookie,也不自动完成绑定。
func WeChatBindingCallback(w http.ResponseWriter, req *http.Request) {
service := hooks.PluginConfigService()
if !config.IsSetupReady() || service == nil || service.ClaimWeChatBinding(req.Context(), req.URL.Query().Get("state"), req.URL.Query().Get("code")) != nil {
wechatcallback.Write(w, false)
return
}
wechatcallback.Write(w, true)
}
func TestWeChatMessage(ctx *appcontext.Context, w http.ResponseWriter, req *http.Request) {
service := weChatRequestService(ctx, w, true)
if service == nil {
return
}
status, err := service.GetOwnWeChatSettings(ctx.UserID)
if err != nil || status.Settings.OpenID == "" || status.NeedsRebind {
writeWeChatError(w, pluginservice.WeChatNotice("请先绑定本人的微信"))
return
}
provider, ok := weChatHook().(framework.TestMessageProvider)
if !ok {
writeWeChatError(w, pluginservice.WeChatNotice("微信推送插件未运行,请联系超级管理员"))
return
}
if err := service.ReserveWeChatTest(ctx.UserID); err != nil {
writeWeChatError(w, err)
return
}
payload, err := json.Marshal(status.Settings)
if err != nil || provider.TestMessage(payload) != nil {
writeWeChatError(w, pluginservice.WeChatNotice("测试消息发送失败,请确认已关注公众号及模板配置正确"))
return
}
response.NewSuccessResponse(nil).FPrint(w)
}
func writeWeChatError(w http.ResponseWriter, err error) {
message := "微信通知操作失败,请稍后重试"
if errors.Is(err, pluginservice.ErrPluginSettingConflict) {
message = "设置已被其他操作更新,请刷新后重试"
} else if errors.Is(err, pluginservice.ErrWeChatBinding) {
message = pluginservice.ErrWeChatBinding.Error()
} else {
// 只公开固定业务提示,避免数据库错误携带内部路径、查询或连接信息。
var notice pluginservice.WeChatNotice
if errors.As(err, &notice) {
message = notice.Error()
}
}
response.NewErrorResponse(response.ParamsError, message, "").FPrint(w)
}