Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: V2版本API重构-登录/base模块 #1096

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions api/v2/bind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package v2

import (
"runtime"

"github.com/labstack/echo/v4"

"sealdice-core/api/v2/middleware"
"sealdice-core/dice"
)

var ApiGroupApp *ApiGroup

// Dice 由于现在还不能将配置拆出,导致middleware中间件没法正经做,只能在这里保存一下Dice进行控制。
var diceInstance *dice.Dice

// 统一格式:前带/后不带/。

// InitBaseRouter 初始化基础路由
func InitBaseRouter(router *echo.Group) {
publicRouter := router.Group("/base")
baseApi := ApiGroupApp.SystemApiGroup.BaseApi
{
publicRouter.GET("/preinfo", baseApi.PreInfo)
publicRouter.GET("/baseInfo", baseApi.BaseInfo, middleware.AuthMiddleware(diceInstance))
publicRouter.GET("/heartbeat", baseApi.HeartBeat, middleware.AuthMiddleware(diceInstance))
publicRouter.GET("/checkSecurity", baseApi.CheckSecurity, middleware.AuthMiddleware(diceInstance))
// 安卓专属停止代码
if runtime.GOOS == "android" {
publicRouter.GET("/force_stop", baseApi.ForceStop, middleware.AuthMiddleware(diceInstance))
}
}
}

func InitLoginRouter(router *echo.Group) {
publicRouter := router.Group("/login")
loginApi := ApiGroupApp.SystemApiGroup.LoginApi
{
publicRouter.POST("/signin", loginApi.DoSignIn)
publicRouter.GET("/salt", loginApi.DoSignInGetSalt)
}
}

func InitRouter(router *echo.Echo, dice *dice.Dice) {
diceInstance = dice
ApiGroupApp = InitSealdiceAPIV2(dice)
group := router.Group("/v2/sd-api")
InitBaseRouter(group)
InitLoginRouter(group)
}
19 changes: 19 additions & 0 deletions api/v2/enter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package v2

import (
"sealdice-core/api/v2/ui"
"sealdice-core/dice"
)

// 有概率以后支持WEBHOOK等等和UI无关代码
type ApiGroup struct {
SystemApiGroup ui.WebUIDiceGroup
}

// InitSealdiceAPIV2 初始化SealdiceAPI V2
// 先初始化,然后再执行router绑定的代码,这样的好处是任何一块代码都可插拔。
func InitSealdiceAPIV2(d *dice.Dice) *ApiGroup {
a := new(ApiGroup)
a.SystemApiGroup.Init(d)
return a
}
34 changes: 34 additions & 0 deletions api/v2/middleware/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package middleware

import (
"github.com/labstack/echo/v4"

"sealdice-core/dice"
"sealdice-core/utils/web/response"
)

// AuthMiddleware 鉴权中间件,接收Dice参数,仅允许可用Token.
func AuthMiddleware(d *dice.Dice) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
authToken := c.Request().Header.Get("Authorization")
// 这里统一使用Dice的目的是,以后考虑直接把DiceManager扬了,改单例模式
if d.Parent.AccessTokens[authToken] {
return next(c)
}
return response.NoAuth(c)
}
}
}

func TestModeMiddleware(d *dice.Dice) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if d.Parent.JustForTest {
// TODO:补充更多细节,比如”展示模式不能进行xxx操作“
return response.FailWithMessage("展示模式,无法进行……", c)
}
return next(c)
}
}
}
5 changes: 5 additions & 0 deletions api/v2/model/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# model

此处存放可能用在API里的所有对应结构。

目前考量:除非有必要,否则尽量不把复杂返回结构体写在函数里,否则根本没法看。
30 changes: 30 additions & 0 deletions api/v2/model/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package model

// VersionDetail 版本号详细信息
type VersionDetail struct {
Major uint64 `json:"major"`
Minor uint64 `json:"minor"`
Patch uint64 `json:"patch"`
Prerelease string `json:"prerelease"`
BuildMetaData string `json:"buildMetaData"`
}

