1
0
mirror of https://github.com/fumiama/NanoBot.git synced 2026-06-16 08:20:31 +08:00

feat: add v2 login method

This commit is contained in:
源文雨
2023-11-12 15:56:40 +09:00
parent c9f27c6514
commit 57b0a7c52c
4 changed files with 171 additions and 50 deletions

118
bot.go
View File

@@ -25,9 +25,10 @@ var (
// Bot 一个机器人实例的配置
type Bot struct {
AppID string `yaml:"AppID"` // AppID is BotAppID开发者ID
Token string `yaml:"Token"` // Token is 机器人令牌
Secret string `yaml:"Secret"` // Secret is 机器人密钥
AppID string `yaml:"AppID"` // AppID is BotAppID开发者ID
Token string `yaml:"Token"` // Token is 机器人令牌 有 Secret 则使用新版 API
token string // token 是通过 secret 获得的残血 token
Secret string `yaml:"Secret"` // Secret is 机器人令牌 V2 (AppSecret/ClientSecret) 沙盒目前虽然能登录但无法收发消息
SuperUsers []string `yaml:"SuperUsers"` // SuperUsers 超级用户
Timeout time.Duration `yaml:"Timeout"` // Timeout is API 调用超时
Handler *Handler `yaml:"-"` // Handler 注册对各种事件的处理
@@ -40,10 +41,12 @@ type Bot struct {
gateway string // gateway 获得的网关
seq uint32 // seq 最新的 s
heartbeat uint32 // heartbeat 心跳周期, 单位毫秒
expiresec int64 // expiresec Token 有效时间
handlers map[string]eventHandlerType // handlers 方便调用的 handler
mu sync.Mutex // 写锁
conn *websocket.Conn // conn 目前的 wss 连接
hbonce sync.Once // hbonce 保证仅执行一次 heartbeat
exonce sync.Once // exonce 保证仅执行一次刷新 token
client *http.Client // client 主要配置 timeout
ready EventReady // ready 连接成功后下发的 bot 基本信息
@@ -55,33 +58,37 @@ func (ctx *Ctx) GetReady() *EventReady {
}
// getinitinfo 获得 gateway 和 shard
func (b *Bot) getinitinfo() (gw string, shard [2]byte, err error) {
func (bot *Bot) getinitinfo() (secret, gw string, shard [2]byte, err error) {
shard[1] = 1
if b.client == nil {
b.client = http.DefaultClient
if bot.client == nil {
bot.client = http.DefaultClient
}
if b.ShardIndex == 0 {
gw, err = b.GetGeneralWSSGateway()
secret = bot.Secret
if bot.Secret != "" {
bot.Secret = ""
}
if bot.ShardIndex == 0 {
gw, err = bot.GetGeneralWSSGateway()
if err != nil {
return
}
} else {
var sgw *ShardWSSGateway
sgw, err = b.GetShardWSSGateway()
sgw, err = bot.GetShardWSSGateway()
if err != nil {
return
}
if b.ShardCount == 0 {
if bot.ShardCount == 0 {
log.Infoln(getLogHeader(), "使用网关推荐Shards数:", sgw.Shards)
b.ShardCount = uint8(sgw.Shards)
bot.ShardCount = uint8(sgw.Shards)
}
if b.ShardCount <= b.ShardIndex {
err = errors.New("shard index " + strconv.Itoa(int(b.ShardIndex)) + " >= suggested size " + strconv.Itoa(sgw.Shards))
if bot.ShardCount <= bot.ShardIndex {
err = errors.New("shard index " + strconv.Itoa(int(bot.ShardIndex)) + " >= suggested size " + strconv.Itoa(sgw.Shards))
return
}
gw = sgw.URL
shard[0] = byte(b.ShardIndex)
shard[1] = byte(b.ShardCount)
shard[0] = byte(bot.ShardIndex)
shard[1] = byte(bot.ShardCount)
}
return
}
@@ -92,11 +99,11 @@ func Start(bots ...*Bot) error {
log.Warnln(getLogHeader(), "已忽略重复调用的", getThisFuncName())
}
for _, b := range bots {
gw, shard, err := b.getinitinfo()
s, gw, shard, err := b.getinitinfo()
if err != nil {
return err
}
go b.Init(gw, shard).Connect().Listen()
go b.Init(s, gw, shard).Connect().Listen()
}
return nil
}
@@ -112,25 +119,25 @@ func Run(preblock func(), bots ...*Bot) error {
return nil
case 1:
b = bots[0]
gw, shard, err := b.getinitinfo()
s, gw, shard, err := b.getinitinfo()
if err != nil {
return err
}
b.Init(gw, shard)
b.Init(s, gw, shard)
default:
for _, b := range bots[:len(bots)-1] {
gw, shard, err := b.getinitinfo()
s, gw, shard, err := b.getinitinfo()
if err != nil {
return err
}
go b.Init(gw, shard).Connect().Listen()
go b.Init(s, gw, shard).Connect().Listen()
}
b = bots[len(bots)-1]
gw, shard, err := b.getinitinfo()
s, gw, shard, err := b.getinitinfo()
if err != nil {
return err
}
b.Init(gw, shard)
b.Init(s, gw, shard)
}
b.Connect()
if preblock != nil {
@@ -141,19 +148,19 @@ func Run(preblock func(), bots ...*Bot) error {
}
// Init 初始化, 只需执行一次
func (b *Bot) Init(gateway string, shard [2]byte) *Bot {
b.gateway = gateway
b.shard = shard
if b.Timeout == 0 {
b.Timeout = time.Minute
func (bot *Bot) Init(secret, gateway string, shard [2]byte) *Bot {
bot.gateway = gateway
bot.shard = shard
if bot.Timeout == 0 {
bot.Timeout = time.Minute
}
b.client = &http.Client{
Timeout: b.Timeout,
bot.client = &http.Client{
Timeout: bot.Timeout,
}
if b.Handler != nil {
h := reflect.ValueOf(b.Handler).Elem()
if bot.Handler != nil {
h := reflect.ValueOf(bot.Handler).Elem()
t := h.Type()
b.handlers = make(map[string]eventHandlerType, h.NumField()*4)
bot.handlers = make(map[string]eventHandlerType, h.NumField()*4)
for i := 0; i < h.NumField(); i++ {
f := h.Field(i)
if f.IsZero() {
@@ -162,17 +169,40 @@ func (b *Bot) Init(gateway string, shard [2]byte) *Bot {
tp := t.Field(i).Name[2:] // skip On
log.Infoln(getLogHeader(), "注册处理函数", tp)
handler := f.Interface()
b.handlers[tp] = eventHandlerType{
bot.handlers[tp] = eventHandlerType{
h: *(*generalHandleType)(unsafe.Add(unsafe.Pointer(&handler), unsafe.Sizeof(uintptr(0)))),
t: t.Field(i).Type.In(2).Elem(),
}
}
}
return b
bot.Secret = secret
if bot.IsV2() {
for {
err := bot.GetAppAccessToken()
if err == nil {
log.Infoln(getLogHeader(), "获得 Token: "+bot.token+", 超时:", bot.expiresec, "秒")
bot.exonce.Do(func() {
go bot.refreshtoken()
})
break
}
log.Infoln(getLogHeader(), "获得 Token 失败:", err)
time.Sleep(time.Second * 3)
}
}
return bot
}
// IsV2 判断是否运行于 V2 API 下
func (bot *Bot) IsV2() bool {
return bot.Secret != ""
}
// Authorization 返回 Authorization Header value
func (bot *Bot) Authorization() string {
if bot.IsV2() {
return "QQBot " + bot.token
}
return "Bot " + bot.AppID + "." + bot.Token
}
@@ -271,6 +301,24 @@ func (bot *Bot) Connect() *Bot {
return bot
}
// refreshtoken 以 Expire 为间隔刷新 Token
func (bot *Bot) refreshtoken() {
for {
time.Sleep(time.Second * 10)
if atomic.LoadUint32(&bot.heartbeat) == 0 {
log.Warnln(getLogHeader(), "等待服务器建立连接...")
continue
}
time.Sleep(time.Duration(bot.expiresec) * time.Second)
err := bot.GetAppAccessToken()
if err != nil {
log.Warnln(getLogHeader(), "刷新 Token 时出现错误:", err)
} else {
log.Infoln(getLogHeader(), "刷新 Token: "+bot.token+", 超时:", bot.expiresec, "秒")
}
}
}
// doheartbeat 按指定间隔进行心跳包发送
func (bot *Bot) doheartbeat() {
payload := struct {