1
0
mirror of https://github.com/fumiama/NanoBot.git synced 2026-06-05 02:30:23 +08:00

finish Init Connect

This commit is contained in:
源文雨
2023-10-15 01:07:38 +09:00
parent 9918dd8ec8
commit 5e3e113fdf
9 changed files with 361 additions and 75 deletions

166
bot.go
View File

@@ -1,6 +1,21 @@
package nano
import "time"
import (
"encoding/base64"
"encoding/json"
"net"
"net/http"
"reflect"
"sync"
"time"
"unsafe"
"github.com/RomiChan/syncx"
"github.com/RomiChan/websocket"
log "github.com/sirupsen/logrus"
)
var clients = syncx.Map[string, *Bot]{}
// Bot 一个机器人实例的配置
type Bot struct {
@@ -11,15 +26,158 @@ type Bot struct {
Timeout time.Duration // Timeout is API 调用超时
Handler *Handler // Handler 注册对各种事件的处理
handlers map[string]GeneralHandleType // handlers 方便调用的 handler
gateway string // gateway 获得的网关
shard [2]byte // shard 分片
seq uint32
handlers map[string]GeneralHandleType // handlers 方便调用的 handler
mu sync.Mutex // 写锁
conn *websocket.Conn
heartbeat uint32 // heartbeat 心跳周期, 单位毫秒
intents uint32
properties json.RawMessage
ready EventReady
}
// Init 初始化, 只需执行一次
func (b *Bot) Init() {
func (b *Bot) Init(gateway string, shard [2]byte, intents uint32, properties json.RawMessage) *Bot {
b.gateway = gateway
b.shard = shard
b.intents = intents
b.properties = properties
if b.Handler != nil {
h := reflect.ValueOf(b.Handler).Elem()
t := h.Type()
b.handlers = make(map[string]GeneralHandleType, h.NumField()*4)
for i := 0; i < h.NumField(); i++ {
f := h.Field(i)
if f.IsZero() {
continue
}
tp := t.Field(i).Name[2:] // skip On
log.Infoln("[bot] 注册处理函数", tp)
handler := f.Interface()
b.handlers[tp] = *(*GeneralHandleType)(unsafe.Add(unsafe.Pointer(&handler), unsafe.Sizeof(uintptr(0))))
}
}
return b
}
// Authorization 返回 Authorization Header value
func (bot *Bot) Authorization() string {
return "Bot " + bot.AppID + "." + bot.Token
}
// receive 收一个 payload
func (bot *Bot) reveive() (payload WebsocketPayload, err error) {
err = bot.conn.ReadJSON(&payload)
return
}
// Connect 连接到 Gateway + 鉴权连接
//
// https://bot.q.qq.com/wiki/develop/api/gateway/reference.html#_1-%E8%BF%9E%E6%8E%A5%E5%88%B0-gateway
func (bot *Bot) Connect() {
network, address := resolveURI(bot.gateway)
log.Infoln("[bot] 开始尝试连接到网关:", network, ", AppID:", bot.AppID)
dialer := websocket.Dialer{
NetDial: func(_, addr string) (net.Conn, error) {
if network == "unix" {
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr
}
filepath, err := base64.RawURLEncoding.DecodeString(host)
if err == nil {
addr = BytesToString(filepath)
}
}
return net.Dial(network, addr) // support unix socket transport
},
}
for {
conn, resp, err := dialer.Dial(address, http.Header{})
if err != nil {
log.Warnf("[bot] 连接到网关 %v 时出现错误: %v", bot.gateway, err)
time.Sleep(2 * time.Second) // 等待两秒后重新连接
continue
}
bot.conn = conn
_ = resp.Body.Close()
payload, err := bot.reveive()
if err != nil {
log.Warnln("[bot] 获取心跳间隔时出现错误:", err)
_ = conn.Close()
time.Sleep(2 * time.Second) // 等待两秒后重新连接
continue
}
bot.heartbeat, err = payload.GetHeartbeatInterval()
if err != nil {
log.Warnln("[bot] 解析心跳间隔时出现错误:", err)
_ = conn.Close()
time.Sleep(2 * time.Second) // 等待两秒后重新连接
continue
}
payload.Op = OpCodeIdentify
err = payload.WrapData(&OpCodeIdentifyMessage{
Token: bot.Authorization(),
Intents: bot.intents,
Shard: bot.shard,
Properties: bot.properties,
})
if err != nil {
log.Warnln("[bot] 包装 Identify 时出现错误:", err)
_ = conn.Close()
time.Sleep(2 * time.Second) // 等待两秒后重新连接
continue
}
err = bot.SendPayload(&payload)
if err != nil {
log.Warnln("[bot] 发送 Identify 时出现错误:", err)
_ = conn.Close()
time.Sleep(2 * time.Second) // 等待两秒后重新连接
continue
}
payload, err = bot.reveive()
if err != nil {
log.Warnln("[bot] 获取 EventReady 时出现错误:", err)
_ = conn.Close()
time.Sleep(2 * time.Second) // 等待两秒后重新连接
continue
}
bot.ready, err = payload.GetEventReady()
if err != nil {
log.Warnln("[bot] 解析 EventReady 时出现错误:", err)
_ = conn.Close()
time.Sleep(2 * time.Second) // 等待两秒后重新连接
continue
}
break
}
clients.Store(bot.ready.User.ID, bot)
log.Infoln("[bot] 连接到网关成功, 用户名:", bot.ready.User.Username)
go bot.doheartbeat()
}
// doheartbeat 按指定间隔进行心跳包收发
func (bot *Bot) doheartbeat() {
payload := struct {
Op OpCode `json:"op"`
D *uint32 `json:"d"`
}{Op: OpCodeHeartbeat}
t := time.NewTicker(time.Duration(bot.heartbeat) * time.Millisecond)
defer t.Stop()
time.Sleep(time.Minute)
for range t.C {
if bot.seq == 0 {
payload.D = nil
} else {
payload.D = &bot.seq
}
bot.mu.Lock()
err := bot.conn.WriteJSON(&payload)
bot.mu.Unlock()
if err != nil {
log.Warnln("[bot] 发送心跳时出现错误:", err)
}
}
}

View File

@@ -1,69 +0,0 @@
package nano
import "encoding/json"
const (
// StandardAPI 正式环境接口域名
StandardAPI = `https://api.sgroup.qq.com`
// SandboxAPI 沙箱环境接口域名
SandboxAPI = `https://sandbox.api.sgroup.qq.com`
)
var (
OpenAPI = StandardAPI // OpenAPI 实际使用的 API, 默认 StandardAPI, 可自行赋值配置
)
// CodeMessageBase 各种消息都有的 code + message 基类
type CodeMessageBase struct {
C int `json:"code"`
M string `json:"message"`
}
// OpCode https://bot.q.qq.com/wiki/develop/api/gateway/opcode.html
type OpCode int
const (
OpCodeDispatch OpCode = iota // Receive
OpCodeHeartbeat // Send/Receive
OpCodeIdentify // Send
OpCodeEmpty1
OpCodeEmpty2
OpCodeEmpty3
OpCodeResume // Send
OpCodeReconnect // Receive
OpCodeEmpty4
OpCodeInvalidSession // Receive
OpCodeHello // Receive
OpCodeHeartbeatACK // Receive/Reply
OpCodeHTTPCallbackACK // Reply
)
// WebsocketPayload payload 指的是在 websocket 连接上传输的数据,网关的上下行消息采用的都是同一个结构
//
// https://bot.q.qq.com/wiki/develop/api/gateway/reference.html
type WebsocketPayload struct {
Op OpCode `json:"op"`
D json.RawMessage `json:"d"`
S int `json:"s"`
T string `json:"t"`
}
// https://bot.q.qq.com/wiki/develop/api/gateway/intents.html
const (
IntentGuilds = 1 << 0
IntentGuildMembers = 1 << 1
IntentGuildMessages = 1 << 9
IntentGuildMessageReactions = 1 << 10
IntentDirectMessage = 1 << 12
IntentOpenForumsEvent = 1 << 18
IntentAudioOrLiveChannelMember = 1 << 19
IntentInteraction = 1 << 26
IntentMessageAudit = 1 << 27
IntentForumsEvent = 1 << 28
IntentAudioAction = 1 << 29
IntentPublicGuildMessages = 1 << 30
IntentAll = IntentGuilds | IntentGuildMembers | IntentGuildMessages | IntentGuildMessageReactions |
IntentDirectMessage | IntentOpenForumsEvent | IntentAudioOrLiveChannelMember | IntentInteraction |
IntentMessageAudit | IntentForumsEvent | IntentAudioAction | IntentPublicGuildMessages
)

8
go.mod
View File

@@ -3,8 +3,14 @@ module github.com/fumiama/NanoBot
go 1.20
require (
github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e
github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5
github.com/fumiama/go-base16384 v1.7.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
)
require golang.org/x/text v0.3.7 // indirect
require (
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
golang.org/x/text v0.3.7 // indirect
)

12
go.sum
View File

@@ -1,14 +1,24 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e h1:wR3MXQ3VbUlPKOOUwLOYgh/QaJThBTYtsl673O3lqSA=
github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e/go.mod h1:vD7Ra3Q9onRtojoY5sMCLQ7JBgjUsrXDnDKyFxqpf9w=
github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5 h1:bBmmB7he0iVN4m5mcehfheeRUEer/Avo4ujnxI3uCqs=
github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5/go.mod h1:0UcFaCkhp6vZw6l5Dpq0Dp673CoF9GdvA8lTfst0GiU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fumiama/go-base16384 v1.7.0 h1:6fep7XPQWxRlh4Hu+KsdH+6+YdUp+w6CwRXtMWSsXCA=
github.com/fumiama/go-base16384 v1.7.0/go.mod h1:OEn+947GV5gsbTAnyuUW/SrfxJYUdYupSIQXOuGOcXM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -1,8 +1,11 @@
package nano
import (
"encoding/base64"
"net/url"
"runtime"
"strings"
"unsafe"
)
func getFuncNameWithSkip(n int) string {
@@ -66,3 +69,49 @@ func UnderlineToCamel(s string) string {
}
return sb.String()
}
// resolveURI github.com/wdvxdr1123/ZeroBot/driver/uri.go
func resolveURI(addr string) (network, address string) {
network, address = "tcp", addr
uri, err := url.Parse(addr)
if err == nil && uri.Scheme != "" {
scheme, ext, _ := strings.Cut(uri.Scheme, "+")
if ext != "" {
network = ext
uri.Scheme = scheme // remove `+unix`/`+tcp4`
if ext == "unix" {
uri.Host, uri.Path, _ = strings.Cut(uri.Path, ":")
uri.Host = base64.StdEncoding.EncodeToString(StringToBytes(uri.Host)) // special handle for unix
}
address = uri.String()
}
}
return
}
// slice is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
//
// Unlike reflect.SliceHeader, its Data field is sufficient to guarantee the
// data it references will not be garbage collected.
type slice struct {
data unsafe.Pointer
len int
cap int
}
// BytesToString 没有内存开销的转换
func BytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
// StringToBytes 没有内存开销的转换
func StringToBytes(s string) (b []byte) {
bh := (*slice)(unsafe.Pointer(&b))
sh := (*slice)(unsafe.Pointer(&s))
bh.data = sh.data
bh.len = sh.len
bh.cap = sh.len
return b
}

21
intent.go Normal file
View File

@@ -0,0 +1,21 @@
package nano
// https://bot.q.qq.com/wiki/develop/api/gateway/intents.html
const (
IntentGuilds = 1 << 0
IntentGuildMembers = 1 << 1
IntentGuildMessages = 1 << 9
IntentGuildMessageReactions = 1 << 10
IntentDirectMessage = 1 << 12
IntentOpenForumsEvent = 1 << 18
IntentAudioOrLiveChannelMember = 1 << 19
IntentInteraction = 1 << 26
IntentMessageAudit = 1 << 27
IntentForumsEvent = 1 << 28
IntentAudioAction = 1 << 29
IntentPublicGuildMessages = 1 << 30
IntentAll = IntentGuilds | IntentGuildMembers | IntentGuildMessages | IntentGuildMessageReactions |
IntentDirectMessage | IntentOpenForumsEvent | IntentAudioOrLiveChannelMember | IntentInteraction |
IntentMessageAudit | IntentForumsEvent | IntentAudioAction | IntentPublicGuildMessages
)

30
opcode.go Normal file
View File

@@ -0,0 +1,30 @@
package nano
import "encoding/json"
// OpCode https://bot.q.qq.com/wiki/develop/api/gateway/opcode.html
type OpCode int
const (
OpCodeDispatch OpCode = iota // Receive
OpCodeHeartbeat // Send/Receive
OpCodeIdentify // Send
OpCodeEmpty1
OpCodeEmpty2
OpCodeEmpty3
OpCodeResume // Send
OpCodeReconnect // Receive
OpCodeEmpty4
OpCodeInvalidSession // Receive
OpCodeHello // Receive
OpCodeHeartbeatACK // Receive/Reply
OpCodeHTTPCallbackACK // Reply
)
// OpCodeIdentifyMessage https://bot.q.qq.com/wiki/develop/api/gateway/reference.html#_2-%E9%89%B4%E6%9D%83%E8%BF%9E%E6%8E%A5
type OpCodeIdentifyMessage struct {
Token string `json:"token"`
Intents uint32 `json:"intents"`
Shard [2]byte `json:"shard"`
Properties json.RawMessage `json:"properties"`
}

View File

@@ -11,6 +11,23 @@ import (
"github.com/pkg/errors"
)
const (
// StandardAPI 正式环境接口域名
StandardAPI = `https://api.sgroup.qq.com`
// SandboxAPI 沙箱环境接口域名
SandboxAPI = `https://sandbox.api.sgroup.qq.com`
)
var (
OpenAPI = StandardAPI // OpenAPI 实际使用的 API, 默认 StandardAPI, 可自行赋值配置
)
// CodeMessageBase 各种消息都有的 code + message 基类
type CodeMessageBase struct {
C int `json:"code"`
M string `json:"message"`
}
func checkrespbaseunsafe(ptr any) error {
respbase := (*CodeMessageBase)(*(*unsafe.Pointer)(unsafe.Add(unsafe.Pointer(&ptr), unsafe.Sizeof(uintptr(0)))))
if respbase.C != 0 {

64
payload.go Normal file
View File

@@ -0,0 +1,64 @@
package nano
import (
"encoding/json"
"errors"
"strconv"
)
// WebsocketPayload payload 指的是在 websocket 连接上传输的数据,网关的上下行消息采用的都是同一个结构
//
// https://bot.q.qq.com/wiki/develop/api/gateway/reference.html
type WebsocketPayload struct {
Op OpCode `json:"op"`
D json.RawMessage `json:"d,omitempty"`
S int `json:"s,omitempty"`
T string `json:"t,omitempty"`
}
// GetHeartbeatInterval OpCodeHello 获得心跳周期 单位毫秒
func (wp *WebsocketPayload) GetHeartbeatInterval() (uint32, error) {
if wp.Op != OpCodeHello {
return 0, errors.New(getThisFuncName() + " unexpected OpCode " + strconv.Itoa(int(wp.Op)))
}
data := &struct {
H uint32 `json:"heartbeat_interval"`
}{}
err := json.Unmarshal(wp.D, data)
return data.H, err
}
// SendPayload 发送 ws 包
func (bot *Bot) SendPayload(wp *WebsocketPayload) error {
bot.mu.Lock()
defer bot.mu.Unlock()
return bot.conn.WriteJSON(wp)
}
// WrapData 将结构体序列化到 wp.D
func (wp *WebsocketPayload) WrapData(v any) (err error) {
wp.D, err = json.Marshal(v)
return
}
// EventReady https://bot.q.qq.com/wiki/develop/api/gateway/reference.html#_2-%E9%89%B4%E6%9D%83%E8%BF%9E%E6%8E%A5
type EventReady struct {
Version int `json:"version"`
SessionID string `json:"session_id"`
User *User `json:"user"`
Shard [2]byte `json:"shard"`
}
// GetEventReady OpCodeDispatch READY
func (wp *WebsocketPayload) GetEventReady() (er EventReady, err error) {
if wp.Op != OpCodeDispatch {
err = errors.New(getThisFuncName() + " unexpected OpCode " + strconv.Itoa(int(wp.Op)))
return
}
if wp.T != "READY" {
err = errors.New(getThisFuncName() + " unexpected event type " + wp.T)
return
}
err = json.Unmarshal(wp.D, &er)
return
}