diff --git a/dice/dice.go b/dice/dice.go index 9fc4ca89..aee72f9c 100644 --- a/dice/dice.go +++ b/dice/dice.go @@ -25,6 +25,7 @@ import ( "sealdice-core/dice/logger" "sealdice-core/dice/model" log "sealdice-core/utils/kratos" + "sealdice-core/utils/public_dice" ) type CmdExecuteResult struct { @@ -183,12 +184,15 @@ type Dice struct { CensorManager *CensorManager `json:"-" yaml:"-"` - Config Config `json:"-" yaml:"-"` - AttrsManager *AttrsManager `json:"-" yaml:"-"` + Config Config `json:"-" yaml:"-"` + AdvancedConfig AdvancedConfig `json:"-" yaml:"-"` + PublicDice *public_dice.PublicDiceClient `json:"-" yaml:"-"` + PublicDiceTimerId cron.EntryID `json:"-" yaml:"-"` + ContainerMode bool `yaml:"-" json:"-"` // 容器模式:禁用内置适配器,不允许使用内置Lagrange和旧的内置Gocq IsAlreadyLoadConfig bool `yaml:"-"` // 如果在loads前崩溃,那么不写入配置,防止覆盖为空的 @@ -263,6 +267,8 @@ func (d *Dice) Init() { d.NewCensorManager() } + go d.PublicDiceSetup() + // 创建js运行时 if d.Config.JsEnable { d.Logger.Info("js扩展支持:开启") @@ -719,3 +725,96 @@ func (d *Dice) ResetQuitInactiveCron() { d.Logger.Infof("退群功能已启动,每 %s 执行一次退群判定", duration.String()) } } + +func (d *Dice) PublicDiceEndpointRefresh() { + cfg := &d.Config.PublicDiceConfig + + var endpointItems []*public_dice.Endpoint + for _, i := range d.ImSession.EndPoints { + if !i.IsPublic { + continue + } + endpointItems = append(endpointItems, &public_dice.Endpoint{ + Platform: i.Platform, + UID: i.UserID, + IsOnline: i.State == 1, + }) + } + + _, code := d.PublicDice.EndpointUpdate(&public_dice.EndpointUpdateRequest{ + DiceID: cfg.ID, + Endpoints: endpointItems, + }, GenerateVerificationKeyForPublicDice) + if code != 200 { + log.Warn("[公骰]无法通过服务器校验,不再进行更新") + return + } +} + +func (d *Dice) PublicDiceInfoRegister() { + cfg := &d.Config.PublicDiceConfig + + pd, code := d.PublicDice.Register(&public_dice.RegisterRequest{ + ID: cfg.ID, + Name: cfg.Name, + Brief: cfg.Brief, + Note: cfg.Note, + }, GenerateVerificationKeyForPublicDice) + if code != 200 { + log.Warn("[公骰]无法通过服务器校验,不再进行骰号注册") + return + } + // 两种可能: 1. 原本ID为空 2. ID 无效,这里会自动变成新的 + if pd.Item.ID != "" && cfg.ID != pd.Item.ID { + cfg.ID = pd.Item.ID + } +} + +func (d *Dice) PublicDiceSetupTick() { + cfg := &d.Config.PublicDiceConfig + + doTickUpdate := func() { + if !cfg.Enable { + d.Cron.Remove(d.PublicDiceTimerId) + return + } + var tickEndpointItems []*public_dice.TickEndpoint + for _, i := range d.ImSession.EndPoints { + if !i.IsPublic { + continue + } + tickEndpointItems = append(tickEndpointItems, &public_dice.TickEndpoint{ + UID: i.UserID, + IsOnline: i.State == 1, + }) + } + d.PublicDice.TickUpdate(&public_dice.TickUpdateRequest{ + ID: cfg.ID, + Endpoints: tickEndpointItems, + }, GenerateVerificationKeyForPublicDice) + } + + if d.PublicDiceTimerId != 0 { + d.Cron.Remove(d.PublicDiceTimerId) + } + + go func() { + // 20s后进行第一次调用,此后3min进行一次更新 + time.Sleep(20 * time.Second) + doTickUpdate() + }() + + d.PublicDiceTimerId, _ = d.Cron.AddFunc("@every 3m", doTickUpdate) +} + +func (d *Dice) PublicDiceSetup() { + d.PublicDice = public_dice.NewClient("https://dice.weizaima.com", "") + + cfg := &d.Config.PublicDiceConfig + if !cfg.Enable { + return + } + d.PublicDiceInfoRegister() + d.PublicDiceEndpointRefresh() + d.PublicDiceSetupTick() +} diff --git a/dice/dice_config.go b/dice/dice_config.go index 3a40c26d..3ba0330d 100644 --- a/dice/dice_config.go +++ b/dice/dice_config.go @@ -41,6 +41,8 @@ type Config struct { NewsConfig `yaml:",inline"` // 敏感词设置 CensorConfig `yaml:",inline"` + // 公骰设置 + PublicDiceConfig `yaml:",inline"` // 其它设置,包含由于被导出无法从 Dice 上迁移过来的配置项,为了在 DefaultConfig 上统一设置默认值增加此结构 DirtyConfig `yaml:",inline"` @@ -223,6 +225,15 @@ type NewsConfig struct { NewsMark string `json:"newsMark" yaml:"newsMark"` // 已读新闻的md5 } +type PublicDiceConfig struct { + Enable bool `json:"publicDiceEnable" yaml:"publicDiceEnable"` + ID string `json:"publicDiceId" yaml:"publicDiceId"` + Name string `json:"publicDiceName" yaml:"publicDiceName"` + Brief string `json:"publicDiceBrief" yaml:"publicDiceBrief"` + Note string `json:"publicDiceNote" yaml:"publicDiceNote"` + Avatar string `json:"publicDiceAvatar" yaml:"publicDiceAvatar"` +} + type CensorConfig struct { EnableCensor bool `json:"enableCensor" yaml:"enableCensor"` // 启用敏感词审查 CensorMode CensorMode `json:"censorMode" yaml:"censorMode"` diff --git a/dice/dice_config_default.go b/dice/dice_config_default.go index d7619f09..92bbef6a 100644 --- a/dice/dice_config_default.go +++ b/dice/dice_config_default.go @@ -97,6 +97,9 @@ var DefaultConfig = Config{ CensorMatchPinyin: false, CensorFilterRegexStr: "", }, + PublicDiceConfig{ + Enable: false, + }, DirtyConfig{ DeckList: nil, CommandPrefix: []string{ diff --git a/dice/verify.go b/dice/verify.go index b8a9b9eb..7ce89937 100644 --- a/dice/verify.go +++ b/dice/verify.go @@ -1,6 +1,7 @@ package dice import ( + "crypto/sha256" "encoding/base64" "fmt" "os" @@ -71,3 +72,37 @@ func GenerateVerificationCode(platform string, userID string, username string, u return fmt.Sprintf("SEAL%%%s", base2048.DefaultEncoding.EncodeToString(dp)) } } + +type payloadPublicDice struct { + Version string `msgpack:"version,omitempty"` + Sign []byte `msgpack:"sign,omitempty"` +} + +func GenerateVerificationKeyForPublicDice(data any) string { + doEcdsaSign := len(SealTrustedClientPrivateKey) > 0 + pp, _ := msgpack.Marshal(data) + + var sign []byte + if doEcdsaSign { + var err error + sign, err = crypto.EcdsaSignRow(pp, SealTrustedClientPrivateKey) + if err != nil { + return "" + } + } else { + h := sha256.New() + h.Write(pp) + sign = h.Sum(nil) + } + + d := payloadPublicDice{ + Version: VERSION.String(), + Sign: sign, + } + + dp, _ := msgpack.Marshal(d) + if doEcdsaSign { + return fmt.Sprintf("SEAL#%s", base64.StdEncoding.EncodeToString(dp)) + } + return fmt.Sprintf("SEAL~%s", base64.StdEncoding.EncodeToString(dp)) +} diff --git a/utils/public_dice/sdk.go b/utils/public_dice/sdk.go new file mode 100644 index 00000000..55e9e741 --- /dev/null +++ b/utils/public_dice/sdk.go @@ -0,0 +1,165 @@ +package public_dice + +import ( + "github.com/monaco-io/request" +) + +// PublicDiceClient SDK客户端 +type PublicDiceClient struct { + baseURL string + token string +} + +// NewClient 创建新的SDK客户端 +func NewClient(baseURL string, token string) *PublicDiceClient { + return &PublicDiceClient{ + baseURL: baseURL, + token: token, + } +} + +// doReq 发送HTTP请求 +func doReq[T any](c *PublicDiceClient, method string, path string, data any, params map[string]string) (*T, int) { + req := request.Client{ + URL: c.baseURL + path, + Method: method, + JSON: data, + Query: params, + } + + var result T + resp := req.Send() + resp.Scan(&result) + + return &result, resp.Code() +} + +// Endpoint 公骰终端信息 +type Endpoint struct { + Platform string `json:"platform" msgpack:",omitempty"` + UID string `json:"uid" msgpack:",omitempty"` + InviteURL string `json:"inviteUrl" msgpack:",omitempty"` + IsOnline bool `json:"isOnline" msgpack:",omitempty"` + + ID string `json:"id" msgpack:",omitempty"` + CreatedAt string `json:"createdAt" msgpack:",omitempty"` + UpdatedAt string `json:"updatedAt" msgpack:",omitempty"` + LastTickTime int64 `json:"lastTickTime" msgpack:",omitempty"` +} + +// DiceInfo 公骰信息 +type DiceInfo struct { + ID string `json:"id" msgpack:",omitempty"` + CreatedAt string `json:"createdAt" msgpack:",omitempty"` + UpdatedAt string `json:"updatedAt" msgpack:",omitempty"` + Name string `json:"name" msgpack:",omitempty"` + Brief string `json:"brief" msgpack:",omitempty"` + Note string `json:"note" msgpack:",omitempty"` + Avatar string `json:"avatar" msgpack:",omitempty"` + Version string `json:"version" msgpack:",omitempty"` + IsOfficialVersion bool `json:"isOfficialVersion" msgpack:",omitempty"` + UpdateTickCount int `json:"updateTickCount" msgpack:",omitempty"` + LastTickTime int64 `json:"lastTickTime" msgpack:",omitempty"` + Endpoints []*Endpoint `json:"endpoints" msgpack:",omitempty"` +} + +// ListResponse 公骰列表响应 +type ListResponse struct { + Items []*DiceInfo `json:"items"` +} + +// ListGet 获取公骰列表 +func (c *PublicDiceClient) ListGet(keyFunc func(data any) string) (*ListResponse, int) { + if keyFunc != nil { + data := keyFunc(nil) + return doReq[ListResponse](c, "GET", "/dice/api/public-dice/list", data, nil) + } + return doReq[ListResponse](c, "GET", "/dice/api/public-dice/list", nil, nil) +} + +// RegisterRequest 注册公骰请求 +type RegisterRequest struct { + ID string `json:"ID,omitempty" msgpack:",omitempty"` + Name string `json:"name,omitempty" msgpack:",omitempty"` // 15字 + Brief string `json:"brief,omitempty" msgpack:",omitempty"` + Note string `json:"note,omitempty" msgpack:",omitempty"` + Avatar string `json:"avatar,omitempty" msgpack:",omitempty"` // 头像?还是用另一个api进行注册比较好?可以省略 + Key string `json:"key,omitempty" msgpack:",omitempty"` +} + +// RegisterResponse 注册公骰响应 +type RegisterResponse struct { + Item DiceInfo `json:"item"` +} + +// Register 注册公骰 +func (c *PublicDiceClient) Register(req *RegisterRequest, keyFunc func(data any) string) (*RegisterResponse, int) { + if keyFunc != nil { + req.Key = keyFunc(req) + } + return doReq[RegisterResponse](c, "POST", "/dice/api/public-dice/register", req, nil) +} + +// DiceUpdateRequest 更新公骰请求 +type DiceUpdateRequest struct { + ID string `json:"id" msgpack:",omitempty"` + Name string `json:"name" msgpack:",omitempty"` + Brief string `json:"brief" msgpack:",omitempty"` + Note string `json:"note" msgpack:",omitempty"` + Key string `json:"key" msgpack:",omitempty"` +} + +// DiceUpdateResponse 更新公骰响应 +type DiceUpdateResponse struct { + Updated int `json:"updated"` +} + +// DiceUpdate 更新公骰 +func (c *PublicDiceClient) DiceUpdate(req *DiceUpdateRequest, keyFunc func(data any) string) (*DiceUpdateResponse, int) { + if keyFunc != nil { + req.Key = keyFunc(req) + } + return doReq[DiceUpdateResponse](c, "POST", "/dice/api/public-dice/register?update=1", req, nil) +} + +// EndpointUpdateRequest 更新公骰SNS账号信息请求 +type EndpointUpdateRequest struct { + DiceID string `json:"diceId" msgpack:",omitempty"` + Key string `json:"key" msgpack:",omitempty"` + Endpoints []*Endpoint `json:"endpoints" msgpack:",omitempty"` +} + +// EndpointUpdateResponse 更新公骰SNS账号信息响应 +type EndpointUpdateResponse struct{} + +// EndpointUpdate 更新公骰SNS账号信息 +func (c *PublicDiceClient) EndpointUpdate(req *EndpointUpdateRequest, keyFunc func(data any) string) (*EndpointUpdateResponse, int) { + if keyFunc != nil { + req.Key = keyFunc(req) + } + return doReq[EndpointUpdateResponse](c, "POST", "/dice/api/public-dice/endpoint-update", req, nil) +} + +// TickUpdateRequest 更新公骰心跳请求 +type TickUpdateRequest struct { + ID string `json:"ID" msgpack:",omitempty"` + Key string `json:"key" msgpack:",omitempty"` + Endpoints []*TickEndpoint `json:"Endpoints" msgpack:",omitempty"` +} + +// TickEndpoint 公骰心跳端点信息 +type TickEndpoint struct { + UID string `json:"uid" msgpack:",omitempty"` + IsOnline bool `json:"isOnline" msgpack:",omitempty"` +} + +// TickUpdateResponse 更新公骰心跳响应 +type TickUpdateResponse struct{} + +// TickUpdate 更新公骰心跳 +func (c *PublicDiceClient) TickUpdate(req *TickUpdateRequest, keyFunc func(data any) string) (*TickUpdateResponse, int) { + if keyFunc != nil { + req.Key = keyFunc(req) + } + return doReq[TickUpdateResponse](c, "POST", "/dice/api/public-dice/tick-update", req, nil) +}