type BaseInfoResponse struct {
AppName string `json:"appName"`
AppChannel string `json:"appChannel"`
Version string `json:"version"`
VersionSimple string `json:"versionSimple"`
VersionDetail VersionDetail `json:"versionDetail"`
VersionNew string `json:"versionNew"`
VersionNewNote string `json:"versionNewNote"`
VersionCode int64 `json:"versionCode"`
VersionNewCode int64 `json:"versionNewCode"`
MemoryAlloc uint64 `json:"memoryAlloc"`
Uptime int64 `json:"uptime"`
MemoryUsedSys uint64 `json:"memoryUsedSys"`
ExtraTitle string `json:"extraTitle"`
OS string `json:"OS"`
Arch string `json:"arch"`
JustForTest bool `json:"justForTest"`
ContainerMode bool `json:"containerMode"`
}
248 changes: 248 additions & 0 deletions api/v2/ui/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package ui

import (
"os"
"runtime"
"runtime/debug"
"strings"
"time"

"github.com/jmoiron/sqlx"
"github.com/labstack/echo/v4"

"sealdice-core/api/v2/model"
"sealdice-core/dice"
"sealdice-core/utils/web/response"
)

// TODO: 注释现在不一定对的上地址,这个还得接着改喵

var startTime = time.Now().Unix()

type fStopEcho struct {
Key string `json:"key"`
}

// BaseApi
// 曾经这里直接嵌入了Dice,但是后来考虑到这个东西应该是和BaseApi分开
// BaseApi不应该能用Dice的方法(而是通过方法控制Dice)
type BaseApi struct {
dice *dice.Dice
}

// PreInfo
// @Tags Base
// @Summary 获取测试模式信息
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Result{data=map[string]interface{},msg=string} "返回测试模式信息"
// @Router /base/preInfo [post]
func (b *BaseApi) PreInfo(c echo.Context) error {
return response.OkWithData(map[string]interface{}{
"testMode": b.dice.Parent.JustForTest,
}, c)
}

// BaseInfo
// @Tags Base
// @Summary 获取基础信息
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Result{data=model.BaseInfoResponse,msg=string} "返回基础信息,包括应用名称、版本、内存使用等"
// @Router /base/baseinfo [get]
func (b *BaseApi) BaseInfo(c echo.Context) error {
// 鉴权后使用
dm := b.dice.Parent
var m runtime.MemStats
runtime.ReadMemStats(&m)

var versionNew string
var versionNewNote string
var versionNewCode int64
if dm.AppVersionOnline != nil {
versionNew = dm.AppVersionOnline.VersionLatestDetail
versionNewNote = dm.AppVersionOnline.VersionLatestNote
versionNewCode = dm.AppVersionOnline.VersionLatestCode
}

extraTitle := b.getName()

versionDetail := model.VersionDetail{
Major: dice.VERSION.Major(),
Minor: dice.VERSION.Minor(),
Patch: dice.VERSION.Patch(),
Prerelease: dice.VERSION.Prerelease(),
BuildMetaData: dice.VERSION.Metadata(),
}
infoResponse := model.BaseInfoResponse{
AppName: dice.APPNAME,
AppChannel: dice.APP_CHANNEL,
Version: dice.VERSION.String(),
VersionSimple: dice.VERSION_MAIN + dice.VERSION_PRERELEASE,
VersionDetail: versionDetail,
VersionNew: versionNew,
VersionNewNote: versionNewNote,
VersionCode: dice.VERSION_CODE,
VersionNewCode: versionNewCode,
MemoryAlloc: m.Alloc,
MemoryUsedSys: m.Sys - m.HeapReleased,
Uptime: time.Now().Unix() - startTime,
ExtraTitle: extraTitle,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
JustForTest: dm.JustForTest,
ContainerMode: dm.ContainerMode,
}

return response.OkWithData(infoResponse, c)
}

