Skip to content

Commit

Permalink
feat(botgo): 增加http回调校验逻辑 1.增加webhook回调验证逻辑 2.优化token manager功能逻辑 (me…
Browse files Browse the repository at this point in the history
…rge request !98)

Squash merge branch 'feat_20240923_callback_validation_story_0' into 'master'
支持接入webhook事件链路:

1. 增加webhook回调验证逻辑
2. 按golang.org/x/oauth2标准实现token source
3. 实现定时刷现access token逻辑
4. 更新examples
5. 更新readme文档
  • Loading branch information
rianli authored and walli committed Oct 10, 2024
1 parent df169eb commit 42cb5b8
Show file tree
Hide file tree
Showing 94 changed files with 774 additions and 5,696 deletions.
169 changes: 92 additions & 77 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,114 +2,129 @@

QQ频道机器人,官方 GOLANG SDK。

![Build](https://github.com/tencent-connect/botgo/actions/workflows/build.yml/badge.svg)
[![Go Reference](https://pkg.go.dev/badge/github.com/tencent-connect/botgo.svg)](https://pkg.go.dev/github.com/tencent-connect/botgo)
[![Examples](https://img.shields.io/badge/BotGo-examples-yellowgreen)](https://github.com/tencent-connect/botgo/tree/master/examples)

## 注意事项
1. websocket 事件推送链路将在24年年底前逐步下线,后续官方不再维护。
2. 新的webhook事件回调链路目前在灰度验证,灰度用户可体验通过页面配置事件监听及回调地址。如未在灰度范围,可联系QQ机器人反馈助手开通。

![反馈机器人](docs/img/feedback_bot.png)

灰度期间,原有机器人仍可使用websocket事件链路接收事件推送。
## 一、quick start
### 1.打开 examples/receive-and-send
### 2.复制 config.yaml.demo -> config.yaml
![img.png](doc/img/copy-config-yaml.png)
### 3.登录[开发者管理端](https://q.qq.com),将BotAppID和机器人秘钥分别填入config.yaml中的appid和secret字段
![find-app-acc.png](doc/img/find-app-acc.png)
![type-in-app-info.png](doc/img/type-in-app-info.png)
### 4.执行go build,然后执行./receive-and-send, 即可收到消息并回复。
![robot-start-console.png](doc/img/robot-start-console.png)
### 5.根据机器人QQ号查找、添加机器人为好友
![add-robot.png](doc/img/add-robot.png)
### 1. QQ机器人创建与配置
1. 创建开发者账号,创建QQ机器人 [QQ机器人开放平台](https://q.qq.com/qqbot)

### 6.发送消息,即可收到回复。
![robot-reply.png](doc/img/robot-reply.png)
![create_bot.png](docs/img/create_bot.png)

## 二、如何使用
2. 配置沙箱成员 (QQ机器人上线前,仅沙箱环境可访问)。新创建机器人会默认将创建者加入沙箱环境。

### 1.请求 openapi 接口,操作资源
![sandbox_setting.png](docs/img/sandbox_setting.png)

```golang
func main() {
token := token.BotToken(conf.AppID, conf.Token)
api := botgo.NewOpenAPI(token).WithTimeout(3 * time.Second)
ctx := context.Background()

ws, err := api.WS(ctx, nil, "")
log.Printf("%+v, err:%v", ws, err)

me, err := api.Me(ctx, nil, "")
log.Printf("%+v, err:%v", me, err)
}
```
### 2. 云函数创建与配置
1. 腾讯云账号开通scf服务 [快速入门](https://cloud.tencent.com/document/product/1154/39271)
2. 创建函数

### 2.使用默认 SessionManager 启动 websocket 连接,接收事件
* 选择模板

```golang
func main() {
token := token.BotToken(conf.AppID, conf.Token)
api := botgo.NewOpenAPI(token).WithTimeout(3 * time.Second)
ctx := context.Background()
ws, err := api.WS(ctx, nil, "")
if err != nil {
log.Printf("%+v, err:%v", ws, err)
}

// 监听哪类事件就需要实现哪类的 handler,定义:websocket/event_handler.go
var atMessage websocket.ATMessageEventHandler = func(event *dto.WSPayload, data *dto.WSATMessageData) error {
fmt.Println(event, data)
return nil
}
intent := websocket.RegisterHandlers(atMessage)
// 启动 session manager 进行 ws 连接的管理,如果接口返回需要启动多个 shard 的连接,这里也会自动启动多个
botgo.NewSessionManager().Start(ws, token, &intent)
}
```
![create_scf.png](docs/img/create_scf.png)

* 启用"公网访问"、"日志投递"

![turn_internet_access.png](docs/img/turn_internet_access.png)

## 三、什么是 SessionManager
* 编辑云函数,启用"固定公网出口IP" (QQ机器人需要配置IP白名单,仅白名单内服务器/容器可访问OpenAPI)

SessionManager,用于管理 websocket 连接的启动,重连等。接口定义在:`session_manager.go`。开发者也可以自己实现自己的 SessionManager。
![scf_setting.png](docs/img/scf_setting.png)

sdk 中实现了两个 SessionManager
![get_internet_ip.png](docs/img/get_internet_ip.png)

- [local](./sessions/local/local.go) 用于在单机上启动多个 shard 的连接。下文用 `local` 代表
- [remote](./sessions/remote/remote.go) 基于 redis 的 list 数据结构,实现分布式的 shard 管理,可以在多个节点上启动多个服务进程。下文用 `remote` 代表
### 3. 使用示例构建、上传云函数部署包
1. 打开 examples/receive-and-send
2. 复制 config.yaml.demo -> config.yaml

另外,也有其他同事基于 etcd 实现了 shard 集群的管理,在 [botgo-plugns](https://github.com/tencent-connect/botgo-plugins) 中。
![img.png](docs/img/copy-config-yaml.png)

## 四、生产环境中的一些建议
3. 登录[开发者管理端](https://q.qq.com),将BotAppID和机器人秘钥分别填入config.yaml中的appid和secret字段

得益于 websocket 的机制,我们可以在本地就启动一个机器人,实现相关逻辑,但是在生产环境中需要考虑扩容,容灾等情况,所以建
议从以下几方面考虑生产环境的部署:
![find-app-acc.png](docs/img/find-app-acc.png)

### 1.公域机器人,优先使用分布式 shard 管理
![type-in-app-info.png](docs/img/type-in-app-info.png)

使用上面提到的分布式的 session manager 或者自己实现一个分布式的 session manager
4. 执行Makefile中build指令
5. 将config.yaml、scf_bootstrap、qqbot-demo(二进制文件)打包,上传至云函数

### 2.提前规划好分片
![上传压缩包](docs/img/upload_scf_zip.png)

分布式 SessionManager 需要解决的最大的问题,就是如何解决 shard 随时增加的问题,类似 kafka 的 rebalance 问题一样,
由于 shard 是基于频道 id 来进行 hash 的,所以在扩容的时候所有的数据都会被重新 hash。
### 4.配置QQ机器人事件监听、回调地址、IP白名单

提前规划好较多的分片,如 20 个分片,有助于在未来机器人接入的频道过多的时候,能够更加平滑的进行实例的扩容。比如如果使用的
`remote`,初始化时候分 20 个分片,但是只启动 2 个进程,那么这2个进程将争抢 20 个分片的消费权,进行消费,当启动更多
的实例之后,伴随着 websocket 要求一定时间进行一次重连,启动的新实例将会平滑的分担分片的数据处理。
1. 复制云函数地址 + "/qqbot"后缀,填入回调地址输入框。点击确认。

### 3.接入和逻辑分离
![img.png](docs/img/copy_scf_addr.png)

接入是指从机器人平台收到事件的服务。逻辑是指处理相关事件的服务
2. 勾选 C2C_MESSAGE_CREATE 事件。点击确认

接入与逻辑分离,有助于提升机器人的事件处理效率和可靠性。一般实现方式类似于以下方案:
![webhook配置](docs/img/webhook_setting.png)

- 接入层:负责维护与平台的 websocket 连接,并接收相关事件,生产到 kafka 等消息中间件中。
如果使用 `local` 那么可能还涉及到分布式锁的问题。可以使用sdk 中的 `sessions/remote/lock` 快速基于 redis 实现分布式锁。

- 逻辑层:从 kafka 消费到事件,并进行对应的处理,或者调用机器人的 openapi 进行相关数据的操作。
3. 将云函数 "固定公网出口IP" 配置到IP白名单中)

提前规划好 kafka 的分片,然后从容的针对逻辑层做水平扩容。或者使用 pulsar(腾讯云上叫 tdmq) 来替代 kafka 避免 rebalance 问题。
![ip_whitlist_setting.png](docs/img/ip_whitlist_setting.png)

### 体验与机器人的对话

给机器人发送消息、富媒体文件,机器人回复消息

## 二、如何使用SDK

```golang

var api openapi.OpenAPI

func main() {
//创建oauth2标准token source
tokenSource := token.NewQQBotTokenSource(
&token.QQBotCredentials{
AppID: "",
AppSecret: "",
})
//启动自动刷新access token协程
if err = token.StartRefreshAccessToken(ctx, tokenSource); err != nil {
log.Fatalln(err)
}
// 初始化 openapi,正式环境
api = botgo.NewOpenAPI(credentials.AppID, tokenSource).WithTimeout(5 * time.Second).SetDebug(true)
// 注册事件处理函数
_ = event.RegisterHandlers(
// 注册c2c消息处理函数
C2CMessageEventHandler(),
)
//注册回调处理函数
http.HandleFunc(path_, func (writer http.ResponseWriter, request *http.Request) {
webhook.HTTPHandler(writer, request, credentials)
})
// 启动http服务监听端口
if err = http.ListenAndServe(fmt.Sprintf("%s:%d", host_, port_), nil); err != nil {
log.Fatal("setup server fatal:", err)
}
}

// C2CMessageEventHandler 实现处理 at 消息的回调
func C2CMessageEventHandler() event.C2CMessageEventHandler {
return func(event *dto.WSPayload, data *dto.WSC2CMessageData) error {
//TODO use api do sth.
return nil
}
}
```

## 、SDK 开发说明
## 、SDK 开发说明 (Deprecated)

请查看[开发说明](./DEVELOP.md)
请查看: [开发说明](./DEVELOP.md)

## 、加入官方社区
## 、加入官方社区

欢迎扫码加入 **QQ 频道开发者社区**

![开发者社区](https://mpqq.gtimg.cn/privacy/qq_guild_developer.png)
![开发者社区](https://mpqq.gtimg.cn/privacy/qq_guild_developer.png)
10 changes: 5 additions & 5 deletions botgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"github.com/tencent-connect/botgo/log"
"github.com/tencent-connect/botgo/openapi"
v1 "github.com/tencent-connect/botgo/openapi/v1"
"github.com/tencent-connect/botgo/token"
"github.com/tencent-connect/botgo/websocket/client"
"golang.org/x/oauth2"
)

func init() {
Expand All @@ -32,11 +32,11 @@ func SelectOpenAPIVersion(version openapi.APIVersion) error {

// NewOpenAPI 创建新的 openapi 实例,会返回当前的 openapi 实现的实例
// 如果需要使用其他版本的实现,需要在调用这个方法之前调用 SelectOpenAPIVersion 方法
func NewOpenAPI(token *token.Manager) openapi.OpenAPI {
return openapi.DefaultImpl.Setup(token, false)
func NewOpenAPI(appID string, tokenSource oauth2.TokenSource) openapi.OpenAPI {
return openapi.DefaultImpl.Setup(appID, tokenSource, false)
}

// NewSandboxOpenAPI 创建测试环境的 openapi 实例
func NewSandboxOpenAPI(token *token.Manager) openapi.OpenAPI {
return openapi.DefaultImpl.Setup(token, true)
func NewSandboxOpenAPI(appID string, tokenSource oauth2.TokenSource) openapi.OpenAPI {
return openapi.DefaultImpl.Setup(appID, tokenSource, true)
}
4 changes: 2 additions & 2 deletions constant/constant.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Package constant 常量定义
package constant

// TraceIDKey 机器人openapi返回的链路追踪ID
const TraceIDKey = "X-Tps-trace-ID"
// HeaderTraceID 机器人openapi返回的链路追踪ID
const HeaderTraceID = "X-Tps-trace-ID"

// APIDomain api domain
var APIDomain = "https://api.sgroup.qq.com"
Expand Down
Binary file removed doc/img/copy-config-yaml.png
Binary file not shown.
Binary file removed doc/img/type-in-app-info.png
Binary file not shown.
File renamed without changes
Binary file added docs/img/chat_with_bot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/copy-config-yaml.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/copy_scf_addr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/create_bot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/create_scf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/create_secret.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/feedback_bot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Binary file added docs/img/get_internet_ip.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/ip_whitlist_setting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/qq_bot_demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
File renamed without changes
Binary file added docs/img/sandbox_setting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/scf_setting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/turn_internet_access.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/type-in-app-info.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/upload_scf_zip.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/webhook_setting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion dto/friend_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package dto

// C2CFriendData c2c 好友事件信息
type C2CFriendData struct {
OpenId string `json:"openid"`
OpenID string `json:"openid"`
Timestamp int `json:"timestamp"` // 添加/删除机器人好友时间戳
Nick string `json:"nick"` // 待事件链路补充
Avatar string `json:"avatar"` // 待事件链路补充
Expand Down
2 changes: 1 addition & 1 deletion dto/interaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Interaction struct {
ChannelID string `json:"channel_id,omitempty"` // 子频道 ID
Version uint32 `json:"version,omitempty"` // 版本,默认为 1
GroupOpenID string `json:"group_openid,omitempty"` // 群OpenID
ChatType uint32 `json:"chat_type,omitempty"` // 按钮场景类型 频道:0 群:1 c2c:2,改成optional为了区分0和没有值
ChatType uint32 `json:"chat_type,omitempty"` // 0: 频道, 1: 群, 2: c2c
Scene string `json:"scene,omitempty"` // 场景 c2c/group/guild
UserOpenID string `json:"user_openid,omitempty"` // 用户ID
Timestamp string `json:"timestamp,omitempty"` // 时间戳
Expand Down
6 changes: 3 additions & 3 deletions dto/keyboard/keyboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ type Permission struct {
// TemplateID 对模板id的封装,兼容官方模板和自定义模板
type TemplateID struct {
// 这两个字段互斥,只填入一个
TemplateId uint32 `json:"template_id,omitempty"` // 官方提供的模板id
CustomTemplateId string `json:"custom_template_id,omitempty"` // 自定义模板
TemplateID uint32 `json:"template_id,omitempty"` // 官方提供的模板id
CustomTemplateID string `json:"custom_template_id,omitempty"` // 自定义模板
}

// SubscribeData 订阅按钮数据
type SubscribeData struct {
TemplateIds []*TemplateID `json:"template_ids,omitempty"` // 订阅按钮对应的模板id列表
TemplateIDs []*TemplateID `json:"template_ids,omitempty"` // 订阅按钮对应的模板id列表
}
12 changes: 6 additions & 6 deletions dto/message_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import "github.com/tencent-connect/botgo/dto/keyboard"
type SendType int

const (
Text SendType = 1 // 文字消息
RichMedia SendType = 2 // 富媒体类消息
Text SendType = 1 // Text 文字消息
RichMedia SendType = 2 // RichMedia 富媒体类消息
)

// APIMessage 消息结构接口
Expand Down Expand Up @@ -65,7 +65,7 @@ type MessageToCreate struct {
EventID string `json:"event_id,omitempty"` // 要回复的事件id, 逻辑同MsgID
Timestamp int64 `json:"timestamp,omitempty"` //TODO delete this

Check failure on line 66 in dto/message_create.go

View workflow job for this annotation

GitHub Actions / build

commentFormatting: put a space between `//` and comment text (gocritic)
MsgSeq uint32 `json:"msg_seq,omitempty"` // 机器人对于回复一个msg_id或者event_id的消息序号,指定后根据这个字段和msg_id或者event_id进行去重

Check failure on line 67 in dto/message_create.go

View workflow job for this annotation

GitHub Actions / build

the line is 146 characters long, which exceeds the maximum of 120 characters. (lll)
SubscribeId string `json:"subscribe_id,omitempty"` // 订阅id,发送订阅消息时使用
SubscribeID string `json:"subscribe_id,omitempty"` // 订阅id,发送订阅消息时使用
InputNotify *InputNotify `json:"input_notify,omitempty"` // 输入状态状态信息
Media *MediaInfo `json:"media,omitempty"` // 富媒体信息
PromptKeyboard *PromptKeyboard `json:"prompt_keyboard,omitempty"` // 消息扩展信息
Expand Down Expand Up @@ -158,11 +158,11 @@ type SettingGuide struct {

// InputNotify 输入状态结构
type InputNotify struct {
InputType int `json:"input_type,omitempty"` //类型 1: "对方正在输入...", 2: 取消展示"]
InputSecond int32 `json:"input_second,omitempty"` //当input_type大于0时有效, 代码状态持续多长时间.
InputType int `json:"input_type,omitempty"` // 类型 1: "对方正在输入...", 2: 取消展示"]
InputSecond int32 `json:"input_second,omitempty"` // 当input_type大于0时有效, 代码状态持续多长时间.
}

// MediaInfo 富媒体信息
type MediaInfo struct {
FileInfo []byte `json:"file_info,omitempty"` //富媒体文件信息,通过上传接口取得
FileInfo []byte `json:"file_info,omitempty"` // 富媒体文件信息,通过上传接口取得
}
13 changes: 13 additions & 0 deletions dto/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dto

// WHValidationReq 机器人回调验证请求Data
type WHValidationReq struct {
PlainToken string `json:"plain_token"`
EventTs string `json:"event_ts"`
}

// WHValidationRsp 机器人回调验证响应结果
type WHValidationRsp struct {
PlainToken string `json:"plain_token"`
Signature string `json:"signature"`
}
16 changes: 9 additions & 7 deletions dto/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package dto
import (
"fmt"

"github.com/tencent-connect/botgo/token"
"golang.org/x/oauth2"
)

// WebsocketAP wss 接入点信息
Expand All @@ -29,12 +29,14 @@ type ShardConfig struct {

// Session 连接的 session 结构,包括链接的所有必要字段
type Session struct {
ID string
URL string
TokenManager *token.Manager
Intent Intent
LastSeq uint32
Shards ShardConfig
ID string
URL string
TokenSource oauth2.TokenSource
Intent Intent
LastSeq uint32
Shards ShardConfig

AppID string
}

// String 输出session字符串
Expand Down
1 change: 1 addition & 0 deletions dto/websocket_opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
WSHello
WSHeartbeatAck
HTTPCallbackAck
HTTPCallbackValidation
)

// opMeans op 对应的含义字符串标识
Expand Down
8 changes: 8 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# QQ机器人examples

## 示例说明
1. apitest 主要演示api调用方法,测试前应用实际的ID替换用例中的ID(user_id、guild_id等)
2. custom-filter 通过自定义 filter 功能,实现自定义链路跟踪 ID,上报模调监控等。
3. custom-logger 主要演示实现自定义logger的方法
4. receive-and-send 演示简单的机器人服务端的实现方法及如何通过腾讯云函数部署。
5. simulate-callback-request 模拟回调请求。开发者完成服务部署前可通过此工具模拟回调请求,实现业务逻辑。
4 changes: 2 additions & 2 deletions examples/apitest/config.yaml.demo
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# 在这个配置文件中补充你的 appid 和 bot token,并修改文件名为 config.yaml
# 在这个配置文件中补充你的 appid 和 secret,并修改文件名为 config.yaml
appid :
token :
secret :
Loading

0 comments on commit 42cb5b8

Please sign in to comment.