Skip to content

Commit 42cb5b8

Browse files
rianliwalli
authored and
walli
committed
feat(botgo): 增加http回调校验逻辑 1.增加webhook回调验证逻辑 2.优化token manager功能逻辑 (merge 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文档
1 parent df169eb commit 42cb5b8

File tree

94 files changed

+774
-5696
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+774
-5696
lines changed

README.md

+92-77
Original file line numberDiff line numberDiff line change
@@ -2,114 +2,129 @@
22

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

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

8+
## 注意事项
9+
1. websocket 事件推送链路将在24年年底前逐步下线,后续官方不再维护。
10+
2. 新的webhook事件回调链路目前在灰度验证,灰度用户可体验通过页面配置事件监听及回调地址。如未在灰度范围,可联系QQ机器人反馈助手开通。
11+
12+
![反馈机器人](docs/img/feedback_bot.png)
13+
14+
灰度期间,原有机器人仍可使用websocket事件链路接收事件推送。
915
## 一、quick start
10-
### 1.打开 examples/receive-and-send
11-
### 2.复制 config.yaml.demo -> config.yaml
12-
![img.png](doc/img/copy-config-yaml.png)
13-
### 3.登录[开发者管理端](https://q.qq.com),将BotAppID和机器人秘钥分别填入config.yaml中的appid和secret字段
14-
![find-app-acc.png](doc/img/find-app-acc.png)
15-
![type-in-app-info.png](doc/img/type-in-app-info.png)
16-
### 4.执行go build,然后执行./receive-and-send, 即可收到消息并回复。
17-
![robot-start-console.png](doc/img/robot-start-console.png)
18-
### 5.根据机器人QQ号查找、添加机器人为好友
19-
![add-robot.png](doc/img/add-robot.png)
16+
### 1. QQ机器人创建与配置
17+
1. 创建开发者账号,创建QQ机器人 [QQ机器人开放平台](https://q.qq.com/qqbot)
2018

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

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

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

28-
```golang
29-
func main() {
30-
token := token.BotToken(conf.AppID, conf.Token)
31-
api := botgo.NewOpenAPI(token).WithTimeout(3 * time.Second)
32-
ctx := context.Background()
33-
34-
ws, err := api.WS(ctx, nil, "")
35-
log.Printf("%+v, err:%v", ws, err)
36-
37-
me, err := api.Me(ctx, nil, "")
38-
log.Printf("%+v, err:%v", me, err)
39-
}
40-
```
25+
### 2. 云函数创建与配置
26+
1. 腾讯云账号开通scf服务 [快速入门](https://cloud.tencent.com/document/product/1154/39271)
27+
2. 创建函数
4128

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

44-
```golang
45-
func main() {
46-
token := token.BotToken(conf.AppID, conf.Token)
47-
api := botgo.NewOpenAPI(token).WithTimeout(3 * time.Second)
48-
ctx := context.Background()
49-
ws, err := api.WS(ctx, nil, "")
50-
if err != nil {
51-
log.Printf("%+v, err:%v", ws, err)
52-
}
53-
54-
// 监听哪类事件就需要实现哪类的 handler,定义:websocket/event_handler.go
55-
var atMessage websocket.ATMessageEventHandler = func(event *dto.WSPayload, data *dto.WSATMessageData) error {
56-
fmt.Println(event, data)
57-
return nil
58-
}
59-
intent := websocket.RegisterHandlers(atMessage)
60-
// 启动 session manager 进行 ws 连接的管理,如果接口返回需要启动多个 shard 的连接,这里也会自动启动多个
61-
botgo.NewSessionManager().Start(ws, token, &intent)
62-
}
63-
```
31+
![create_scf.png](docs/img/create_scf.png)
32+
33+
* 启用"公网访问"、"日志投递"
34+
35+
![turn_internet_access.png](docs/img/turn_internet_access.png)
6436

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

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

69-
sdk 中实现了两个 SessionManager
41+
![get_internet_ip.png](docs/img/get_internet_ip.png)
7042

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

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

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

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

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

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

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

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

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

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

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

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

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

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

105-
提前规划好 kafka 的分片,然后从容的针对逻辑层做水平扩容。或者使用 pulsar(腾讯云上叫 tdmq) 来替代 kafka 避免 rebalance 问题。
73+
![ip_whitlist_setting.png](docs/img/ip_whitlist_setting.png)
74+
75+
### 体验与机器人的对话
76+
77+
给机器人发送消息、富媒体文件,机器人回复消息
78+
79+
## 二、如何使用SDK
80+
81+
```golang
82+
83+
var api openapi.OpenAPI
84+
85+
func main() {
86+
//创建oauth2标准token source
87+
tokenSource := token.NewQQBotTokenSource(
88+
&token.QQBotCredentials{
89+
AppID: "",
90+
AppSecret: "",
91+
})
92+
//启动自动刷新access token协程
93+
if err = token.StartRefreshAccessToken(ctx, tokenSource); err != nil {
94+
log.Fatalln(err)
95+
}
96+
// 初始化 openapi,正式环境
97+
api = botgo.NewOpenAPI(credentials.AppID, tokenSource).WithTimeout(5 * time.Second).SetDebug(true)
98+
// 注册事件处理函数
99+
_ = event.RegisterHandlers(
100+
// 注册c2c消息处理函数
101+
C2CMessageEventHandler(),
102+
)
103+
//注册回调处理函数
104+
http.HandleFunc(path_, func (writer http.ResponseWriter, request *http.Request) {
105+
webhook.HTTPHandler(writer, request, credentials)
106+
})
107+
// 启动http服务监听端口
108+
if err = http.ListenAndServe(fmt.Sprintf("%s:%d", host_, port_), nil); err != nil {
109+
log.Fatal("setup server fatal:", err)
110+
}
111+
}
112+
113+
// C2CMessageEventHandler 实现处理 at 消息的回调
114+
func C2CMessageEventHandler() event.C2CMessageEventHandler {
115+
return func(event *dto.WSPayload, data *dto.WSC2CMessageData) error {
116+
//TODO use api do sth.
117+
return nil
118+
}
119+
}
120+
```
106121

107-
## 、SDK 开发说明
122+
## 、SDK 开发说明 (Deprecated)
108123

109-
请查看[开发说明](./DEVELOP.md)
124+
请查看: [开发说明](./DEVELOP.md)
110125

111-
## 、加入官方社区
126+
## 、加入官方社区
112127

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

115-
![开发者社区](https://mpqq.gtimg.cn/privacy/qq_guild_developer.png)
130+
![开发者社区](https://mpqq.gtimg.cn/privacy/qq_guild_developer.png)

botgo.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import (
66
"github.com/tencent-connect/botgo/log"
77
"github.com/tencent-connect/botgo/openapi"
88
v1 "github.com/tencent-connect/botgo/openapi/v1"
9-
"github.com/tencent-connect/botgo/token"
109
"github.com/tencent-connect/botgo/websocket/client"
10+
"golang.org/x/oauth2"
1111
)
1212

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

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

3939
// NewSandboxOpenAPI 创建测试环境的 openapi 实例
40-
func NewSandboxOpenAPI(token *token.Manager) openapi.OpenAPI {
41-
return openapi.DefaultImpl.Setup(token, true)
40+
func NewSandboxOpenAPI(appID string, tokenSource oauth2.TokenSource) openapi.OpenAPI {
41+
return openapi.DefaultImpl.Setup(appID, tokenSource, true)
4242
}

constant/constant.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Package constant 常量定义
22
package constant
33

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

77
// APIDomain api domain
88
var APIDomain = "https://api.sgroup.qq.com"

doc/img/copy-config-yaml.png

-39.9 KB
Binary file not shown.

doc/img/type-in-app-info.png

-8.42 KB
Binary file not shown.
File renamed without changes.

docs/img/chat_with_bot.png

235 KB
Loading

docs/img/copy-config-yaml.png

17.7 KB
Loading

docs/img/copy_scf_addr.png

123 KB
Loading

docs/img/create_bot.png

72.4 KB
Loading

docs/img/create_scf.png

247 KB
Loading

docs/img/create_secret.png

69.3 KB
Loading

docs/img/feedback_bot.png

3.8 KB
Loading
File renamed without changes.

docs/img/get_internet_ip.png

29 KB
Loading

docs/img/ip_whitlist_setting.png

93.2 KB
Loading

docs/img/qq_bot_demo.png

3.84 KB
Loading
File renamed without changes.
File renamed without changes.

docs/img/sandbox_setting.png

92.5 KB
Loading

docs/img/scf_setting.png

59.7 KB
Loading

docs/img/turn_internet_access.png

97.2 KB
Loading

docs/img/type-in-app-info.png

8.29 KB
Loading

docs/img/upload_scf_zip.png

93.9 KB
Loading

docs/img/webhook_setting.png

107 KB
Loading

dto/friend_add.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package dto
22

33
// C2CFriendData c2c 好友事件信息
44
type C2CFriendData struct {
5-
OpenId string `json:"openid"`
5+
OpenID string `json:"openid"`
66
Timestamp int `json:"timestamp"` // 添加/删除机器人好友时间戳
77
Nick string `json:"nick"` // 待事件链路补充
88
Avatar string `json:"avatar"` // 待事件链路补充

dto/interaction.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type Interaction struct {
1212
ChannelID string `json:"channel_id,omitempty"` // 子频道 ID
1313
Version uint32 `json:"version,omitempty"` // 版本,默认为 1
1414
GroupOpenID string `json:"group_openid,omitempty"` // 群OpenID
15-
ChatType uint32 `json:"chat_type,omitempty"` // 按钮场景类型 频道:0 群:1 c2c:2,改成optional为了区分0和没有值
15+
ChatType uint32 `json:"chat_type,omitempty"` // 0: 频道, 1: 群, 2: c2c
1616
Scene string `json:"scene,omitempty"` // 场景 c2c/group/guild
1717
UserOpenID string `json:"user_openid,omitempty"` // 用户ID
1818
Timestamp string `json:"timestamp,omitempty"` // 时间戳

dto/keyboard/keyboard.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,11 @@ type Permission struct {
9898
// TemplateID 对模板id的封装,兼容官方模板和自定义模板
9999
type TemplateID struct {
100100
// 这两个字段互斥,只填入一个
101-
TemplateId uint32 `json:"template_id,omitempty"` // 官方提供的模板id
102-
CustomTemplateId string `json:"custom_template_id,omitempty"` // 自定义模板
101+
TemplateID uint32 `json:"template_id,omitempty"` // 官方提供的模板id
102+
CustomTemplateID string `json:"custom_template_id,omitempty"` // 自定义模板
103103
}
104104

105105
// SubscribeData 订阅按钮数据
106106
type SubscribeData struct {
107-
TemplateIds []*TemplateID `json:"template_ids,omitempty"` // 订阅按钮对应的模板id列表
107+
TemplateIDs []*TemplateID `json:"template_ids,omitempty"` // 订阅按钮对应的模板id列表
108108
}

dto/message_create.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import "github.com/tencent-connect/botgo/dto/keyboard"
66
type SendType int
77

88
const (
9-
Text SendType = 1 // 文字消息
10-
RichMedia SendType = 2 // 富媒体类消息
9+
Text SendType = 1 // Text 文字消息
10+
RichMedia SendType = 2 // RichMedia 富媒体类消息
1111
)
1212

1313
// APIMessage 消息结构接口
@@ -65,7 +65,7 @@ type MessageToCreate struct {
6565
EventID string `json:"event_id,omitempty"` // 要回复的事件id, 逻辑同MsgID
6666
Timestamp int64 `json:"timestamp,omitempty"` //TODO delete this
6767
MsgSeq uint32 `json:"msg_seq,omitempty"` // 机器人对于回复一个msg_id或者event_id的消息序号,指定后根据这个字段和msg_id或者event_id进行去重
68-
SubscribeId string `json:"subscribe_id,omitempty"` // 订阅id,发送订阅消息时使用
68+
SubscribeID string `json:"subscribe_id,omitempty"` // 订阅id,发送订阅消息时使用
6969
InputNotify *InputNotify `json:"input_notify,omitempty"` // 输入状态状态信息
7070
Media *MediaInfo `json:"media,omitempty"` // 富媒体信息
7171
PromptKeyboard *PromptKeyboard `json:"prompt_keyboard,omitempty"` // 消息扩展信息
@@ -158,11 +158,11 @@ type SettingGuide struct {
158158

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

165165
// MediaInfo 富媒体信息
166166
type MediaInfo struct {
167-
FileInfo []byte `json:"file_info,omitempty"` //富媒体文件信息,通过上传接口取得
167+
FileInfo []byte `json:"file_info,omitempty"` // 富媒体文件信息,通过上传接口取得
168168
}

dto/webhook.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package dto
2+
3+
// WHValidationReq 机器人回调验证请求Data
4+
type WHValidationReq struct {
5+
PlainToken string `json:"plain_token"`
6+
EventTs string `json:"event_ts"`
7+
}
8+
9+
// WHValidationRsp 机器人回调验证响应结果
10+
type WHValidationRsp struct {
11+
PlainToken string `json:"plain_token"`
12+
Signature string `json:"signature"`
13+
}

dto/websocket.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package dto
33
import (
44
"fmt"
55

6-
"github.com/tencent-connect/botgo/token"
6+
"golang.org/x/oauth2"
77
)
88

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

3030
// Session 连接的 session 结构,包括链接的所有必要字段
3131
type Session struct {
32-
ID string
33-
URL string
34-
TokenManager *token.Manager
35-
Intent Intent
36-
LastSeq uint32
37-
Shards ShardConfig
32+
ID string
33+
URL string
34+
TokenSource oauth2.TokenSource
35+
Intent Intent
36+
LastSeq uint32
37+
Shards ShardConfig
38+
39+
AppID string
3840
}
3941

4042
// String 输出session字符串

dto/websocket_opcode.go

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
WSHello
1919
WSHeartbeatAck
2020
HTTPCallbackAck
21+
HTTPCallbackValidation
2122
)
2223

2324
// opMeans op 对应的含义字符串标识

examples/README.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# QQ机器人examples
2+
3+
## 示例说明
4+
1. apitest 主要演示api调用方法,测试前应用实际的ID替换用例中的ID(user_id、guild_id等)
5+
2. custom-filter 通过自定义 filter 功能,实现自定义链路跟踪 ID,上报模调监控等。
6+
3. custom-logger 主要演示实现自定义logger的方法
7+
4. receive-and-send 演示简单的机器人服务端的实现方法及如何通过腾讯云函数部署。
8+
5. simulate-callback-request 模拟回调请求。开发者完成服务部署前可通过此工具模拟回调请求,实现业务逻辑。

examples/apitest/config.yaml.demo

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
# 在这个配置文件中补充你的 appid 和 bot token,并修改文件名为 config.yaml
1+
# 在这个配置文件中补充你的 appid 和 secret,并修改文件名为 config.yaml
22
appid :
3-
token :
3+
secret :

0 commit comments

Comments
 (0)