// ForceStop
// @Tags Base
// @Summary 安卓专属:强制停止程序
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param key body fStopEcho true "强制停止的密钥"
// @Success 200 {object} response.Result{msg=string} "执行成功"
// @Failure 400 {object} response.Result{msg=string} "参数绑定错误/执行错误/Key不匹配等等"
// @Router /base/force-stop [post]
func (b *BaseApi) ForceStop(c echo.Context) error {
// 此处不再判断是否为安卓,直接控制若是安卓再注册对应API即可
defer b.cleanupAndExit()
// this is a dangerous api, so we need to check the key
haskey := false
for _, s := range os.Environ() {
if strings.HasPrefix(s, "FSTOP_KEY=") {
key := strings.Split(s, "=")[1]
v := fStopEcho{}
err := c.Bind(&v)
if err != nil {
return response.FailWithMessage(response.GetGenericErrorMsg(err), c)
}
if v.Key == key {
haskey = true
break
} else {
return response.FailWithMessage("检查到FSTOP_KEY不对应,停止执行", c)
}
}
}
if !haskey {
return response.FailWithMessage("检查到环境中不含有FSTOP_KEY,停止执行", c)
}
return response.OkWithMessage("执行成功", c)
}

// HeartBeat
// @Tags Base
// @Summary 心跳检测
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Result{msg=string} "返回心跳检测信息"
// @Router /base/heartbeat [get]
func (b *BaseApi) HeartBeat(c echo.Context) error {
// 需要鉴权
return response.OkWithMessage("HEARTBEATS", c)
}

// CheckSecurity
// @Tags Base
// @Summary 检查是否需要进行安全提醒
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Result{data=map[string]bool,msg=string} "返回安全检查结果"
// @Router /base/security/check [get]
func (b *BaseApi) CheckSecurity(c echo.Context) error {
// 需要鉴权
isPublicService := strings.HasPrefix(b.dice.Parent.ServeAddress, "0.0.0.0") || b.dice.Parent.ServeAddress == ":3211"
isEmptyPassword := b.dice.Parent.UIPasswordHash == ""
return response.OkWithData(map[string]bool{
"isOk": !(isEmptyPassword && isPublicService),
}, c)
}

// 工具函数
// cleanupAndExit 清理资源并退出程序
func (b *BaseApi) cleanupAndExit() {
logger := b.dice.Logger
logger.Info("程序即将退出,进行清理……")

defer func() {
if err := recover(); err != nil {
logger.Errorf("异常: %v\n堆栈: %v", err, string(debug.Stack()))
}
}()
// TODO:已经是单例模式了,这里其实不需要这么套娃
for _, i := range b.dice.Parent.Dice {
if i.IsAlreadyLoadConfig {
i.Config.BanList.SaveChanged(i)
i.AttrsManager.CheckForSave()
i.Save(true)

// 关闭存储
for _, j := range i.ExtList {
if j.Storage != nil {
if err := j.StorageClose(); err != nil {
logger.Errorf("异常: %v\n堆栈: %v", err, string(debug.Stack()))
}
}
}
i.IsAlreadyLoadConfig = false
}

b.closeDBConnections()
}

// 清理gocqhttp
for _, i := range b.dice.Parent.Dice {
if i.ImSession != nil && i.ImSession.EndPoints != nil {
for _, j := range i.ImSession.EndPoints {
dice.BuiltinQQServeProcessKill(i, j)
}
}
}

if b.dice.Parent.Help != nil {
b.dice.Parent.Help.Close()
}
if b.dice.Parent.IsReady {
b.dice.Parent.Save()
}
if b.dice.Parent.Cron != nil {
b.dice.Parent.Cron.Stop()
}
logger.Info("清理完成,程序即将退出")
os.Exit(0) //nolint:gocritic
}

// closeDBConnections 关闭数据库连接
func (b *BaseApi) closeDBConnections() {
diceInstance := b.dice
closeConnection := func(db *sqlx.DB) {
if db != nil {
_ = db.Close()
}
}

closeConnection(diceInstance.DBData)
closeConnection(diceInstance.DBLogs)

if cm := diceInstance.CensorManager; cm != nil {
closeConnection(cm.DB)
}
}

func (b *BaseApi) getName() string {
defer func() {
// 防止报错
_ = recover()
}()

ctx := &dice.MsgContext{Dice: b.dice, EndPoint: nil, Session: b.dice.ImSession}
return dice.DiceFormatTmpl(ctx, "核心:骰子名字")
}
Loading