├── .github ├── ISSUE_TEMPLATE │ └── ------.md └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── .golangci.yml ├── DEVELOP.md ├── LICENSE ├── README.md ├── botgo.go ├── botgo_test.go ├── constant └── constant.go ├── docs └── img │ ├── add-robot.png │ ├── chat_with_bot.png │ ├── copy-config-yaml.png │ ├── copy_scf_addr.png │ ├── create_bot.png │ ├── create_scf.png │ ├── create_secret.png │ ├── feedback_bot.png │ ├── find-app-acc.png │ ├── get_internet_ip.png │ ├── ip_whitlist_setting.png │ ├── qq_bot_demo.png │ ├── robot-reply.png │ ├── robot-start-console.png │ ├── sandbox_setting.png │ ├── scf_setting.png │ ├── turn_internet_access.png │ ├── type-in-app-info.png │ ├── upload_scf_zip.png │ └── webhook_setting.png ├── dto ├── README.md ├── announces.go ├── api_permissions.go ├── audio.go ├── channel.go ├── channel_permissions.go ├── direct_message.go ├── dto.go ├── duration.go ├── emoji.go ├── enter_aio.go ├── forum.go ├── friend_add.go ├── guild.go ├── http_gateway.go ├── interaction.go ├── interaction_search.go ├── keyboard │ └── keyboard.go ├── member.go ├── message.go ├── message │ └── message.go ├── message_ark.go ├── message_audit.go ├── message_create.go ├── message_delete.go ├── message_reaction.go ├── message_setting.go ├── mute.go ├── pager.go ├── pins.go ├── role.go ├── schedule.go ├── subscribe.go ├── timestamp.go ├── user.go ├── webhook.go ├── websocket.go ├── websocket_event.go ├── websocket_event_test.go ├── websocket_intent.go ├── websocket_opcode.go └── websocket_payload.go ├── errs └── err.go ├── event ├── event.go ├── register.go └── register_test.go ├── examples ├── README.md ├── apitest │ ├── announces_test.go │ ├── api_permissions_test.go │ ├── channel_test.go │ ├── direct_message_test.go │ ├── guild_test.go │ ├── interaction_test.go │ ├── main_test.go │ ├── member_test.go │ ├── message_reaction_test.go │ ├── message_setting_test.go │ ├── message_test.go │ ├── mute_test.go │ ├── permission_test.go │ ├── pins_test.go │ ├── role_test.go │ └── schedule_test.go ├── config.yaml.demo ├── custom-filter │ ├── README.md │ └── main.go ├── custom-logger │ ├── README.md │ ├── logger.go │ ├── logger_test.go │ └── main.go ├── go.mod ├── go.sum ├── receive-and-send │ ├── Makefile │ ├── README.md │ ├── cmd_action.go │ ├── forum.go │ ├── ip.go │ ├── main.go │ ├── process.go │ └── scf_bootstrap └── simulate-callback-request │ └── main.go ├── go.mod ├── go.sum ├── interaction ├── search │ ├── simulate_search.go │ └── simulate_search_test.go ├── signature │ ├── interaction.go │ └── interaction_test.go └── webhook │ ├── webhook.go │ └── webhook_test.go ├── log ├── console.go ├── console_test.go ├── log.go ├── log_test.go └── logger.go ├── oauth.go ├── openapi ├── filter.go ├── iface.go ├── openapi.go ├── options │ └── options.go ├── type.go ├── v1 │ ├── README.md │ ├── announces.go │ ├── api_permissions.go │ ├── audio.go │ ├── channel_permissions.go │ ├── channels.go │ ├── direct_message.go │ ├── guilds.go │ ├── interaction.go │ ├── me.go │ ├── member.go │ ├── message.go │ ├── message_reaction.go │ ├── message_setting.go │ ├── mute.go │ ├── openapi.go │ ├── pins.go │ ├── resource.go │ ├── role.go │ ├── schedule.go │ ├── webhook.go │ └── ws.go └── version.go ├── register.go ├── scripts └── git_hooks │ ├── commit-msg │ ├── install.sh │ ├── pre-commit │ └── prepare-commit-msg ├── session_manager.go ├── sessions ├── local │ └── local.go ├── manager │ ├── manager.go │ └── manager_test.go ├── remote │ ├── README.md │ ├── errors.go │ ├── lock │ │ ├── lock.go │ │ └── lock_test.go │ ├── option.go │ ├── remote.go │ └── session_producer.go └── sessions.go ├── token └── token_source.go ├── version └── version.go └── websocket ├── client ├── README.md └── client.go ├── iface.go └── websocket.go /.github/ISSUE_TEMPLATE/------.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 平台接口需求 3 | about: 对于平台的接口需求使用此模板提 issue 4 | title: 接口需求反馈 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 想要的能力是 11 | 12 | ## 基于想要的能力,能够实现的是 13 | 14 | ## 如果未提供该能力,造成的影响是 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Run golangci-lint 22 | uses: golangci/golangci-lint-action@v2.5.2 23 | with: 24 | # version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 25 | version: latest 26 | # the token is used for fetching patch of a pull request to show only new issues 27 | github-token: ${{ github.token }} 28 | 29 | - name: Build 30 | run: go build -v ./... 31 | 32 | - name: Setup Redis 33 | uses: zhulik/redis-action@1.1.0 34 | 35 | - name: Test 36 | run: go test -v ./... 37 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '41 18 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | *.out 12 | *.log 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | .idea 17 | # 用于存储demo中使用的 token,不提交到仓库 18 | examples/apitest/config.yaml.demo 19 | config.yaml 20 | demo 21 | 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # details in https://golangci-lint.run/usage/configuration/ 2 | run: 3 | timeout: 3m 4 | 5 | output: 6 | sort-results: true 7 | 8 | linters-settings: 9 | funlen: 10 | lines: 80 11 | statements: 80 12 | goconst: 13 | min-len: 2 14 | min-occurrences: 2 15 | gocyclo: 16 | min-complexity: 10 17 | goimports: 18 | local-prefixes: tencent-connect/ 19 | govet: 20 | check-shadowing: true 21 | lll: 22 | line-length: 120 23 | tab-width: 4 24 | gocritic: 25 | enabled-checks: 26 | - nestingReduce 27 | settings: 28 | nestingReduce: 29 | bodyWidth: 5 30 | 31 | linters: 32 | disable-all: true 33 | enable: 34 | - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. 35 | - deadcode 36 | - funlen 37 | - goconst 38 | - gocyclo 39 | - gofmt 40 | - ineffassign 41 | - staticcheck 42 | - structcheck # 当非导出结构嵌入另一个结构, 前一个结构被使用就不会监测到, 这个需要每个业务自己屏蔽 43 | - typecheck 44 | - goimports 45 | - gosimple 46 | - govet 47 | - lll 48 | - rowserrcheck 49 | - errcheck 50 | - unused 51 | - varcheck 52 | - gocritic 53 | - dupl 54 | - ifshort 55 | - whitespace 56 | - bodyclose 57 | - sqlclosecheck 58 | 59 | issues: 60 | exclude-use-default: true 61 | 62 | # The list of ids of default excludes to include or disable. By default it's empty. 63 | # 为了避免 exclude default 导致一些问题被隐藏,所以需要自己声明不想要排除的问题,在 https://golangci-lint.run/usage/configuration/ 搜索 --exclude-use-default 64 | include: 65 | - EXC0004 # govet (possible misuse of unsafe.Pointer|should have signature) 66 | - EXC0005 # staticcheck ineffective break statement. Did you mean to break out of the outer loop 67 | - EXC0011 # stylecheck (comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form) 68 | - EXC0012 # revive 69 | - EXC0013 # revive 70 | - EXC0014 # revive 71 | - EXC0015 # revive 72 | 73 | exclude-rules: 74 | - path: _test\.go 75 | linters: 76 | - funlen # 规范说单测函数,单个函数可以到160行,但是工具不好做区分处理,这里就直接不检查单测的函数长度 77 | - lll 78 | - goconst 79 | - gocyclo 80 | - golint 81 | - staticcheck 82 | - revive 83 | - dupl 84 | - ifshort 85 | - linters: 86 | - staticcheck 87 | text: "SA1019: package github.com/golang/protobuf" # 它会说 'SA1019: package github.com/golang/protobuf/proto is deprecated: Use the "google.golang.org/protobuf/proto" package instead.', 但是这个库更更新很稳定 88 | - linters: 89 | - staticcheck 90 | text: "SA6002: argument should be pointer-like to avoid allocations" # sync.pool.Put(buf), slice `var buf []byte` will tiger this 91 | - linters: 92 | - staticcheck 93 | text: "SA3000: TestMain should call os.Exit to set exit code" 94 | - linters: 95 | - lll 96 | source: "^//go:generate " # Exclude lll issues for long lines with go:generate 97 | 98 | max-same-issues: 0 99 | new: false 100 | max-issues-per-linter: 0 101 | fix: false # 如果设置为 true,会导致一些问题不被扫描出来,而且也不会自动 fix,而且在ci上跑,会莫名的报错 102 | 103 | service: 104 | golangci-lint-version: 1.23.x 105 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # 开发说明 2 | 3 | ## 一、SDK 的设计模式 4 | 5 | 分为三个主要模块 6 | 7 | - openapi 用于请求 http 的 openapi 8 | - websocket 用于监听事件网关,接收事件消息 9 | - sessions 实现 session_manager 接口,用于管理 websocket 实例的新建,重连等 10 | 11 | openapi 接口定义:`openapi/iface.go`,同时 sdk 中提供了 v1 的实现,后续 openapi 有新版本的时候,可以增加对应新版本的实现。 12 | websocket 接口定义:`websocket/iface.go`,sdk 实现了默认版本的 client,如果开发者有更好的实现,也可以进行替换 13 | 14 | 15 | ## 二、SDK 增加新接口or新事件开发说明 16 | 17 | ### 1. 如何增加新的 openapi 接口调用方法(预计耗时3min) 18 | 19 | - Step1: dto 中增加对应的对象 20 | - Step2: openapi 的接口定义中,增加新方法的定义 21 | - Step3:在 openapi 的实现中,实现这个新的方法 22 | 23 | ### 2. 如何增加新的 websocket 事件(预计耗时10min) 24 | 25 | - Step1: dto 中增加对应的对象 `dto/websocket_payload.go` 26 | - Step2: 新增 intent,以及事件对应的 intent(如果有)`dto/intents.go` 27 | - Step3: 新增事件类型与 intent 的关系 `dto/websocket_event.go` 28 | - Step4: 新增 event handler 类型,并在注册方法中补充断言,`websocket/event_handler.go` 29 | - Step5:websocket 的具体实现中,针对收到的 message 进行解析,判断 type 是否符合新添加的时间类型,解析为 dto 之后,调用对应的 handler `websocket/client/event.go` 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BotGo 2 | 3 | QQ频道机器人,官方 GOLANG SDK。 4 | 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/tencent-connect/botgo.svg)](https://pkg.go.dev/github.com/tencent-connect/botgo) 6 | [![Examples](https://img.shields.io/badge/BotGo-examples-yellowgreen)](https://github.com/tencent-connect/botgo/tree/master/examples) 7 | 8 | ## 注意事项 9 | 1. websocket 事件推送链路将在24年年底前逐步下线,后续官方不再维护。 10 | 2. 新的webhook事件回调链路目前在灰度验证,灰度用户可体验通过页面配置事件监听及回调地址。如未在灰度范围,可联系QQ机器人反馈助手开通。 11 | 12 | ![反馈机器人](docs/img/feedback_bot.png) 13 | 14 | 灰度期间,原有机器人仍可使用websocket事件链路接收事件推送。 15 | ## 一、quick start 16 | ### 1. QQ机器人创建与配置 17 | 1. 创建开发者账号,创建QQ机器人 [QQ机器人开放平台](https://q.qq.com/qqbot) 18 | 19 | ![create_bot.png](docs/img/create_bot.png) 20 | 21 | 2. 配置沙箱成员 (QQ机器人上线前,仅沙箱环境可访问)。新创建机器人会默认将创建者加入沙箱环境。 22 | 23 | ![sandbox_setting.png](docs/img/sandbox_setting.png) 24 | 25 | ### 2. 云函数创建与配置 26 | 1. 腾讯云账号开通scf服务 [快速入门](https://cloud.tencent.com/document/product/1154/39271) 27 | 2. 创建函数 28 | 29 | * 选择模板 30 | 31 | ![create_scf.png](docs/img/create_scf.png) 32 | 33 | * 启用"公网访问"、"日志投递" 34 | 35 | ![turn_internet_access.png](docs/img/turn_internet_access.png) 36 | 37 | * 编辑云函数,启用"固定公网出口IP" (QQ机器人需要配置IP白名单,仅白名单内服务器/容器可访问OpenAPI) 38 | 39 | ![scf_setting.png](docs/img/scf_setting.png) 40 | 41 | ![get_internet_ip.png](docs/img/get_internet_ip.png) 42 | 43 | ### 3. 使用示例构建、上传云函数部署包 44 | 1. 打开 examples/receive-and-send 45 | 2. 复制 config.yaml.demo -> config.yaml 46 | 47 | ![img.png](docs/img/copy-config-yaml.png) 48 | 49 | 3. 登录[开发者管理端](https://q.qq.com),将BotAppID和机器人秘钥分别填入config.yaml中的appid和secret字段 50 | 51 | ![find-app-acc.png](docs/img/find-app-acc.png) 52 | 53 | ![type-in-app-info.png](docs/img/type-in-app-info.png) 54 | 55 | 4. 执行Makefile中build指令 56 | 5. 将config.yaml、scf_bootstrap、qqbot-demo(二进制文件)打包,上传至云函数 57 | 58 | ![上传压缩包](docs/img/upload_scf_zip.png) 59 | 60 | ### 4.配置QQ机器人事件监听、回调地址、IP白名单 61 | 62 | 1. 复制云函数地址 + "/qqbot"后缀,填入回调地址输入框。点击确认。 63 | 64 | ![img.png](docs/img/copy_scf_addr.png) 65 | 66 | 2. 勾选 C2C_MESSAGE_CREATE 事件。点击确认。 67 | 68 | ![webhook配置](docs/img/webhook_setting.png) 69 | 70 | 71 | 3. 将云函数 "固定公网出口IP" 配置到IP白名单中) 72 | 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 | ``` 121 | 122 | ## 三、SDK 开发说明 (Deprecated) 123 | 124 | 请查看: [开发说明](./DEVELOP.md) 125 | 126 | ## 四、加入官方社区 127 | 128 | 欢迎扫码加入 **QQ 频道开发者社区**。 129 | 130 | ![开发者社区](https://mpqq.gtimg.cn/privacy/qq_guild_developer.png) -------------------------------------------------------------------------------- /botgo.go: -------------------------------------------------------------------------------- 1 | // Package botgo 是一个QQ频道机器人 sdk 的 golang 实现 2 | package botgo 3 | 4 | import ( 5 | "github.com/tencent-connect/botgo/errs" 6 | "github.com/tencent-connect/botgo/log" 7 | "github.com/tencent-connect/botgo/openapi" 8 | v1 "github.com/tencent-connect/botgo/openapi/v1" 9 | "github.com/tencent-connect/botgo/websocket/client" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | func init() { 14 | v1.Setup() // 注册 v1 接口 15 | client.Setup() // 注册 websocket client 实现 16 | } 17 | 18 | // NewSessionManager 获得 session manager 实例 19 | func NewSessionManager() SessionManager { 20 | return defaultSessionManager 21 | } 22 | 23 | // SelectOpenAPIVersion 指定使用哪个版本的 api 实现,如果不指定,sdk将默认使用第一个 setup 的 api 实现 24 | func SelectOpenAPIVersion(version openapi.APIVersion) error { 25 | if _, ok := openapi.VersionMapping[version]; !ok { 26 | log.Errorf("version %v openapi not found or setup", version) 27 | return errs.ErrNotFoundOpenAPI 28 | } 29 | openapi.DefaultImpl = openapi.VersionMapping[version] 30 | return nil 31 | } 32 | 33 | // NewOpenAPI 创建新的 openapi 实例,会返回当前的 openapi 实现的实例 34 | // 如果需要使用其他版本的实现,需要在调用这个方法之前调用 SelectOpenAPIVersion 方法 35 | func NewOpenAPI(appID string, tokenSource oauth2.TokenSource) openapi.OpenAPI { 36 | return openapi.DefaultImpl.Setup(appID, tokenSource, false) 37 | } 38 | 39 | // NewSandboxOpenAPI 创建测试环境的 openapi 实例 40 | func NewSandboxOpenAPI(appID string, tokenSource oauth2.TokenSource) openapi.OpenAPI { 41 | return openapi.DefaultImpl.Setup(appID, tokenSource, true) 42 | } 43 | -------------------------------------------------------------------------------- /botgo_test.go: -------------------------------------------------------------------------------- 1 | package botgo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tencent-connect/botgo/openapi" 7 | ) 8 | 9 | func TestUseOpenAPIVersion(t *testing.T) { 10 | type args struct { 11 | version openapi.APIVersion 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | wantErr bool 17 | }{ 18 | { 19 | "not found", args{version: 0}, true, 20 | }, 21 | { 22 | "v1 found", args{version: 1}, false, 23 | }, 24 | } 25 | for _, tt := range tests { 26 | t.Run(tt.name, func(t *testing.T) { 27 | if err := SelectOpenAPIVersion(tt.args.version); (err != nil) != tt.wantErr { 28 | t.Errorf("SelectOpenAPIVersion() error = %v, wantErr %v", err, tt.wantErr) 29 | } 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /constant/constant.go: -------------------------------------------------------------------------------- 1 | // Package constant 常量定义 2 | package constant 3 | 4 | // HeaderTraceID 机器人openapi返回的链路追踪ID 5 | const HeaderTraceID = "X-Tps-trace-ID" 6 | 7 | // APIDomain api domain 8 | var APIDomain = "https://api.sgroup.qq.com" 9 | 10 | // SandBoxAPIDomain sandbox domain 11 | var SandBoxAPIDomain = "https://sandbox.api.sgroup.qq.com" 12 | 13 | // TokenDomain token domain 14 | var TokenDomain = "https://bots.qq.com" 15 | -------------------------------------------------------------------------------- /docs/img/add-robot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/add-robot.png -------------------------------------------------------------------------------- /docs/img/chat_with_bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/chat_with_bot.png -------------------------------------------------------------------------------- /docs/img/copy-config-yaml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/copy-config-yaml.png -------------------------------------------------------------------------------- /docs/img/copy_scf_addr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/copy_scf_addr.png -------------------------------------------------------------------------------- /docs/img/create_bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/create_bot.png -------------------------------------------------------------------------------- /docs/img/create_scf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/create_scf.png -------------------------------------------------------------------------------- /docs/img/create_secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/create_secret.png -------------------------------------------------------------------------------- /docs/img/feedback_bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/feedback_bot.png -------------------------------------------------------------------------------- /docs/img/find-app-acc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/find-app-acc.png -------------------------------------------------------------------------------- /docs/img/get_internet_ip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/get_internet_ip.png -------------------------------------------------------------------------------- /docs/img/ip_whitlist_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/ip_whitlist_setting.png -------------------------------------------------------------------------------- /docs/img/qq_bot_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/qq_bot_demo.png -------------------------------------------------------------------------------- /docs/img/robot-reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/robot-reply.png -------------------------------------------------------------------------------- /docs/img/robot-start-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/robot-start-console.png -------------------------------------------------------------------------------- /docs/img/sandbox_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/sandbox_setting.png -------------------------------------------------------------------------------- /docs/img/scf_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/scf_setting.png -------------------------------------------------------------------------------- /docs/img/turn_internet_access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/turn_internet_access.png -------------------------------------------------------------------------------- /docs/img/type-in-app-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/type-in-app-info.png -------------------------------------------------------------------------------- /docs/img/upload_scf_zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/upload_scf_zip.png -------------------------------------------------------------------------------- /docs/img/webhook_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencent-connect/botgo/fe31c0dfe469001e0f783d2f07e7de7bd08b403f/docs/img/webhook_setting.png -------------------------------------------------------------------------------- /dto/README.md: -------------------------------------------------------------------------------- 1 | ## dto 2 | 3 | 与 openapi/websocket 通信时所使用的对象。 -------------------------------------------------------------------------------- /dto/announces.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // Announces 公告对象 4 | type Announces struct { 5 | // 频道 ID 6 | GuildID string `json:"guild_id"` 7 | // 子频道 ID 8 | ChannelID string `json:"channel_id"` 9 | // 用来创建公告的消息 ID 10 | MessageID string `json:"message_id"` 11 | // 公告类别 0:成员公告,1:欢迎公告,默认为成员公告 12 | AnnouncesType uint32 `json:"announces_type"` 13 | // 推荐子频道详情数组 14 | RecommendChannels []RecommendChannel `json:"recommend_channels,omitempty"` 15 | } 16 | 17 | // ChannelAnnouncesToCreate 创建子频道公告结构体定义 18 | type ChannelAnnouncesToCreate struct { 19 | MessageID string `json:"message_id"` // 用来创建公告的消息ID 20 | } 21 | 22 | // GuildAnnouncesToCreate 创建频道全局公告结构体定义 23 | type GuildAnnouncesToCreate struct { 24 | ChannelID string `json:"channel_id"` // 用来创建公告的子频道 ID 25 | MessageID string `json:"message_id"` // 用来创建公告的消息 ID 26 | AnnouncesType uint32 `json:"announces_type"` // 公告类别 0:成员公告,1:欢迎公告,默认为成员公告 27 | RecommendChannels []RecommendChannel `json:"recommend_channels"` // 推荐子频道详情列表 28 | } 29 | 30 | // RecommendChannel 推荐子频道详情 31 | type RecommendChannel struct { 32 | ChannelID string `json:"channel_id"` // 子频道 ID 33 | Introduce string `json:"introduce"` // 推荐语 34 | } 35 | -------------------------------------------------------------------------------- /dto/api_permissions.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // APIPermissions API 权限列表对象 4 | type APIPermissions struct { 5 | APIList []*APIPermission `json:"apis,omitempty"` // API 权限列表 6 | } 7 | 8 | // APIPermission API 权限对象 9 | type APIPermission struct { 10 | Path string `json:"path,omitempty"` // API 接口名,例如 /guilds/{guild_id}/members/{user_id} 11 | Method string `json:"method,omitempty"` // 请求方法,例如 GET 12 | Desc string `json:"desc,omitempty"` // API 接口名称,例如 获取频道信 13 | AuthStatus int `json:"auth_status,omitempty"` // 授权状态,auth_stats 为 1 时已授权 14 | } 15 | 16 | // APIPermissionDemandIdentify API 权限需求标识对象 17 | type APIPermissionDemandIdentify struct { 18 | Path string `json:"path,omitempty"` // API 接口名,例如 /guilds/{guild_id}/members/{user_id} 19 | Method string `json:"method,omitempty"` // 请求方法,例如 GET 20 | } 21 | 22 | // APIPermissionDemand 接口权限需求对象 23 | type APIPermissionDemand struct { 24 | GuildID string `json:"guild_id,omitempty"` // 频道 ID 25 | ChannelID string `json:"channel_id,omitempty"` // 子频道 ID 26 | APIIdentify *APIPermissionDemandIdentify `json:"api_identify,omitempty"` // 权限接口唯一标识 27 | Title string `json:"title,omitempty"` // 接口权限链接中的接口权限描述信息 28 | Desc string `json:"desc,omitempty"` // 接口权限链接中的机器人可使用功能的描述信息 29 | } 30 | 31 | // APIPermissionDemandToCreate 创建频道 API 接口权限授权链接结构体定义 32 | type APIPermissionDemandToCreate struct { 33 | ChannelID string `json:"channel_id"` // 子频道 ID 34 | APIIdentify *APIPermissionDemandIdentify `json:"api_identify,omitempty"` // 接口权限链接中的接口权限描述信息 35 | Desc string `json:"desc"` // 接口权限链接中的机器人可使用功能的描述信息 36 | } 37 | -------------------------------------------------------------------------------- /dto/audio.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // AudioStatus 音频状态 4 | type AudioStatus uint32 5 | 6 | // 音频状态 7 | const ( 8 | AudioStatusStart = iota 9 | AudioStatusPause 10 | AudioStatusResume 11 | AudioStatusStop 12 | ) 13 | 14 | // AudioControl 音频控制对象 15 | type AudioControl struct { 16 | URL string `json:"audio_url"` 17 | Text string `json:"text"` 18 | Status AudioStatus `json:"status"` 19 | } 20 | 21 | // AudioAction 音频动作 22 | type AudioAction struct { 23 | GuildID string `json:"guild_id"` 24 | ChannelID string `json:"channel_id"` 25 | URL string `json:"audio_url"` 26 | Text string `json:"text"` 27 | } 28 | -------------------------------------------------------------------------------- /dto/channel.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // ChannelType 频道类型定义 4 | type ChannelType int 5 | 6 | // 子频道类型定义 7 | const ( 8 | ChannelTypeText ChannelType = iota 9 | _ 10 | ChannelTypeVoice 11 | _ 12 | ChannelTypeCategory 13 | ChannelTypeLive = 10000 + iota // 直播子频道 14 | ChannelTypeApplication // 应用子频道 15 | ChannelTypeForum // 论坛子频道 16 | ) 17 | 18 | // ChannelSubType 子频道子类型定义 19 | type ChannelSubType int 20 | 21 | // 子频道子类型定义 22 | const ( 23 | ChannelSubTypeChat ChannelSubType = iota // 闲聊,默认子类型 24 | ChannelSubTypeNotice // 公告 25 | ChannelSubTypeGuide // 攻略 26 | ChannelSubTypeTeamGame // 开黑 27 | ) 28 | 29 | // ChannelPrivateType 频道可见性类型定义 30 | type ChannelPrivateType int 31 | 32 | // 频道可见性类型定义 33 | const ( 34 | ChannelPrivateTypePublic ChannelPrivateType = iota // 公开频道 35 | ChannelPrivateTypeOnlyAdmin // 群主管理员可见 36 | ChannelPrivateTypeAdminAndMember // 群主管理员+指定成员 37 | ) 38 | 39 | // SpeakPermissionType 发言权限类型定义 40 | type SpeakPermissionType int 41 | 42 | // 发言权限类型定义 43 | const ( 44 | SpeakPermissionTypePublic SpeakPermissionType = iota + 1 // 公开发言权限 45 | SpeakPermissionTypeAdminAndMember // 指定成员可发言 46 | ) 47 | 48 | // Channel 频道结构定义 49 | type Channel struct { 50 | // 频道ID 51 | ID string `json:"id"` 52 | // 群ID 53 | GuildID string `json:"guild_id"` 54 | ChannelValueObject 55 | } 56 | 57 | // ChannelValueObject 频道的值对象部分 58 | type ChannelValueObject struct { 59 | // 频道名称 60 | Name string `json:"name,omitempty"` 61 | // 频道类型 62 | Type ChannelType `json:"type,omitempty"` 63 | // 排序位置 64 | Position int64 `json:"position,omitempty"` 65 | // 父频道的ID 66 | ParentID string `json:"parent_id,omitempty"` 67 | // 拥有者ID 68 | OwnerID string `json:"owner_id,omitempty"` 69 | // 子频道子类型 70 | SubType ChannelSubType `json:"sub_type,omitempty"` 71 | // 子频道可见性类型 72 | PrivateType ChannelPrivateType `json:"private_type,omitempty"` 73 | // 创建私密子频道的时候,同时带上 userID,能够将这些成员添加为私密子频道的成员 74 | // 注意:只有创建私密子频道的时候才支持这个参数 75 | PrivateUserIDs []string `json:"private_user_ids,omitempty"` 76 | // 发言权限 77 | SpeakPermission SpeakPermissionType `json:"speak_permission,omitempty"` 78 | // 应用子频道的应用ID,仅应用子频道有效,定义请参考 79 | // [文档](https://bot.q.qq.com/wiki/develop/api/openapi/channel/model.html) 80 | ApplicationID string `json:"application_id,omitempty"` 81 | // 机器人在此频道上拥有的权限, 定义请参考 82 | // [文档](https://bot.q.qq.com/wiki/develop/api/openapi/channel_permissions/model.html#permissions) 83 | Permissions string `json:"permissions,omitempty"` 84 | // 操作人 85 | OpUserID string `json:"op_user_id,omitempty"` 86 | } 87 | -------------------------------------------------------------------------------- /dto/channel_permissions.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // ChannelPermissions 子频道权限 4 | type ChannelPermissions struct { 5 | ChannelID string `json:"channel_id,omitempty"` 6 | UserID string `json:"user_id,omitempty"` 7 | Permissions string `json:"permissions,omitempty"` 8 | } 9 | 10 | // ChannelRolesPermissions 子频道身份组权限 11 | type ChannelRolesPermissions struct { 12 | ChannelID string `json:"channel_id,omitempty"` 13 | RoleID string `json:"role_id,omitempty"` 14 | Permissions string `json:"permissions,omitempty"` 15 | } 16 | 17 | // UpdateChannelPermissions 修改子频道权限参数 18 | type UpdateChannelPermissions struct { 19 | Add string `json:"add,omitempty"` 20 | Remove string `json:"remove,omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /dto/direct_message.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // DirectMessage 私信结构定义,一个 DirectMessage 为两个用户之间的一个私信频道,简写为 DM 4 | type DirectMessage struct { 5 | // 频道ID 6 | GuildID string `json:"guild_id"` 7 | // 子频道id 8 | ChannelID string `json:"channel_id"` 9 | // 私信频道创建的时间戳 10 | CreateTime string `json:"create_time"` 11 | } 12 | 13 | // DirectMessageToCreate 创建私信频道的结构体定义 14 | type DirectMessageToCreate struct { 15 | // 频道ID 16 | SourceGuildID string `json:"source_guild_id"` 17 | // 用户ID 18 | RecipientID string `json:"recipient_id"` 19 | } 20 | -------------------------------------------------------------------------------- /dto/dto.go: -------------------------------------------------------------------------------- 1 | // Package dto 维护了用于与机器人接口通信的数据结构对象。 2 | package dto 3 | -------------------------------------------------------------------------------- /dto/duration.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | // Duration 支持能够直接配置中解析出来 time.Duration 类型的数据 10 | // 需要实现对应类型的 Unmarshaler 接口 11 | type Duration time.Duration 12 | 13 | // UnmarshalJSON 实现json的解析接口 14 | func (d *Duration) UnmarshalJSON(bytes []byte) error { 15 | var s = strings.Trim(string(bytes), "\"'") 16 | t, err := time.ParseDuration(s) 17 | if err != nil { 18 | return fmt.Errorf("failed to parse '%s' to time.Duration: %v", s, err) 19 | } 20 | 21 | *d = Duration(t) 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /dto/emoji.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // Emoji 表情 4 | type Emoji struct { 5 | ID string `json:"id"` 6 | Type int `json:"type"` 7 | } 8 | -------------------------------------------------------------------------------- /dto/enter_aio.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // EnterAIO 进入aio的事件 4 | type EnterAIO struct { 5 | UserOpenid string `json:"user_openid,omitempty"` // 用户openid 6 | FromSource string `json:"from_source,omitempty"` // 进入aio的来源 7 | } 8 | -------------------------------------------------------------------------------- /dto/forum.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // Thread 主题事件主体内容 4 | type Thread struct { 5 | GuildID string `json:"guild_id"` 6 | ChannelID string `json:"channel_id"` 7 | AuthorID string `json:"author_id"` 8 | ThreadInfo ThreadInfo `json:"thread_info"` 9 | } 10 | 11 | // ThreadInfo 主题信息 12 | type ThreadInfo struct { 13 | ThreadID string `json:"thread_id"` 14 | Title string `json:"title"` 15 | Content string `json:"content"` 16 | DateTime string `json:"date_time"` 17 | } 18 | 19 | // Post 帖子事件主体内容 20 | type Post struct { 21 | GuildID string `json:"guild_id"` 22 | ChannelID string `json:"channel_id"` 23 | AuthorID string `json:"author_id"` 24 | PostInfo PostInfo `json:"post_info"` 25 | } 26 | 27 | // PostInfo 帖子内容 28 | type PostInfo struct { 29 | ThreadID string `json:"thread_id"` 30 | PostID string `json:"post_id"` 31 | Content string `json:"content"` 32 | DateTime string `json:"date_time"` 33 | } 34 | 35 | // Reply 回复事件主体内容 36 | type Reply struct { 37 | GuildID string `json:"guild_id"` 38 | ChannelID string `json:"channel_id"` 39 | AuthorID string `json:"author_id"` 40 | ReplyInfo ReplyInfo `json:"reply_info"` 41 | } 42 | 43 | // ReplyInfo 回复内容 44 | type ReplyInfo struct { 45 | ThreadID string `json:"thread_id"` 46 | PostID string `json:"post_id"` 47 | ReplyID string `json:"reply_id"` 48 | Content string `json:"content"` 49 | DateTime string `json:"date_time"` 50 | } 51 | 52 | // ForumAuditResult 帖子审核事件主体内容 53 | type ForumAuditResult struct { 54 | TaskID string `json:"task_id"` 55 | GuildID string `json:"guild_id"` 56 | ChannelID string `json:"channel_id"` 57 | AuthorID string `json:"author_id"` 58 | ThreadID string `json:"thread_id"` 59 | PostID string `json:"post_id"` 60 | ReplyID string `json:"reply_id"` 61 | PublishType uint32 `json:"type"` 62 | Result uint32 `json:"result"` 63 | ErrMsg string `json:"err_msg"` 64 | DateTime string `json:"date_time"` 65 | } 66 | -------------------------------------------------------------------------------- /dto/friend_add.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // C2CFriendData c2c 好友事件信息 4 | type C2CFriendData struct { 5 | OpenID string `json:"openid"` 6 | Timestamp int `json:"timestamp"` // 添加/删除机器人好友时间戳 7 | Nick string `json:"nick"` // 待事件链路补充 8 | Avatar string `json:"avatar"` // 待事件链路补充 9 | } 10 | -------------------------------------------------------------------------------- /dto/guild.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // Guild 频道结构定义 4 | type Guild struct { 5 | // 频道ID(与客户端上看到的频道ID不同) 6 | ID string `json:"id"` 7 | // 频道名称 8 | Name string `json:"name"` 9 | // 频道头像 10 | Icon string `json:"icon"` 11 | // 拥有者ID 12 | OwnerID string `json:"owner_id"` 13 | // 是否为拥有者 14 | IsOwner bool `json:"owner"` 15 | // 成员数量 16 | MemberCount int `json:"member_count"` 17 | // 最大成员数目 18 | MaxMembers int64 `json:"max_members"` 19 | // 频道描述 20 | Desc string `json:"description"` 21 | // 当前用户加入群的时间 22 | // 此字段只在GUILD_CREATE事件中使用 23 | JoinedAt Timestamp `json:"joined_at"` 24 | // 频道列表 25 | Channels []*Channel `json:"channels"` 26 | // 游戏绑定公会区服ID 27 | UnionWorldID string `json:"union_world_id"` 28 | // 游戏绑定公会/战队ID 29 | UnionOrgID string `json:"union_org_id"` 30 | // 操作人 31 | OpUserID string `json:"op_user_id,omitempty"` 32 | } 33 | -------------------------------------------------------------------------------- /dto/http_gateway.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // HTTPIdentity 鉴权数据 4 | type HTTPIdentity struct { 5 | Intents Intent `json:"intents"` 6 | Shards [2]uint32 `json:"shards"` // array of two integers (shard_id, num_shards) 7 | Callback string `json:"callback_url"` 8 | } 9 | 10 | // HTTPReady ready,鉴权后返回 11 | type HTTPReady struct { 12 | Version int `json:"version"` 13 | SessionID string `json:"session_id"` 14 | Bot struct { 15 | ID string `json:"id"` 16 | Username string `json:"username"` 17 | } `json:"bot"` 18 | Shard [2]uint32 `json:"shard"` 19 | } 20 | 21 | // HTTPSession session 对象 22 | type HTTPSession struct { 23 | AppID int64 `json:"app_id"` 24 | SessionID string `json:"session_id"` 25 | CallbackURL string `json:"callback_url"` 26 | Env string `json:"env"` 27 | Intents int64 `json:"intents"` 28 | LastHeartbeatTime string `json:"last_heartbeat_time"` 29 | State string `json:"state"` 30 | Shards [2]int64 `json:"shards"` 31 | } 32 | -------------------------------------------------------------------------------- /dto/interaction.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "encoding/json" 4 | 5 | // Interaction 互动行为对象 6 | type Interaction struct { 7 | ID string `json:"id,omitempty"` // 互动行为唯一标识 8 | ApplicationID string `json:"application_id,omitempty"` // 应用ID 9 | Type InteractionType `json:"type,omitempty"` // 互动类型 10 | Data *InteractionData `json:"data,omitempty"` // 互动数据 11 | GuildID string `json:"guild_id,omitempty"` // 频道 ID 12 | ChannelID string `json:"channel_id,omitempty"` // 子频道 ID 13 | Version uint32 `json:"version,omitempty"` // 版本,默认为 1 14 | GroupOpenID string `json:"group_openid,omitempty"` // 群OpenID 15 | ChatType uint32 `json:"chat_type,omitempty"` // 0: 频道, 1: 群, 2: c2c 16 | Scene string `json:"scene,omitempty"` // 场景 c2c/group/guild 17 | UserOpenID string `json:"user_openid,omitempty"` // 用户ID 18 | Timestamp string `json:"timestamp,omitempty"` // 时间戳 19 | GroupMemberOpenID string `json:"group_member_openid,omitempty"` // 群成员OpenID 20 | 21 | } 22 | 23 | // InteractionType 互动类型 24 | type InteractionType uint32 25 | 26 | const ( 27 | // InteractionTypePing ping 28 | InteractionTypePing InteractionType = 1 29 | // InteractionTypeCommand 命令 30 | InteractionTypeCommand InteractionType = 2 31 | ) 32 | 33 | // InteractionData 互动数据 34 | type InteractionData struct { 35 | Name string `json:"name,omitempty"` // 标题 36 | Type InteractionDataType `json:"type,omitempty"` // 数据类型,不同数据类型对应不同的 resolved 数据 37 | Resolved json.RawMessage `json:"resolved,omitempty"` // 跟不同的互动类型和数据类型有关系的数据 38 | } 39 | 40 | // InteractionDataType 互动数据类型 41 | type InteractionDataType uint32 42 | 43 | const ( 44 | // InteractionDataTypeChatSearch 聊天框搜索 45 | InteractionDataTypeChatSearch InteractionDataType = 9 46 | // InteractionDataTypeInlineKeyboardClick 消息按钮点击 47 | InteractionDataTypeInlineKeyboardClick = 11 48 | // InteractionDataTypeCallbackCommandClick C2C菜单点击 49 | InteractionDataTypeCallbackCommandClick = 12 50 | // InteractionDataTypeMessageFeedbackClick 智能体消息反馈 51 | InteractionDataTypeMessageFeedbackClick = 13 52 | // InteractionDataTypeClearSessionClick 清空会话按钮点击 53 | InteractionDataTypeClearSessionClick = 14 54 | ) 55 | -------------------------------------------------------------------------------- /dto/interaction_search.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // SearchInputResolved 搜索类型的输入数据 4 | type SearchInputResolved struct { 5 | Keyword string `json:"keyword,omitempty"` 6 | } 7 | 8 | // SearchRsp 搜索返回数据 9 | type SearchRsp struct { 10 | Layouts []SearchLayout `json:"layouts"` 11 | } 12 | 13 | // SearchLayout 搜索结果的布局 14 | type SearchLayout struct { 15 | LayoutType LayoutType 16 | ActionType ActionType 17 | Title string 18 | Records []SearchRecord 19 | } 20 | 21 | // LayoutType 布局类型 22 | type LayoutType uint32 23 | 24 | const ( 25 | // LayoutTypeImageText 左图右文 26 | LayoutTypeImageText LayoutType = 0 27 | ) 28 | 29 | // ActionType 每行数据的点击行为 30 | type ActionType uint32 31 | 32 | const ( 33 | // ActionTypeSendARK 发送 ark 消息 34 | ActionTypeSendARK ActionType = 0 35 | ) 36 | 37 | // SearchRecord 每一条搜索结果 38 | type SearchRecord struct { 39 | Cover string `json:"cover"` 40 | Title string `json:"title"` 41 | Tips string `json:"tips"` 42 | URL string `json:"url"` 43 | } 44 | 45 | // Resolved 通用的互动反馈 46 | type Resolved struct { 47 | Keyword string `json:"keyword"` 48 | UserID string `json:"user_id"` 49 | Request string `json:"request"` 50 | MessageID string `json:"message_id"` 51 | MemberNick string `json:"member_nick"` 52 | ButtonData string `json:"button_data"` 53 | ButtonID string `json:"button_id"` 54 | FeatureID string `json:"feature_id"` 55 | FeedbackOpt string `json:"feedback_opt"` // 智能体反馈选项,LIKE点赞,UNLIKE点踩 56 | Checked int32 `json:"checked"` // 智能体反馈选项是否选中 57 | } 58 | -------------------------------------------------------------------------------- /dto/keyboard/keyboard.go: -------------------------------------------------------------------------------- 1 | // Package keyboard 消息按钮 2 | package keyboard 3 | 4 | // ActionType 按钮操作类型 5 | type ActionType uint32 6 | 7 | // PermissionType 按钮的权限类型 8 | type PermissionType uint32 9 | 10 | const ( 11 | // ActionTypeURL http 或 小程序 客户端识别 schema, data字段为链接 12 | ActionTypeURL ActionType = 0 13 | // ActionTypeCallback 回调互动回调地址, data 传给互动回调地址 14 | ActionTypeCallback ActionType = 1 15 | // ActionTypeAtBot at机器人, 根据 at_bot_show_channel_list 决定在当前频道或用户选择频道,自动在输入框 @bot data 16 | ActionTypeAtBot ActionType = 2 17 | // ActionTypeMQQAPI 客户端native跳转链接 18 | ActionTypeMQQAPI ActionType = 3 19 | // ActionTypeSubscribe 订阅按钮 20 | ActionTypeSubscribe ActionType = 4 21 | 22 | // PermissionTypeSpecifyUserIDs 仅指定这条消息的人可操作 23 | PermissionTypeSpecifyUserIDs PermissionType = 0 24 | // PermissionTypManager 仅频道管理者可操作 25 | PermissionTypManager PermissionType = 1 26 | // PermissionTypAll 所有人可操作 27 | PermissionTypAll PermissionType = 2 28 | // PermissionTypSpecifyRoleIDs 指定身份组可操作 29 | PermissionTypSpecifyRoleIDs PermissionType = 3 30 | ) 31 | 32 | // MessageKeyboard 消息按钮组件 33 | type MessageKeyboard struct { 34 | ID string `json:"id,omitempty"` // 消息按钮组件模板 ID 35 | Content *CustomKeyboard `json:"content,omitempty"` // 消息按钮组件自定义内容 36 | } 37 | 38 | // CustomKeyboard 自定义 Keyboard 39 | type CustomKeyboard struct { 40 | Rows []*Row `json:"rows,omitempty"` // 行数组 41 | Style *KeyboardStyle `json:"style,omitempty"` // 按钮样式 42 | } 43 | 44 | // KeyboardStyle 键盘样式 45 | type KeyboardStyle struct { 46 | FontSize string `json:"font_size,omitempty"` // 字体大小 47 | } 48 | 49 | // Row 每行结构 50 | type Row struct { 51 | Buttons []*Button `json:"buttons,omitempty"` // 每行按钮 52 | } 53 | 54 | // Button 单个按纽 55 | type Button struct { 56 | ID string `json:"id,omitempty"` // 按钮 ID 57 | RenderData *RenderData `json:"render_data,omitempty"` // 渲染展示字段 58 | Action *Action `json:"action,omitempty"` // 该按纽操作相关字段 59 | GroupID string `json:"group_id,omitempty"` // 分组ID, 同一分组内有一个按钮操作后, 其它按钮则变灰不可点击 注意:只有当action.type = 1 时才有效 60 | } 61 | 62 | // RenderData 按纽渲染展示 63 | type RenderData struct { 64 | Label string `json:"label,omitempty"` // 按纽上的文字 65 | VisitedLabel string `json:"visited_label,omitempty"` // 点击后按纽上文字 66 | Style int `json:"style,omitempty"` // 按钮样式,0:灰色线框,1:蓝色线框 3: 白色背景+红色字体, 4:蓝色背景+白色字体 67 | } 68 | 69 | // Action 按纽点击操作 70 | type Action struct { 71 | Type ActionType `json:"type,omitempty"` // 操作类型 72 | Permission *Permission `json:"permission,omitempty"` // 可操作 73 | ClickLimit uint32 `json:"click_limit,omitempty"` // 可点击的次数, 默认不限 74 | Data string `json:"data,omitempty"` // 操作相关数据 75 | Enter bool `json:"enter"` 76 | AtBotShowChannelList bool `json:"at_bot_show_channel_list,omitempty"` // false:当前 true:弹出展示子频道选择器 77 | SubscribeData SubscribeData `json:"subscribe_data,omitempty"` // 订阅按钮数据,type=ActionTypeSubscribe时使用 78 | Modal *Modal `json:"modal,omitempty"` // 用户点击二次确认操作 79 | } 80 | 81 | // Modal 二次确认数据 82 | type Modal struct { 83 | Content string `json:"content,omitempty"` // 二次确认的提示文本,如果不为空则会进行二次确认. 注意:最多40个字符, 不能有URL 84 | ConfirmText string `json:"confirm_text,omitempty"` // 二次确认提示确认按钮中展示的文字,可以为空, 默认为"确认" 注意:最多4个字符 85 | CancelText string `json:"cancel_text,omitempty"` // 二次确认提示取消按钮中的文字,可以为空,默认为"取消" 注意:最多4个字符 86 | } 87 | 88 | // Permission 按纽操作权限 89 | type Permission struct { 90 | // Type 操作权限类型 91 | Type PermissionType `json:"type,omitempty"` 92 | // SpecifyRoleIDs 身份组 93 | SpecifyRoleIDs []string `json:"specify_role_ids,omitempty"` 94 | // SpecifyUserIDs 指定 UserID 95 | SpecifyUserIDs []string `json:"specify_user_ids,omitempty"` 96 | } 97 | 98 | // TemplateID 对模板id的封装,兼容官方模板和自定义模板 99 | type TemplateID struct { 100 | // 这两个字段互斥,只填入一个 101 | TemplateID uint32 `json:"template_id,omitempty"` // 官方提供的模板id 102 | CustomTemplateID string `json:"custom_template_id,omitempty"` // 自定义模板 103 | } 104 | 105 | // SubscribeData 订阅按钮数据 106 | type SubscribeData struct { 107 | TemplateIDs []*TemplateID `json:"template_ids,omitempty"` // 订阅按钮对应的模板id列表 108 | } 109 | -------------------------------------------------------------------------------- /dto/member.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // Member 群成员 4 | type Member struct { 5 | GuildID string `json:"guild_id"` 6 | JoinedAt Timestamp `json:"joined_at"` 7 | Nick string `json:"nick"` 8 | User *User `json:"user"` 9 | Roles []string `json:"roles"` 10 | OpUserID string `json:"op_user_id,omitempty"` 11 | } 12 | 13 | // DeleteHistoryMsgDay 消息撤回天数 14 | type DeleteHistoryMsgDay = int 15 | 16 | // 支持的消息撤回天数,除这些天数之外,传递其他值将不会撤回任何消息 17 | const ( 18 | NoDelete = 0 // 不删除任何消息 19 | DeleteThreeDays DeleteHistoryMsgDay = 3 // 3天 20 | DeleteSevenDays DeleteHistoryMsgDay = 7 // 7天 21 | DeleteFifteenDays DeleteHistoryMsgDay = 15 // 15天 22 | DeleteThirtyDays DeleteHistoryMsgDay = 30 // 30天 23 | DeleteAll DeleteHistoryMsgDay = -1 // 删除所有消息 24 | ) 25 | 26 | // MemberDeleteOpts 删除成员额外参数 27 | type MemberDeleteOpts struct { 28 | AddBlackList bool `json:"add_blacklist"` 29 | DeleteHistoryMsgDays DeleteHistoryMsgDay `json:"delete_history_msg_days"` 30 | } 31 | 32 | // MemberDeleteOption 删除成员选项 33 | type MemberDeleteOption func(*MemberDeleteOpts) 34 | 35 | // WithAddBlackList 将当前成员同时添加到频道黑名单中 36 | func WithAddBlackList(b bool) MemberDeleteOption { 37 | return func(o *MemberDeleteOpts) { 38 | o.AddBlackList = b 39 | } 40 | } 41 | 42 | // WithDeleteHistoryMsg 删除成员时同时撤回消息 43 | func WithDeleteHistoryMsg(days DeleteHistoryMsgDay) MemberDeleteOption { 44 | return func(o *MemberDeleteOpts) { 45 | o.DeleteHistoryMsgDays = days 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /dto/message.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // Message 消息结构体定义 4 | type Message struct { 5 | // 消息ID 6 | ID string `json:"id"` 7 | // 子频道ID 8 | ChannelID string `json:"channel_id"` 9 | // 频道ID 10 | GuildID string `json:"guild_id"` 11 | // 群ID 12 | GroupID string `json:"group_id"` 13 | 14 | // 内容 15 | Content string `json:"content"` 16 | // 发送时间 17 | Timestamp Timestamp `json:"timestamp"` 18 | // 消息编辑时间 19 | EditedTimestamp Timestamp `json:"edited_timestamp"` 20 | // 是否@all 21 | MentionEveryone bool `json:"mention_everyone"` 22 | // 消息发送方 23 | Author *User `json:"author"` 24 | // 消息发送方Author的member属性,只是部分属性 25 | Member *Member `json:"member"` 26 | // 附件 27 | Attachments []*MessageAttachment `json:"attachments"` 28 | // 结构化消息-embeds 29 | Embeds []*Embed `json:"embeds"` 30 | // 消息中的提醒信息(@)列表 31 | Mentions []*User `json:"mentions"` 32 | // ark 消息 33 | Ark *Ark `json:"ark"` 34 | // 私信消息 35 | DirectMessage bool `json:"direct_message"` 36 | // 子频道 seq,用于消息间的排序,seq 在同一子频道中按从先到后的顺序递增,不同的子频道之前消息无法排序 37 | SeqInChannel string `json:"seq_in_channel"` 38 | // 引用的消息 39 | MessageReference *MessageReference `json:"message_reference,omitempty"` 40 | // 私信场景下,该字段用来标识从哪个频道发起的私信 41 | SrcGuildID string `json:"src_guild_id"` 42 | // 上传富媒体文件后返回的文件信息。 注意以群或者C2C消息上传后, 同类型可以重复使用,不同类型需要不能使用。 43 | FileInfo []byte `json:"file_info,omitempty"` 44 | // 上传富媒体文件后的有效期, 单位:秒, 在有效期内可以重复使用。 45 | TTL uint `json:"ttl,omitempty"` 46 | // 消息场景描述 47 | MessageScene MessageScene `json:"message_scene,omitempty"` 48 | } 49 | 50 | // Embed 结构 51 | type Embed struct { 52 | Title string `json:"title,omitempty"` 53 | Description string `json:"description,omitempty"` 54 | Prompt string `json:"prompt"` // 消息弹窗内容,消息列表摘要 55 | Thumbnail MessageEmbedThumbnail `json:"thumbnail,omitempty"` 56 | Fields []*EmbedField `json:"fields,omitempty"` 57 | } 58 | 59 | // MessageEmbedThumbnail embed 消息的缩略图对象 60 | type MessageEmbedThumbnail struct { 61 | URL string `json:"url"` 62 | } 63 | 64 | // EmbedField Embed字段描述 65 | type EmbedField struct { 66 | Name string `json:"name,omitempty"` 67 | Value string `json:"value,omitempty"` 68 | } 69 | 70 | // MessageAttachment 附件定义 71 | type MessageAttachment struct { 72 | URL string `json:"url,omitempty"` 73 | FileName string `json:"filename,omitempty"` 74 | Height int `json:"height,omitempty"` 75 | Size int `json:"size,omitempty"` 76 | Width int `json:"width,omitempty"` 77 | ContentType string `json:"content_type,omitempty"` // voice:语音, image/xxx: 图片 video/xxx: 视频 78 | } 79 | 80 | // MessageReactionUsers 消息表情表态用户列表 81 | type MessageReactionUsers struct { 82 | Users []*User `json:"users,omitempty"` 83 | Cookie string `json:"cookie,omitempty"` 84 | IsEnd bool `json:"is_end,omitempty"` 85 | } 86 | 87 | // MessageScene 消息场景 88 | type MessageScene struct { 89 | Source string `json:"source,omitempty"` // 消息来源, realtime_voice: 实时通话场景, ai_search: AI搜索 其它默认为AIO消息 90 | CallbackData string `json:"callback_data,omitempty"` // 回调数据 91 | } 92 | -------------------------------------------------------------------------------- /dto/message/message.go: -------------------------------------------------------------------------------- 1 | // Package message 内提供了用于支撑处理消息对象的工具和方法。 2 | package message 3 | 4 | import ( 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | // 用于过滤 at 结构的正则 11 | var atRE = regexp.MustCompile(`<@!\d+>`) 12 | 13 | // 用于过滤用户发送消息中的空格符号,\u00A0 是   的 unicode 编码,某些 mac/pc 版本,连续多个空格的时候会转换成这个符号发送到后台 14 | const spaceCharSet = " \u00A0" 15 | 16 | // CMD 一个简单的指令结构 17 | type CMD struct { 18 | Cmd string 19 | Content string 20 | } 21 | 22 | // ETLInput 清理输出 23 | // - 去掉@结构 24 | // - trim 25 | func ETLInput(input string) string { 26 | etlData := string(atRE.ReplaceAll([]byte(input), []byte(""))) 27 | etlData = strings.Trim(etlData, spaceCharSet) 28 | return etlData 29 | } 30 | 31 | // MentionUser 返回 at 用户的内嵌格式 32 | // https://bot.q.qq.com/wiki/develop/api/openapi/message/message_format.html 33 | func MentionUser(userID string) string { 34 | return fmt.Sprintf("<@%s>", userID) 35 | } 36 | 37 | // MentionAllUser 返回 at all 的内嵌格式 38 | func MentionAllUser() string { 39 | return "@everyone" 40 | } 41 | 42 | // MentionChannel 提到子频道的格式 43 | func MentionChannel(channelID string) string { 44 | return fmt.Sprintf("<#%s>", channelID) 45 | } 46 | 47 | // Emoji emoji 内嵌格式,参考 https://bot.q.qq.com/wiki/develop/api/openapi/emoji/model.html 48 | // 只支持 type = 1 的系统表情 49 | func Emoji(id int) string { 50 | return fmt.Sprintf("", id) 51 | } 52 | 53 | // ParseCommand 解析命令,支持 `{cmd} {content}` 的命令格式 54 | func ParseCommand(input string) *CMD { 55 | input = ETLInput(input) 56 | s := strings.Split(input, " ") 57 | if len(s) < 2 { 58 | return &CMD{ 59 | Cmd: strings.Trim(input, spaceCharSet), 60 | Content: "", 61 | } 62 | } 63 | return &CMD{ 64 | Cmd: strings.Trim(s[0], spaceCharSet), 65 | Content: strings.Join(s[1:], " "), 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /dto/message_ark.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // MessageArk ark模板消息 4 | type MessageArk struct { 5 | Ark *Ark `json:"ark,omitempty"` 6 | } 7 | 8 | // Ark 消息模版 9 | type Ark struct { 10 | TemplateID int `json:"template_id,omitempty"` // ark 模版 ID 11 | KV []*ArkKV `json:"kv,omitempty"` // ArkKV 数组 12 | } 13 | 14 | // ArkKV Ark 键值对 15 | type ArkKV struct { 16 | Key string `json:"key,omitempty"` 17 | Value string `json:"value,omitempty"` 18 | Obj []*ArkObj `json:"obj,omitempty"` 19 | } 20 | 21 | // ArkObj Ark 对象 22 | type ArkObj struct { 23 | ObjKV []*ArkObjKV `json:"obj_kv,omitempty"` 24 | } 25 | 26 | // ArkObjKV Ark 对象键值对 27 | type ArkObjKV struct { 28 | Key string `json:"key,omitempty"` 29 | Value string `json:"value,omitempty"` 30 | } 31 | -------------------------------------------------------------------------------- /dto/message_audit.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // MessageAudit 消息审核结构体定义 4 | type MessageAudit struct { 5 | // 审核 ID 6 | AuditID string `json:"audit_id"` 7 | // 消息 ID 8 | MessageID string `json:"message_id"` 9 | // 频道 ID 10 | GuildID string `json:"guild_id"` 11 | // 子频道 ID 12 | ChannelID string `json:"channel_id"` 13 | // 审核时间 14 | AuditTime string `json:"audit_time"` 15 | // 创建时间 16 | CreateTime string `json:"create_time"` 17 | // 子频道 seq,用于消息间的排序,seq 在同一子频道中按从先到后的顺序递增,不同的子频道之前消息无法排序 18 | SeqInChannel string `json:"seq_in_channel"` 19 | } 20 | -------------------------------------------------------------------------------- /dto/message_delete.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // MessageDelete 消息删除结构体定义 4 | type MessageDelete struct { 5 | // 消息 6 | Message Message `json:"message"` 7 | // 操作用户 8 | OpUser User `json:"op_user"` 9 | } 10 | -------------------------------------------------------------------------------- /dto/message_reaction.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // ReactionTargetType 表情表态对象类型 4 | type ReactionTargetType = int32 5 | 6 | const ( 7 | // ReactionTargetTypeMsg 消息 8 | ReactionTargetTypeMsg = iota 9 | // ReactionTargetTypeFeed 帖子 10 | ReactionTargetTypeFeed 11 | // ReactionTargetTypeComment 评论 12 | ReactionTargetTypeComment 13 | // ReactionTargetTypeReply 回复 14 | ReactionTargetTypeReply 15 | ) 16 | 17 | // MessageReaction 表情表态动作 18 | type MessageReaction struct { 19 | UserID string `json:"user_id"` 20 | ChannelID string `json:"channel_id"` 21 | GuildID string `json:"guild_id"` 22 | Target ReactionTarget `json:"target"` 23 | Emoji Emoji `json:"emoji"` 24 | } 25 | 26 | // ReactionTarget 表态对象类型 27 | type ReactionTarget struct { 28 | ID string `json:"id"` 29 | Type ReactionTargetType `json:"type"` 30 | } 31 | -------------------------------------------------------------------------------- /dto/message_setting.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // MessageSetting 消息频率设置信息 4 | type MessageSetting struct { 5 | DisableCreateDm bool `json:"disable_create_dm,omitempty"` 6 | DisablePushMsg bool `json:"disable_push_msg,omitempty"` 7 | ChannelIDs []string `json:"channel_ids,omitempty"` 8 | ChannelPushMaxNum int `json:"channel_push_max_num,omitempty"` 9 | } 10 | -------------------------------------------------------------------------------- /dto/mute.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // UpdateGuildMute 更新频道相关禁言的Body参数 4 | type UpdateGuildMute struct { 5 | // 禁言截止时间戳,单位秒 6 | MuteEndTimestamp string `json:"mute_end_timestamp,omitempty"` 7 | // 禁言多少秒(两个字段二选一,默认以mute_end_timstamp为准) 8 | MuteSeconds string `json:"mute_seconds,omitempty"` 9 | // 批量禁言的成员列表(全员禁言时不填写该字段) 10 | UserIDs []string `json:"user_ids,omitempty"` 11 | } 12 | 13 | // UpdateGuildMuteResponse 批量禁言的回参 14 | type UpdateGuildMuteResponse struct { 15 | // 批量禁言成功的成员列表 16 | UserIDs []string `json:"user_ids,omitempty"` 17 | } 18 | -------------------------------------------------------------------------------- /dto/pager.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // Pager 分页器接口需要实现将对象转换为分页参数的方法 4 | type Pager interface { 5 | // QueryParams 转换为 query 参数 6 | QueryParams() map[string]string 7 | } 8 | 9 | // GuildMembersPager 分页器 10 | type GuildMembersPager struct { 11 | After string `json:"after"` // 读此id之后的数据,如果是第一次请求填0,默认为0 12 | Limit string `json:"limit"` // 分页大小,1-1000,默认是1 13 | } 14 | 15 | // QueryParams 转换为 query 参数 16 | func (g *GuildMembersPager) QueryParams() map[string]string { 17 | query := make(map[string]string) 18 | if g.Limit != "" { 19 | query["limit"] = g.Limit 20 | } 21 | if g.After != "" { 22 | query["after"] = g.After 23 | } 24 | return query 25 | } 26 | 27 | // GuildRoleMembersPager 分页器 28 | type GuildRoleMembersPager struct { 29 | StartIndex string `json:"start_index"` 30 | Limit string `json:"limit"` 31 | } 32 | 33 | // QueryParams 转换为 query 参数 34 | func (g *GuildRoleMembersPager) QueryParams() map[string]string { 35 | query := make(map[string]string) 36 | if g.Limit != "" { 37 | query["limit"] = g.Limit 38 | } 39 | if g.StartIndex != "" { 40 | query["start_index"] = g.StartIndex 41 | } 42 | return query 43 | } 44 | 45 | // GuildPager 分页器 46 | type GuildPager struct { 47 | Before string `json:"before"` // 读此id之前的数据 48 | After string `json:"after"` // 读此id之后的数据 49 | Limit string `json:"limit"` // 分页大小,1-100,默认是 100 50 | } 51 | 52 | // QueryParams 转换为 query 参数 53 | func (g *GuildPager) QueryParams() map[string]string { 54 | query := make(map[string]string) 55 | if g.Limit != "" { 56 | query["limit"] = g.Limit 57 | } 58 | if g.After != "" { 59 | query["after"] = g.After 60 | } 61 | // 优先 after 62 | if g.After == "" && g.Before != "" { 63 | query["before"] = g.Before 64 | } 65 | return query 66 | } 67 | 68 | // MessagesPager 消息分页 69 | type MessagesPager struct { 70 | Type MessagePagerType // 拉取类型 71 | ID string // 消息ID 72 | Limit string `json:"limit"` // 最大 20 73 | } 74 | 75 | // QueryParams 转换为 query 参数 76 | func (m *MessagesPager) QueryParams() map[string]string { 77 | query := make(map[string]string) 78 | if m.Limit != "" { 79 | query["limit"] = m.Limit 80 | } 81 | if m.Type != "" && m.ID != "" { 82 | query[string(m.Type)] = m.ID 83 | } 84 | return query 85 | } 86 | 87 | // MessageReactionPager 分页器 88 | type MessageReactionPager struct { 89 | Cookie string `json:"cookie"` // 分页游标 90 | Limit string `json:"limit"` // 分页大小,1-1000,默认是20 91 | } 92 | 93 | // QueryParams 转换为 query 参数 94 | func (g *MessageReactionPager) QueryParams() map[string]string { 95 | query := make(map[string]string) 96 | if g.Limit != "" { 97 | query["limit"] = g.Limit 98 | } 99 | if g.Cookie != "" { 100 | query["cookie"] = g.Cookie 101 | } 102 | return query 103 | } 104 | 105 | // MessagePagerType 消息翻页拉取方式 106 | type MessagePagerType string 107 | 108 | const ( 109 | // MPTAround 拉取消息ID上下的消息 110 | MPTAround MessagePagerType = "around" 111 | // MPTBefore 拉取消息ID之前的消息 112 | MPTBefore MessagePagerType = "before" 113 | // MPTAfter 拉取消息ID之后的消息 114 | MPTAfter MessagePagerType = "after" 115 | ) 116 | -------------------------------------------------------------------------------- /dto/pins.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // PinsMessage 精华消息对象 4 | type PinsMessage struct { 5 | // 频道 ID 6 | GuildID string `json:"guild_id"` 7 | // 子频道 ID 8 | ChannelID string `json:"channel_id"` 9 | // 消息 ID 数组 10 | MessageIDs []string `json:"message_ids"` 11 | } 12 | -------------------------------------------------------------------------------- /dto/role.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // GuildRoles 频道用户组列表返回 4 | type GuildRoles struct { 5 | GuildID string `json:"guild_id"` 6 | Roles []*Role `json:"roles"` 7 | NumLimit string `json:"role_num_limit"` 8 | } 9 | 10 | // Role 频道身份组 11 | type Role struct { 12 | ID RoleID `json:"id,omitempty"` 13 | Name string `json:"name"` 14 | Color uint32 `json:"color"` 15 | Hoist uint32 `json:"hoist"` 16 | MemberCount uint32 `json:"number,omitempty"` // 不会被修改,创建接口修改 17 | MemberLimit uint32 `json:"member_limit,omitempty"` // 不会被修改,创建接口修改 18 | } 19 | 20 | // DefaultColor 用户组默认颜色值 21 | const DefaultColor = 4278245297 22 | 23 | // RoleID 用户组ID 24 | type RoleID string 25 | 26 | // UpdateRoleInfo 身份组可更改数据 27 | type UpdateRoleInfo struct { 28 | Name string `json:"name"` 29 | Color uint32 `json:"color"` 30 | Hoist uint32 `json:"hoist"` 31 | } 32 | 33 | // UpdateRoleFilter 身份组可更改数据,修改的 34 | type UpdateRoleFilter struct { 35 | Name uint32 `json:"name"` 36 | Color uint32 `json:"color"` 37 | Hoist uint32 `json:"hoist"` 38 | } 39 | 40 | // UpdateRole role 更新请求承载 41 | type UpdateRole struct { 42 | GuildID string `json:"guild_id"` 43 | Filter *UpdateRoleFilter `json:"filter"` 44 | Update *Role `json:"info"` 45 | } 46 | 47 | // UpdateResult 创建,删除等行为的返回 48 | type UpdateResult struct { 49 | RoleID `json:"role_id"` 50 | GuildID string `json:"guild_id"` 51 | Role *Role `json:"role"` 52 | } 53 | 54 | // MemberAddRoleBody 增加子频道管理员身份组时附加内容 55 | type MemberAddRoleBody struct { 56 | // 子频道对象 57 | Channel *Channel `json:"channel"` 58 | } 59 | -------------------------------------------------------------------------------- /dto/schedule.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // Schedule 日程对象 4 | type Schedule struct { 5 | ID string `json:"id,omitempty"` 6 | Name string `json:"name,omitempty"` 7 | Description string `json:"description,omitempty"` 8 | StartTimestamp string `json:"start_timestamp,omitempty"` 9 | EndTimestamp string `json:"end_timestamp,omitempty"` 10 | JumpChannelID string `json:"jump_channel_id,omitempty"` 11 | RemindType string `json:"remind_type,omitempty"` 12 | Creator *Member `json:"creator,omitempty"` 13 | } 14 | 15 | // ScheduleWrapper 创建、修改日程的中间对象 16 | type ScheduleWrapper struct { 17 | Schedule *Schedule `json:"schedule,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /dto/subscribe.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // SubscribeMessageStatusData 订阅消息模板授权数据 4 | type SubscribeMessageStatusData struct { 5 | GroupOpenid string `json:"group_openid"` // 群openid,如果是群订阅消息这里有值 6 | Openid string `json:"openid"` // 用户openid,用户订阅消息取这个值 7 | Result []SubscribeMsgTemplateResult `json:"result"` // 授权操作结果 8 | } 9 | 10 | // SubscribeMsgTemplateResult 订阅模板授权操作结果 11 | type SubscribeMsgTemplateResult struct { 12 | TemplateID int `json:"template_id"` // 官方模板id 13 | CustomTemplateID string `json:"custom_template_id"` // 自定义模板id 14 | Op uint32 `json:"op"` // 模板授权操作 1-允许 2-拒绝 15 | SubscribeID string `json:"subscribe_id"` // 订阅id 16 | UpdateTs uint64 `json:"update_ts"` // 订阅状态更新时间戳 17 | } 18 | -------------------------------------------------------------------------------- /dto/timestamp.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "time" 4 | 5 | // Timestamp 时间戳 6 | type Timestamp string 7 | 8 | // Time 时间字符串格式转换 9 | func (t Timestamp) Time() (time.Time, error) { 10 | return time.Parse(time.RFC3339, string(t)) 11 | } 12 | -------------------------------------------------------------------------------- /dto/user.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // User 用户 4 | type User struct { 5 | ID string `json:"id"` 6 | Username string `json:"username"` 7 | Avatar string `json:"avatar"` 8 | Bot bool `json:"bot"` 9 | UnionOpenID string `json:"union_openid"` // 特殊关联应用的 openid 10 | UnionUserAccount string `json:"union_user_account"` // 机器人关联的用户信息,与union_openid关联的应用是同一个 11 | } 12 | -------------------------------------------------------------------------------- /dto/webhook.go: -------------------------------------------------------------------------------- 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 | DataVersion string `json:"data_version"` //数据格式版本号 14 | } 15 | -------------------------------------------------------------------------------- /dto/websocket.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/oauth2" 7 | ) 8 | 9 | // WebsocketAP wss 接入点信息 10 | type WebsocketAP struct { 11 | URL string `json:"url"` 12 | Shards uint32 `json:"shards"` 13 | SessionStartLimit SessionStartLimit `json:"session_start_limit"` 14 | } 15 | 16 | // SessionStartLimit 链接频控信息 17 | type SessionStartLimit struct { 18 | Total uint32 `json:"total"` 19 | Remaining uint32 `json:"remaining"` 20 | ResetAfter uint32 `json:"reset_after"` 21 | MaxConcurrency uint32 `json:"max_concurrency"` 22 | } 23 | 24 | // ShardConfig 连接的 shard 配置,ShardID 从 0 开始,ShardCount 最小为 1 25 | type ShardConfig struct { 26 | ShardID uint32 27 | ShardCount uint32 28 | } 29 | 30 | // Session 连接的 session 结构,包括链接的所有必要字段 31 | type Session struct { 32 | ID string 33 | URL string 34 | TokenSource oauth2.TokenSource 35 | Intent Intent 36 | LastSeq uint32 37 | Shards ShardConfig 38 | 39 | AppID string 40 | } 41 | 42 | // String 输出session字符串 43 | func (s *Session) String() string { 44 | return fmt.Sprintf("[ws][ID:%s][Shard:(%d/%d)][Intent:%d]", 45 | s.ID, s.Shards.ShardID, s.Shards.ShardCount, s.Intent) 46 | } 47 | 48 | // WSUser 当前连接的用户信息 49 | type WSUser struct { 50 | ID string `json:"id"` 51 | Username string `json:"username"` 52 | Bot bool `json:"bot"` 53 | } 54 | -------------------------------------------------------------------------------- /dto/websocket_event.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | func init() { 4 | eventIntentMap = transposeIntentEventMap(intentEventMap) 5 | } 6 | 7 | // 事件类型 8 | const ( 9 | EventGuildCreate EventType = "GUILD_CREATE" 10 | EventGuildUpdate EventType = "GUILD_UPDATE" 11 | EventGuildDelete EventType = "GUILD_DELETE" 12 | EventChannelCreate EventType = "CHANNEL_CREATE" 13 | EventChannelUpdate EventType = "CHANNEL_UPDATE" 14 | EventChannelDelete EventType = "CHANNEL_DELETE" 15 | EventGuildMemberAdd EventType = "GUILD_MEMBER_ADD" 16 | EventGuildMemberUpdate EventType = "GUILD_MEMBER_UPDATE" 17 | EventGuildMemberRemove EventType = "GUILD_MEMBER_REMOVE" 18 | EventMessageCreate EventType = "MESSAGE_CREATE" 19 | EventMessageReactionAdd EventType = "MESSAGE_REACTION_ADD" 20 | EventMessageReactionRemove EventType = "MESSAGE_REACTION_REMOVE" 21 | EventAtMessageCreate EventType = "AT_MESSAGE_CREATE" 22 | EventPublicMessageDelete EventType = "PUBLIC_MESSAGE_DELETE" 23 | EventDirectMessageCreate EventType = "DIRECT_MESSAGE_CREATE" 24 | EventDirectMessageDelete EventType = "DIRECT_MESSAGE_DELETE" 25 | EventAudioStart EventType = "AUDIO_START" 26 | EventAudioFinish EventType = "AUDIO_FINISH" 27 | EventAudioOnMic EventType = "AUDIO_ON_MIC" 28 | EventAudioOffMic EventType = "AUDIO_OFF_MIC" 29 | EventMessageAuditPass EventType = "MESSAGE_AUDIT_PASS" 30 | EventMessageAuditReject EventType = "MESSAGE_AUDIT_REJECT" 31 | EventMessageDelete EventType = "MESSAGE_DELETE" 32 | EventForumThreadCreate EventType = "FORUM_THREAD_CREATE" 33 | EventForumThreadUpdate EventType = "FORUM_THREAD_UPDATE" 34 | EventForumThreadDelete EventType = "FORUM_THREAD_DELETE" 35 | EventForumPostCreate EventType = "FORUM_POST_CREATE" 36 | EventForumPostDelete EventType = "FORUM_POST_DELETE" 37 | EventForumReplyCreate EventType = "FORUM_REPLY_CREATE" 38 | EventForumReplyDelete EventType = "FORUM_REPLY_DELETE" 39 | EventForumAuditResult EventType = "FORUM_PUBLISH_AUDIT_RESULT" 40 | EventInteractionCreate EventType = "INTERACTION_CREATE" 41 | EventGroupAtMessageCreate EventType = "GROUP_AT_MESSAGE_CREATE" 42 | EventC2CMessageCreate EventType = "C2C_MESSAGE_CREATE" 43 | EventSubscribeMsgStatus EventType = "SUBSCRIBE_MESSAGE_STATUS" 44 | EventC2CFriendAdd EventType = "FRIEND_ADD" 45 | EventC2CFriendDel EventType = "FRIEND_DEL" 46 | EventEnterAIO EventType = "ENTER_AIO" 47 | ) 48 | 49 | // intentEventMap 不同 intent 对应的事件定义 50 | var intentEventMap = map[Intent][]EventType{ 51 | IntentGuilds: { 52 | EventGuildCreate, EventGuildUpdate, EventGuildDelete, 53 | EventChannelCreate, EventChannelUpdate, EventChannelDelete, 54 | }, 55 | IntentGuildMembers: {EventGuildMemberAdd, EventGuildMemberUpdate, EventGuildMemberRemove}, 56 | IntentGuildMessages: {EventMessageCreate, EventMessageDelete}, 57 | IntentGroupMessages: {EventGroupAtMessageCreate, EventC2CMessageCreate, EventSubscribeMsgStatus, 58 | EventC2CFriendAdd, EventC2CFriendDel}, 59 | 60 | IntentGuildMessageReactions: {EventMessageReactionAdd, EventMessageReactionRemove}, 61 | IntentGuildAtMessage: {EventAtMessageCreate, EventPublicMessageDelete}, 62 | IntentDirectMessages: {EventDirectMessageCreate, EventDirectMessageDelete}, 63 | IntentAudio: {EventAudioStart, EventAudioFinish, EventAudioOnMic, EventAudioOffMic}, 64 | IntentAudit: {EventMessageAuditPass, EventMessageAuditReject}, 65 | IntentForum: { 66 | EventForumThreadCreate, EventForumThreadUpdate, EventForumThreadDelete, EventForumPostCreate, 67 | EventForumPostDelete, EventForumReplyCreate, EventForumReplyDelete, EventForumAuditResult, 68 | }, 69 | IntentInteraction: {EventInteractionCreate}, 70 | IntentEnterAIO: {EventEnterAIO}, 71 | } 72 | 73 | var eventIntentMap = transposeIntentEventMap(intentEventMap) 74 | 75 | // transposeIntentEventMap 转置 intent 与 event 的关系,用于根据 event 找到 intent 76 | func transposeIntentEventMap(input map[Intent][]EventType) map[EventType]Intent { 77 | result := make(map[EventType]Intent) 78 | for i, eventTypes := range input { 79 | for _, s := range eventTypes { 80 | result[s] = i 81 | } 82 | } 83 | return result 84 | } 85 | 86 | // EventToIntent 事件转换对应的Intent 87 | func EventToIntent(events ...EventType) Intent { 88 | var i Intent 89 | for _, event := range events { 90 | i = i | eventIntentMap[event] 91 | } 92 | return i 93 | } 94 | -------------------------------------------------------------------------------- /dto/websocket_event_test.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_transposeIntentEventMap(t *testing.T) { 10 | t.Run("transpose", func(t *testing.T) { 11 | re := transposeIntentEventMap(intentEventMap) 12 | assert.Equal(t, re[EventAudioFinish], IntentAudio) 13 | assert.Equal(t, re[EventAudioOffMic], IntentAudio) 14 | assert.Equal(t, re[EventChannelCreate], IntentGuilds) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /dto/websocket_intent.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // Intent 类型 4 | type Intent int 5 | 6 | // websocket intent 声明 7 | const ( 8 | // IntentGuilds 包含 9 | // - GUILD_CREATE 10 | // - GUILD_UPDATE 11 | // - GUILD_DELETE 12 | // - GUILD_ROLE_CREATE 13 | // - GUILD_ROLE_UPDATE 14 | // - GUILD_ROLE_DELETE 15 | // - CHANNEL_CREATE 16 | // - CHANNEL_UPDATE 17 | // - CHANNEL_DELETE 18 | // - CHANNEL_PINS_UPDATE 19 | IntentGuilds Intent = 1 << iota 20 | 21 | // IntentGuildMembers 包含 22 | // - GUILD_MEMBER_ADD 23 | // - GUILD_MEMBER_UPDATE 24 | // - GUILD_MEMBER_REMOVE 25 | IntentGuildMembers 26 | 27 | IntentGuildBans 28 | IntentGuildEmojis 29 | IntentGuildIntegrations 30 | IntentGuildWebhooks 31 | IntentGuildInvites 32 | IntentGuildVoiceStates 33 | IntentGuildPresences 34 | IntentGuildMessages 35 | 36 | // IntentGuildMessageReactions 包含 37 | // - MESSAGE_REACTION_ADD 38 | // - MESSAGE_REACTION_REMOVE 39 | IntentGuildMessageReactions 40 | 41 | IntentGuildMessageTyping 42 | IntentDirectMessages 43 | IntentDirectMessageReactions 44 | IntentDirectMessageTyping 45 | 46 | IntentEnterAIO Intent = 1 << 23 // 进入aio事件 47 | 48 | // IntentGroupMessages 群消息事件 49 | // - GROUP_AT_MESSAGE_CREATE // 群中@机器人时的消息 50 | IntentGroupMessages Intent = 1 << 25 // 群消息事件 51 | 52 | IntentInteraction Intent = 1 << 26 // 互动事件 53 | IntentAudit Intent = 1 << 27 // 审核事件 54 | // IntentForum 论坛事件 55 | // - THREAD_CREATE // 当用户创建主题时 56 | // - THREAD_UPDATE // 当用户更新主题时 57 | // - THREAD_DELETE // 当用户删除主题时 58 | // - POST_CREATE // 当用户创建帖子时 59 | // - POST_DELETE // 当用户删除帖子时 60 | // - REPLY_CREATE // 当用户回复评论时 61 | // - REPLY_DELETE // 当用户回复评论时 62 | // - FORUM_PUBLISH_AUDIT_RESULT // 当用户发表审核通过时 63 | IntentForum Intent = 1 << 28 // 论坛事件 64 | 65 | // IntentAudio 66 | // - AUDIO_START // 音频开始播放时 67 | // - AUDIO_FINISH // 音频播放结束时 68 | IntentAudio Intent = 1 << 29 // 音频机器人事件 69 | IntentGuildAtMessage Intent = 1 << 30 // 只接收@消息事件 70 | 71 | IntentNone Intent = 0 72 | ) 73 | -------------------------------------------------------------------------------- /dto/websocket_opcode.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // OPCode websocket op 码 4 | type OPCode int 5 | 6 | // WS OPCode 7 | const ( 8 | WSDispatchEvent OPCode = iota 9 | WSHeartbeat 10 | WSIdentity 11 | _ // Presence Update 12 | _ // Voice State Update 13 | _ 14 | WSResume 15 | WSReconnect 16 | _ // Request Guild Members 17 | WSInvalidSession 18 | WSHello 19 | WSHeartbeatAck 20 | HTTPCallbackAck 21 | HTTPCallbackValidation 22 | ) 23 | 24 | // opMeans op 对应的含义字符串标识 25 | var opMeans = map[OPCode]string{ 26 | WSDispatchEvent: "Event", 27 | WSHeartbeat: "Heartbeat", 28 | WSIdentity: "Identity", 29 | WSResume: "Resume", 30 | WSReconnect: "Reconnect", 31 | WSInvalidSession: "InvalidSession", 32 | WSHello: "Hello", 33 | WSHeartbeatAck: "HeartbeatAck", 34 | } 35 | 36 | // OPMeans 返回 op 含义 37 | func OPMeans(op OPCode) string { 38 | means, ok := opMeans[op] 39 | if !ok { 40 | means = "unknown" 41 | } 42 | return means 43 | } 44 | -------------------------------------------------------------------------------- /dto/websocket_payload.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // EventType 事件类型 4 | type EventType string 5 | 6 | // WSPayload websocket 消息结构 7 | type WSPayload struct { 8 | WSPayloadBase 9 | Data interface{} `json:"d,omitempty"` 10 | RawMessage []byte `json:"-"` // 原始的 message 数据 11 | Session *Session 12 | } 13 | 14 | // WSPayloadBase 基础消息结构,排除了 data 15 | type WSPayloadBase struct { 16 | OPCode OPCode `json:"op"` 17 | Seq uint32 `json:"s,omitempty"` 18 | Type EventType `json:"t,omitempty"` 19 | EventID string `json:"id,omitempty"` 20 | } 21 | 22 | // 以下为发送到 websocket 的 data 23 | 24 | // WSIdentityData 鉴权数据 25 | type WSIdentityData struct { 26 | Token string `json:"token"` 27 | Intents Intent `json:"intents"` 28 | Shard []uint32 `json:"shard"` // array of two integers (shard_id, num_shards) 29 | Properties struct { 30 | Os string `json:"$os,omitempty"` 31 | Browser string `json:"$browser,omitempty"` 32 | Device string `json:"$device,omitempty"` 33 | } `json:"properties,omitempty"` 34 | } 35 | 36 | // WSResumeData 重连数据 37 | type WSResumeData struct { 38 | Token string `json:"token"` 39 | SessionID string `json:"session_id"` 40 | Seq uint32 `json:"seq"` 41 | } 42 | 43 | // 以下为会收到的事件data 44 | 45 | // WSHelloData hello 返回 46 | type WSHelloData struct { 47 | HeartbeatInterval int `json:"heartbeat_interval"` 48 | } 49 | 50 | // WSReadyData ready,鉴权后返回 51 | type WSReadyData struct { 52 | Version int `json:"version"` 53 | SessionID string `json:"session_id"` 54 | User struct { 55 | ID string `json:"id"` 56 | Username string `json:"username"` 57 | Bot bool `json:"bot"` 58 | } `json:"user"` 59 | Shard []uint32 `json:"shard"` 60 | } 61 | 62 | // WSGuildData 频道 payload 63 | type WSGuildData Guild 64 | 65 | // WSGuildMemberData 频道成员 payload 66 | type WSGuildMemberData Member 67 | 68 | // WSChannelData 子频道 payload 69 | type WSChannelData Channel 70 | 71 | // WSMessageData 消息 payload 72 | type WSMessageData Message 73 | 74 | // WSATMessageData only at 机器人的消息 payload 75 | type WSATMessageData Message 76 | 77 | // WSDirectMessageData 私信消息 payload 78 | type WSDirectMessageData Message 79 | 80 | // WSMessageDeleteData 消息 payload 81 | type WSMessageDeleteData MessageDelete 82 | 83 | // WSPublicMessageDeleteData 公域机器人的消息删除 payload 84 | type WSPublicMessageDeleteData MessageDelete 85 | 86 | // WSDirectMessageDeleteData 私信消息 payload 87 | type WSDirectMessageDeleteData MessageDelete 88 | 89 | // WSAudioData 音频机器人的音频流事件 90 | type WSAudioData AudioAction 91 | 92 | // WSMessageReactionData 表情表态事件 93 | type WSMessageReactionData MessageReaction 94 | 95 | // WSMessageAuditData 消息审核事件 96 | type WSMessageAuditData MessageAudit 97 | 98 | // WSThreadData 主题事件 99 | type WSThreadData Thread 100 | 101 | // WSPostData 帖子事件 102 | type WSPostData Post 103 | 104 | // WSReplyData 帖子回复事件 105 | type WSReplyData Reply 106 | 107 | // WSForumAuditData 帖子审核事件 108 | type WSForumAuditData ForumAuditResult 109 | 110 | // WSInteractionData 互动事件 111 | type WSInteractionData Interaction 112 | 113 | // ***************** 群消息/C2C消息 ***************** 114 | 115 | // WSGroupATMessageData 群@机器人的事件 116 | type WSGroupATMessageData Message 117 | 118 | // WSC2CMessageData c2c消息事件 119 | type WSC2CMessageData Message 120 | 121 | // ************************************************ 122 | 123 | // WSC2CFriendData C2C 好友事件 124 | type WSC2CFriendData C2CFriendData 125 | 126 | // ************************************************ 127 | 128 | // WSSubscribeMsgStatus 订阅消息模板授权状态变更事件 129 | type WSSubscribeMsgStatus SubscribeMessageStatusData 130 | 131 | // WSEnterAIOData 进入aio事件 132 | type WSEnterAIOData EnterAIO 133 | -------------------------------------------------------------------------------- /errs/err.go: -------------------------------------------------------------------------------- 1 | // Package errs 是 SDK 里面的错误类型的集合,同时封装了 SDK 专用的错误类型。 2 | package errs 3 | 4 | import ( 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | // ErrNeedReConnect reconnect 10 | ErrNeedReConnect = New(CodeNeedReConnect, "need reconnect") 11 | // ErrInvalidSession 无效的 session 12 | ErrInvalidSession = New(CodeConnCloseCantResume, "invalid session") 13 | // ErrURLInvalid ws ap url 异常 14 | ErrURLInvalid = New(CodeConnCloseCantIdentify, "ws ap url is invalid") 15 | // ErrSessionLimit session 数量受到限制 16 | ErrSessionLimit = New(CodeConnCloseCantIdentify, "session num limit") 17 | 18 | // ErrNotFoundOpenAPI 未找到对应版本的openapi实现 19 | ErrNotFoundOpenAPI = New(CodeNotFoundOpenAPI, "not found openapi version") 20 | // ErrPagerIsNil 分页器为空 21 | ErrPagerIsNil = New(CodePagerIsNil, "pager is nil") 22 | ) 23 | 24 | // sdk 错误码 25 | const ( 26 | CodeNeedReConnect = 9000 27 | // CodeInvalidSession 无效的的 session id 请重新连接 28 | CodeInvalidSession = 9001 29 | CodeURLInvalid = 9002 30 | CodeNotFoundOpenAPI = 9003 31 | CodeSessionLimit = 9004 32 | // CodeConnCloseCantResume 关闭连接错误码,收拢 websocket close error,不允许 resume 33 | CodeConnCloseCantResume = 9005 34 | // CodeConnCloseCantIdentify 不允许连接的关闭连接错误,比如机器人被封禁 35 | CodeConnCloseCantIdentify = 9006 36 | // CodePagerIsNil 分页器为空 37 | CodePagerIsNil = 9007 38 | ) 39 | 40 | // websocket错误码 41 | const ( 42 | WSCodeBackendUnknownError = 4000 // 未知错误 43 | WSCodeBackendUnknownOpCode = 4001 // 请求中的opcode非法 44 | WSCodeBackendDecodeError = 4002 // 请求发送的数据解析失败,数据格式有问题 45 | WSCodeBackendNotAuthenticate = 4003 // 尚未进行身份校验 46 | WSCodeBackendAuthenticationFail = 4004 // 身份校验失败 47 | WSCodeBackendAlreadyAuthenticate = 4005 // 重复的身份校验 48 | WSCodeBackendSessionNoLongerValid = 4006 // session已失效 49 | WSCodeBackendInvalidSeq = 4007 // 非法的序号 50 | WSCodeBackendRateLimit = 4008 // 请求超频 51 | WSCodeBackendSessionTimeOut = 4009 // session过期,需要重连 52 | WSCodeBackendInvalidShard = 4010 // 非法的shard 53 | WSCodeBackendShardingRequired = 4011 // 当前shard承载数据过多,需要重连已重新分配shard 54 | WSCodeBackendInvalidAPIVersion = 4012 // 非法的api版本号 55 | WSCodeBackendInvalidIntents = 4013 // 非法的intents参数 56 | WSCodeBackendDisallowdIntents = 4014 // 使用了未授权使用的intents 57 | WSCodeBackendBotOffline = 4914 // 机器人已经被下架 58 | WSCodeBackendBotBanned = 4915 // 机器人被封禁 59 | ) 60 | 61 | // openapi错误码 62 | const ( 63 | APICodeTokenExpireOrNotExist = 11244 // token过期或者不存在 64 | ) 65 | 66 | // Err sdk err 67 | type Err struct { 68 | code int 69 | text string 70 | trace string // 错误追踪ID,可用于向平台反馈问题 71 | } 72 | 73 | // New 创建一个新错误 74 | func New(code int, text string, trace ...string) error { 75 | err := &Err{ 76 | code: code, 77 | text: text, 78 | } 79 | if len(trace) > 0 { 80 | err.trace = trace[0] 81 | } 82 | return err 83 | } 84 | 85 | // Error 将错误转换为 sdk 的错误类型 86 | func Error(err error) *Err { 87 | if e, ok := err.(*Err); ok { 88 | return e 89 | } 90 | return &Err{ 91 | code: 9999, 92 | text: err.Error(), 93 | } 94 | } 95 | 96 | // Error 输出错误信息 97 | func (e Err) Error() string { 98 | return fmt.Sprintf("code:%v, text:%v, traceID:%s", e.code, e.text, e.trace) 99 | } 100 | 101 | // Code 获取错误码 102 | func (e Err) Code() int { 103 | return e.code 104 | } 105 | 106 | // Text 获取错误信息 107 | func (e Err) Text() string { 108 | return e.text 109 | } 110 | 111 | // Trace 获取错误追踪ID 112 | func (e Err) Trace() string { 113 | return e.trace 114 | } 115 | -------------------------------------------------------------------------------- /event/register_test.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/tencent-connect/botgo/dto" 9 | ) 10 | 11 | func TestRegisterHandlers(t *testing.T) { 12 | var guild GuildEventHandler = func(event *dto.WSPayload, data *dto.WSGuildData) error { 13 | return nil 14 | } 15 | var message MessageEventHandler = func(event *dto.WSPayload, data *dto.WSMessageData) error { 16 | return nil 17 | } 18 | var audio AudioEventHandler = func(event *dto.WSPayload, data *dto.WSAudioData) error { 19 | return nil 20 | } 21 | 22 | t.Run( 23 | "test intent", func(t *testing.T) { 24 | i := RegisterHandlers(guild, message, audio) 25 | fmt.Println(i) 26 | assert.Equal(t, dto.IntentGuildMessages, i&dto.IntentGuildMessages) 27 | assert.Equal(t, dto.IntentGuilds, i&dto.IntentGuilds) 28 | assert.Equal(t, dto.IntentAudio, i&dto.IntentAudio) 29 | }, 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 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 模拟回调请求。开发者完成服务部署前可通过此工具模拟回调请求,实现业务逻辑。 9 | -------------------------------------------------------------------------------- /examples/apitest/announces_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/tencent-connect/botgo/dto" 8 | ) 9 | 10 | func TestAnnounces(t *testing.T) { 11 | 12 | t.Run( 13 | "create channel announce", func(t *testing.T) { 14 | messageInfo, err := api.PostMessage( 15 | ctx, testChannelID, &dto.MessageToCreate{ 16 | Content: "子频道公共创建", 17 | }, 18 | ) 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | announces, err := api.CreateChannelAnnounces( 23 | ctx, testChannelID, &dto.ChannelAnnouncesToCreate{ 24 | MessageID: messageInfo.ID, 25 | }, 26 | ) 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | t.Logf("announces:%+v", announces) 31 | }, 32 | ) 33 | t.Run( 34 | "delete channel announce", func(t *testing.T) { 35 | time.Sleep(3 * time.Second) 36 | if err := api.DeleteChannelAnnounces(ctx, testChannelID, testMessageID); err != nil { 37 | t.Error(err) 38 | } 39 | 40 | }, 41 | ) 42 | t.Run( 43 | "clean channel announce no check message id", func(t *testing.T) { 44 | time.Sleep(3 * time.Second) 45 | err := api.CleanChannelAnnounces(ctx, testChannelID) 46 | if err != nil { 47 | t.Error(err) 48 | } 49 | }, 50 | ) 51 | t.Run( 52 | "create guild announce", func(t *testing.T) { 53 | time.Sleep(3 * time.Second) 54 | announces, err := api.CreateGuildAnnounces( 55 | ctx, testGuildID, &dto.GuildAnnouncesToCreate{ 56 | MessageID: testMessageID, 57 | ChannelID: testChannelID, 58 | }, 59 | ) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | t.Logf("announces:%+v", announces) 64 | }, 65 | ) 66 | t.Run( 67 | "create recommend channel guild announce", func(t *testing.T) { 68 | time.Sleep(3 * time.Second) 69 | announces, err := api.CreateGuildAnnounces( 70 | ctx, testGuildID, &dto.GuildAnnouncesToCreate{ 71 | AnnouncesType: 0, 72 | RecommendChannels: []dto.RecommendChannel{ 73 | { 74 | ChannelID: "1146349", 75 | Introduce: "子频道 1146349 欢迎语", 76 | }, 77 | { 78 | ChannelID: "1703191", 79 | Introduce: "子频道 1703191 欢迎语", 80 | }, 81 | { 82 | ChannelID: "2651556", 83 | Introduce: "子频道 2651556 欢迎语", 84 | }, 85 | }, 86 | }, 87 | ) 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | t.Logf("announces:%+v", announces) 92 | }, 93 | ) 94 | t.Run( 95 | "delete guild announce", func(t *testing.T) { 96 | time.Sleep(3 * time.Second) 97 | if err := api.DeleteGuildAnnounces(ctx, testGuildID, testMessageID); err != nil { 98 | t.Error(err) 99 | } 100 | }, 101 | ) 102 | t.Run( 103 | "clean guild announce no check message id", func(t *testing.T) { 104 | time.Sleep(3 * time.Second) 105 | err := api.CleanGuildAnnounces(ctx, testGuildID) 106 | if err != nil { 107 | t.Error(err) 108 | } 109 | }, 110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /examples/apitest/api_permissions_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | func TestGetAPIPermissions(t *testing.T) { 10 | apiIdentify := &dto.APIPermissionDemandIdentify{} 11 | t.Run( 12 | "get api permissions", func(t *testing.T) { 13 | result, err := api.GetAPIPermissions(ctx, testGuildID) 14 | if err != nil { 15 | t.Error(err) 16 | } 17 | for _, v := range result.APIList { 18 | t.Logf("api permissions:%+v", v) 19 | } 20 | if len(result.APIList) > 0 { 21 | apiIdentify.Path = result.APIList[0].Path 22 | apiIdentify.Method = result.APIList[0].Method 23 | } 24 | 25 | }, 26 | ) 27 | t.Run( 28 | "create API permission demand", func(t *testing.T) { 29 | demand, err := api.RequireAPIPermissions(ctx, testGuildID, &dto.APIPermissionDemandToCreate{ 30 | ChannelID: testChannelID, 31 | APIIdentify: apiIdentify, 32 | Desc: "授权链接", 33 | }) 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | t.Logf("demand:%+v", demand) 38 | }, 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /examples/apitest/channel_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | func TestChannel(t *testing.T) { 10 | t.Run( 11 | "channel list", func(t *testing.T) { 12 | list, err := api.Channels(ctx, testGuildID) 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | for _, channel := range list { 17 | t.Logf("%+v", channel) 18 | } 19 | t.Logf(api.TraceID()) 20 | }, 21 | ) 22 | t.Run( 23 | "create and modify and delete", func(t *testing.T) { 24 | testGuildID = "3326534247441079828" 25 | channel, err := api.CreatePrivateChannel( 26 | ctx, testGuildID, &dto.ChannelValueObject{ 27 | Name: "机器人创建的频道", 28 | Type: dto.ChannelTypeText, 29 | Position: 0, 30 | ParentID: "0", // 父ID,正常应该找到一个分组ID,如果传0,就不归属在任何一个分组中 31 | }, []string{testMemberID}, 32 | ) 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | 37 | t.Log(channel) 38 | channelNew, err := api.PatchChannel( 39 | ctx, channel.ID, &dto.ChannelValueObject{ 40 | Name: "机器人修改的频道-修改", 41 | }, 42 | ) 43 | if err != nil { 44 | t.Error(err) 45 | } 46 | t.Log(channelNew) 47 | if channelNew.Name == channel.Name { 48 | t.Error("channel name not modified") 49 | } 50 | err = api.DeleteChannel(ctx, channelNew.ID) 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | }, 55 | ) 56 | t.Run( 57 | "get voice channel member list test", func(t *testing.T) { 58 | testChannelID := "1572139" 59 | members, err := api.ListVoiceChannelMembers(ctx, testChannelID) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | for _, member := range members { 64 | t.Logf("member%v", member) 65 | } 66 | 67 | }, 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /examples/apitest/direct_message_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRetractDMMessage(t *testing.T) { 8 | msgID := "10c7fac2c28dcac27a1a1231343431313532313831383136323933383420801e28003095a8683824402448cefba48e0650b1acf8fa05" 9 | t.Run( 10 | "私信消息撤回", func(t *testing.T) { 11 | err := api.RetractDMMessage(ctx, "6234704349443091672", msgID) 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | t.Logf("msg id : %v, is deleted", msgID) 16 | }, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /examples/apitest/guild_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | func TestGuild(t *testing.T) { 10 | t.Run( 11 | "guild info", func(t *testing.T) { 12 | guild, err := api.Guild(ctx, testGuildID) 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | t.Log(guild) 17 | }, 18 | ) 19 | t.Run( 20 | "my join guilds", func(t *testing.T) { 21 | guilds, err := api.MeGuilds( 22 | ctx, &dto.GuildPager{ 23 | Limit: "100", 24 | }, 25 | ) 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | for _, guild := range guilds { 30 | t.Log(guild) 31 | } 32 | }, 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /examples/apitest/interaction_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/tencent-connect/botgo/dto" 8 | ) 9 | 10 | func TestInteractions(t *testing.T) { 11 | t.Run( 12 | "put interaction", func(t *testing.T) { 13 | body, _ := json.Marshal( 14 | dto.InteractionData{ 15 | Name: "interaction", 16 | Type: 2, 17 | Resolved: json.RawMessage("test"), 18 | }, 19 | ) 20 | err := api.PutInteraction(ctx, testInteractionD, string(body)) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | }, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /examples/apitest/main_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "gopkg.in/yaml.v3" 11 | 12 | "github.com/tencent-connect/botgo" 13 | "github.com/tencent-connect/botgo/openapi" 14 | "github.com/tencent-connect/botgo/token" 15 | ) 16 | 17 | var conf struct { 18 | AppID uint64 `yaml:"appid"` 19 | Secret string `yaml:"secret"` 20 | } 21 | var api openapi.OpenAPI 22 | 23 | var ( 24 | testGuildID = "3326534247441079828" // replace your guild id 25 | testChannelID = "1595028" // replace your channel id 26 | testMessageID = `08e092eeb983afef9e0110f9bb5d1a1231343431313532313836373838333234303420801e 27 | 28003091c4bb02380c400c48d8a7928d06` // replace your channel id 28 | testRolesID = `10054557` // replace your roles id 29 | testMemberID = `1201318637970874066` // replace your member id 30 | testMarkdownTemplateID = 1231231231231231 // replace your markdown template id 31 | testInteractionD = "e924431f-aaed-4e78-8375-9295b1f1e0ef" // replace your interaction id 32 | ctx context.Context 33 | ) 34 | 35 | func TestMain(m *testing.M) { 36 | ctx = context.Background() 37 | content, err := os.ReadFile("./config.yaml") 38 | if err != nil { 39 | log.Println("read conf failed") 40 | os.Exit(1) 41 | } 42 | credentials := &token.QQBotCredentials{} 43 | if err := yaml.Unmarshal(content, credentials); err != nil { 44 | log.Println(err) 45 | os.Exit(1) 46 | } 47 | log.Println(credentials) 48 | appid := credentials.AppID 49 | tokenSource := token.NewQQBotTokenSource(credentials) 50 | api = botgo.NewOpenAPI(appid, tokenSource).WithTimeout(3 * time.Second) 51 | os.Exit(m.Run()) 52 | } 53 | -------------------------------------------------------------------------------- /examples/apitest/member_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | func Test_Member(t *testing.T) { 10 | var userId string 11 | t.Run( 12 | "list member", func(t *testing.T) { 13 | members, err := api.GuildMembers( 14 | ctx, testGuildID, &dto.GuildMembersPager{ 15 | After: "0", 16 | Limit: "10", 17 | }, 18 | ) 19 | for _, member := range members { 20 | t.Logf("user: %+v", member.User.Username) 21 | t.Logf("roles: %+v", member.Roles) 22 | userId = member.User.ID 23 | } 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | }, 28 | ) 29 | t.Run( 30 | "get member", func(t *testing.T) { 31 | member, err := api.GuildMember(ctx, testGuildID, userId) 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | t.Logf("member: %+v", member) 36 | t.Logf("user: %+v", member.User) 37 | }, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /examples/apitest/message_reaction_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | func TestMessageReaction(t *testing.T) { 10 | t.Run( 11 | "Create Message Reaction", func(t *testing.T) { 12 | err := api.CreateMessageReaction(ctx, testChannelID, testMessageID, dto.Emoji{Type: 1, ID: "43"}) 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | t.Logf("err:%+v", err) 17 | }, 18 | ) 19 | t.Run( 20 | "Delete Own Reaction", func(t *testing.T) { 21 | err := api.DeleteOwnMessageReaction(ctx, testChannelID, testMessageID, dto.Emoji{Type: 1, ID: "43"}) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | t.Logf("err:%+v", err) 26 | }, 27 | ) 28 | t.Run( 29 | "Get Reaction Users", func(t *testing.T) { 30 | users, err := api.GetMessageReactionUsers(ctx, testChannelID, testMessageID, dto.Emoji{Type: 1, ID: "43"}, 31 | &dto.MessageReactionPager{ 32 | Limit: "20", 33 | }) 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | t.Logf("err:%+v", err) 38 | 39 | _, err = api.GetMessageReactionUsers(ctx, testChannelID, testMessageID, dto.Emoji{Type: 1, ID: "43"}, 40 | &dto.MessageReactionPager{ 41 | Cookie: users.Cookie, 42 | Limit: "20", 43 | }) 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | t.Logf("err:%+v", err) 48 | }, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /examples/apitest/message_setting_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMessageSetting(t *testing.T) { 8 | t.Run( 9 | "get message setting", func(t *testing.T) { 10 | settingInfo, err := api.GetMessageSetting( 11 | ctx, testGuildID, 12 | ) 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | t.Logf("settingIfno:%+v", settingInfo) 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /examples/apitest/mute_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | 8 | "github.com/tencent-connect/botgo/dto" 9 | ) 10 | 11 | var ( 12 | testMuteGuildID = "3326534247441079828" // replace your guild id 13 | testMuteUserID = "144111002883982087" // replace your user id 14 | ) 15 | 16 | func Test_mute(t *testing.T) { 17 | t.Run( 18 | "频道禁言", func(t *testing.T) { 19 | mute := &dto.UpdateGuildMute{ 20 | MuteEndTimestamp: strconv.FormatInt(time.Now().Unix()+600, 10), 21 | } 22 | err := api.GuildMute(ctx, testMuteGuildID, mute) 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | t.Logf("Testing_Succ") 27 | }, 28 | ) 29 | t.Run( 30 | "频道指定成员禁言", func(t *testing.T) { 31 | mute := &dto.UpdateGuildMute{ 32 | MuteEndTimestamp: strconv.FormatInt(time.Now().Unix()+600, 10), 33 | } 34 | err := api.MemberMute(ctx, testGuildID, testMuteUserID, mute) 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | t.Logf("Testing_Succ") 39 | }, 40 | ) 41 | t.Run( 42 | "频道指定批量成员禁言", func(t *testing.T) { 43 | mute := &dto.UpdateGuildMute{ 44 | MuteEndTimestamp: strconv.FormatInt(time.Now().Unix()+600, 10), 45 | UserIDs: []string{testMuteUserID}, 46 | } 47 | _, err := api.MultiMemberMute(ctx, testGuildID, mute) 48 | if err != nil { 49 | t.Error(err) 50 | } 51 | t.Logf("Testing_Succ") 52 | }, 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /examples/apitest/permission_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | func Test_ChannelRolesPermissions(t *testing.T) { 10 | t.Run( 11 | "update roles Permissions", func(t *testing.T) { 12 | updatePermissions := &dto.UpdateChannelPermissions{ 13 | Add: "5", 14 | } 15 | err := api.PutChannelRolesPermissions(ctx, testChannelID, testRolesID, updatePermissions) 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | }, 20 | ) 21 | t.Run( 22 | "get roles Permissions", func(t *testing.T) { 23 | permissions, err := api.ChannelRolesPermissions(ctx, testChannelID, testRolesID) 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | t.Logf("permissions: %+v", permissions) 28 | }, 29 | ) 30 | } 31 | 32 | func Test_ChannelMembersPermissions(t *testing.T) { 33 | t.Run( 34 | "update members Permissions", func(t *testing.T) { 35 | updatePermissions := &dto.UpdateChannelPermissions{ 36 | Add: "5", 37 | } 38 | err := api.PutChannelPermissions(ctx, testChannelID, testMemberID, updatePermissions) 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | }, 43 | ) 44 | t.Run( 45 | "get members Permissions", func(t *testing.T) { 46 | permissions, err := api.ChannelPermissions(ctx, testChannelID, testMemberID) 47 | if err != nil { 48 | t.Error(err) 49 | } 50 | t.Logf("permissions: %+v", permissions) 51 | }, 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /examples/apitest/pins_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestPins(t *testing.T) { 9 | 10 | t.Run( 11 | "add pins", func(t *testing.T) { 12 | pins, err := api.AddPins( 13 | ctx, testChannelID, testMessageID, 14 | ) 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | t.Logf("pins:%+v", pins) 19 | }, 20 | ) 21 | t.Run( 22 | "get pins", func(t *testing.T) { 23 | time.Sleep(3 * time.Second) 24 | pins, err := api.GetPins(ctx, testChannelID) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | t.Logf("pins:%+v", pins) 29 | 30 | }, 31 | ) 32 | t.Run( 33 | "delete pins", func(t *testing.T) { 34 | time.Sleep(3 * time.Second) 35 | err := api.DeletePins(ctx, testChannelID, testMessageID) 36 | if err != nil { 37 | t.Error(err) 38 | } 39 | }, 40 | ) 41 | t.Run( 42 | "clean pins no check message id", func(t *testing.T) { 43 | time.Sleep(3 * time.Second) 44 | err := api.CleanPins(ctx, testChannelID) 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | }, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /examples/apitest/role_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/tencent-connect/botgo/dto" 9 | ) 10 | 11 | const ( 12 | manageChannelPermission = uint64(1) << 1 13 | defaultRoleTypeChannelAdmin = "5" 14 | patchRoleModifyName = "test role modify" 15 | ) 16 | 17 | // Test_role 身份组相关接口用例 18 | func Test_role(t *testing.T) { 19 | var roleID dto.RoleID 20 | var err error 21 | 22 | t.Run( 23 | "拉取身份组列表", func(t *testing.T) { 24 | roles, err := api.Roles(ctx, testGuildID) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | t.Logf("%+v", roles) 29 | for _, role := range roles.Roles { 30 | t.Logf("%+v", role) 31 | } 32 | }, 33 | ) 34 | t.Run( 35 | "创建身份组", func(t *testing.T) { 36 | postRoleResult, err := api.PostRole( 37 | ctx, testGuildID, &dto.Role{ 38 | Name: "test role", 39 | Color: 4278245297, 40 | Hoist: 0, 41 | }, 42 | ) 43 | if err != nil { 44 | t.Error(err) 45 | } 46 | roleID = postRoleResult.RoleID 47 | t.Logf("postRoleResult: %+v", postRoleResult) 48 | }, 49 | ) 50 | t.Run( 51 | "修改身份组", func(t *testing.T) { 52 | patchRoleResult, err := api.PatchRole( 53 | ctx, testGuildID, roleID, &dto.Role{ 54 | Name: patchRoleModifyName, 55 | }, 56 | ) 57 | if err != nil { 58 | t.Error(err) 59 | } 60 | reflect.DeepEqual(patchRoleModifyName, patchRoleResult.Role.Name) 61 | t.Logf("patchRoleResult: %+v", patchRoleResult) 62 | }, 63 | ) 64 | t.Run( 65 | "删除身份组", func(t *testing.T) { 66 | err = api.DeleteRole(ctx, testGuildID, roleID) 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | t.Logf("role id : %v, is deleted", roleID) 71 | }, 72 | ) 73 | } 74 | 75 | func Test_roleWithMember(t *testing.T) { 76 | var roleID dto.RoleID 77 | 78 | t.Run( 79 | "添加人到身份组", func(t *testing.T) { 80 | members, err := api.GuildMembers( 81 | ctx, testGuildID, &dto.GuildMembersPager{ 82 | After: "0", 83 | Limit: "1", 84 | }, 85 | ) 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | userID := members[0].User.ID 90 | err = api.MemberAddRole(ctx, testGuildID, roleID, userID, nil) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | member, err := api.GuildMember(ctx, testGuildID, userID) 95 | var roleFound bool 96 | for _, role := range member.Roles { 97 | if role == string(roleID) { 98 | roleFound = true 99 | } 100 | } 101 | if !roleFound { 102 | t.Error("not found role id been add") 103 | } 104 | }, 105 | ) 106 | t.Run( 107 | "添加人到子频道管理员身份组并指定子频道", func(t *testing.T) { 108 | members, err := api.GuildMembers( 109 | ctx, testGuildID, &dto.GuildMembersPager{ 110 | After: "0", 111 | Limit: "1", 112 | }, 113 | ) 114 | if err != nil { 115 | t.Error(err) 116 | } 117 | userID := members[0].User.ID 118 | channels, err := api.Channels(ctx, testGuildID) 119 | if err != nil { 120 | t.Error(err) 121 | } 122 | channelID := channels[len(channels)-1].ID 123 | t.Logf("testGuildID: %+v, channelID: %+v", testGuildID, channelID) 124 | err = api.MemberAddRole( 125 | ctx, testGuildID, defaultRoleTypeChannelAdmin, userID, &dto.MemberAddRoleBody{ 126 | Channel: &dto.Channel{ 127 | ID: channelID, 128 | }, 129 | }, 130 | ) 131 | if err != nil { 132 | t.Error(err) 133 | } 134 | channelPermissions, err := api.ChannelPermissions(ctx, channelID, userID) 135 | if err != nil { 136 | t.Error(err) 137 | } 138 | channelPermissionsUint, err := strconv.ParseUint(channelPermissions.Permissions, 10, 64) 139 | if err != nil { 140 | t.Error(err) 141 | } 142 | t.Logf( 143 | "channelPermissionsUint: %+v, channelPermissions.Permissions: %+v", 144 | channelPermissionsUint, channelPermissions.Permissions, 145 | ) 146 | if channelPermissionsUint&manageChannelPermission != 2 { 147 | t.Error("not found channel permissions been add") 148 | } 149 | }, 150 | ) 151 | t.Run( 152 | "在子频道管理员身份组删除用户并指定子频道", func(t *testing.T) { 153 | members, err := api.GuildMembers( 154 | ctx, testGuildID, &dto.GuildMembersPager{ 155 | After: "0", 156 | Limit: "1", 157 | }, 158 | ) 159 | if err != nil { 160 | t.Error(err) 161 | } 162 | userID := members[0].User.ID 163 | channels, err := api.Channels(ctx, testGuildID) 164 | if err != nil { 165 | t.Error(err) 166 | } 167 | channelID := channels[len(channels)-1].ID 168 | t.Logf("testGuildID: %+v, channelID: %+v", testGuildID, channelID) 169 | err = api.MemberDeleteRole( 170 | ctx, testGuildID, defaultRoleTypeChannelAdmin, userID, &dto.MemberAddRoleBody{ 171 | Channel: &dto.Channel{ 172 | ID: channelID, 173 | }, 174 | }, 175 | ) 176 | if err != nil { 177 | t.Error(err) 178 | } 179 | channelPermissions, err := api.ChannelPermissions(ctx, channelID, userID) 180 | if err != nil { 181 | t.Error(err) 182 | } 183 | channelPermissionsUint, err := strconv.ParseUint(channelPermissions.Permissions, 10, 64) 184 | if err != nil { 185 | t.Error(err) 186 | } 187 | t.Logf("channelPermissionsUint: %+v", channelPermissionsUint) 188 | if channelPermissionsUint&manageChannelPermission == 2 { 189 | t.Error("not found channel permissions been add") 190 | } 191 | }, 192 | ) 193 | } 194 | -------------------------------------------------------------------------------- /examples/apitest/schedule_test.go: -------------------------------------------------------------------------------- 1 | package apitest 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | var ( 10 | scheduleID string 11 | ) 12 | 13 | func Test_schedule(t *testing.T) { 14 | t.Run( 15 | "拉取日程列表", func(t *testing.T) { 16 | rsp, err := api.ListSchedules(ctx, testChannelID, 0) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | for _, v := range rsp { 21 | t.Logf("%v", v) 22 | scheduleID = v.ID 23 | } 24 | }, 25 | ) 26 | t.Run( 27 | "拉取单个日程", func(t *testing.T) { 28 | rsp, err := api.GetSchedule(ctx, testChannelID, scheduleID) 29 | if err != nil { 30 | t.Error(err) 31 | } 32 | t.Logf("%v", rsp) 33 | }, 34 | ) 35 | t.Run( 36 | "创建日程", func(t *testing.T) { 37 | schedule := &dto.Schedule{ 38 | Name: "测试创建", 39 | StartTimestamp: "1639110300000", 40 | EndTimestamp: "1639110900000", 41 | RemindType: "0", 42 | } 43 | rsp, err := api.CreateSchedule(ctx, testChannelID, schedule) 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | t.Logf("%v", rsp) 48 | scheduleID = rsp.ID 49 | }, 50 | ) 51 | t.Run( 52 | "修改日程", func(t *testing.T) { 53 | schedule := &dto.Schedule{ 54 | Name: "测试修改", 55 | StartTimestamp: "1639110300000", 56 | EndTimestamp: "1639110900000", 57 | RemindType: "0", 58 | } 59 | rsp, err := api.ModifySchedule(ctx, testChannelID, scheduleID, schedule) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | t.Logf("%v", rsp) 64 | scheduleID = rsp.ID 65 | }, 66 | ) 67 | t.Run( 68 | "删除日程", func(t *testing.T) { 69 | err := api.DeleteSchedule(ctx, testChannelID, scheduleID) 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | }, 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /examples/config.yaml.demo: -------------------------------------------------------------------------------- 1 | # 在这个配置文件中补充你的 appid 和 secret,并修改文件名为 config.yaml 2 | appid : 3 | secret : -------------------------------------------------------------------------------- /examples/custom-filter/README.md: -------------------------------------------------------------------------------- 1 | # custom-filter 2 | 3 | ## 演示功能 4 | 5 | 通过自定义 filter 功能,实现自定义链路跟踪 ID,上报模调监控等。 -------------------------------------------------------------------------------- /examples/custom-filter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/google/uuid" 13 | "github.com/tencent-connect/botgo" 14 | "github.com/tencent-connect/botgo/dto" 15 | "github.com/tencent-connect/botgo/dto/message" 16 | "github.com/tencent-connect/botgo/event" 17 | "github.com/tencent-connect/botgo/interaction/webhook" 18 | "gopkg.in/yaml.v3" 19 | 20 | "github.com/tencent-connect/botgo/constant" 21 | "github.com/tencent-connect/botgo/openapi" 22 | "github.com/tencent-connect/botgo/token" 23 | ) 24 | 25 | const ( 26 | host_ = "0.0.0.0" 27 | port_ = 9000 28 | path_ = "/qqbot" 29 | ) 30 | 31 | func main() { 32 | openapi.RegisterReqFilter("set-trace", ReqFilter) 33 | openapi.RegisterRespFilter("get-trace", RespFilter) 34 | // 加载 appid 和 token 35 | content, err := os.ReadFile("config.yaml") 36 | if err != nil { 37 | log.Fatalln("load config file failed, err:", err) 38 | } 39 | credentials := &token.QQBotCredentials{} 40 | if err = yaml.Unmarshal(content, &credentials); err != nil { 41 | log.Fatalln("parse config failed, err:", err) 42 | } 43 | tokenSource := token.NewQQBotTokenSource(credentials) 44 | ctx, cancel := context.WithCancel(context.Background()) 45 | defer cancel() //释放刷新协程 46 | if err = token.StartRefreshAccessToken(ctx, tokenSource); err != nil { 47 | log.Fatalln(err) 48 | } 49 | // 初始化 openapi,正式环境 50 | api := botgo.NewOpenAPI(credentials.AppID, tokenSource).WithTimeout(5 * time.Second).SetDebug(true) 51 | // 根据不同的回调,生成 intents 52 | _ = event.RegisterHandlers(GuildATMessageEventHandler(api)) 53 | // 初始化 openapi,正式环境 54 | http.HandleFunc(path_, func(writer http.ResponseWriter, request *http.Request) { 55 | webhook.HTTPHandler(writer, request, credentials) 56 | }) 57 | if err = http.ListenAndServe(fmt.Sprintf("%s:%d", host_, port_), nil); err != nil { 58 | log.Fatal("setup server fatal:", err) 59 | } 60 | } 61 | 62 | // ReqFilter 自定义请求过滤器 63 | func ReqFilter(req *http.Request, _ *http.Response) error { 64 | req.Header.Set("X-Custom-TraceID", uuid.NewString()) 65 | return nil 66 | } 67 | 68 | // RespFilter 自定义响应过滤器 69 | func RespFilter(req *http.Request, resp *http.Response) error { 70 | log.Println("trace id added by req filter", req.Header.Get("X-Custom-TraceID")) 71 | log.Println("trace id return by openapi", resp.Header.Get(constant.HeaderTraceID)) 72 | return nil 73 | } 74 | 75 | // GuildATMessageEventHandler 实现处理 at 消息的回调 76 | func GuildATMessageEventHandler(api openapi.OpenAPI) event.ATMessageEventHandler { 77 | return func(event *dto.WSPayload, data *dto.WSATMessageData) error { 78 | log.Printf("[%s] %s", event.Type, data.Content) 79 | input := strings.ToLower(message.ETLInput(data.Content)) 80 | log.Printf("clear input content is: %s", input) 81 | return nil 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/custom-logger/README.md: -------------------------------------------------------------------------------- 1 | # custom-logger 2 | 3 | ## 演示功能 4 | 5 | 自定义实现 logger,步骤: 6 | 7 | - 按照 `log.Logger` 的接口定义,实现自己的 logger,比如写入到文件,或者写入到远程 8 | - 使用 `botgo.SetLogger` 将新的 logger 设置到 sdk 中,sdk 就会使用这个 logger 来写日志了 -------------------------------------------------------------------------------- /examples/custom-logger/logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | "strings" 9 | "time" 10 | 11 | "go.uber.org/zap" 12 | "go.uber.org/zap/zapcore" 13 | ) 14 | 15 | // FileLogger file logger 16 | type FileLogger struct { 17 | logger *zap.Logger 18 | } 19 | 20 | type logLevel zapcore.Level 21 | 22 | const ( 23 | DebugLevel = logLevel(zapcore.DebugLevel) 24 | InfoLevel = logLevel(zapcore.InfoLevel) 25 | WarnLevel = logLevel(zapcore.WarnLevel) 26 | FatalLevel = logLevel(zapcore.FatalLevel) 27 | ) 28 | 29 | // New creates a new FileLogger. 30 | func New(logPath string, minLogLevel logLevel) (FileLogger, error) { 31 | file, err := os.Create(fmt.Sprintf("%s/botgo.log", logPath)) 32 | if err != nil { 33 | return FileLogger{}, err 34 | } 35 | return FileLogger{ 36 | logger: zap.New( 37 | zapcore.NewCore( 38 | zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), 39 | zapcore.AddSync(file), 40 | zapcore.Level(minLogLevel), 41 | ), 42 | ), 43 | }, nil 44 | } 45 | 46 | // Debug logs a message at DebugLevel. The message includes any fields passed 47 | func (f FileLogger) Debug(v ...interface{}) { 48 | f.logger.Debug(output(v...)) 49 | } 50 | 51 | // Info logs a message at InfoLevel. The message includes any fields passed 52 | func (f FileLogger) Info(v ...interface{}) { 53 | f.logger.Info(output(v...)) 54 | } 55 | 56 | // Warn logs a message at WarnLevel. The message includes any fields passed 57 | func (f FileLogger) Warn(v ...interface{}) { 58 | f.logger.Warn(output(v...)) 59 | } 60 | 61 | // Error logs a message at ErrorLevel. The message includes any fields passed 62 | func (f FileLogger) Error(v ...interface{}) { 63 | f.logger.Error(output(v...)) 64 | } 65 | 66 | // Debugf logs a message at DebugLevel. The message includes any fields passed 67 | func (f FileLogger) Debugf(format string, v ...interface{}) { 68 | f.logger.Debug(output(fmt.Sprintf(format, v...))) 69 | } 70 | 71 | // Infof logs a message at InfoLevel. The message includes any fields passed 72 | func (f FileLogger) Infof(format string, v ...interface{}) { 73 | f.logger.Info(output(fmt.Sprintf(format, v...))) 74 | } 75 | 76 | // Warnf logs a message at WarnLevel. The message includes any fields passed 77 | func (f FileLogger) Warnf(format string, v ...interface{}) { 78 | f.logger.Warn(output(fmt.Sprintf(format, v...))) 79 | } 80 | 81 | // Errorf logs a message at ErrorLevel. The message includes any fields passed 82 | func (f FileLogger) Errorf(format string, v ...interface{}) { 83 | f.logger.Error(output(fmt.Sprintf(format, v...))) 84 | } 85 | 86 | // Sync flushes any buffered log entries. 87 | func (f FileLogger) Sync() error { 88 | return f.logger.Sync() 89 | } 90 | 91 | func output(v ...interface{}) string { 92 | pc, file, line, _ := runtime.Caller(3) 93 | file = filepath.Base(file) 94 | funcName := strings.TrimPrefix(filepath.Ext(runtime.FuncForPC(pc).Name()), ".") 95 | 96 | logFormat := "%s %s:%d:%s " + fmt.Sprint(v...) + "\n" 97 | date := time.Now().Format("2006-01-02 15:04:05") 98 | return fmt.Sprintf(logFormat, date, file, line, funcName) 99 | } 100 | -------------------------------------------------------------------------------- /examples/custom-logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFileLogger_Debug(t *testing.T) { 8 | l, err := New("./", DebugLevel) 9 | if err != nil { 10 | t.Error(err) 11 | } 12 | l.Debug("abc") 13 | } 14 | -------------------------------------------------------------------------------- /examples/custom-logger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/tencent-connect/botgo" 13 | "github.com/tencent-connect/botgo/dto" 14 | "github.com/tencent-connect/botgo/dto/message" 15 | "github.com/tencent-connect/botgo/event" 16 | "github.com/tencent-connect/botgo/interaction/webhook" 17 | "github.com/tencent-connect/botgo/openapi" 18 | "github.com/tencent-connect/botgo/token" 19 | "gopkg.in/yaml.v3" 20 | ) 21 | 22 | const ( 23 | host_ = "0.0.0.0" 24 | port_ = 9000 25 | path_ = "/qqbot" 26 | ) 27 | 28 | func main() { 29 | // 初始化新的文件 logger,并使用相对路径来作为日志存放位置,设置最小日志界别为 DebugLevel 30 | logger, err := New("./", DebugLevel) 31 | if err != nil { 32 | log.Fatalln("error log new", err) 33 | } 34 | // 把新的 logger 设置到 sdk 上,替换掉老的控制台 logger 35 | botgo.SetLogger(logger) 36 | content, err := os.ReadFile("config.yaml") 37 | if err != nil { 38 | log.Fatalln("load config file failed, err:", err) 39 | } 40 | credentials := &token.QQBotCredentials{} 41 | if err = yaml.Unmarshal(content, &credentials); err != nil { 42 | log.Fatalln("parse config failed, err:", err) 43 | } 44 | 45 | tokenSource := token.NewQQBotTokenSource(credentials) 46 | ctx, cancel := context.WithCancel(context.Background()) 47 | defer cancel() //释放刷新协程 48 | if err = token.StartRefreshAccessToken(ctx, tokenSource); err != nil { 49 | log.Fatalln(err) 50 | } 51 | // 初始化 openapi,正式环境 52 | api := botgo.NewOpenAPI(credentials.AppID, tokenSource).WithTimeout(5 * time.Second).SetDebug(true) 53 | // 根据不同的回调,生成 intents 54 | _ = event.RegisterHandlers(GuildATMessageEventHandler(api)) 55 | http.HandleFunc(path_, func(writer http.ResponseWriter, request *http.Request) { 56 | webhook.HTTPHandler(writer, request, credentials) 57 | }) 58 | if err = http.ListenAndServe(fmt.Sprintf("%s:%d", host_, port_), nil); err != nil { 59 | log.Fatal("setup server fatal:", err) 60 | } 61 | } 62 | 63 | // GuildATMessageEventHandler 实现处理 at 消息的回调 64 | func GuildATMessageEventHandler(api openapi.OpenAPI) event.ATMessageEventHandler { 65 | return func(event *dto.WSPayload, data *dto.WSATMessageData) error { 66 | log.Printf("[%s] %s", event.Type, data.Content) 67 | input := strings.ToLower(message.ETLInput(data.Content)) 68 | log.Printf("clear input content is: %s", input) 69 | return nil 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tencent-connect/botgo/examples 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/google/uuid v1.3.0 7 | github.com/tencent-connect/botgo v0.1.7 8 | go.uber.org/zap v1.19.1 9 | gopkg.in/yaml.v3 v3.0.1 10 | ) 11 | 12 | require ( 13 | github.com/go-resty/resty/v2 v2.6.0 // indirect 14 | github.com/gorilla/websocket v1.4.2 // indirect 15 | github.com/tidwall/gjson v1.9.3 // indirect 16 | github.com/tidwall/match v1.1.1 // indirect 17 | github.com/tidwall/pretty v1.2.0 // indirect 18 | go.uber.org/atomic v1.9.0 // indirect 19 | go.uber.org/multierr v1.7.0 // indirect 20 | golang.org/x/net v0.19.0 // indirect 21 | golang.org/x/oauth2 v0.23.0 // indirect 22 | golang.org/x/sync v0.1.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /examples/receive-and-send/Makefile: -------------------------------------------------------------------------------- 1 | #执行指令 /usr/bin/make -f Makefile -C ./ build 2 | #打压缩包 zip scf_code.zip * -r 3 | BINARY_NAME=qqbot-svr 4 | 5 | build: 6 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn" -o $(BINARY_NAME) -v 7 | 8 | -------------------------------------------------------------------------------- /examples/receive-and-send/README.md: -------------------------------------------------------------------------------- 1 | # receive-and-send 2 | 3 | 服务可部署在server/serverless节点上。 [quick start](../../README.md#一quick-start)介绍了如何将服务端部署到腾讯云函数,便于快速体验、验证。 4 | -------------------------------------------------------------------------------- /examples/receive-and-send/cmd_action.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/tencent-connect/botgo/dto" 8 | ) 9 | 10 | func (p Processor) setEmoji(ctx context.Context, channelID string, messageID string) { 11 | err := p.api.CreateMessageReaction( 12 | ctx, channelID, messageID, dto.Emoji{ 13 | ID: "307", 14 | Type: 1, 15 | }, 16 | ) 17 | if err != nil { 18 | log.Println(err) 19 | } 20 | } 21 | 22 | func (p Processor) setPins(ctx context.Context, channelID, msgID string) { 23 | _, err := p.api.AddPins(ctx, channelID, msgID) 24 | if err != nil { 25 | log.Println(err) 26 | } 27 | } 28 | 29 | func (p Processor) setAnnounces(ctx context.Context, data *dto.WSATMessageData) { 30 | if _, err := p.api.CreateChannelAnnounces( 31 | ctx, data.ChannelID, 32 | &dto.ChannelAnnouncesToCreate{MessageID: data.ID}, 33 | ); err != nil { 34 | log.Println(err) 35 | } 36 | } 37 | 38 | func (p Processor) sendChannelReply(ctx context.Context, channelID string, toCreate *dto.MessageToCreate) error { 39 | if _, err := p.api.PostMessage(ctx, channelID, toCreate); err != nil { 40 | log.Println(err) 41 | return err 42 | } 43 | return nil 44 | } 45 | 46 | func (p Processor) sendGroupReply(ctx context.Context, groupID string, toCreate dto.APIMessage) error { 47 | log.Printf("EVENT ID:%v", toCreate.GetEventID()) 48 | if _, err := p.api.PostGroupMessage(ctx, groupID, toCreate); err != nil { 49 | log.Println(err) 50 | return err 51 | } 52 | return nil 53 | } 54 | 55 | func (p Processor) sendC2CReply(ctx context.Context, userID string, toCreate dto.APIMessage) error { 56 | log.Printf("EVENT ID:%v", toCreate.GetEventID()) 57 | if _, err := p.api.PostC2CMessage(ctx, userID, toCreate); err != nil { 58 | log.Println(err) 59 | return err 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /examples/receive-and-send/forum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | "github.com/tencent-connect/botgo/event" 8 | ) 9 | 10 | // ThreadEventHandler 论坛主贴事件 11 | func ThreadEventHandler() event.ThreadEventHandler { 12 | return func(event *dto.WSPayload, data *dto.WSThreadData) error { 13 | fmt.Println(event, data) 14 | return nil 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/receive-and-send/ip.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // getIP 一个简易的获取本机 IP 方法 8 | func getIP() string { 9 | addrs, err := net.InterfaceAddrs() 10 | if err != nil { 11 | return "0.0.0.0" 12 | } 13 | for _, address := range addrs { 14 | // 检查ip地址判断是否回环地址 15 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 16 | if ipnet.IP.To4() != nil { 17 | return ipnet.IP.String() 18 | } 19 | } 20 | } 21 | return "0.0.0.1" 22 | } 23 | -------------------------------------------------------------------------------- /examples/receive-and-send/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/tencent-connect/botgo" 13 | "github.com/tencent-connect/botgo/dto" 14 | "github.com/tencent-connect/botgo/dto/message" 15 | "github.com/tencent-connect/botgo/event" 16 | "github.com/tencent-connect/botgo/interaction/webhook" 17 | "github.com/tencent-connect/botgo/token" 18 | "gopkg.in/yaml.v3" 19 | ) 20 | 21 | const ( 22 | host_ = "0.0.0.0" 23 | port_ = 9000 24 | path_ = "/qqbot" 25 | ) 26 | 27 | // 消息处理器,持有 openapi 对象 28 | var processor Processor 29 | 30 | func main() { 31 | // 加载 appid 和 token 32 | content, err := os.ReadFile("config.yaml") 33 | if err != nil { 34 | log.Fatalln("load config file failed, err:", err) 35 | } 36 | credentials := &token.QQBotCredentials{ 37 | AppID: "", 38 | AppSecret: "", 39 | } 40 | if err = yaml.Unmarshal(content, &credentials); err != nil { 41 | log.Fatalln("parse config failed, err:", err) 42 | } 43 | log.Println("credentials:", credentials) 44 | tokenSource := token.NewQQBotTokenSource(credentials) 45 | ctx, cancel := context.WithCancel(context.Background()) 46 | defer cancel() //释放刷新协程 47 | if err = token.StartRefreshAccessToken(ctx, tokenSource); err != nil { 48 | log.Fatalln(err) 49 | } 50 | // 初始化 openapi,正式环境 51 | api := botgo.NewOpenAPI(credentials.AppID, tokenSource).WithTimeout(5 * time.Second).SetDebug(true) 52 | processor = Processor{api: api} 53 | // 注册处理函数 54 | _ = event.RegisterHandlers( 55 | // ***********消息事件*********** 56 | // 群@机器人消息事件 57 | GroupATMessageEventHandler(), 58 | // C2C消息事件 59 | C2CMessageEventHandler(), 60 | // 频道@机器人事件 61 | ChannelATMessageEventHandler(), 62 | ) 63 | http.HandleFunc(path_, func(writer http.ResponseWriter, request *http.Request) { 64 | webhook.HTTPHandler(writer, request, credentials) 65 | }) 66 | if err = http.ListenAndServe(fmt.Sprintf("%s:%d", host_, port_), nil); err != nil { 67 | log.Fatal("setup server fatal:", err) 68 | } 69 | } 70 | 71 | // ChannelATMessageEventHandler 实现处理 at 消息的回调 72 | func ChannelATMessageEventHandler() event.ATMessageEventHandler { 73 | return func(event *dto.WSPayload, data *dto.WSATMessageData) error { 74 | input := strings.ToLower(message.ETLInput(data.Content)) 75 | return processor.ProcessChannelMessage(input, data) 76 | } 77 | } 78 | 79 | // InteractionHandler 处理内联交互事件 80 | func InteractionHandler() event.InteractionEventHandler { 81 | return func(event *dto.WSPayload, data *dto.WSInteractionData) error { 82 | fmt.Println(data) 83 | return processor.ProcessInlineSearch(data) 84 | } 85 | } 86 | 87 | // GroupATMessageEventHandler 实现处理 at 消息的回调 88 | func GroupATMessageEventHandler() event.GroupATMessageEventHandler { 89 | return func(event *dto.WSPayload, data *dto.WSGroupATMessageData) error { 90 | input := strings.ToLower(message.ETLInput(data.Content)) 91 | return processor.ProcessGroupMessage(input, data) 92 | } 93 | } 94 | 95 | // C2CMessageEventHandler 实现处理 at 消息的回调 96 | func C2CMessageEventHandler() event.C2CMessageEventHandler { 97 | return func(event *dto.WSPayload, data *dto.WSC2CMessageData) error { 98 | return processor.ProcessC2CMessage(string(event.RawMessage), data) 99 | } 100 | } 101 | 102 | // C2CFriendEventHandler 实现处理好友关系变更的回调 103 | func C2CFriendEventHandler() event.C2CFriendEventHandler { 104 | return func(event *dto.WSPayload, data *dto.WSC2CFriendData) error { 105 | fmt.Println(data) 106 | return processor.ProcessFriend(string(event.Type), data) 107 | } 108 | } 109 | 110 | // GuildEventHandler 处理频道事件 111 | func GuildEventHandler() event.GuildEventHandler { 112 | return func(event *dto.WSPayload, data *dto.WSGuildData) error { 113 | fmt.Println(data) 114 | return nil 115 | } 116 | } 117 | 118 | // ChannelEventHandler 处理子频道事件 119 | func ChannelEventHandler() event.ChannelEventHandler { 120 | return func(event *dto.WSPayload, data *dto.WSChannelData) error { 121 | fmt.Println(data) 122 | return nil 123 | } 124 | } 125 | 126 | // GuildMemberEventHandler 处理成员变更事件 127 | func GuildMemberEventHandler() event.GuildMemberEventHandler { 128 | return func(event *dto.WSPayload, data *dto.WSGuildMemberData) error { 129 | fmt.Println(data) 130 | return nil 131 | } 132 | } 133 | 134 | // GuildDirectMessageHandler 处理频道私信事件 135 | func GuildDirectMessageHandler() event.DirectMessageEventHandler { 136 | return func(event *dto.WSPayload, data *dto.WSDirectMessageData) error { 137 | fmt.Println(data) 138 | return nil 139 | } 140 | } 141 | 142 | // GuildMessageHandler 处理消息事件 143 | func GuildMessageHandler() event.MessageEventHandler { 144 | return func(event *dto.WSPayload, data *dto.WSMessageData) error { 145 | fmt.Println(data) 146 | return nil 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /examples/receive-and-send/process.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "strings" 9 | "time" 10 | 11 | "github.com/tencent-connect/botgo/dto" 12 | "github.com/tencent-connect/botgo/openapi" 13 | ) 14 | 15 | // Processor is a struct to process message 16 | type Processor struct { 17 | api openapi.OpenAPI 18 | } 19 | 20 | // ProcessChannelMessage is a function to process message 21 | func (p Processor) ProcessChannelMessage(input string, data *dto.WSATMessageData) error { 22 | msg := generateDemoMessage(input, dto.Message(*data)) 23 | if err := p.sendChannelReply(context.Background(), data.ChannelID, msg); err != nil { 24 | _ = p.sendChannelReply(context.Background(), data.GroupID, genErrMessage(dto.Message(*data), err)) 25 | } 26 | return nil 27 | } 28 | 29 | // ProcessInlineSearch is a function to process inline search 30 | func (p Processor) ProcessInlineSearch(interaction *dto.WSInteractionData) error { 31 | if interaction.Data.Type != dto.InteractionDataTypeChatSearch { 32 | return fmt.Errorf("interaction data type not chat search") 33 | } 34 | search := &dto.SearchInputResolved{} 35 | if err := json.Unmarshal(interaction.Data.Resolved, search); err != nil { 36 | log.Println(err) 37 | return err 38 | } 39 | if search.Keyword != "test" { 40 | return fmt.Errorf("resolved search key not allowed") 41 | } 42 | searchRsp := &dto.SearchRsp{ 43 | Layouts: []dto.SearchLayout{ 44 | { 45 | LayoutType: 0, 46 | ActionType: 0, 47 | Title: "内联搜索", 48 | Records: []dto.SearchRecord{ 49 | { 50 | Cover: "https://pub.idqqimg.com/pc/misc/files/20211208/311cfc87ce394c62b7c9f0508658cf25.png", 51 | Title: "内联搜索标题", 52 | Tips: "内联搜索 tips", 53 | URL: "https://www.qq.com", 54 | }, 55 | }, 56 | }, 57 | }, 58 | } 59 | body, _ := json.Marshal(searchRsp) 60 | if err := p.api.PutInteraction(context.Background(), interaction.ID, string(body)); err != nil { 61 | log.Println("api call putInteractionInlineSearch error: ", err) 62 | return err 63 | } 64 | return nil 65 | } 66 | 67 | func genErrMessage(data dto.Message, err error) *dto.MessageToCreate { 68 | return &dto.MessageToCreate{ 69 | Timestamp: time.Now().UnixMilli(), 70 | Content: fmt.Sprintf("处理异常:%v", err), 71 | MessageReference: &dto.MessageReference{ 72 | // 引用这条消息 73 | MessageID: data.ID, 74 | IgnoreGetMessageError: true, 75 | }, 76 | MsgID: data.ID, 77 | } 78 | } 79 | 80 | // ProcessGroupMessage 回复群消息 81 | func (p Processor) ProcessGroupMessage(input string, data *dto.WSGroupATMessageData) error { 82 | msg := generateDemoMessage(input, dto.Message(*data)) 83 | if err := p.sendGroupReply(context.Background(), data.GroupID, msg); err != nil { 84 | _ = p.sendGroupReply(context.Background(), data.GroupID, genErrMessage(dto.Message(*data), err)) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // ProcessC2CMessage 回复C2C消息 91 | func (p Processor) ProcessC2CMessage(input string, data *dto.WSC2CMessageData) error { 92 | userID := "" 93 | if data.Author != nil && data.Author.ID != "" { 94 | userID = data.Author.ID 95 | } 96 | msg := generateDemoMessage(input, dto.Message(*data)) 97 | if err := p.sendC2CReply(context.Background(), userID, msg); err != nil { 98 | _ = p.sendC2CReply(context.Background(), userID, genErrMessage(dto.Message(*data), err)) 99 | } 100 | return nil 101 | } 102 | 103 | func generateDemoMessage(input string, data dto.Message) *dto.MessageToCreate { 104 | log.Printf("收到指令: %+v", input) 105 | msg := "" 106 | if len(input) > 0 { 107 | msg += "收到:" + input 108 | } 109 | for _, _v := range data.Attachments { 110 | msg += ",收到文件类型:" + _v.ContentType 111 | } 112 | return &dto.MessageToCreate{ 113 | Timestamp: time.Now().UnixMilli(), 114 | Content: msg, 115 | MessageReference: &dto.MessageReference{ 116 | // 引用这条消息 117 | MessageID: data.ID, 118 | IgnoreGetMessageError: true, 119 | }, 120 | MsgID: data.ID, 121 | } 122 | } 123 | 124 | // ProcessFriend 处理 c2c 好友事件 125 | func (p Processor) ProcessFriend(wsEventType string, data *dto.WSC2CFriendData) error { 126 | // 请注意,这里是主动推送添加好友事件,后续改为 event id 被动消息 127 | replyMsg := dto.MessageToCreate{ 128 | Timestamp: time.Now().UnixMilli(), 129 | Content: "", 130 | } 131 | var content string 132 | switch strings.ToLower(wsEventType) { 133 | case strings.ToLower(string(dto.EventC2CFriendAdd)): 134 | log.Println("添加好友") 135 | content = fmt.Sprintf("ID为 %s 的用户添加机器人为好友", data.OpenID) 136 | case strings.ToLower(string(dto.EventC2CFriendDel)): 137 | log.Println("删除好友") 138 | default: 139 | log.Println(wsEventType) 140 | return nil 141 | } 142 | replyMsg.Content = content 143 | _, err := p.api.PostC2CMessage( 144 | context.Background(), 145 | data.OpenID, 146 | replyMsg, 147 | ) 148 | if err != nil { 149 | log.Println(err) 150 | return err 151 | } 152 | return nil 153 | } 154 | -------------------------------------------------------------------------------- /examples/receive-and-send/scf_bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./qqbot-svr -------------------------------------------------------------------------------- /examples/simulate-callback-request/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "os" 11 | "strconv" 12 | "time" 13 | 14 | "github.com/tencent-connect/botgo/dto" 15 | "github.com/tencent-connect/botgo/interaction/signature" 16 | "github.com/tencent-connect/botgo/token" 17 | "gopkg.in/yaml.v3" 18 | ) 19 | 20 | const host = "http://localhost" 21 | const port = ":9000" 22 | const path = "/qqbot" 23 | const url = host + port + path 24 | 25 | func main() { 26 | // 加载 appid 和 token 27 | content, err := os.ReadFile("config.yaml") 28 | if err != nil { 29 | log.Fatalln("load config file failed, err:", err) 30 | } 31 | credentials := &token.QQBotCredentials{} 32 | if err = yaml.Unmarshal(content, credentials); err != nil { 33 | log.Fatalln("parse config failed, err:", err) 34 | } 35 | log.Println("credentials:", credentials) 36 | if err != nil { 37 | log.Fatalln(err) 38 | } 39 | go simulateRequest(credentials) 40 | var ln string 41 | fmt.Scanln() 42 | _, _ = fmt.Sscanln("%v", ln) 43 | fmt.Println("end") 44 | } 45 | 46 | func simulateRequest(credentials *token.QQBotCredentials) { 47 | // 等待 http 服务启动 48 | time.Sleep(3 * time.Second) 49 | var heartbeat = &dto.WSPayload{ 50 | WSPayloadBase: dto.WSPayloadBase{ 51 | OPCode: dto.WSHeartbeat, 52 | }, 53 | Data: 123, 54 | } 55 | payload, _ := json.Marshal(heartbeat) 56 | send(payload, credentials) 57 | 58 | var dispatchEvent = &dto.WSPayload{ 59 | WSPayloadBase: dto.WSPayloadBase{ 60 | OPCode: dto.WSDispatchEvent, 61 | Seq: 1, 62 | Type: dto.EventMessageReactionAdd, 63 | }, 64 | Data: dto.WSMessageReactionData{ 65 | UserID: "123", 66 | ChannelID: "111", 67 | GuildID: "222", 68 | Target: dto.ReactionTarget{ 69 | ID: "333", 70 | Type: dto.ReactionTargetTypeMsg, 71 | }, 72 | Emoji: dto.Emoji{ 73 | ID: "42", 74 | Type: 1, 75 | }, 76 | }, 77 | RawMessage: nil, 78 | } 79 | payload, _ = json.Marshal(dispatchEvent) 80 | fmt.Println(string(payload)) 81 | send(payload, credentials) 82 | } 83 | 84 | func send(payload []byte, credentials *token.QQBotCredentials) { 85 | header := http.Header{} 86 | header.Set(signature.HeaderTimestamp, strconv.FormatUint(uint64(time.Now().Unix()), 10)) 87 | 88 | sig, err := signature.Generate(credentials.AppSecret, header, payload) 89 | if err != nil { 90 | fmt.Println(err) 91 | return 92 | } 93 | header.Set(signature.HeaderSig, sig) 94 | 95 | req, err := http.NewRequest("POST", url, bytes.NewReader(payload)) 96 | if err != nil { 97 | fmt.Println(err) 98 | return 99 | } 100 | req.Header = header.Clone() 101 | 102 | client := &http.Client{} 103 | resp, err := client.Do(req) 104 | if err != nil { 105 | fmt.Println(err) 106 | return 107 | } 108 | 109 | defer resp.Body.Close() 110 | r, _ := io.ReadAll(resp.Body) 111 | fmt.Printf("receive resp: %s", string(r)) 112 | } 113 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tencent-connect/botgo 2 | 3 | go 1.16 4 | 5 | 6 | require ( 7 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 8 | github.com/go-redis/redis/v8 v8.11.4 9 | github.com/go-resty/resty/v2 v2.6.0 10 | github.com/google/uuid v1.3.0 11 | github.com/gorilla/websocket v1.4.2 12 | github.com/kr/pretty v0.3.0 // indirect 13 | github.com/rogpeppe/go-internal v1.9.0 // indirect 14 | github.com/stretchr/testify v1.8.1 15 | github.com/tidwall/gjson v1.9.3 16 | golang.org/x/net v0.19.0 // indirect 17 | golang.org/x/oauth2 v0.23.0 18 | golang.org/x/sync v0.1.0 19 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 20 | gopkg.in/yaml.v3 v3.0.1 21 | ) 22 | -------------------------------------------------------------------------------- /interaction/search/simulate_search.go: -------------------------------------------------------------------------------- 1 | // Package search 模拟内联搜索 2 | package search 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/tencent-connect/botgo/dto" 14 | "github.com/tencent-connect/botgo/interaction/signature" 15 | "github.com/tencent-connect/botgo/log" 16 | ) 17 | 18 | const maxRespBuffer = 65535 19 | 20 | // Config 搜索请求配置 21 | type Config struct { 22 | AppID string 23 | EndPoint string // 回调url地址 24 | Secret string 25 | } 26 | 27 | // SimulateSearch 模拟内联搜索请求 28 | // 开发者可以使用本方法请求自己的服务器进行平台内联搜索的模拟,避免在平台上触发搜索请求。提升联调效率。 29 | func SimulateSearch(config *Config, keyword string) (*dto.SearchRsp, error) { 30 | interactionData := &dto.InteractionData{ 31 | Name: "search", 32 | Type: dto.InteractionDataTypeChatSearch, 33 | } 34 | interactionData.Resolved, _ = json.Marshal(dto.SearchInputResolved{Keyword: keyword}) 35 | interaction := &dto.Interaction{ 36 | ApplicationID: config.AppID, 37 | Type: dto.InteractionTypeCommand, 38 | Data: interactionData, 39 | Version: 1, 40 | } 41 | jsonStr, _ := json.Marshal(interaction) 42 | timestamp := strconv.FormatUint(uint64(time.Now().Unix()), 10) 43 | 44 | // calc sig 45 | header := http.Header{} 46 | header.Set(signature.HeaderTimestamp, timestamp) 47 | sig, err := signature.Generate(config.Secret, header, jsonStr) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | // build req 53 | req, err := http.NewRequest(http.MethodPost, config.EndPoint, bytes.NewReader(jsonStr)) 54 | if err != nil { 55 | return nil, err 56 | } 57 | req.Header.Set(signature.HeaderTimestamp, timestamp) 58 | req.Header.Set(signature.HeaderSig, sig) 59 | log.Info(req) 60 | 61 | // parse resp 62 | client := http.Client{} 63 | resp, err := client.Do(req) 64 | if err != nil { 65 | return nil, err 66 | } 67 | log.Info(resp) 68 | defer func() { 69 | resp.Body.Close() 70 | }() 71 | 72 | // parse resp body 73 | body, err := ioutil.ReadAll(io.LimitReader(resp.Body, maxRespBuffer)) 74 | if err != nil { 75 | return nil, err 76 | } 77 | log.Info(string(body)) 78 | result := &dto.SearchRsp{} 79 | if err = json.Unmarshal(body, result); err != nil { 80 | return nil, err 81 | } 82 | 83 | return result, nil 84 | } 85 | -------------------------------------------------------------------------------- /interaction/search/simulate_search_test.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tencent-connect/botgo/log" 7 | ) 8 | 9 | func TestSimulateSearch(t *testing.T) { 10 | got, err := SimulateSearch( 11 | &Config{ 12 | AppID: "1", 13 | EndPoint: "https://www.qq.com", 14 | Secret: "a", 15 | }, "hello", 16 | ) 17 | if err != nil { 18 | // 这里用于模拟,默认的 testcase 肯定是失败的,所以这里不断言 19 | log.Error(err) 20 | } 21 | log.Info(got) 22 | } 23 | -------------------------------------------------------------------------------- /interaction/signature/interaction.go: -------------------------------------------------------------------------------- 1 | // Package signature 用于处理平台和机器人开发者之间的互动请求中的签名验证 2 | package signature 3 | 4 | import ( 5 | "bytes" 6 | "crypto/ed25519" 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/tencent-connect/botgo/log" 14 | ) 15 | 16 | const ( 17 | // HeaderSig 请求签名 18 | HeaderSig = "X-Signature-Ed25519" 19 | // HeaderTimestamp 跟请求签名对应的时间戳,用于验证签名 20 | HeaderTimestamp = "X-Signature-Timestamp" 21 | ) 22 | 23 | type ed25519Key struct { 24 | PublicKey ed25519.PublicKey 25 | PrivateKey ed25519.PrivateKey 26 | } 27 | 28 | // Verify 验证签名,需要传入 http 头,httpBody 29 | // 请在方法外部从 http request 上读取了 body 之后再交给签名验证方法进行验证,避免重复读取 30 | func Verify(secret string, header http.Header, httpBody []byte) (bool, error) { 31 | // 生成密钥 32 | key, err := genKey(secret) 33 | if err != nil { 34 | log.Errorf("genPublicKey error, %v", err) 35 | return false, err 36 | } 37 | sigBuffer, err := decodeSigBuffer(header.Get(HeaderSig)) 38 | if err != nil { 39 | log.Errorf("decodeSigBuffer error, %v", err) 40 | return false, err 41 | } 42 | content, err := genOriginalContent(header.Get(HeaderTimestamp), httpBody) 43 | if err != nil { 44 | log.Errorf("get original content error: %v", err) 45 | } 46 | return ed25519.Verify(key.PublicKey, content, sigBuffer), nil 47 | } 48 | 49 | // Generate 生成签名,sdk 中的改方法,主要用于与验证签名方法配合进行验证 50 | func Generate(secret string, header http.Header, httpBody []byte) (string, error) { 51 | key, err := genKey(secret) 52 | if err != nil { 53 | log.Errorf("genPrivateKey error, %v", err) 54 | return "", err 55 | } 56 | content, err := genOriginalContent(header.Get(HeaderTimestamp), httpBody) 57 | if err != nil { 58 | log.Errorf("get original content error: %v", err) 59 | } 60 | return hex.EncodeToString(ed25519.Sign(key.PrivateKey, content)), nil 61 | } 62 | 63 | func genOriginalContent(timestamp string, body []byte) ([]byte, error) { 64 | if timestamp == "" { 65 | return nil, errors.New("timestamp is nil") 66 | } 67 | // 按照 timestamp+Body 顺序组成签名体 68 | var msg bytes.Buffer 69 | msg.WriteString(timestamp) 70 | msg.Write(body) 71 | return msg.Bytes(), nil 72 | } 73 | 74 | // genKey 根据 seed 生成公钥,私钥,私钥用于请求方加密,公钥用于服务方验证 75 | func genKey(secret string) (*ed25519Key, error) { 76 | seed, err := getSeed(secret) 77 | if err != nil { 78 | return nil, err 79 | } 80 | publicKey, privateKey, err := ed25519.GenerateKey(strings.NewReader(seed)) 81 | if err != nil { 82 | return nil, err 83 | } 84 | return &ed25519Key{ 85 | PublicKey: publicKey, 86 | PrivateKey: privateKey, 87 | }, nil 88 | } 89 | 90 | func decodeSigBuffer(signature string) ([]byte, error) { 91 | if signature == "" { 92 | return nil, errors.New("not found signature") 93 | } 94 | sigBuf, err := hex.DecodeString(signature) 95 | if err != nil { 96 | return nil, fmt.Errorf("hex decode signature failed: %v", err) 97 | } 98 | if len(sigBuf) != ed25519.SignatureSize || sigBuf[63]&224 != 0 { 99 | return nil, errors.New("signature decode result is not a valid buf") 100 | } 101 | return sigBuf, nil 102 | } 103 | 104 | // getSeed 使用 secret 生成算法 seed 105 | func getSeed(secret string) (string, error) { 106 | if secret == "" { 107 | return "", errors.New("secret invalid") 108 | } 109 | seed := secret 110 | for len(seed) < ed25519.SeedSize { 111 | seed = strings.Repeat(seed, 2) 112 | } 113 | return seed[:ed25519.SeedSize], nil 114 | } 115 | -------------------------------------------------------------------------------- /interaction/signature/interaction_test.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestVerify(t *testing.T) { 9 | type args struct { 10 | secret string 11 | header http.Header 12 | httpBody []byte 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want bool 18 | wantErr bool 19 | }{ 20 | { 21 | name: "test sig", 22 | args: args{ 23 | 24 | secret: "123456abcdef", 25 | header: map[string][]string{ 26 | "X-Signature-Ed25519": {"e949b5b94ef4103df903fb031d1d16e358db3db83e79e117edd404c8508be3ce8a76d7bad1bed353194c126a1a5915b4ad8b5288c1191cc53a12acffccd82004"}, 27 | "X-Signature-Timestamp": {"1728981195"}}, 28 | httpBody: []byte(`{"id":"ROBOT1.0_veoihSEXDc8Q.g-6eLpNIa11bH8MisOjn-m-LKxCPntMk6exUXgcWCGpVO7L2QKTNZzjZzFFDSbiOFcqAPWyVA!!","content":"哦一下","timestamp":"2024-10-15T16:33:15+08:00","author":{"id":"675860273","user_openid":"675860273"}}`), 29 | }, 30 | want: true, 31 | wantErr: false, 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | got, err := Verify(tt.args.secret, tt.args.header, tt.args.httpBody) 37 | if (err != nil) != tt.wantErr { 38 | t.Errorf("Verify() error = %v, wantErr %v", err, tt.wantErr) 39 | return 40 | } 41 | if got != tt.want { 42 | t.Errorf("Verify() got = %v, want %v", got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /interaction/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | // Package webhook HTTP回调处理 2 | package webhook 3 | 4 | import ( 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/tencent-connect/botgo/constant" 11 | "github.com/tencent-connect/botgo/dto" 12 | "github.com/tencent-connect/botgo/event" 13 | "github.com/tencent-connect/botgo/interaction/signature" 14 | "github.com/tencent-connect/botgo/log" 15 | "github.com/tencent-connect/botgo/token" 16 | ) 17 | 18 | type ack struct { 19 | Op dto.OPCode `json:"op"` 20 | Data uint32 `json:"d"` 21 | } 22 | 23 | // GenHeartbeatACK 生成 http gateway 的心跳回包 24 | func GenHeartbeatACK(seq uint32) string { 25 | s, _ := json.Marshal(ack{Op: dto.WSHeartbeatAck, Data: seq}) 26 | return string(s) 27 | } 28 | 29 | // GenDispatchACK 生成事件包的回包,如果处理失败,则返回的 d 为 1,服务端会尝试重试 30 | func GenDispatchACK(success bool) string { 31 | var r uint32 32 | if !success { 33 | r = 1 34 | } 35 | s, _ := json.Marshal(ack{Op: dto.HTTPCallbackAck, Data: r}) 36 | return string(s) 37 | } 38 | 39 | // Deprecated: DefaultGetSecretFunc 默认的获取 secret 的函数,默认从环境变量读取 40 | // 开发者如果需要从自己的配置文件,或者是其他地方获取 secret,可以重写这个函数 41 | var DefaultGetSecretFunc = func() string { 42 | return os.Getenv("QQBotSecret") 43 | } 44 | 45 | // HTTPHandler 用户处理回调时间,该函数实现的是 https://pkg.go.dev/net/http#HandleFunc 所要求的 handler 46 | // 会自动进行签名验证,心跳包回复,以及根据使用 event.RegisterHandlers 注册的 handler 去执行不同的 handler 来处理事件 47 | // 如果开发者不想在接收事件的地方处理,可以实现 DefaultHandlers.Plain 然后在内部处理相关的异步生产或者转发的逻辑 48 | func HTTPHandler(w http.ResponseWriter, r *http.Request, credentials *token.QQBotCredentials) { 49 | defer r.Body.Close() 50 | body := make([]byte, r.ContentLength) 51 | if _, err := r.Body.Read(body); err != nil && err != io.EOF { 52 | log.Errorf("read http callback body error: %s", err) 53 | return 54 | } 55 | log.Debugf("http callback body: %s,len:%d", string(body), len(body)) 56 | log.Debugf("http callback header: %v", r.Header) 57 | traceID := r.Header.Get(constant.HeaderTraceID) 58 | // 签名验证 59 | if pass, err := signature.Verify(credentials.AppSecret, r.Header, body); err != nil || !pass { 60 | log.Errorf("signature verify failed, err: %v, traceID: %s", err, traceID) 61 | return 62 | } 63 | // 解析 payload 64 | payload := &dto.WSPayload{} 65 | if err := json.Unmarshal(body, payload); err != nil { 66 | log.Errorf("unmarshal http callback body error: %s, traceID: %s", err, traceID) 67 | return 68 | } 69 | log.Info("payload:%+v", payload) 70 | // 原始数据放入,parse 的时候需要从里面提取 d 71 | payload.RawMessage = body 72 | payload.Session = &dto.Session{AppID: credentials.AppID} 73 | var result string 74 | if payload.OPCode == dto.HTTPCallbackValidation { 75 | data, ok := payload.Data.(map[string]interface{}) 76 | if !ok { 77 | log.Errorf("callback data invalid: %+v, traceID: %s", payload.Data, traceID) 78 | return 79 | } 80 | plainToken, ptOk := data["plain_token"].(string) 81 | eventTs, etOk := data["event_ts"].(string) 82 | if !ptOk || !etOk { 83 | log.Errorf("callback data invalid: %+v, traceID: %s", payload.Data, traceID) 84 | } 85 | req := &dto.WHValidationReq{ 86 | PlainToken: plainToken, 87 | EventTs: eventTs, 88 | } 89 | validationRsp := GenValidationACK(req, r.Header, credentials.AppSecret) 90 | if validationRsp != nil { 91 | if _, err := w.Write(validationRsp); err != nil { 92 | log.Errorf("write http callback response error: %s, traceID: %s", err, traceID) 93 | return 94 | } 95 | } 96 | return 97 | } 98 | 99 | result = parsePayload(payload, traceID) 100 | if result != "" { 101 | if _, err := w.Write([]byte(result)); err != nil { 102 | log.Errorf("write http callback response error: %s, traceID: %s", err, traceID) 103 | return 104 | } 105 | } 106 | } 107 | 108 | func parsePayload(payload *dto.WSPayload, traceID string) string { 109 | // 处理心跳包 110 | if payload.OPCode == dto.WSHeartbeat { 111 | return GenHeartbeatACK(uint32(payload.Data.(float64))) 112 | } 113 | // 处理事件 114 | if payload.OPCode == dto.WSDispatchEvent { 115 | // 解析具体事件,并投递给业务注册的 handler 116 | if err := event.ParseAndHandle(payload); err != nil { 117 | log.Errorf( 118 | "parseAndHandle failed, %v, traceID:%s, payload: %v", err, 119 | traceID, payload, 120 | ) 121 | return GenDispatchACK(false) 122 | } 123 | return GenDispatchACK(true) 124 | } 125 | 126 | return "" 127 | } 128 | 129 | // GenValidationACK 生成回调校验回包 130 | func GenValidationACK(req *dto.WHValidationReq, header http.Header, secret string) []byte { 131 | h := header.Clone() 132 | h.Set(signature.HeaderTimestamp, req.EventTs) 133 | sig, err := signature.Generate(secret, h, []byte(req.PlainToken)) 134 | if err != nil { 135 | log.Errorf("generate signature failed:%+v", err) 136 | return nil 137 | } 138 | rsp, err := json.Marshal( 139 | &dto.WHValidationRsp{ 140 | PlainToken: req.PlainToken, 141 | Signature: sig, 142 | }) 143 | if err != nil { 144 | log.Errorf("handle validation failed:", err) 145 | return nil 146 | } 147 | return rsp 148 | } 149 | -------------------------------------------------------------------------------- /interaction/webhook/webhook_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestGenHeartbeatACK(t *testing.T) { 9 | j := GenHeartbeatACK(1314) 10 | if !strings.Contains(j, "11") || !strings.Contains(j, "1314") { 11 | t.Error("GenHeartbeatACK error") 12 | } 13 | } 14 | 15 | func TestGenDispatchACK(t *testing.T) { 16 | j := GenDispatchACK(false) 17 | if !strings.Contains(j, "12") || !strings.Contains(j, "1") { 18 | t.Error("GenDispatchACK error") 19 | } 20 | j = GenDispatchACK(true) 21 | if !strings.Contains(j, "12") || !strings.Contains(j, "0") { 22 | t.Error("GenDispatchACK error") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /log/console.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | var _ Logger = (*consoleLogger)(nil) 12 | 13 | // consoleLogger 命令行日志实现 14 | type consoleLogger struct{} 15 | 16 | // Debug 日志 17 | func (consoleLogger) Debug(v ...interface{}) { 18 | output("Debug", fmt.Sprint(v...)) 19 | } 20 | 21 | // Info 日志 22 | func (consoleLogger) Info(v ...interface{}) { 23 | output("Info", fmt.Sprint(v...)) 24 | } 25 | 26 | // Warn 日志 27 | func (consoleLogger) Warn(v ...interface{}) { 28 | output("Warning", fmt.Sprint(v...)) 29 | } 30 | 31 | // Error 32 | func (consoleLogger) Error(v ...interface{}) { 33 | output("Error", fmt.Sprint(v...)) 34 | } 35 | 36 | // Debugf Debug Format 日志 37 | func (consoleLogger) Debugf(format string, v ...interface{}) { 38 | output("Debug", fmt.Sprintf(format, v...)) 39 | } 40 | 41 | // Infof Info Format 日志 42 | func (consoleLogger) Infof(format string, v ...interface{}) { 43 | output("Info", fmt.Sprintf(format, v...)) 44 | } 45 | 46 | // Warnf Warning Format 日志 47 | func (consoleLogger) Warnf(format string, v ...interface{}) { 48 | output("Warning", fmt.Sprintf(format, v...)) 49 | } 50 | 51 | // Errorf Error Format 日志 52 | func (consoleLogger) Errorf(format string, v ...interface{}) { 53 | output("Error", fmt.Sprintf(format, v...)) 54 | } 55 | 56 | // Sync 控制台 logger 不需要 sync 57 | func (consoleLogger) Sync() error { 58 | return nil 59 | } 60 | 61 | func output(level string, v ...interface{}) { 62 | pc, file, line, _ := runtime.Caller(3) 63 | file = filepath.Base(file) 64 | funcName := strings.TrimPrefix(filepath.Ext(runtime.FuncForPC(pc).Name()), ".") 65 | 66 | logFormat := "[%s] %s %s:%d:%s " + fmt.Sprint(v...) + "\n" 67 | date := time.Now().Format("2006-01-02 15:04:05") 68 | fmt.Printf(logFormat, level, date, file, line, funcName) 69 | } 70 | -------------------------------------------------------------------------------- /log/console_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_log(t *testing.T) { 8 | t.Run("output", func(t *testing.T) { 9 | output("Info", "abc", "def") 10 | }) 11 | 12 | t.Run("Debug", func(t *testing.T) { 13 | c := consoleLogger{} 14 | c.Debug("debug log") 15 | }) 16 | 17 | t.Run("Debugf", func(t *testing.T) { 18 | c := consoleLogger{} 19 | c.Debugf("debugf %s", "log") 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | // Package log 是 SDK 的 logger 接口定义与内置的 logger。 2 | package log 3 | 4 | // DefaultLogger 默认logger 5 | var DefaultLogger = Logger(new(consoleLogger)) 6 | 7 | // Debug log.Debug 8 | func Debug(v ...interface{}) { 9 | DefaultLogger.Debug(v...) 10 | } 11 | 12 | // Info log.Info 13 | func Info(v ...interface{}) { 14 | DefaultLogger.Info(v...) 15 | } 16 | 17 | // Warn log.Warn 18 | func Warn(v ...interface{}) { 19 | DefaultLogger.Warn(v...) 20 | } 21 | 22 | // Error log.Error 23 | func Error(v ...interface{}) { 24 | DefaultLogger.Error(v...) 25 | } 26 | 27 | // Debugf log.Debugf 28 | func Debugf(format string, v ...interface{}) { 29 | DefaultLogger.Debugf(format, v...) 30 | } 31 | 32 | // Infof log.Infof 33 | func Infof(format string, v ...interface{}) { 34 | DefaultLogger.Infof(format, v...) 35 | } 36 | 37 | // Warnf log.Warnf 38 | func Warnf(format string, v ...interface{}) { 39 | DefaultLogger.Warnf(format, v...) 40 | } 41 | 42 | // Errorf log.Errorf 43 | func Errorf(format string, v ...interface{}) { 44 | DefaultLogger.Errorf(format, v...) 45 | } 46 | 47 | // Sync logger Sync calls to flush buffer 48 | func Sync() { 49 | _ = DefaultLogger.Sync() 50 | } 51 | -------------------------------------------------------------------------------- /log/log_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDebug(t *testing.T) { 8 | Debug("debug log") 9 | Error("error log") 10 | Warn("warn log") 11 | Info("info log") 12 | Debugf("%s log", "debugf") 13 | Errorf("%s log", "errorf") 14 | Warnf("%s log", "warnf") 15 | Infof("%s log", "infof") 16 | } 17 | -------------------------------------------------------------------------------- /log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | // Logger 日志需要实现的接口定义 4 | type Logger interface { 5 | Debug(v ...interface{}) 6 | Info(v ...interface{}) 7 | Warn(v ...interface{}) 8 | Error(v ...interface{}) 9 | 10 | Debugf(format string, v ...interface{}) 11 | Infof(format string, v ...interface{}) 12 | Warnf(format string, v ...interface{}) 13 | Errorf(format string, v ...interface{}) 14 | // Sync logger Sync calls to flush buffer 15 | Sync() error 16 | } 17 | -------------------------------------------------------------------------------- /oauth.go: -------------------------------------------------------------------------------- 1 | package botgo 2 | -------------------------------------------------------------------------------- /openapi/filter.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | ) 7 | 8 | // 提供一组过滤器支持,开发者可以通过请求过滤器和返回过滤器,实现模调上报,耗时监控等能力。 9 | 10 | // HTTPFilter 请求过滤器 11 | type HTTPFilter func(req *http.Request, response *http.Response) error 12 | 13 | var ( 14 | filterLock = sync.RWMutex{} 15 | reqFilterChainSet = map[string]HTTPFilter{} 16 | reqFilterChains []string 17 | respFilterChainSet = map[string]HTTPFilter{} 18 | respFilterChains []string 19 | ) 20 | 21 | // RegisterReqFilter 注册请求过滤器 22 | func RegisterReqFilter(name string, filter HTTPFilter) { 23 | if _, ok := reqFilterChainSet[name]; ok { 24 | return 25 | } 26 | filterLock.Lock() 27 | defer filterLock.Unlock() 28 | reqFilterChainSet[name] = filter 29 | reqFilterChains = append(reqFilterChains, name) 30 | } 31 | 32 | // RegisterRespFilter 注册返回过滤器 33 | func RegisterRespFilter(name string, filter HTTPFilter) { 34 | if _, ok := respFilterChainSet[name]; ok { 35 | return 36 | } 37 | filterLock.Lock() 38 | defer filterLock.Unlock() 39 | respFilterChainSet[name] = filter 40 | respFilterChains = append(respFilterChains, name) 41 | } 42 | 43 | // DoReqFilterChains 按照注册顺序执行请求过滤器 44 | func DoReqFilterChains(req *http.Request, resp *http.Response) error { 45 | for _, name := range reqFilterChains { 46 | if _, ok := reqFilterChainSet[name]; !ok { 47 | continue 48 | } 49 | if err := reqFilterChainSet[name](req, resp); err != nil { 50 | return err 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | // DoRespFilterChains 按照注册顺序执行返回过滤器 57 | func DoRespFilterChains(req *http.Request, resp *http.Response) error { 58 | for _, name := range respFilterChains { 59 | if _, ok := respFilterChainSet[name]; !ok { 60 | continue 61 | } 62 | if err := respFilterChainSet[name](req, resp); err != nil { 63 | return err 64 | } 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /openapi/openapi.go: -------------------------------------------------------------------------------- 1 | // Package openapi 声明了 sdk 所使用的 openapi 接口。 2 | package openapi 3 | 4 | import ( 5 | "net/http" 6 | "sync" 7 | ) 8 | 9 | // VersionMapping openapi 版本管理 10 | var VersionMapping = map[APIVersion]OpenAPI{} 11 | 12 | // DefaultImpl 默认 openapi 实现 13 | var DefaultImpl OpenAPI 14 | 15 | var ( 16 | versionMapLock = sync.RWMutex{} 17 | once sync.Once 18 | ) 19 | 20 | // 这些状态码不会当做错误处理 21 | // 未排除 201,202 : 用于提示创建异步任务成功,所以不屏蔽错误 22 | var successStatusSet = map[int]bool{ 23 | http.StatusOK: true, 24 | http.StatusNoContent: true, 25 | } 26 | 27 | // Register 注册 openapi 的实现,如果默认实现为空,则将第一个注册的设置为默认实现 28 | func Register(version APIVersion, api OpenAPI) { 29 | versionMapLock.Lock() 30 | VersionMapping[version] = api 31 | setDefaultOnce(api) 32 | versionMapLock.Unlock() 33 | } 34 | 35 | // IsSuccessStatus 是否是成功的状态码 36 | func IsSuccessStatus(code int) bool { 37 | if _, ok := successStatusSet[code]; ok { 38 | return true 39 | } 40 | return false 41 | } 42 | 43 | func setDefaultOnce(api OpenAPI) { 44 | once.Do(func() { 45 | if DefaultImpl == nil { 46 | DefaultImpl = api 47 | } 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /openapi/options/options.go: -------------------------------------------------------------------------------- 1 | // Package options openapi options 2 | package options 3 | 4 | // Options are openapi options 5 | type Options struct { 6 | URL string 7 | HideTip bool // 撤回消息隐藏小灰条可选参数, true: 隐藏小灰条 8 | } 9 | 10 | // Option sets client options. 11 | type Option func(*Options) 12 | 13 | // WithURL replace default send URL 14 | func WithURL(url string) Option { 15 | return func(o *Options) { 16 | o.URL = url 17 | } 18 | } 19 | 20 | // WithHideTip hide tip 21 | func WithHideTip() Option { 22 | return func(o *Options) { 23 | o.HideTip = true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /openapi/type.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | -------------------------------------------------------------------------------- /openapi/v1/README.md: -------------------------------------------------------------------------------- 1 | ## v1 2 | openapi v1 的实现 3 | 4 | 依赖: 5 | - github.com/go-resty/resty/v2 -------------------------------------------------------------------------------- /openapi/v1/announces.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | // CreateChannelAnnounces 创建子频道公告 10 | func (o *openAPI) CreateChannelAnnounces(ctx context.Context, channelID string, 11 | announce *dto.ChannelAnnouncesToCreate) (*dto.Announces, error) { 12 | resp, err := o.request(ctx). 13 | SetResult(dto.Announces{}). 14 | SetPathParam("channel_id", channelID). 15 | SetBody(announce). 16 | Post(o.getURL(channelAnnouncesURI)) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return resp.Result().(*dto.Announces), nil 21 | } 22 | 23 | // DeleteChannelAnnounces 删除子频道公告,会校验 messageID 24 | func (o *openAPI) DeleteChannelAnnounces(ctx context.Context, channelID, messageID string) error { 25 | _, err := o.request(ctx). 26 | SetResult(dto.Announces{}). 27 | SetPathParam("channel_id", channelID). 28 | SetPathParam("message_id", messageID). 29 | Delete(o.getURL(channelAnnounceURI)) 30 | return err 31 | } 32 | 33 | // CleanChannelAnnounces 删除子频道公告,不校验 messageID 34 | func (o *openAPI) CleanChannelAnnounces(ctx context.Context, channelID string) error { 35 | _, err := o.request(ctx). 36 | SetResult(dto.Announces{}). 37 | SetPathParam("channel_id", channelID). 38 | SetPathParam("message_id", "all"). 39 | Delete(o.getURL(channelAnnounceURI)) 40 | return err 41 | } 42 | 43 | // CreateGuildAnnounces 创建频道全局公告 44 | func (o *openAPI) CreateGuildAnnounces(ctx context.Context, guildID string, 45 | announce *dto.GuildAnnouncesToCreate) (*dto.Announces, error) { 46 | resp, err := o.request(ctx). 47 | SetResult(dto.Announces{}). 48 | SetPathParam("guild_id", guildID). 49 | SetBody(announce). 50 | Post(o.getURL(guildAnnouncesURI)) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return resp.Result().(*dto.Announces), nil 55 | } 56 | 57 | // DeleteGuildAnnounces 删除频道全局公告,会校验 messageID 58 | func (o *openAPI) DeleteGuildAnnounces(ctx context.Context, guildID, messageID string) error { 59 | _, err := o.request(ctx). 60 | SetResult(dto.Announces{}). 61 | SetPathParam("guild_id", guildID). 62 | SetPathParam("message_id", messageID). 63 | Delete(o.getURL(guildAnnounceURI)) 64 | return err 65 | } 66 | 67 | // CleanGuildAnnounces 删除道全局公告,不校验 messageID 68 | func (o *openAPI) CleanGuildAnnounces(ctx context.Context, guildID string) error { 69 | _, err := o.request(ctx). 70 | SetResult(dto.Announces{}). 71 | SetPathParam("guild_id", guildID). 72 | SetPathParam("message_id", "all"). 73 | Delete(o.getURL(guildAnnounceURI)) 74 | return err 75 | } 76 | -------------------------------------------------------------------------------- /openapi/v1/api_permissions.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | // GetAPIPermissions 获取频道可用权限列表 10 | func (o *openAPI) GetAPIPermissions(ctx context.Context, guildID string) (*dto.APIPermissions, error) { 11 | resp, err := o.request(ctx). 12 | SetResult(dto.APIPermissions{}). 13 | SetPathParam("guild_id", guildID). 14 | Get(o.getURL(apiPermissionURI)) 15 | if err != nil { 16 | return nil, err 17 | } 18 | return resp.Result().(*dto.APIPermissions), nil 19 | } 20 | 21 | // RequireAPIPermissions 创建频道 API 接口权限授权链接 22 | func (o *openAPI) RequireAPIPermissions(ctx context.Context, 23 | guildID string, demand *dto.APIPermissionDemandToCreate) (*dto.APIPermissionDemand, error) { 24 | resp, err := o.request(ctx). 25 | SetResult(dto.APIPermissionDemand{}). 26 | SetPathParam("guild_id", guildID). 27 | SetBody(demand). 28 | Post(o.getURL(apiPermissionDemandURI)) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return resp.Result().(*dto.APIPermissionDemand), nil 33 | } 34 | -------------------------------------------------------------------------------- /openapi/v1/audio.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | "github.com/tencent-connect/botgo/log" 8 | ) 9 | 10 | // PostAudio AudioAPI 接口实现 11 | func (o *openAPI) PostAudio(ctx context.Context, channelID string, value *dto.AudioControl) (*dto.AudioControl, error) { 12 | // 目前服务端成功不回包 13 | _, err := o.request(ctx). 14 | SetResult(dto.Channel{}). 15 | SetPathParam("channel_id", channelID). 16 | SetBody(value). 17 | Post(o.getURL(audioControlURI)) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return value, nil 23 | } 24 | 25 | // PutMic 上麦接口实现 26 | func (o *openAPI) PutMic(ctx context.Context, channelID string) error { 27 | _, err := o.request(ctx). 28 | SetPathParam("channel_id", channelID). 29 | Put(o.getURL(micURI)) 30 | if err != nil { 31 | log.Errorf("put mic fail:%+v", err) 32 | } 33 | return err 34 | } 35 | 36 | // DeleteMic 上麦接口实现 37 | func (o *openAPI) DeleteMic(ctx context.Context, channelID string) error { 38 | _, err := o.request(ctx). 39 | SetPathParam("channel_id", channelID). 40 | Delete(o.getURL(micURI)) 41 | if err != nil { 42 | log.Errorf("delete mic fail:%+v", err) 43 | } 44 | return err 45 | } 46 | -------------------------------------------------------------------------------- /openapi/v1/channel_permissions.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/tencent-connect/botgo/dto" 9 | ) 10 | 11 | // ChannelPermissions 获取指定子频道的权限 12 | func (o *openAPI) ChannelPermissions(ctx context.Context, channelID, userID string) (*dto.ChannelPermissions, error) { 13 | rsp, err := o.request(ctx). 14 | SetResult(dto.ChannelPermissions{}). 15 | SetPathParam("channel_id", channelID). 16 | SetPathParam("user_id", userID). 17 | Get(o.getURL(channelPermissionsURI)) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return rsp.Result().(*dto.ChannelPermissions), nil 22 | } 23 | 24 | // ChannelRolesPermissions 获取指定子频道身份组的权限 25 | func (o *openAPI) ChannelRolesPermissions(ctx context.Context, 26 | channelID, roleID string) (*dto.ChannelRolesPermissions, error) { 27 | rsp, err := o.request(ctx). 28 | SetResult(dto.ChannelRolesPermissions{}). 29 | SetPathParam("channel_id", channelID). 30 | SetPathParam("role_id", roleID). 31 | Get(o.getURL(channelRolesPermissionsURI)) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return rsp.Result().(*dto.ChannelRolesPermissions), nil 36 | } 37 | 38 | // PutChannelPermissions 修改指定子频道的权限 39 | func (o *openAPI) PutChannelPermissions(ctx context.Context, channelID, userID string, 40 | p *dto.UpdateChannelPermissions) error { 41 | if p.Add != "" { 42 | if _, err := strconv.ParseUint(p.Add, 10, 64); err != nil { 43 | return fmt.Errorf("invalid parameter add: %v", err) 44 | } 45 | } 46 | if p.Remove != "" { 47 | if _, err := strconv.ParseUint(p.Remove, 10, 64); err != nil { 48 | return fmt.Errorf("invalid parameter remove: %v", err) 49 | } 50 | } 51 | _, err := o.request(ctx). 52 | SetPathParam("channel_id", channelID). 53 | SetPathParam("user_id", userID). 54 | SetBody(p). 55 | Put(o.getURL(channelPermissionsURI)) 56 | return err 57 | } 58 | 59 | // PutChannelRolesPermissions 修改指定子频道的权限 60 | func (o *openAPI) PutChannelRolesPermissions(ctx context.Context, channelID, roleID string, 61 | p *dto.UpdateChannelPermissions) error { 62 | if p.Add != "" { 63 | if _, err := strconv.ParseUint(p.Add, 10, 64); err != nil { 64 | return fmt.Errorf("invalid parameter add: %v", err) 65 | } 66 | } 67 | if p.Remove != "" { 68 | if _, err := strconv.ParseUint(p.Remove, 10, 64); err != nil { 69 | return fmt.Errorf("invalid parameter remove: %v", err) 70 | } 71 | } 72 | _, err := o.request(ctx). 73 | SetPathParam("channel_id", channelID). 74 | SetPathParam("role_id", roleID). 75 | SetBody(p). 76 | Put(o.getURL(channelRolesPermissionsURI)) 77 | return err 78 | } 79 | -------------------------------------------------------------------------------- /openapi/v1/channels.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/tencent-connect/botgo/dto" 8 | ) 9 | 10 | // Channel 拉取指定子频道信息 11 | func (o *openAPI) Channel(ctx context.Context, channelID string) (*dto.Channel, error) { 12 | resp, err := o.request(ctx). 13 | SetResult(dto.Channel{}). 14 | SetPathParam("channel_id", channelID). 15 | Get(o.getURL(channelURI)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return resp.Result().(*dto.Channel), nil 21 | } 22 | 23 | // Channels 拉取子频道列表 24 | func (o *openAPI) Channels(ctx context.Context, guildID string) ([]*dto.Channel, error) { 25 | resp, err := o.request(ctx). 26 | SetPathParam("guild_id", guildID). 27 | Get(o.getURL(channelsURI)) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | channels := make([]*dto.Channel, 0) 33 | if err := json.Unmarshal(resp.Body(), &channels); err != nil { 34 | return nil, err 35 | } 36 | 37 | return channels, nil 38 | } 39 | 40 | // PostChannel 创建子频道 41 | func (o *openAPI) PostChannel(ctx context.Context, 42 | guildID string, value *dto.ChannelValueObject) (*dto.Channel, error) { 43 | resp, err := o.request(ctx). 44 | SetResult(dto.Channel{}). 45 | SetPathParam("guild_id", guildID). 46 | SetBody(value). 47 | Post(o.getURL(channelsURI)) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return resp.Result().(*dto.Channel), nil 53 | } 54 | 55 | // PatchChannel 修改子频道 56 | func (o *openAPI) PatchChannel(ctx context.Context, 57 | channelID string, value *dto.ChannelValueObject) (*dto.Channel, error) { 58 | resp, err := o.request(ctx). 59 | SetResult(dto.Channel{}). 60 | SetPathParam("channel_id", channelID). 61 | SetBody(value). 62 | Patch(o.getURL(channelURI)) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return resp.Result().(*dto.Channel), nil 68 | } 69 | 70 | // DeleteChannel 删除指定子频道 71 | func (o *openAPI) DeleteChannel(ctx context.Context, channelID string) error { 72 | _, err := o.request(ctx). 73 | SetResult(dto.Channel{}). 74 | SetPathParam("channel_id", channelID). 75 | Delete(o.getURL(channelURI)) 76 | return err 77 | } 78 | 79 | // CreatePrivateChannel 创建私密子频道,底层是用的是 PostChannel 能力 80 | // ChannelValueObject 中的 PrivateType 不需要填充,本方法会自动填充 81 | func (o *openAPI) CreatePrivateChannel(ctx context.Context, guildID string, value *dto.ChannelValueObject, 82 | userIDs []string) (*dto.Channel, error) { 83 | value.PrivateType = dto.ChannelPrivateTypeAdminAndMember 84 | if len(userIDs) != 0 { 85 | value.PrivateUserIDs = userIDs 86 | value.PrivateType = dto.ChannelPrivateTypeOnlyAdmin 87 | } 88 | return o.PostChannel(ctx, guildID, value) 89 | } 90 | 91 | // ListVoiceChannelMembers 查询语音子频道成员列表 92 | func (o *openAPI) ListVoiceChannelMembers(ctx context.Context, channelID string) ([]*dto.Member, error) { 93 | resp, err := o.request(ctx). 94 | SetPathParam("channel_id", channelID). 95 | Get(o.getURL(voiceChannelMembersURI)) 96 | if err != nil { 97 | return nil, err 98 | } 99 | members := make([]*dto.Member, 0) 100 | if err := json.Unmarshal(resp.Body(), &members); err != nil { 101 | return nil, err 102 | } 103 | return members, nil 104 | } 105 | -------------------------------------------------------------------------------- /openapi/v1/direct_message.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/tencent-connect/botgo/dto" 8 | "github.com/tencent-connect/botgo/openapi/options" 9 | ) 10 | 11 | // CreateDirectMessage 创建私信频道 12 | func (o *openAPI) CreateDirectMessage(ctx context.Context, 13 | dm *dto.DirectMessageToCreate, opt ...options.Option) (*dto.DirectMessage, error) { 14 | reqCMD := o.request(ctx). 15 | SetResult(dto.DirectMessage{}). 16 | SetBody(dm) 17 | 18 | resp, err := baseRequest(ctx, reqCMD, http.MethodPost, o.getURL(userMeDMURI), opt...) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return resp.Result().(*dto.DirectMessage), nil 23 | } 24 | 25 | // PostDirectMessage 在私信频道内发消息 26 | func (o *openAPI) PostDirectMessage(ctx context.Context, 27 | dm *dto.DirectMessage, msg *dto.MessageToCreate, opt ...options.Option) (*dto.Message, error) { 28 | reqCMD := o.request(ctx). 29 | SetResult(dto.Message{}). 30 | SetPathParam("guild_id", dm.GuildID). 31 | SetBody(msg) 32 | 33 | resp, err := baseRequest(ctx, reqCMD, http.MethodPost, o.getURL(dmsURI), opt...) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return resp.Result().(*dto.Message), nil 38 | } 39 | 40 | // RetractDMMessage 撤回私信消息 41 | func (o *openAPI) RetractDMMessage(ctx context.Context, 42 | guildID, msgID string, opt ...options.Option) error { 43 | reqCMD := o.request(ctx). 44 | SetPathParam("guild_id", guildID). 45 | SetPathParam("message_id", msgID) 46 | 47 | _, err := baseRequest(ctx, reqCMD, http.MethodDelete, o.getURL(dmsMessageURI), opt...) 48 | return err 49 | } 50 | 51 | // PostDMSettingGuide 发送私信设置引导, jumpGuildID为设置引导要跳转的频道ID 52 | func (o *openAPI) PostDMSettingGuide(ctx context.Context, 53 | dm *dto.DirectMessage, jumpGuildID string, opt ...options.Option) (*dto.Message, error) { 54 | msg := &dto.SettingGuideToCreate{ 55 | SettingGuide: &dto.SettingGuide{ 56 | GuildID: jumpGuildID, 57 | }, 58 | } 59 | reqCMD := o.request(ctx). 60 | SetResult(dto.Message{}). 61 | SetPathParam("guild_id", dm.GuildID). 62 | SetBody(msg) 63 | 64 | resp, err := baseRequest(ctx, reqCMD, http.MethodPost, o.getURL(dmSettingGuideURI), opt...) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return resp.Result().(*dto.Message), nil 69 | } 70 | -------------------------------------------------------------------------------- /openapi/v1/guilds.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | // Guild 拉取频道信息 10 | func (o *openAPI) Guild(ctx context.Context, guildID string) (*dto.Guild, error) { 11 | resp, err := o.request(ctx). 12 | SetResult(dto.Guild{}). 13 | SetPathParam("guild_id", guildID). 14 | Get(o.getURL(guildURI)) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return resp.Result().(*dto.Guild), nil 20 | } 21 | -------------------------------------------------------------------------------- /openapi/v1/interaction.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // HeaderCallbackAppID 互动按钮第三方回调 appID 8 | const HeaderCallbackAppID = "X-Callback-AppID" 9 | 10 | // PutInteraction 更新 interaction 11 | func (o *openAPI) PutInteraction(ctx context.Context, 12 | interactionID string, body string) error { 13 | _, err := o.request(ctx). 14 | SetHeader(HeaderCallbackAppID, o.GetAppID()). 15 | SetPathParam("interaction_id", interactionID). 16 | SetBody(body). 17 | Put(o.getURL(interactionsURI)) 18 | return err 19 | } 20 | -------------------------------------------------------------------------------- /openapi/v1/me.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/tencent-connect/botgo/dto" 8 | "github.com/tencent-connect/botgo/errs" 9 | ) 10 | 11 | // Me 拉取当前用户的信息 12 | func (o *openAPI) Me(ctx context.Context) (*dto.User, error) { 13 | resp, err := o.request(ctx). 14 | SetResult(dto.User{}). 15 | Get(o.getURL(userMeURI)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return resp.Result().(*dto.User), nil 21 | } 22 | 23 | // MeGuilds 拉取当前用户加入的频道列表 24 | func (o *openAPI) MeGuilds(ctx context.Context, pager *dto.GuildPager) ([]*dto.Guild, error) { 25 | if pager == nil { 26 | return nil, errs.ErrPagerIsNil 27 | } 28 | resp, err := o.request(ctx). 29 | SetQueryParams(pager.QueryParams()). 30 | Get(o.getURL(userMeGuildsURI)) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | guilds := make([]*dto.Guild, 0) 36 | if err := json.Unmarshal(resp.Body(), &guilds); err != nil { 37 | return nil, err 38 | } 39 | 40 | return guilds, nil 41 | } 42 | -------------------------------------------------------------------------------- /openapi/v1/member.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/tencent-connect/botgo/dto" 8 | "github.com/tencent-connect/botgo/errs" 9 | ) 10 | 11 | // MemberAddRole 添加成员角色 12 | func (o *openAPI) MemberAddRole( 13 | ctx context.Context, guildID string, roleID dto.RoleID, userID string, 14 | value *dto.MemberAddRoleBody, 15 | ) error { 16 | if value == nil { 17 | value = new(dto.MemberAddRoleBody) 18 | } 19 | _, err := o.request(ctx). 20 | SetPathParam("guild_id", guildID). 21 | SetPathParam("role_id", string(roleID)). 22 | SetPathParam("user_id", userID). 23 | SetBody(value). 24 | Put(o.getURL(memberRoleURI)) 25 | return err 26 | } 27 | 28 | // MemberDeleteRole 删除成员角色 29 | func (o *openAPI) MemberDeleteRole( 30 | ctx context.Context, guildID string, roleID dto.RoleID, userID string, 31 | value *dto.MemberAddRoleBody, 32 | ) error { 33 | if value == nil { 34 | value = new(dto.MemberAddRoleBody) 35 | } 36 | _, err := o.request(ctx). 37 | SetPathParam("guild_id", guildID). 38 | SetPathParam("role_id", string(roleID)). 39 | SetPathParam("user_id", userID). 40 | SetBody(value). 41 | Delete(o.getURL(memberRoleURI)) 42 | return err 43 | } 44 | 45 | // GuildMember 拉取频道指定成员 46 | func (o *openAPI) GuildMember(ctx context.Context, guildID, userID string) (*dto.Member, error) { 47 | resp, err := o.request(ctx). 48 | SetResult(dto.Member{}). 49 | SetPathParam("guild_id", guildID). 50 | SetPathParam("user_id", userID). 51 | Get(o.getURL(guildMemberURI)) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return resp.Result().(*dto.Member), nil 57 | } 58 | 59 | // GuildMembers 分页拉取频道内成员列表 60 | func (o *openAPI) GuildMembers( 61 | ctx context.Context, 62 | guildID string, pager *dto.GuildMembersPager, 63 | ) ([]*dto.Member, error) { 64 | if pager == nil { 65 | return nil, errs.ErrPagerIsNil 66 | } 67 | resp, err := o.request(ctx). 68 | SetPathParam("guild_id", guildID). 69 | SetQueryParams(pager.QueryParams()). 70 | Get(o.getURL(guildMembersURI)) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | members := make([]*dto.Member, 0) 76 | if err := json.Unmarshal(resp.Body(), &members); err != nil { 77 | return nil, err 78 | } 79 | 80 | return members, nil 81 | } 82 | 83 | // GuildRoleMembers 分页拉取频道内身份组成员列表 84 | func (o *openAPI) GuildRoleMembers( 85 | ctx context.Context, guildID string, roleID string, pager *dto.GuildRoleMembersPager, 86 | ) ([]*dto.Member, string, error) { 87 | if pager == nil { 88 | return nil, "", errs.ErrPagerIsNil 89 | } 90 | resp, err := o.request(ctx). 91 | SetPathParam("guild_id", guildID). 92 | SetPathParam("role_id", roleID). 93 | SetQueryParams(pager.QueryParams()). 94 | Get(o.getURL(guildRoleMemberURI)) 95 | if err != nil { 96 | return nil, "", err 97 | } 98 | 99 | type res struct { 100 | Data []*dto.Member `json:"data"` 101 | Next string `json:"next"` 102 | } 103 | var roleMembersRsp res 104 | if err := json.Unmarshal(resp.Body(), &roleMembersRsp); err != nil { 105 | return nil, "", err 106 | } 107 | 108 | return roleMembersRsp.Data, roleMembersRsp.Next, nil 109 | } 110 | 111 | // DeleteGuildMember 将指定成员踢出频道 112 | func (o *openAPI) DeleteGuildMember(ctx context.Context, guildID, userID string, opts ...dto.MemberDeleteOption) error { 113 | opt := &dto.MemberDeleteOpts{} 114 | for _, o := range opts { 115 | o(opt) 116 | } 117 | _, err := o.request(ctx). 118 | SetResult(dto.Member{}). 119 | SetPathParam("guild_id", guildID). 120 | SetPathParam("user_id", userID). 121 | SetBody(opt). 122 | Delete(o.getURL(guildMemberURI)) 123 | return err 124 | } 125 | -------------------------------------------------------------------------------- /openapi/v1/message_reaction.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "strconv" 7 | 8 | "github.com/tencent-connect/botgo/dto" 9 | "github.com/tencent-connect/botgo/errs" 10 | ) 11 | 12 | // CreateMessageReaction 对消息发表表情表态 13 | func (o *openAPI) CreateMessageReaction(ctx context.Context, 14 | channelID, messageID string, emoji dto.Emoji) error { 15 | _, err := o.request(ctx). 16 | SetPathParam("channel_id", channelID). 17 | SetPathParam("message_id", messageID). 18 | SetPathParam("emoji_type", strconv.FormatUint(uint64(emoji.Type), 10)). 19 | SetPathParam("emoji_id", emoji.ID). 20 | Put(o.getURL(messageReactionURI)) 21 | if err != nil { 22 | return err 23 | } 24 | return nil 25 | } 26 | 27 | // DeleteOwnMessageReaction 删除自己的消息表情表态 28 | func (o *openAPI) DeleteOwnMessageReaction(ctx context.Context, 29 | channelID, messageID string, emoji dto.Emoji) error { 30 | _, err := o.request(ctx). 31 | SetPathParam("channel_id", channelID). 32 | SetPathParam("message_id", messageID). 33 | SetPathParam("emoji_type", strconv.FormatUint(uint64(emoji.Type), 10)). 34 | SetPathParam("emoji_id", emoji.ID). 35 | Delete(o.getURL(messageReactionURI)) 36 | if err != nil { 37 | return err 38 | } 39 | return nil 40 | } 41 | 42 | // GetMessageReactionUsers 获取消息表情表态用户列表 43 | func (o *openAPI) GetMessageReactionUsers(ctx context.Context, channelID, messageID string, emoji dto.Emoji, 44 | pager *dto.MessageReactionPager) (*dto.MessageReactionUsers, error) { 45 | if pager == nil { 46 | return nil, errs.ErrPagerIsNil 47 | } 48 | resp, err := o.request(ctx). 49 | SetPathParam("channel_id", channelID). 50 | SetPathParam("message_id", messageID). 51 | SetPathParam("emoji_type", strconv.FormatUint(uint64(emoji.Type), 10)). 52 | SetPathParam("emoji_id", emoji.ID). 53 | SetQueryParams(pager.QueryParams()). 54 | Get(o.getURL(messageReactionURI)) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | messageReactionUsers := &dto.MessageReactionUsers{} 60 | if err := json.Unmarshal(resp.Body(), &messageReactionUsers); err != nil { 61 | return nil, err 62 | } 63 | 64 | return messageReactionUsers, nil 65 | } 66 | -------------------------------------------------------------------------------- /openapi/v1/message_setting.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | // GetMessageSetting 获取频道消息频率设置信息 10 | func (o *openAPI) GetMessageSetting(ctx context.Context, guildID string) (*dto.MessageSetting, error) { 11 | resp, err := o.request(ctx). 12 | SetResult(dto.MessageSetting{}). 13 | SetPathParam("guild_id", guildID). 14 | Get(o.getURL(messageSettingURI)) 15 | if err != nil { 16 | return nil, err 17 | } 18 | return resp.Result().(*dto.MessageSetting), nil 19 | } 20 | -------------------------------------------------------------------------------- /openapi/v1/mute.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/tencent-connect/botgo/log" 8 | 9 | "github.com/tencent-connect/botgo/dto" 10 | ) 11 | 12 | // GuildMute 频道禁言 13 | func (o *openAPI) GuildMute(ctx context.Context, guildID string, mute *dto.UpdateGuildMute) error { 14 | _, err := o.request(ctx). 15 | SetPathParam("guild_id", guildID). 16 | SetBody(mute). 17 | Patch(o.getURL(guildMuteURI)) 18 | if err != nil { 19 | return err 20 | } 21 | return nil 22 | } 23 | 24 | // MemberMute 频道指定成员禁言 25 | func (o *openAPI) MemberMute(ctx context.Context, guildID, userID string, 26 | mute *dto.UpdateGuildMute) error { 27 | _, err := o.request(ctx). 28 | SetPathParam("guild_id", guildID). 29 | SetPathParam("user_id", userID). 30 | SetBody(mute). 31 | Patch(o.getURL(guildMembersMuteURI)) 32 | if err != nil { 33 | return err 34 | } 35 | return nil 36 | } 37 | 38 | // MultiMemberMute 频道批量成员禁言 39 | func (o *openAPI) MultiMemberMute(ctx context.Context, guildID string, 40 | mute *dto.UpdateGuildMute) (*dto.UpdateGuildMuteResponse, error) { 41 | if len(mute.UserIDs) == 0 { 42 | return nil, errors.New("no user id param") 43 | } 44 | rsp, err := o.request(ctx). 45 | SetPathParam("guild_id", guildID). 46 | SetBody(mute). 47 | SetResult(dto.UpdateGuildMuteResponse{}). 48 | Patch(o.getURL(guildMuteURI)) 49 | if err != nil { 50 | return nil, err 51 | } 52 | log.Infof("MultiMemberMute rsp result: %#v", rsp.Result()) 53 | return rsp.Result().(*dto.UpdateGuildMuteResponse), nil 54 | } 55 | -------------------------------------------------------------------------------- /openapi/v1/openapi.go: -------------------------------------------------------------------------------- 1 | // Package v1 是 openapi v1 版本的实现。 2 | package v1 3 | 4 | import ( 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/go-resty/resty/v2" // resty 是一个优秀的 rest api 客户端,可以极大的减少开发基于 rest 标准接口求请求的封装工作量 13 | "github.com/tencent-connect/botgo/constant" 14 | "github.com/tencent-connect/botgo/errs" 15 | "github.com/tencent-connect/botgo/log" 16 | "github.com/tencent-connect/botgo/openapi" 17 | "github.com/tencent-connect/botgo/version" 18 | "golang.org/x/oauth2" 19 | ) 20 | 21 | // MaxIdleConns 默认指定空闲连接池大小 22 | const MaxIdleConns = 3000 23 | 24 | type openAPI struct { 25 | appID string 26 | tokenSource oauth2.TokenSource 27 | timeout time.Duration 28 | 29 | sandbox bool // 请求沙箱环境 30 | debug bool // debug 模式,调试sdk时候使用 31 | lastTraceID string // lastTraceID id 32 | 33 | restyClient *resty.Client // resty client 复用 34 | } 35 | 36 | // Setup 注册 37 | func Setup() { 38 | openapi.Register(openapi.APIv1, &openAPI{}) 39 | } 40 | 41 | // Version 创建当前版本 42 | func (o *openAPI) Version() openapi.APIVersion { 43 | return openapi.APIv1 44 | } 45 | 46 | // TraceID 获取 lastTraceID id 47 | func (o *openAPI) TraceID() string { 48 | return o.lastTraceID 49 | } 50 | 51 | // Setup 生成一个实例 52 | func (o *openAPI) Setup(botAppID string, tokenSource oauth2.TokenSource, inSandbox bool) openapi.OpenAPI { 53 | api := &openAPI{ 54 | appID: botAppID, 55 | tokenSource: tokenSource, 56 | timeout: 5 * time.Second, 57 | sandbox: inSandbox, 58 | } 59 | api.setupClient(botAppID) // 初始化可复用的 client 60 | return api 61 | } 62 | 63 | // WithTimeout 设置请求接口超时时间 64 | func (o *openAPI) WithTimeout(duration time.Duration) openapi.OpenAPI { 65 | o.restyClient.SetTimeout(duration) 66 | return o 67 | } 68 | 69 | // SetDebug 设置调试模式, 输出更多过程日志 70 | func (o *openAPI) SetDebug(debug bool) openapi.OpenAPI { 71 | o.restyClient.Debug = debug 72 | return o 73 | } 74 | 75 | // Transport 透传请求 76 | func (o *openAPI) Transport(ctx context.Context, method, url string, body interface{}) ([]byte, error) { 77 | resp, err := o.request(ctx).SetBody(body).Execute(method, url) 78 | return resp.Body(), err 79 | } 80 | 81 | // 初始化 client 82 | func (o *openAPI) setupClient(appID string) { 83 | o.restyClient = resty.New(). 84 | SetTransport(createTransport(nil, MaxIdleConns)). // 自定义 transport 85 | SetLogger(log.DefaultLogger). 86 | SetDebug(o.debug). 87 | SetTimeout(o.timeout). 88 | SetHeader("User-Agent", version.String()). 89 | SetHeader("X-Union-Appid", appID). 90 | SetPreRequestHook( 91 | func(_ *resty.Client, request *http.Request) error { 92 | // 执行请求前过滤器 93 | // 由于在 `OnBeforeRequest` 的时候,request 还没生成,所以 filter 不能使用,所以放到 `PreRequestHook` 94 | return openapi.DoReqFilterChains(request, nil) 95 | }, 96 | ). 97 | OnBeforeRequest( 98 | func(c *resty.Client, _ *resty.Request) error { 99 | tk, err := o.tokenSource.Token() 100 | if err != nil { 101 | log.Errorf("[setupClient] retrieve token failed:%s", err) 102 | return err 103 | } 104 | c.SetAuthScheme(tk.TokenType) 105 | log.Debugf("token type:%s", tk.TokenType) 106 | c.SetAuthToken(tk.AccessToken) 107 | return nil 108 | }, 109 | ). 110 | // 设置请求之后的钩子,打印日志,判断状态码 111 | OnAfterResponse( 112 | func(_ *resty.Client, resp *resty.Response) error { 113 | log.Infof("%v", respInfo(resp)) 114 | // 执行请求后过滤器 115 | if err := openapi.DoRespFilterChains(resp.Request.RawRequest, resp.RawResponse); err != nil { 116 | return err 117 | } 118 | traceID := resp.Header().Get(constant.HeaderTraceID) 119 | o.lastTraceID = traceID 120 | // 非成功含义的状态码,需要返回 error 供调用方识别 121 | if !openapi.IsSuccessStatus(resp.StatusCode()) { 122 | o.handleError(resp) 123 | return errs.New(resp.StatusCode(), string(resp.Body()), traceID) 124 | } 125 | return nil 126 | }, 127 | ) 128 | } 129 | 130 | // request 每个请求,都需要创建一个 request 131 | func (o *openAPI) request(ctx context.Context) *resty.Request { 132 | return o.restyClient.R().SetContext(ctx) 133 | } 134 | 135 | // GetAppID 获取接口地址,会处理沙箱环境判断 136 | func (o *openAPI) GetAppID() string { 137 | if o == nil { 138 | return "" 139 | } 140 | return o.appID 141 | } 142 | 143 | // errBody 请求出错情况下的body结构 144 | type errBody struct { 145 | Message string `json:"message"` // 错误原因 146 | Code int `json:"code"` // 错误码,后续废弃 147 | ErrCode int `json:"err_code"` // 错误码 148 | TraceID string `json:"trace_id"` // 服务端traceID, 用于问题排查 149 | } 150 | 151 | // handleError 处理openapi调用失败的情况 152 | func (o *openAPI) handleError(resp *resty.Response) { 153 | var b errBody 154 | err := json.Unmarshal(resp.Body(), &b) 155 | if err != nil { 156 | log.Errorf("parse errBody fail, err:%v, body:%s", err, string(resp.Body())) 157 | return 158 | } 159 | if b.ErrCode == errs.APICodeTokenExpireOrNotExist || b.Code == errs.APICodeTokenExpireOrNotExist { 160 | log.Errorf("token expire or not exist, update token") 161 | _, _ = o.tokenSource.Token() 162 | } 163 | } 164 | 165 | // respInfo 用于输出日志的时候格式化数据 166 | func respInfo(resp *resty.Response) string { 167 | bodyJSON, _ := json.Marshal(resp.Request.Body) 168 | return fmt.Sprintf( 169 | "[OPENAPI]%v %v, traceID:%v, status:%v, elapsed:%v req: %v, resp: %v", 170 | resp.Request.Method, 171 | resp.Request.URL, 172 | resp.Header().Get(constant.HeaderTraceID), 173 | resp.Status(), 174 | resp.Time(), 175 | string(bodyJSON), 176 | string(resp.Body()), 177 | ) 178 | } 179 | func createTransport(localAddr net.Addr, idleConns int) *http.Transport { 180 | dialer := &net.Dialer{ 181 | Timeout: 60 * time.Second, 182 | KeepAlive: 60 * time.Second, 183 | } 184 | if localAddr != nil { 185 | dialer.LocalAddr = localAddr 186 | } 187 | return &http.Transport{ 188 | Proxy: http.ProxyFromEnvironment, 189 | DialContext: dialer.DialContext, 190 | ForceAttemptHTTP2: true, 191 | MaxIdleConns: idleConns, 192 | IdleConnTimeout: 90 * time.Second, 193 | TLSHandshakeTimeout: 10 * time.Second, 194 | ExpectContinueTimeout: 1 * time.Second, 195 | MaxIdleConnsPerHost: idleConns, 196 | MaxConnsPerHost: idleConns, 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /openapi/v1/pins.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | // AddPins 添加精华消息 10 | func (o *openAPI) AddPins(ctx context.Context, channelID string, messageID string) (*dto.PinsMessage, error) { 11 | resp, err := o.request(ctx). 12 | SetResult(dto.PinsMessage{}). 13 | SetPathParam("channel_id", channelID). 14 | SetPathParam("message_id", messageID). 15 | Put(o.getURL(pinURI)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return resp.Result().(*dto.PinsMessage), nil 20 | } 21 | 22 | // DeletePins 删除精华消息 23 | func (o *openAPI) DeletePins(ctx context.Context, channelID, messageID string) error { 24 | _, err := o.request(ctx). 25 | SetResult(dto.PinsMessage{}). 26 | SetPathParam("channel_id", channelID). 27 | SetPathParam("message_id", messageID). 28 | Delete(o.getURL(pinURI)) 29 | return err 30 | } 31 | 32 | // GetPins 获取精华消息 33 | func (o *openAPI) GetPins(ctx context.Context, channelID string) (*dto.PinsMessage, error) { 34 | resp, err := o.request(ctx). 35 | SetResult(dto.PinsMessage{}). 36 | SetPathParam("channel_id", channelID). 37 | Get(o.getURL(pinsURI)) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return resp.Result().(*dto.PinsMessage), nil 42 | } 43 | 44 | // CleanPins 清除全部精华消息 45 | func (o *openAPI) CleanPins(ctx context.Context, channelID string) error { 46 | _, err := o.request(ctx). 47 | SetResult(dto.PinsMessage{}). 48 | SetPathParam("channel_id", channelID). 49 | SetPathParam("message_id", "all"). 50 | Delete(o.getURL(pinURI)) 51 | return err 52 | } 53 | -------------------------------------------------------------------------------- /openapi/v1/resource.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tencent-connect/botgo/constant" 7 | ) 8 | 9 | type uri string 10 | 11 | // 目前提供的接口的 uri 12 | const ( 13 | guildURI uri = "/guilds/{guild_id}" 14 | guildMembersURI uri = "/guilds/{guild_id}/members" 15 | guildMemberURI uri = "/guilds/{guild_id}/members/{user_id}" 16 | guildRoleMemberURI uri = "/guilds/{guild_id}/roles/{role_id}/members" 17 | guildMuteURI uri = "/guilds/{guild_id}/mute" // 频道禁言 18 | guildMembersMuteURI uri = "/guilds/{guild_id}/members/{user_id}/mute" // 频道指定成员禁言 19 | 20 | channelsURI uri = "/guilds/{guild_id}/channels" 21 | channelURI uri = "/channels/{channel_id}" 22 | 23 | channelPermissionsURI uri = "/channels/{channel_id}/members/{user_id}/permissions" 24 | channelRolesPermissionsURI uri = "/channels/{channel_id}/roles/{role_id}/permissions" 25 | 26 | messagesURI uri = "/channels/{channel_id}/messages" 27 | groupMessagesURI uri = "/v2/groups/{group_id}/messages" 28 | groupRichMediaURI uri = "/v2/groups/{group_id}/files" 29 | 30 | c2cMessagesURI uri = "/v2/users/{user_id}/messages" 31 | c2cRichMediaURI uri = "/v2/users/{user_id}/files" 32 | 33 | retractC2cMessageURI uri = "/v2/users/{user_id}/messages/{message_id}" 34 | retractGroupMessageURI uri = "/v2/groups/{group_id}/messages/{message_id}" 35 | 36 | messageURI uri = "/channels/{channel_id}/messages/{message_id}" 37 | 38 | userMeURI uri = "/users/@me" 39 | userMeGuildsURI uri = "/users/@me/guilds" 40 | userMeDMURI uri = "/users/@me/dms" 41 | 42 | gatewayURI uri = "/gateway" // nolint 43 | gatewayBotURI uri = "/gateway/bot" 44 | 45 | audioControlURI uri = "/channels/{channel_id}/audio" 46 | micURI uri = "/channels/{channel_id}/mic" 47 | 48 | rolesURI uri = "/guilds/{guild_id}/roles" 49 | roleURI uri = "/guilds/{guild_id}/roles/{role_id}" 50 | 51 | memberRoleURI uri = "/guilds/{guild_id}/members/{user_id}/roles/{role_id}" 52 | 53 | dmsURI uri = "/dms/{guild_id}/messages" 54 | dmsMessageURI uri = "/dms/{guild_id}/messages/{message_id}" 55 | 56 | channelAnnouncesURI = "/channels/{channel_id}/announces" 57 | channelAnnounceURI = "/channels/{channel_id}/announces/{message_id}" 58 | 59 | guildAnnouncesURI = "/guilds/{guild_id}/announces" 60 | guildAnnounceURI = "/guilds/{guild_id}/announces/{message_id}" 61 | 62 | schedulesURI uri = "/channels/{channel_id}/schedules" 63 | scheduleURI uri = "/channels/{channel_id}/schedules/{schedule_id}" 64 | 65 | apiPermissionURI uri = "/guilds/{guild_id}/api_permission" 66 | apiPermissionDemandURI uri = "/guilds/{guild_id}/api_permission/demand" 67 | 68 | pinsURI = "/channels/{channel_id}/pins" 69 | pinURI = "/channels/{channel_id}/pins/{message_id}" 70 | 71 | messageReactionURI uri = "/channels/{channel_id}/messages/{message_id}/reactions/{emoji_type}/{emoji_id}" 72 | 73 | interactionsURI = "/interactions/{interaction_id}" 74 | 75 | httpSessionsURI uri = "/gateway/webhook/sessions" 76 | httpSessionURI uri = "/gateway/webhook/sessions/{session_id}" 77 | 78 | messageSettingURI uri = "/guilds/{guild_id}/message/setting" 79 | 80 | voiceChannelMembersURI uri = "/channels/{channel_id}/voice/members" 81 | 82 | settingGuideURI uri = "/channels/{channel_id}/settingguide" 83 | dmSettingGuideURI uri = "/dms/{guild_id}/settingguide" 84 | ) 85 | 86 | // getURL 获取接口地址,会处理沙箱环境判断 87 | func (o *openAPI) getURL(endpoint uri) string { 88 | d := constant.APIDomain 89 | if o.sandbox { 90 | d = constant.SandBoxAPIDomain 91 | } 92 | return fmt.Sprintf("%s%s", d, endpoint) 93 | } 94 | -------------------------------------------------------------------------------- /openapi/v1/role.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | "github.com/tencent-connect/botgo/log" 8 | ) 9 | 10 | func (o *openAPI) Roles(ctx context.Context, guildID string) (*dto.GuildRoles, error) { 11 | resp, err := o.request(ctx). 12 | SetResult(dto.GuildRoles{}). 13 | SetPathParam("guild_id", guildID). 14 | Get(o.getURL(rolesURI)) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return resp.Result().(*dto.GuildRoles), nil 20 | } 21 | 22 | func (o *openAPI) PostRole(ctx context.Context, guildID string, role *dto.Role) (*dto.UpdateResult, error) { 23 | if role.Color == 0 { 24 | role.Color = dto.DefaultColor 25 | } 26 | // openapi 上修改哪个字段,就需要传递哪个 filter 27 | filter := &dto.UpdateRoleFilter{ 28 | Name: 1, 29 | Color: 1, 30 | Hoist: 1, 31 | } 32 | body := &dto.UpdateRole{ 33 | GuildID: guildID, 34 | Filter: filter, 35 | Update: role, 36 | } 37 | log.Debug(body) 38 | resp, err := o.request(ctx). 39 | SetPathParam("guild_id", guildID). 40 | SetResult(dto.UpdateResult{}). 41 | SetBody(body). 42 | Post(o.getURL(rolesURI)) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return resp.Result().(*dto.UpdateResult), nil 48 | } 49 | 50 | func (o *openAPI) PatchRole(ctx context.Context, 51 | guildID string, roleID dto.RoleID, role *dto.Role) (*dto.UpdateResult, error) { 52 | if role.Color == 0 { 53 | role.Color = dto.DefaultColor 54 | } 55 | filter := &dto.UpdateRoleFilter{ 56 | Name: 1, 57 | Color: 1, 58 | Hoist: 1, 59 | } 60 | body := &dto.UpdateRole{ 61 | GuildID: guildID, 62 | Filter: filter, 63 | Update: role, 64 | } 65 | resp, err := o.request(ctx). 66 | SetPathParam("guild_id", guildID). 67 | SetPathParam("role_id", string(roleID)). 68 | SetResult(dto.UpdateResult{}). 69 | SetBody(body). 70 | Patch(o.getURL(roleURI)) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return resp.Result().(*dto.UpdateResult), nil 76 | } 77 | 78 | func (o *openAPI) DeleteRole(ctx context.Context, guildID string, roleID dto.RoleID) error { 79 | _, err := o.request(ctx). 80 | SetPathParam("guild_id", guildID). 81 | SetPathParam("role_id", string(roleID)). 82 | Delete(o.getURL(roleURI)) 83 | return err 84 | } 85 | -------------------------------------------------------------------------------- /openapi/v1/schedule.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | "github.com/tencent-connect/botgo/dto" 8 | ) 9 | 10 | // ListSchedules 查询某个子频道下,since开始的当天的日程列表。若since为0,默认返回当天的日程列表 11 | func (o *openAPI) ListSchedules(ctx context.Context, channelID string, since uint64) ([]*dto.Schedule, error) { 12 | rsp, err := o.request(ctx). 13 | SetResult([]*dto.Schedule{}). 14 | SetPathParam("channel_id", channelID). 15 | SetQueryParam("since", strconv.FormatUint(since, 10)). 16 | Get(o.getURL(schedulesURI)) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return *rsp.Result().(*[]*dto.Schedule), nil 21 | } 22 | 23 | // GetSchedule 获取单个日程信息 24 | func (o *openAPI) GetSchedule(ctx context.Context, channelID, scheduleID string) (*dto.Schedule, error) { 25 | rsp, err := o.request(ctx). 26 | SetResult(dto.Schedule{}). 27 | SetPathParam("channel_id", channelID). 28 | SetPathParam("schedule_id", scheduleID). 29 | Get(o.getURL(scheduleURI)) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return rsp.Result().(*dto.Schedule), nil 34 | } 35 | 36 | // CreateSchedule 创建日程 37 | func (o *openAPI) CreateSchedule(ctx context.Context, channelID string, schedule *dto.Schedule) (*dto.Schedule, error) { 38 | rsp, err := o.request(ctx). 39 | SetResult(dto.Schedule{}). 40 | SetPathParam("channel_id", channelID). 41 | SetBody(dto.ScheduleWrapper{Schedule: schedule}). 42 | Post(o.getURL(schedulesURI)) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return rsp.Result().(*dto.Schedule), nil 47 | } 48 | 49 | // ModifySchedule 修改日程 50 | func (o *openAPI) ModifySchedule(ctx context.Context, 51 | channelID, scheduleID string, schedule *dto.Schedule) (*dto.Schedule, error) { 52 | rsp, err := o.request(ctx). 53 | SetResult(dto.Schedule{}). 54 | SetPathParam("channel_id", channelID). 55 | SetPathParam("schedule_id", scheduleID). 56 | SetBody(dto.ScheduleWrapper{Schedule: schedule}). 57 | Patch(o.getURL(scheduleURI)) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return rsp.Result().(*dto.Schedule), nil 62 | } 63 | 64 | // DeleteSchedule 删除日程 65 | func (o *openAPI) DeleteSchedule(ctx context.Context, channelID, scheduleID string) error { 66 | _, err := o.request(ctx). 67 | SetPathParam("channel_id", channelID). 68 | SetPathParam("schedule_id", scheduleID). 69 | Delete(o.getURL(scheduleURI)) 70 | return err 71 | } 72 | -------------------------------------------------------------------------------- /openapi/v1/webhook.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/tencent-connect/botgo/dto" 8 | ) 9 | 10 | // CreateSession 创建一个新的 http 事件回调 11 | func (o *openAPI) CreateSession(ctx context.Context, identity dto.HTTPIdentity) (*dto.HTTPReady, error) { 12 | resp, err := o.request(ctx). 13 | SetResult(dto.HTTPReady{}). 14 | SetBody(identity). 15 | Post(o.getURL(httpSessionsURI)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return resp.Result().(*dto.HTTPReady), nil 21 | } 22 | 23 | // CheckSessions 定期检查 http 回调 session 的健康情况,服务端会自动 resume 非活跃状态的 session 24 | func (o *openAPI) CheckSessions(ctx context.Context) ([]*dto.HTTPSession, error) { 25 | resp, err := o.request(ctx). 26 | SetQueryParam("action", "check"). 27 | Patch(o.getURL(httpSessionsURI)) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | sessions := make([]*dto.HTTPSession, 0) 33 | if err := json.Unmarshal(resp.Body(), &sessions); err != nil { 34 | return nil, err 35 | } 36 | 37 | return sessions, nil 38 | } 39 | 40 | // GetActiveSessionList 拉取活跃的 http session 列表 41 | func (o *openAPI) SessionList(ctx context.Context) ([]*dto.HTTPSession, error) { 42 | resp, err := o.request(ctx). 43 | Get(o.getURL(httpSessionsURI)) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | sessions := make([]*dto.HTTPSession, 0) 49 | if err := json.Unmarshal(resp.Body(), &sessions); err != nil { 50 | return nil, err 51 | } 52 | 53 | return sessions, nil 54 | } 55 | 56 | // RemoveSession 停止某个 session 57 | func (o *openAPI) RemoveSession(ctx context.Context, sessionID string) error { 58 | _, err := o.request(ctx). 59 | SetPathParam("session_id", sessionID). 60 | Delete(o.getURL(httpSessionURI)) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /openapi/v1/ws.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tencent-connect/botgo/dto" 7 | ) 8 | 9 | // WS 获取带分片 WSS 接入点 10 | func (o *openAPI) WS(ctx context.Context, _ map[string]string, _ string) (*dto.WebsocketAP, error) { 11 | resp, err := o.request(ctx). 12 | SetResult(dto.WebsocketAP{}). 13 | Get(o.getURL(gatewayBotURI)) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return resp.Result().(*dto.WebsocketAP), nil 19 | } 20 | -------------------------------------------------------------------------------- /openapi/version.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ( 8 | // APIVersion 接口版本 9 | APIVersion = uint32 10 | ) 11 | 12 | // 接口版本,后续增加版本直接直接增加新的常量 13 | const ( 14 | APIv1 APIVersion = 1 + iota 15 | _ 16 | ) 17 | 18 | // APIVersionString 返回version的字符串格式 ex: v1 19 | func APIVersionString(version APIVersion) string { 20 | return fmt.Sprintf("v%v", version) 21 | } 22 | -------------------------------------------------------------------------------- /register.go: -------------------------------------------------------------------------------- 1 | package botgo 2 | 3 | import ( 4 | "github.com/tencent-connect/botgo/dto" 5 | "github.com/tencent-connect/botgo/event" 6 | "github.com/tencent-connect/botgo/log" 7 | "github.com/tencent-connect/botgo/openapi" 8 | "github.com/tencent-connect/botgo/websocket" 9 | ) 10 | 11 | // SetLogger 设置 logger,需要实现 sdk 的 log.Logger 接口 12 | func SetLogger(logger log.Logger) { 13 | log.DefaultLogger = logger 14 | } 15 | 16 | // SetSessionManager 注册自己实现的 session manager 17 | func SetSessionManager(m SessionManager) { 18 | defaultSessionManager = m 19 | } 20 | 21 | // SetWebsocketClient 替换 websocket 实现 22 | func SetWebsocketClient(c websocket.WebSocket) { 23 | websocket.Register(c) 24 | } 25 | 26 | // SetOpenAPIClient 注册 openapi 的不同实现,需要设置版本 27 | func SetOpenAPIClient(v openapi.APIVersion, c openapi.OpenAPI) { 28 | openapi.Register(v, c) 29 | } 30 | 31 | // RegisterDispatchEventHandler 注册回调事件处理器 32 | func RegisterDispatchEventHandler(eventType dto.EventType, f func(event *dto.WSPayload, message []byte) error) { 33 | event.RegisterHandler(dto.WSDispatchEvent, eventType, f) 34 | } 35 | -------------------------------------------------------------------------------- /scripts/git_hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 检查是否携带了 scope: 4 | COMMIT_MSG=$(cat $1 | grep -E "^.+:\s.+(\s|\S)*") 5 | if [ -z "$COMMIT_MSG" ]; then 6 | echo -e "提交信息不符合标准规范, 请使用规范格式, Example: \n\nserver: 增加某某错误处理处理 \n\n--issue=#123" 7 | exit 1 8 | fi 9 | 10 | # 检查是否携带了需求ID 11 | COMMIT_MSG=$(cat $1 | grep -E "\-\-(bug|story|task|issue)=(\#)?([0-9])+") 12 | if [ -z "$COMMIT_MSG" ]; then 13 | echo -e "提交信息不符合标准规范, 请使用规范格式, Example: \n\nserver: 增加某某错误处理处理 \n\n--issue=#123" 14 | exit 1 15 | fi 16 | 17 | # 检查第一行是否有需求ID 18 | COMMIT_MSG=$(head -n 1 $1 | grep -E "\-\-(bug|story|task|issue)=(\#)?([0-9])+") 19 | if [ -n "$COMMIT_MSG" ]; then 20 | echo "请不要在标题携带 bug/story/task/issue 等 \n\n" 21 | echo "${COMMIT_MSG}" 22 | exit 1 23 | fi 24 | 25 | # 检查第一行是否超长,75 字节 26 | COMMIT_MSG=$(head -n 1 $1) 27 | if [ ${#COMMIT_MSG} -gt 75 ]; then 28 | echo "CommitMsg 第一行过长,请保持在 75 字节以内 \n\n" 29 | echo "${COMMIT_MSG}" 30 | exit 1 31 | fi 32 | 33 | exit 0 34 | -------------------------------------------------------------------------------- /scripts/git_hooks/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git config core.hooksPath scripts/git_hooks && echo ">> local githook has been set" && git config --get core.hooksPath 4 | -------------------------------------------------------------------------------- /scripts/git_hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | # 检查当前目录下是否有 golang 文件,如果有则执行 golangci-lint run 5 | go_lint() { 6 | # 当前目录是否有 go 文件 7 | if [ -z "$(search_go_mod)" ]; then 8 | exit 0 9 | fi 10 | echo "发现 go mod,执行 golint 检查" && golangci-lint version && golangci-lint run 11 | } 12 | 13 | # 递归向上寻找 go.mod 14 | search_go_mod() { 15 | pwd=$(pwd) 16 | git_top=$(git rev-parse --show-toplevel) 17 | 18 | go_mod=$(find . -maxdepth 1 -name "go.mod") 19 | if [ -n "$go_mod" ];then 20 | echo "$go_mod" 21 | fi 22 | 23 | if [ "$pwd" == "$git_top" ]; then 24 | return 0 25 | fi 26 | 27 | up=$(git rev-parse --show-cdup) 28 | if [ -n "$up" ]; then 29 | cd $up && search_go_mod 30 | fi 31 | } 32 | 33 | go_lint 34 | -------------------------------------------------------------------------------- /scripts/git_hooks/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 获取分支名 4 | git_branch() { 5 | git rev-parse --abbrev-ref HEAD 6 | } 7 | 8 | # 自动给 commit 追加--story, --bug等 9 | auto_append_id() { 10 | branch=$(git_branch) 11 | idstr=$(echo "$branch" | grep -oE "(issue)_([0-9]*)$") 12 | # 分支名中存在 story 等关键词,自动替换为 --story=xxx 13 | if [ -n "$idstr" ]; then 14 | append_str="--${idstr//_/=#}" 15 | echo -e "\n$append_str" >> "$1" 16 | fi 17 | } 18 | 19 | # 如果未补充git type,自动在message前补充 git type 20 | # https://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html 21 | auto_prepend_type() { 22 | checkScope=$(cat $1 | grep -E "^.+:\s.+(\s|\S)*") 23 | if [ -n "$checkScope" ]; then 24 | return 25 | fi 26 | prepend_str="$(gen_scope)"": " 27 | content=$(cat "$1") 28 | echo "$prepend_str""$content" > "$1" 29 | } 30 | 31 | # 根据提交的变更,自动生成 scope 32 | gen_scope(){ 33 | scope=$(get_change_paths | uniq -c | sort -r | head -1 | awk '{print $2}') 34 | echo "$scope" 35 | } 36 | 37 | # 获取修改内容的路径 38 | get_change_paths(){ 39 | # 提取已经 git add 的修改内容,并且判断,如果目录层级大于2,则提取二级目录名,否则使用以及目录名或者根目录文件名 40 | git status --porcelain | grep -E "^(M|A|D|R|C|U)" | awk '{print $2}' | awk -F/ '{if (NF>2) print $(NF-2)"/"$(NF-1); else print $1}' 41 | } 42 | 43 | auto_prepend_type "$1" 44 | auto_append_id "$1" -------------------------------------------------------------------------------- /session_manager.go: -------------------------------------------------------------------------------- 1 | package botgo 2 | 3 | import ( 4 | "github.com/tencent-connect/botgo/dto" 5 | "github.com/tencent-connect/botgo/sessions/local" 6 | "golang.org/x/oauth2" 7 | ) 8 | 9 | // defaultSessionManager 默认实现的 session manager 为单机版本 10 | // 如果业务要自行实现分布式的 session 管理,则实现 SessionManger 后替换掉 defaultSessionManager 11 | var defaultSessionManager SessionManager = local.New() 12 | 13 | // SessionManager 接口,管理session 14 | type SessionManager interface { 15 | // Start 启动连接,默认使用 apInfo 中的 shards 作为 shard 数量,如果有需要自己指定 shard 数,请修 apInfo 中的信息 16 | Start(apInfo *dto.WebsocketAP, tokenSource oauth2.TokenSource, intents *dto.Intent) error 17 | } 18 | -------------------------------------------------------------------------------- /sessions/local/local.go: -------------------------------------------------------------------------------- 1 | // Package local 基于 golang chan 实现的单机 manager。 2 | package local 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | 8 | "github.com/tencent-connect/botgo/dto" 9 | "github.com/tencent-connect/botgo/log" 10 | "github.com/tencent-connect/botgo/sessions/manager" 11 | "github.com/tencent-connect/botgo/websocket" 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | // New 创建本地session管理器 16 | func New() *ChanManager { 17 | return &ChanManager{} 18 | } 19 | 20 | // ChanManager 默认的本地 session manager 实现 21 | type ChanManager struct { 22 | sessionChan chan dto.Session 23 | } 24 | 25 | // Start 启动本地 session manager 26 | func (l *ChanManager) Start(apInfo *dto.WebsocketAP, tokenSource oauth2.TokenSource, intents *dto.Intent) error { 27 | defer log.Sync() 28 | if err := manager.CheckSessionLimit(apInfo); err != nil { 29 | log.Errorf("[ws/session/local] session limited apInfo: %+v", apInfo) 30 | return err 31 | } 32 | startInterval := manager.CalcInterval(apInfo.SessionStartLimit.MaxConcurrency) 33 | log.Infof("[ws/session/local] will start %d sessions and per session start interval is %s", 34 | apInfo.Shards, startInterval) 35 | 36 | // 按照shards数量初始化,用于启动连接的管理 37 | l.sessionChan = make(chan dto.Session, apInfo.Shards) 38 | for i := uint32(0); i < apInfo.Shards; i++ { 39 | session := dto.Session{ 40 | URL: apInfo.URL, 41 | TokenSource: tokenSource, 42 | Intent: *intents, 43 | LastSeq: 0, 44 | Shards: dto.ShardConfig{ 45 | ShardID: i, 46 | ShardCount: apInfo.Shards, 47 | }, 48 | } 49 | l.sessionChan <- session 50 | } 51 | 52 | for session := range l.sessionChan { 53 | // MaxConcurrency 代表的是每 5s 可以连多少个请求 54 | time.Sleep(startInterval) 55 | go l.newConnect(session) 56 | } 57 | return nil 58 | } 59 | 60 | // newConnect 启动一个新的连接,如果连接在监听过程中报错了,或者被远端关闭了链接,需要识别关闭的原因,能否继续 resume 61 | // 如果能够 resume,则往 sessionChan 中放入带有 sessionID 的 session 62 | // 如果不能,则清理掉 sessionID,将 session 放入 sessionChan 中 63 | // session 的启动,交给 start 中的 for 循环执行,session 不自己递归进行重连,避免递归深度过深 64 | func (l *ChanManager) newConnect(session dto.Session) { 65 | defer func() { 66 | // panic 留下日志,放回 session 67 | if err := recover(); err != nil { 68 | websocket.PanicHandler(err, &session) 69 | l.sessionChan <- session 70 | } 71 | }() 72 | wsClient := websocket.ClientImpl.New(session) 73 | if err := wsClient.Connect(); err != nil { 74 | log.Error(err) 75 | l.sessionChan <- session // 连接失败,丢回去队列排队重连 76 | return 77 | } 78 | var err error 79 | // 如果 session id 不为空,则执行的是 resume 操作,如果为空,则执行的是 identify 操作 80 | if session.ID != "" { 81 | err = wsClient.Resume() 82 | } else { 83 | // 初次鉴权 84 | err = wsClient.Identify() 85 | } 86 | if err != nil { 87 | log.Errorf("[ws/session] Identify/Resume err %+v", err) 88 | return 89 | } 90 | if err = wsClient.Listening(); err != nil { 91 | log.Errorf("[ws/session] Listening err %+v", err) 92 | currentSession := wsClient.Session() 93 | // 对于不能够进行重连的session,需要清空 session id 与 seq 94 | if manager.CanNotResume(err) { 95 | currentSession.ID = "" 96 | currentSession.LastSeq = 0 97 | } 98 | // 一些错误不能够鉴权,比如机器人被封禁,这里就直接退出了 99 | if manager.CanNotIdentify(err) { 100 | msg := fmt.Sprintf("can not identify because server return %+v, so process exit", err) 101 | log.Errorf(msg) 102 | panic(msg) // 当机器人被下架,或者封禁,将不能再连接,所以 panic 103 | } 104 | // 将 session 放到 session chan 中,用于启动新的连接,当前连接退出 105 | l.sessionChan <- *currentSession 106 | return 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /sessions/manager/manager.go: -------------------------------------------------------------------------------- 1 | // Package manager 实现 session manager 所需要的公共方法。 2 | package manager 3 | 4 | import ( 5 | "math" 6 | "time" 7 | 8 | "github.com/tencent-connect/botgo/dto" 9 | "github.com/tencent-connect/botgo/errs" 10 | ) 11 | 12 | // CanNotResumeErrSet 不能进行 resume 操作的错误码 13 | var CanNotResumeErrSet = map[int]bool{ 14 | errs.CodeConnCloseCantResume: true, 15 | } 16 | 17 | // CanNotIdentifyErrSet 不能进行 identify 操作的错误码 18 | var CanNotIdentifyErrSet = map[int]bool{ 19 | errs.CodeConnCloseCantIdentify: true, 20 | } 21 | 22 | // concurrencyTimeWindowSec 并发时间窗口,单位秒 23 | const concurrencyTimeWindowSec = 2 24 | 25 | // CalcInterval 根据并发要求,计算连接启动间隔 26 | func CalcInterval(maxConcurrency uint32) time.Duration { 27 | if maxConcurrency == 0 { 28 | maxConcurrency = 1 29 | } 30 | f := math.Round(concurrencyTimeWindowSec / float64(maxConcurrency)) 31 | if f == 0 { 32 | f = 1 33 | } 34 | return time.Duration(f) * time.Second 35 | } 36 | 37 | // CanNotResume 是否是不能够 resume 的错误 38 | func CanNotResume(err error) bool { 39 | e := errs.Error(err) 40 | if flag, ok := CanNotResumeErrSet[e.Code()]; ok { 41 | return flag 42 | } 43 | return false 44 | } 45 | 46 | // CanNotIdentify 是否是不能够 identify 的错误 47 | func CanNotIdentify(err error) bool { 48 | e := errs.Error(err) 49 | if flag, ok := CanNotIdentifyErrSet[e.Code()]; ok { 50 | return flag 51 | } 52 | return false 53 | } 54 | 55 | // CheckSessionLimit 检查链接数是否达到限制,如果达到限制需要等待重置 56 | func CheckSessionLimit(apInfo *dto.WebsocketAP) error { 57 | if apInfo.Shards > apInfo.SessionStartLimit.Remaining { 58 | return errs.ErrSessionLimit 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /sessions/manager/manager_test.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func Test_calcInterval(t *testing.T) { 9 | type args struct { 10 | maxConcurrency uint32 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want time.Duration 16 | }{ 17 | {"c1", args{maxConcurrency: 1}, concurrencyTimeWindowSec * time.Second}, 18 | {"c3", args{maxConcurrency: 3}, 1 * time.Second}, 19 | {"c5", args{maxConcurrency: 5}, 1 * time.Second}, 20 | {"c10", args{maxConcurrency: 10}, 1 * time.Second}, 21 | {"c100", args{maxConcurrency: 100}, 1 * time.Second}, 22 | } 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | if got := CalcInterval(tt.args.maxConcurrency); got != tt.want { 26 | t.Errorf("CalcInterval() = %v, want %v", got, tt.want) 27 | } 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sessions/remote/README.md: -------------------------------------------------------------------------------- 1 | # 分布式 session manager 2 | 3 | 这是一个基于 `redis` 的 `list` 数据结构的分布式 session manager。 4 | 5 | ## 实现原理 6 | 7 | 1.基于 redis 实现的分布式锁,启动的时候先抢锁,抢到锁的服务实例根据从 openapi 拉取到的 shards 进行 session 的分发 8 | 9 | 2.启动一个本地的 `sessionProduceChan` 的消费,用于将需重连的 session 重新 push 到 redis 中,如果 push 失败,放回 chan 进行下一次重试 10 | 11 | 3.启动一个消费者,从 redis pop 数据,解析 session,然后创建新的 websocket client 连接 12 | 13 | 4.如果在处理 websocket 数据过程中出现连接错误等情况,将 session 放回到 `sessionProduceChan` 中,重新进行分发 14 | 15 | ## 并发控制 16 | 17 | 由于服务端对于同时连接的 websocket 连接有并发限制,所以从 `sessionProduceChan` 拿到一个 session push 到 redis 之前,会等待一个并发间隔 18 | 19 | 在创建了一个新的 websocket 连接时候,也会等待一个时间间隔 20 | 21 | ## 使用方法 22 | 23 | [参考代码](../../testcase/redis_session_manager_test.go) 24 | -------------------------------------------------------------------------------- /sessions/remote/errors.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | // ErrGotLockFailed 抢锁失败 9 | ErrGotLockFailed = errors.New("compete for init sessions failed, wait to consume session") 10 | // ErrSessionMarshalFailed 从redis中读取session后解析失败 11 | ErrSessionMarshalFailed = errors.New("session marshal failed") 12 | // ErrProduceFailed 生产session失败 13 | ErrProduceFailed = errors.New("produce session failed") 14 | // ErrorNotOk redis 写失败 15 | ErrorNotOk = errors.New("redis write not ok") 16 | ) 17 | -------------------------------------------------------------------------------- /sessions/remote/lock/lock.go: -------------------------------------------------------------------------------- 1 | // Package lock 一个基于 redis 的分布式锁实现。 2 | package lock 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "time" 8 | 9 | redis "github.com/go-redis/redis/v8" 10 | "github.com/tencent-connect/botgo/log" 11 | ) 12 | 13 | // ErrorNotOk redis 写失败 14 | var ErrorNotOk = errors.New("redis write not ok") 15 | 16 | // Lock 一个基于redis的锁实现 17 | type Lock struct { 18 | lockKey string 19 | lockValue string 20 | client *redis.Client 21 | renewTicker *time.Ticker // 用于续期的ticker,默认为超时时间的 1/3 22 | stopRenewChan chan bool // 用于停止 renew 23 | } 24 | 25 | // New 创建一个锁 26 | func New(key, value string, client *redis.Client) *Lock { 27 | return &Lock{ 28 | lockKey: key, 29 | lockValue: value, 30 | client: client, 31 | } 32 | } 33 | 34 | // Lock 加锁 35 | func (l *Lock) Lock(ctx context.Context, expire time.Duration) error { 36 | success, err := l.client.SetNX(ctx, l.lockKey, l.lockValue, expire).Result() 37 | if err != nil { 38 | return err 39 | } 40 | if !success { 41 | return ErrorNotOk 42 | } 43 | return nil 44 | } 45 | 46 | // StartRenew 开始续期任务,需要放到 goroutine 中执行 47 | func (l *Lock) StartRenew(ctx context.Context, expire time.Duration) { 48 | if expire == 0 { 49 | return 50 | } 51 | l.stopRenewChan = make(chan bool) 52 | l.renewTicker = time.NewTicker(expire / 3) 53 | defer l.renewTicker.Stop() 54 | for { 55 | select { 56 | case <-ctx.Done(): 57 | log.Infof("[lock] context done, stop renew, %+v", l) 58 | return 59 | case <-l.stopRenewChan: 60 | log.Infof("[lock] renew stop, %+v", l) 61 | return 62 | case <-l.renewTicker.C: 63 | if err := l.Renew(ctx, expire); err != nil { 64 | log.Errorf("[lock] renew lock failed, lock: %+v, err: %v", l, err) 65 | continue 66 | } 67 | log.Debugf("[lock] renew lock ok, lock: %+v", l) 68 | } 69 | } 70 | } 71 | 72 | // StopRenew 停掉续期 73 | func (l *Lock) StopRenew() { 74 | if l.stopRenewChan == nil { 75 | return 76 | } 77 | l.stopRenewChan <- true 78 | } 79 | 80 | // Renew 续期锁 81 | func (l *Lock) Renew(ctx context.Context, expire time.Duration) error { 82 | script := ` 83 | if redis.call("get", KEYS[1]) == ARGV[1] then 84 | return redis.call("expire", KEYS[1], ARGV[2]) 85 | else 86 | return 0 87 | end 88 | ` 89 | renew := redis.NewScript(script) 90 | return renew.Run(ctx, l.client, []string{l.lockKey}, l.lockValue, expire.Seconds()).Err() 91 | } 92 | 93 | // Release 释放锁 94 | func (l *Lock) Release(ctx context.Context) error { 95 | script := ` 96 | if redis.call("get", KEYS[1]) == ARGV[1] then 97 | return redis.call("del", KEYS[1]) 98 | else 99 | return 0 100 | end 101 | ` 102 | release := redis.NewScript(script) 103 | return release.Run(ctx, l.client, []string{l.lockKey}, l.lockValue).Err() 104 | } 105 | -------------------------------------------------------------------------------- /sessions/remote/lock/lock_test.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | redis "github.com/go-redis/redis/v8" 9 | ) 10 | 11 | func TestLock_Lock(t *testing.T) { 12 | conn := redis.NewClient(&redis.Options{ 13 | Addr: "localhost:6379", 14 | DialTimeout: 800 * time.Millisecond, 15 | ReadTimeout: 3 * time.Second, 16 | WriteTimeout: 3 * time.Second, 17 | }) 18 | 19 | key := "lock-key-test" 20 | value := "ddd" 21 | expire := 4 * time.Second 22 | ctx := context.Background() 23 | 24 | lock := New(key, value, conn) 25 | 26 | t.Run("lock", func(t *testing.T) { 27 | if err := lock.Lock(ctx, expire); err != nil { 28 | t.Error(err) 29 | } 30 | }) 31 | t.Run("renew", func(t *testing.T) { 32 | if err := lock.Renew(ctx, expire); err != nil { 33 | t.Error(err) 34 | } 35 | }) 36 | t.Run("release", func(t *testing.T) { 37 | if err := lock.Release(ctx); err != nil { 38 | t.Error(err) 39 | } 40 | }) 41 | 42 | t.Run("renew goroutine and check", func(t *testing.T) { 43 | if err := lock.Lock(ctx, expire); err != nil { 44 | t.Error(err) 45 | } 46 | go lock.StartRenew(ctx, expire) 47 | time.Sleep(expire + 2*time.Second) 48 | // renew 持续在跑,这里不应该再抢到锁 49 | if err := lock.Lock(ctx, expire); err == nil { 50 | t.Error("want lock err, but got nil") 51 | } 52 | lock.StopRenew() 53 | time.Sleep(expire + 2*time.Second) 54 | if err := lock.Lock(ctx, expire); err != nil { 55 | t.Error(err) 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /sessions/remote/option.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | // Option is a function that configures a Remote. 4 | type Option func(manager *RedisManager) 5 | 6 | // WithClusterKey 自定义集群key,用于创建分布式锁与redis list 7 | func WithClusterKey(key string) Option { 8 | return func(m *RedisManager) { 9 | m.clusterKey = key 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /sessions/remote/session_producer.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/tencent-connect/botgo/dto" 9 | "github.com/tencent-connect/botgo/log" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | // distributeSession 根据 shards 生产初始化的 session,这里需要抢一个分布式锁,抢到锁的服务器,负责把session都生产到 redis 中 14 | func (r *RedisManager) distributeSession( 15 | apInfo *dto.WebsocketAP, tokenSource oauth2.TokenSource, intents *dto.Intent) error { 16 | // clear,报错也不影响 17 | if err := r.client.Del(context.Background(), r.sessionQueueKey); err != nil { 18 | log.Errorf("[ws/session/redis] clear session list failed: %v", err) 19 | } 20 | for i := uint32(0); i < apInfo.Shards; i++ { 21 | session := dto.Session{ 22 | URL: apInfo.URL, 23 | TokenSource: tokenSource, 24 | Intent: *intents, 25 | LastSeq: 0, 26 | Shards: dto.ShardConfig{ 27 | ShardID: i, 28 | ShardCount: apInfo.Shards, 29 | }, 30 | } 31 | r.sessionProduceChan <- session 32 | } 33 | 34 | return nil 35 | } 36 | 37 | // sessionProducer 从 chan 取到session,push 到 redis,push 失败放回 chan 38 | func (r *RedisManager) sessionProducer(startInterval time.Duration) { 39 | for session := range r.sessionProduceChan { 40 | time.Sleep(startInterval) // 每次生产需要等待一个间隔,控制消费者连接并发 41 | if err := r.produce(session); err != nil { 42 | log.Errorf("[ws/session/redis] produce session failed: %v", err) 43 | r.sessionProduceChan <- session // 放回去重试 44 | } 45 | } 46 | } 47 | 48 | func (r *RedisManager) produce(session dto.Session) error { 49 | data, err := json.Marshal(session) 50 | log.Debugf("[ws][session/redis] produce session data is %s", string(data)) 51 | if err != nil { 52 | return ErrSessionMarshalFailed 53 | } 54 | return r.client.LPush(context.Background(), r.sessionQueueKey, data).Err() 55 | } 56 | -------------------------------------------------------------------------------- /sessions/sessions.go: -------------------------------------------------------------------------------- 1 | // Package sessions 提供了用于处理 websocket 的多实例会话的相关功能。 2 | package sessions 3 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // Package version sdk 版本声明。 2 | package version 3 | 4 | import ( 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | // version sdk 版本 10 | version = "v0.0.2" 11 | sdkName = "BotGoSDK" 12 | ) 13 | 14 | // String 输出版本号 15 | func String() string { 16 | return fmt.Sprintf("%s/%s", sdkName, version) 17 | } 18 | -------------------------------------------------------------------------------- /websocket/client/README.md: -------------------------------------------------------------------------------- 1 | ## client 2 | 3 | websocket 客户端实现 4 | 5 | 依赖: 6 | - github.com/gorilla/websocket 7 | - github.com/tidwall/gjson -------------------------------------------------------------------------------- /websocket/iface.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "github.com/tencent-connect/botgo/dto" 5 | ) 6 | 7 | // WebSocket 需要实现的接口 8 | type WebSocket interface { 9 | // New 创建一个新的ws实例,需要传递 session 对象 10 | New(session dto.Session) WebSocket 11 | // Connect 连接到 wss 地址 12 | Connect() error 13 | // Identify 鉴权连接 14 | Identify() error 15 | // Session 拉取 session 信息,包括 token,shard,seq 等 16 | Session() *dto.Session 17 | // Resume 重连 18 | Resume() error 19 | // Listening 监听websocket事件 20 | Listening() error 21 | // Write 发送数据 22 | Write(message *dto.WSPayload) error 23 | // Close 关闭连接 24 | Close() 25 | } 26 | -------------------------------------------------------------------------------- /websocket/websocket.go: -------------------------------------------------------------------------------- 1 | // Package websocket SDK 需要实现的 websocket 定义。 2 | package websocket 3 | 4 | import ( 5 | "runtime" 6 | "syscall" 7 | 8 | "github.com/tencent-connect/botgo/dto" 9 | "github.com/tencent-connect/botgo/event" 10 | "github.com/tencent-connect/botgo/log" 11 | ) 12 | 13 | var ( 14 | // ClientImpl websocket 实现 15 | ClientImpl WebSocket 16 | // ResumeSignal 用于强制 resume 连接的信号量 17 | ResumeSignal syscall.Signal 18 | ) 19 | 20 | // Register 注册 websocket 实现 21 | func Register(ws WebSocket) { 22 | ClientImpl = ws 23 | } 24 | 25 | // RegisterResumeSignal 注册用于通知 client 将连接进行 resume 的信号 26 | func RegisterResumeSignal(signal syscall.Signal) { 27 | ResumeSignal = signal 28 | } 29 | 30 | // PanicBufLen Panic 堆栈大小 31 | var PanicBufLen = 1024 32 | 33 | // PanicHandler 处理websocket场景的 panic ,打印堆栈 34 | func PanicHandler(e interface{}, session *dto.Session) { 35 | buf := make([]byte, PanicBufLen) 36 | buf = buf[:runtime.Stack(buf, false)] 37 | log.Errorf("[PANIC]%s\n%v\n%s\n", session, e, buf) 38 | } 39 | 40 | // RegisterHandlers 兼容老版本的注册方式 41 | func RegisterHandlers(handlers ...interface{}) dto.Intent { 42 | return event.RegisterHandlers(handlers...) 43 | } 44 | --------------------------------------------------------------------------------