From 3dbb8d7b3413908aed75a6dc01aee29203f4e316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BA=90=E6=96=87=E9=9B=A8?= <41315874+fumiama@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:10:13 +0900 Subject: [PATCH] finish base --- bot.go | 16 ++++++ codegen/getopenapiof/main.go | 50 +++++++++++++++++++ define.go | 14 ++++++ go.mod | 5 ++ go.sum | 2 + helper.go | 34 +++++++++++++ http.go | 53 ++++++++++++++++++++ openapi.go | 34 +++++++++++++ openapi_channel.go | 36 ++++++++++++++ openapi_codegen_getopenapiof.go | 87 +++++++++++++++++++++++++++++++++ openapi_guild.go | 28 +++++++++++ openapi_user.go | 31 ++++++++++++ openapi_wss.go | 37 ++++++++++++++ 13 files changed, 427 insertions(+) create mode 100644 bot.go create mode 100644 codegen/getopenapiof/main.go create mode 100644 define.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 helper.go create mode 100644 http.go create mode 100644 openapi.go create mode 100644 openapi_channel.go create mode 100644 openapi_codegen_getopenapiof.go create mode 100644 openapi_guild.go create mode 100644 openapi_user.go create mode 100644 openapi_wss.go diff --git a/bot.go b/bot.go new file mode 100644 index 0000000..db3819f --- /dev/null +++ b/bot.go @@ -0,0 +1,16 @@ +package nano + +import "time" + +// Bot 一个机器人实例的配置 +type Bot struct { + AppID string // AppID is BotAppID(开发者ID) + Token string // Token is 机器人令牌 + Key string // Key is 机器人密钥 + Timeout time.Duration // Timeout is API 调用超时 +} + +// Authorization 返回 Authorization Header value +func (bot *Bot) Authorization() string { + return "Bot " + bot.AppID + "." + bot.Token +} diff --git a/codegen/getopenapiof/main.go b/codegen/getopenapiof/main.go new file mode 100644 index 0000000..4a66a5b --- /dev/null +++ b/codegen/getopenapiof/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "strings" +) + +const head = `// Code generated by codegen/getopenapiof. DO NOT EDIT. + +package nano + +import ( + "unsafe" + + "github.com/pkg/errors" +) +` + +const template = ` +func (bot *Bot) getOpenAPIof[T any](ep string) (*[T any], error) { + resp := &struct { + CodeMessageBase + [T any] + }{} + err := bot.GetOpenAPI(ep, resp) + if err != nil { + err = errors.Wrap(err, getCallerFuncName()) + return nil, err + } + return (*[T any])(unsafe.Add(unsafe.Pointer(resp), unsafe.Sizeof(CodeMessageBase{}))), nil +} +` + +func main() { + f, err := os.Create("openapi_codegen_getopenapiof.go") + if err != nil { + panic(err) + } + defer f.Close() + _, err = f.WriteString(head) + if err != nil { + panic(err) + } + for _, name := range os.Args[1:] { + _, err = f.WriteString(strings.ReplaceAll(template, "[T any]", name)) + if err != nil { + panic(err) + } + } +} diff --git a/define.go b/define.go new file mode 100644 index 0000000..825be93 --- /dev/null +++ b/define.go @@ -0,0 +1,14 @@ +package nano + +var ( + // StandardAPI 正式环境接口域名 + StandardAPI = `https://api.sgroup.qq.com` + // SandboxAPI 沙箱环境接口域名 + SandboxAPI = `https://sandbox.api.sgroup.qq.com` +) + +// CodeMessageBase 各种消息都有的 code + message 基类 +type CodeMessageBase struct { + C int `json:"code"` + M string `json:"message"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..17c6ae2 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/fumiama/NanoBot + +go 1.20 + +require github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7c401c3 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..f931603 --- /dev/null +++ b/helper.go @@ -0,0 +1,34 @@ +package nano + +import ( + "runtime" + "strings" +) + +// getCurrentFuncName 获取当前函数名 +func getCurrentFuncName() string { + pc, _, _, ok := runtime.Caller(1) + if !ok { + return "" + } + fullname := runtime.FuncForPC(pc).Name() + i := strings.LastIndex(fullname, ".") + 1 + if i <= 0 || i >= len(fullname) { + return fullname + } + return fullname[i:] +} + +// getCallerFuncName 获取调用者函数名 +func getCallerFuncName() string { + pc, _, _, ok := runtime.Caller(2) + if !ok { + return "" + } + fullname := runtime.FuncForPC(pc).Name() + i := strings.LastIndex(fullname, ".") + 1 + if i <= 0 || i >= len(fullname) { + return fullname + } + return fullname[i:] +} diff --git a/http.go b/http.go new file mode 100644 index 0000000..c1af5b8 --- /dev/null +++ b/http.go @@ -0,0 +1,53 @@ +package nano + +import ( + "fmt" + "net/http" + "net/url" + "reflect" + "strings" +) + +// NewHTTPEndpointGetRequestWithAuth 新建带鉴权头的 HTTP 请求 +func NewHTTPEndpointGetRequestWithAuth(ep string, auth string) (req *http.Request, err error) { + req, err = http.NewRequest("GET", StandardAPI+ep, nil) + if err != nil { + return + } + req.Header.Add("Authorization", auth) + return +} + +// WriteHTTPQueryIfNotNil 如果非空则将请求添加到 baseurl 后 +// +// ex. WriteHTTPQueryIfNotNil("http://a.com/api", "a", 0, "b", 1, "c", 2) is http://a.com/api?b=1&c=2 +func WriteHTTPQueryIfNotNil(baseurl string, queries ...any) string { + if len(queries) == 0 { + return baseurl + } + hasstart := false + queryname := "" + sb := strings.Builder{} + for i, q := range queries { + if i%2 == 0 { + queryname = q.(string) + continue + } + if reflect.ValueOf(q).IsZero() { + continue + } + if !hasstart { + sb.WriteString(baseurl) + sb.WriteByte('?') + hasstart = true + } + sb.WriteString(queryname) + sb.WriteByte('=') + sb.WriteString(url.QueryEscape(fmt.Sprint(q))) + sb.WriteByte('&') + } + if sb.Len() <= 4 { + return baseurl + } + return sb.String()[:sb.Len()-1] +} diff --git a/openapi.go b/openapi.go new file mode 100644 index 0000000..f531a78 --- /dev/null +++ b/openapi.go @@ -0,0 +1,34 @@ +package nano + +import ( + "encoding/json" + "net/http" + "strconv" + "unsafe" + + "github.com/pkg/errors" +) + +//go:generate go run codegen/getopenapiof/main.go ShardWSSGateway User Guild GuildArray Channel ChannelArray + +// GetOpenAPI 从 ep 获取 json 结构化数据写到 ptr, ptr 必须在开头继承 CodeMessageBase +func (bot *Bot) GetOpenAPI(ep string, ptr any) error { + req, err := NewHTTPEndpointGetRequestWithAuth(ep, bot.Authorization()) + if err != nil { + return errors.Wrap(err, getCallerFuncName()) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return errors.Wrap(err, getCallerFuncName()) + } + defer resp.Body.Close() + err = json.NewDecoder(resp.Body).Decode(ptr) + if err != nil { + return errors.Wrap(err, getCallerFuncName()) + } + respbbase := (*CodeMessageBase)(*(*unsafe.Pointer)(unsafe.Add(unsafe.Pointer(&ptr), unsafe.Sizeof(uintptr(0))))) + if respbbase.C != 0 { + return errors.Wrap(errors.New("code: "+strconv.Itoa(respbbase.C)+", msg: "+respbbase.M), getCallerFuncName()) + } + return nil +} diff --git a/openapi_channel.go b/openapi_channel.go new file mode 100644 index 0000000..842a3fb --- /dev/null +++ b/openapi_channel.go @@ -0,0 +1,36 @@ +package nano + +// Channel 子频道对象 +// +// https://bot.q.qq.com/wiki/develop/api/openapi/channel/model.html +type Channel struct { + ID string `json:"id"` + GuildID string `json:"guild_id"` + Name string `json:"name"` + Type int `json:"type"` + SubType int `json:"sub_type"` + Position int `json:"position"` + ParentID string `json:"parent_id"` + OwnerID string `json:"owner_id"` + PrivateType int `json:"private_type"` + SpeakPermission int `json:"speak_permission"` + ApplicationID string `json:"application_id"` + Permissions string `json:"permissions"` +} + +// ChannelArray []Channel 的别名 +type ChannelArray []Channel + +// GetChannelsOfGuild 获取 id 指定的频道下的子频道列表 +// +// https://bot.q.qq.com/wiki/develop/api/openapi/channel/get_channels.html +func (bot *Bot) GetChannelsOfGuild(id string) (*ChannelArray, error) { + return bot.getOpenAPIofChannelArray("/guilds/" + id + "/channels") +} + +// GetChannelByID 用于获取 id 指定的子频道的详情 +// +// https://bot.q.qq.com/wiki/develop/api/openapi/channel/get_channel.html +func (bot *Bot) GetChannelByID(id string) (*Channel, error) { + return bot.getOpenAPIofChannel("/channels/" + id) +} diff --git a/openapi_codegen_getopenapiof.go b/openapi_codegen_getopenapiof.go new file mode 100644 index 0000000..6813109 --- /dev/null +++ b/openapi_codegen_getopenapiof.go @@ -0,0 +1,87 @@ +// Code generated by codegen/getopenapiof. DO NOT EDIT. + +package nano + +import ( + "unsafe" + + "github.com/pkg/errors" +) + +func (bot *Bot) getOpenAPIofShardWSSGateway(ep string) (*ShardWSSGateway, error) { + resp := &struct { + CodeMessageBase + ShardWSSGateway + }{} + err := bot.GetOpenAPI(ep, resp) + if err != nil { + err = errors.Wrap(err, getCallerFuncName()) + return nil, err + } + return (*ShardWSSGateway)(unsafe.Add(unsafe.Pointer(resp), unsafe.Sizeof(CodeMessageBase{}))), nil +} + +func (bot *Bot) getOpenAPIofUser(ep string) (*User, error) { + resp := &struct { + CodeMessageBase + User + }{} + err := bot.GetOpenAPI(ep, resp) + if err != nil { + err = errors.Wrap(err, getCallerFuncName()) + return nil, err + } + return (*User)(unsafe.Add(unsafe.Pointer(resp), unsafe.Sizeof(CodeMessageBase{}))), nil +} + +func (bot *Bot) getOpenAPIofGuild(ep string) (*Guild, error) { + resp := &struct { + CodeMessageBase + Guild + }{} + err := bot.GetOpenAPI(ep, resp) + if err != nil { + err = errors.Wrap(err, getCallerFuncName()) + return nil, err + } + return (*Guild)(unsafe.Add(unsafe.Pointer(resp), unsafe.Sizeof(CodeMessageBase{}))), nil +} + +func (bot *Bot) getOpenAPIofGuildArray(ep string) (*GuildArray, error) { + resp := &struct { + CodeMessageBase + GuildArray + }{} + err := bot.GetOpenAPI(ep, resp) + if err != nil { + err = errors.Wrap(err, getCallerFuncName()) + return nil, err + } + return (*GuildArray)(unsafe.Add(unsafe.Pointer(resp), unsafe.Sizeof(CodeMessageBase{}))), nil +} + +func (bot *Bot) getOpenAPIofChannel(ep string) (*Channel, error) { + resp := &struct { + CodeMessageBase + Channel + }{} + err := bot.GetOpenAPI(ep, resp) + if err != nil { + err = errors.Wrap(err, getCallerFuncName()) + return nil, err + } + return (*Channel)(unsafe.Add(unsafe.Pointer(resp), unsafe.Sizeof(CodeMessageBase{}))), nil +} + +func (bot *Bot) getOpenAPIofChannelArray(ep string) (*ChannelArray, error) { + resp := &struct { + CodeMessageBase + ChannelArray + }{} + err := bot.GetOpenAPI(ep, resp) + if err != nil { + err = errors.Wrap(err, getCallerFuncName()) + return nil, err + } + return (*ChannelArray)(unsafe.Add(unsafe.Pointer(resp), unsafe.Sizeof(CodeMessageBase{}))), nil +} diff --git a/openapi_guild.go b/openapi_guild.go new file mode 100644 index 0000000..8b2c0d3 --- /dev/null +++ b/openapi_guild.go @@ -0,0 +1,28 @@ +package nano + +import "time" + +// Guild 频道对象 +// +// https://bot.q.qq.com/wiki/develop/api/openapi/guild/model.html +type Guild struct { + ID string `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + OwnerID string `json:"owner_id"` + Owner bool `json:"owner"` + JoinedAt time.Time `json:"joined_at"` + MemberCount int `json:"member_count"` + MaxMembers int `json:"max_members"` + Description string `json:"description"` +} + +// GuildArray []Guild 的别名 +type GuildArray []Guild + +// GetGuildByID 获取 id 指定的频道的详情 +// +// https://bot.q.qq.com/wiki/develop/api/openapi/guild/get_guild.html +func (bot *Bot) GetGuildByID(id string) (*Guild, error) { + return bot.getOpenAPIofGuild("/guilds/" + id) +} diff --git a/openapi_user.go b/openapi_user.go new file mode 100644 index 0000000..1479745 --- /dev/null +++ b/openapi_user.go @@ -0,0 +1,31 @@ +package nano + +// User 用户对象 +// +// https://bot.q.qq.com/wiki/develop/api/openapi/user/model.html +type User struct { + ID string `json:"id"` + Username string `json:"username"` + Avatar string `json:"avatar"` + Bot bool `json:"bot"` + UnionOpenid string `json:"union_openid"` + UnionUserAccount string `json:"union_user_account"` +} + +// GetMe 获取当前用户(机器人)详情 +// +// https://bot.q.qq.com/wiki/develop/api/openapi/user/me.html +func (bot *Bot) GetMe() (*User, error) { + return bot.getOpenAPIofUser("/users/@me") +} + +// GetMyGuilds 获取当前用户(机器人)频道列表,支持分页 +// +// https://bot.q.qq.com/wiki/develop/api/openapi/user/guilds.html +func (bot *Bot) GetMyGuilds(before, after string, limit int) (*GuildArray, error) { + return bot.getOpenAPIofGuildArray(WriteHTTPQueryIfNotNil("/users/@me/guilds", + "before", before, + "after", after, + "limit", limit, + )) +} diff --git a/openapi_wss.go b/openapi_wss.go new file mode 100644 index 0000000..eb13c98 --- /dev/null +++ b/openapi_wss.go @@ -0,0 +1,37 @@ +package nano + +// GetGeneralWSSGateway 获取通用 WSS 接入点 +// +// https://bot.q.qq.com/wiki/develop/api/openapi/wss/url_get.html +func (bot *Bot) GetGeneralWSSGateway() (string, error) { + resp := struct { + CodeMessageBase + U string `json:"url"` + }{} + err := bot.GetOpenAPI("/gateway", &resp) + if err != nil { + return "", err + } + return resp.U, nil +} + +// ShardWSSGateway 带分片 WSS 接入点响应数据 +// +// https://bot.q.qq.com/wiki/develop/api/openapi/wss/shard_url_get.html#%E8%BF%94%E5%9B%9E +type ShardWSSGateway struct { + URL string `json:"url"` + Shards int `json:"shards"` + SessionStartLimit struct { + Total int `json:"total"` + Remaining int `json:"remaining"` + ResetAfter int `json:"reset_after"` + MaxConcurrency int `json:"max_concurrency"` + } `json:"session_start_limit"` +} + +// GetShardWSSGateway 获取带分片 WSS 接入点 +// +// https://bot.q.qq.com/wiki/develop/api/openapi/wss/shard_url_get.html +func (bot *Bot) GetShardWSSGateway() (*ShardWSSGateway, error) { + return bot.getOpenAPIofShardWSSGateway("/gateway/bot") +}