├── .gitignore ├── LICENSE ├── README.md ├── README.zh-cn.md ├── SDK ├── appconfig │ └── appconfig.go ├── auth │ ├── authorization.go │ ├── authorization_test.go │ └── default_appticket_manager.go ├── authentication │ ├── auth_mini_program.go │ ├── authentication.go │ ├── default_session_manager.go │ ├── mini_program.go │ └── open_sso.go ├── chat │ ├── chat.go │ ├── chat_ex.go │ ├── chat_ex_test.go │ └── chat_test.go ├── common │ ├── crypto_simple.go │ ├── crypto_util.go │ ├── db_client_interface.go │ ├── db_client_redis.go │ ├── env.go │ ├── error.go │ ├── http_util.go │ ├── log_interface.go │ ├── logger.go │ └── util.go ├── event │ ├── bot_recv_msg.go │ ├── card.go │ └── event.go ├── message │ ├── card_builder.go │ ├── card_builder_test.go │ ├── image.go │ ├── image_test.go │ ├── message.go │ ├── message_test.go │ ├── richtext_builder.go │ └── richtext_builder_test.go └── protocol │ ├── authentication.go │ ├── authorization.go │ ├── bot_recv_msg.go │ ├── card.go │ ├── card_element.go │ ├── chat.go │ ├── event.go │ ├── message.go │ └── openapi.go ├── demo ├── bot_receive_and_response │ └── bot_receive_and_response.go ├── sdk_init │ └── sdk_init.go ├── send_card │ └── send_card.go ├── send_message │ └── send_message.go └── source │ ├── lark0.jpg │ ├── lark1.jpg │ └── lark2.jpg ├── docs ├── us │ ├── app_access_token.md │ ├── authentication.md │ ├── send_card.md │ ├── send_message.md │ ├── tenant_access_token.md │ ├── webhook_card.md │ └── webhook_event.md └── zh │ ├── app_access_token.md │ ├── authentication.md │ ├── send_card.md │ ├── send_message.md │ ├── tenant_access_token.md │ ├── webhook_card.md │ └── webhook_event.md ├── generatecode ├── config.go ├── demo.yml ├── generate_code.go ├── template_gin.go ├── template_handler.go ├── tpl_card.go ├── tpl_event.go ├── tpl_gin_callback.go ├── tpl_gin_main.go └── tpl_regist.go ├── go.mod └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | 3 | .idea 4 | .DS_Store 5 | *.log 6 | *.swp 7 | *.out 8 | 9 | demo1.yml 10 | botframework-go 11 | go.sum 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Bytedance Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # botframework-go 2 | **Deprecated, please use [oapi-sdk-go](https://github.com/larksuite/oapi-sdk-go)** 3 | 4 | botframework-go is a golang Software Development Kit which gives tools and interfaces to developers needed to access to Lark Open Platfom APIs. 5 | It support for developing a robot application or mini-program application based on the Lark Open Platfom. 6 | It support for generateing code using Gin framework. 7 | 8 | # List of Main Features 9 | ## Subscribe to Event Notification 10 | Interfaces 11 | - EventRegister 12 | - EventCallback 13 | 14 | List of supported notification 15 | - Request approved 16 | - Leave approved 17 | - Overtime approved 18 | - Shift change approved 19 | - Correction request approved 20 | - Business trip approved 21 | - App enabled 22 | - Contacts updates 23 | - Message 24 | - Bot removed from group chat 25 | - Bot invited to group chat 26 | - Create p2p chat 27 | - App ticket event 28 | - App status change 29 | - User in or out the group chat 30 | - Disband chat 31 | - Change group chat config 32 | - Order paid 33 | - Create a widget instance event 34 | - Delete a widget instance event 35 | - Read message 36 | - App uninstall 37 | 38 | ## Authorization 39 | Obtain tenant_access_token (ISV apps or internal apps) 40 | - GetTenantAccessToken 41 | 42 | Obtain app_access_token (ISV apps or internal apps) 43 | - GetAppAccessToken 44 | 45 | Re-pushing app_ticket 46 | - ReSendAppTicket 47 | 48 | ## Authentication 49 | - Mini Program Authentication 50 | - Open SSO Authentication 51 | 52 | ## Bot Send Message 53 | Interfaces 54 | - SendTextMessage 55 | - SendImageMessage 56 | - SendRichTextMessage 57 | - SendShareChatMessage 58 | - SendCardMessage 59 | - SendTextMessageBatch 60 | - SendImageMessageBatch 61 | - SendRichTextMessageBatch 62 | - SendShareChatMessageBatch 63 | - SendCardMessageBatch 64 | 65 | ## Group 66 | Interfaces 67 | - GetChatInfo 68 | - GetChatList 69 | - CheckUserInGroup 70 | - CheckBotInGroup 71 | - CheckUserBotInSameGroup 72 | - UpdateChatInfo 73 | - CreateChat 74 | - AddUserToChat 75 | - DeleteUserFromChat 76 | - DisbandChat 77 | 78 | ## Message Builder 79 | - Richtext Builder 80 | - Card Builder 81 | 82 | # Directory Description 83 | - SDK: Lark open platform APIs 84 | - appconfig: Appinfo config 85 | - auth: Authorization 86 | - authentication: Authentication 87 | - chat: Group 88 | - common: Common functions/definition 89 | - event: Event notification/card action callback/bot command callback 90 | - message: Bot send message 91 | - protocol: Lark open platform protocol 92 | - generatecode: Generate code using Gin framework 93 | 94 | # SDK Instruction 95 | ## Log Initialization 96 | Log Initialization function `func InitLogger(log LogInterface, option interface{})`. demo: `common.InitLogger(common.NewCommonLogger(), common.DefaultOption())` 97 | 98 | Developers can use the custom log library by implementing the LogInterface interface 99 | 100 | ## SDK Initialization 101 | You must init app config before using SDK. You can follow these steps to do it. 102 | 1. Get necessary information, such as AppID, AppSecret, VerifyToken and EncryptKey. But you'd better not use clear text in code. Getting them from database or environment variable is a better way. 103 | 2. Init app config. 104 | 3. Get appTickey if your app is Independent Software Vendor App, otherwise you can ignore this. 105 | 4. Register events what you want. 106 | 107 | ### Example code 108 | 1. get config from redis. 109 | [demo code](./demo/sdk_init/sdk_init.go) 110 | 2. get config from environment variable. 111 | ```go 112 | conf := &appconfig.AppConfig{ 113 | AppID: os.Getenv("AppID"), //get it from lark-voucher and basic information。 114 | AppType: protocol.InternalApp, //AppType only has two types: Independent Software Vendor App(ISVApp) or Internal App. 115 | AppSecret: os.Getenv("AppSecret"), //get it from lark-voucher and basic information. 116 | VerifyToken: os.Getenv("VerifyToken"), //get it from lark-event subscriptions. 117 | EncryptKey: os.Getenv("EncryptKey"), //get it from lark-event subscriptions. 118 | } 119 | ``` 120 | 121 | ## DB-client Initialization 122 | `DBClient` SDK DB Client interface. 123 | It is used for: read/write app ticket ; read/write user session data for authentication operations; read sensitive information such as app secret. 124 | 125 | `DefaultRedisClient` Default Redis Client. Need to be initialized before use. 126 | demo: 127 | ```golang 128 | redisClient := &common.DefaultRedisClient{} 129 | err := redisClient.InitDB(map[string]string{"addr": "127.0.0.1:6379"}) 130 | if err != nil { 131 | return fmt.Errorf("init redis-client error[%v]", err) 132 | } 133 | ``` 134 | 135 | Developers can use the custom db client library by implementing the DBClient interface 136 | 137 | # Generate code using Gin framework 138 | ## Config 139 | ```yml 140 | ServiceInfo: 141 | Path: github.com/larksuite/demo # relative path in GOPATH or go module name 142 | GenCodePath: # Code generation absolute path. If it is empty, the configuration item named "Path" will be used. 143 | EventWebhook: /webhook/event 144 | CardWebhook: /webhook/card 145 | AppID: cli_12345 # your app id 146 | Description: test_demo # your app description 147 | IsISVApp: false # ISV App flag, false is default 148 | EventList: 149 | - EventName: Message # required 150 | # - EventName: AppTicket # use as needed, ISVApp must 151 | # - EventName: Approval # use as needed 152 | # - EventName: LeaveApproval # use as needed 153 | # - EventName: WorkApproval # use as needed 154 | # - EventName: ShiftApproval # use as needed 155 | # - EventName: RemedyApproval # use as needed 156 | # - EventName: TripApproval # use as needed 157 | # - EventName: AppOpen # use as needed 158 | # - EventName: ContactUser # use as needed 159 | # - EventName: ContactDept # use as needed 160 | # - EventName: ContactScope # use as needed 161 | # - EventName: RemoveBot # use as needed 162 | # - EventName: AddBot # use as needed 163 | # - EventName: P2PChatCreate # use as needed 164 | # - EventName: AppStatusChange # use as needed 165 | # - EventName: UserToChat # use as needed 166 | # - EventName: ChatDisband # use as needed 167 | # - EventName: GroupSettingUpdate # use as needed 168 | # - EventName: OrderPaid # use as needed 169 | # - EventName: CreateWidgetInstance # use as needed 170 | # - EventName: DeleteWidgetInstance # use as needed 171 | # - EventName: MessageRead # use as needed 172 | CommandList: 173 | - Cmd: default # required 174 | Description: Text that is empty or isnot matched 175 | # - Cmd: help 176 | # Description: Text that begin with the word help 177 | # - Cmd: show 178 | # Description: Text that begin with the word show 179 | CardActionList: 180 | - MethodName: create 181 | - MethodName: delete 182 | - MethodName: update 183 | ``` 184 | 185 | ## Command 186 | ```shell 187 | # cd projectPath 188 | go build 189 | ./botframework-go -f ./generatecode/demo.yml 190 | ``` 191 | 192 | ## Generate Code Rule 193 | - If the code is first generated, all code files are generated by the configuration file. 194 | - If you modify the configuration file later, and regenerate the code on the original path, only the `./handler/regist.go` file will be forced updated, other files are not updated to avoid overwriting user-defined code. 195 | - Because the `./handler/regist.go` file will be forced update, you should not write your business code in the file. 196 | 197 | # demo 198 | - [Manage AppAccessToken](./docs/us/app_access_token.md) 199 | - [Manage TenantAccessToken](./docs/us/tenant_access_token.md) 200 | - [Webhook_Event](./docs/us/webhook_event.md) 201 | - [Webhook_Card](./docs/us/webhook_card.md) 202 | - [Send Message](./docs/us/send_message.md) 203 | - [Build Card](./docs/us/send_card.md) 204 | - [Authentication](./docs/us/authentication.md) 205 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | # botframework-go 2 | **已过时,请使用 [oapi-sdk-go](https://github.com/larksuite/oapi-sdk-go)** 3 | 4 | 飞书开放平台应用开发接口 golang 版本 SDK,支持开发者快速搭建和开发飞书应用。 5 | 支持开发基于飞书开放平台的机器人应用、小程序应用。 6 | 支持自动生成 gin 框架代码。 7 | 8 | # 支持接口列表 9 | ## 订阅事件通知 10 | 接口: 11 | - EventRegister 12 | - EventCallback 13 | 14 | 支持事件列表: 15 | - 审批通过 16 | - 请假审批 17 | - 加班审批 18 | - 换班审批 19 | - 补卡审批 20 | - 出差审批 21 | - 开通应用 22 | - 通讯录变更 23 | - 接收消息 24 | - 机器人被移出群聊 25 | - 机器人被邀请进入群聊 26 | - 用户和机器人的会话首次被创建 27 | - app_ticket事件 28 | - 应用启动、停用事件 29 | - 用户进群出群事件通知 30 | - 解散群通知 31 | - 群配置修改事件 32 | - 应用商店应用购买 33 | - 创建小组件实例事件 34 | - 删除小组件实例事件 35 | - 消息已读事件通知 36 | - 应用程序卸载事件 37 | 38 | ## 授权 39 | 获取 tenant_access_token (支持 ISV apps or internal apps) 40 | - GetTenantAccessToken 41 | 42 | 获取 app_access_token (支持 ISV apps or internal apps) 43 | - GetAppAccessToken 44 | 45 | 触发推送 app_ticket 46 | - ReSendAppTicket 47 | 48 | ## 身份验证 49 | - 小程序身份验证 50 | - Open SSO 身份验证 51 | 52 | ## 机器人发送消息 53 | 接口 54 | - SendTextMessage 55 | - SendImageMessage 56 | - SendRichTextMessage 57 | - SendShareChatMessage 58 | - SendCardMessage 59 | - SendTextMessageBatch 60 | - SendImageMessageBatch 61 | - SendRichTextMessageBatch 62 | - SendShareChatMessageBatch 63 | - SendCardMessageBatch 64 | 65 | ## 群信息和群管理 66 | 接口 67 | - GetChatInfo 68 | - GetChatList 69 | - CheckUserInGroup 70 | - CheckBotInGroup 71 | - CheckUserBotInSameGroup 72 | - UpdateChatInfo 73 | - CreateChat 74 | - AddUserToChat 75 | - DeleteUserFromChat 76 | - DisbandChat 77 | 78 | ## 消息构造 79 | - 富文本构造 80 | - 卡片构造 81 | 82 | # 目录说明 83 | - SDK: 封装开放平台相关通用操作 84 | - appconfig: 应用相关配置信息 85 | - auth: 封装开放平台授权相关接口 86 | - authentication: 封装身份认证相关接口 87 | - chat: 封装开放平台机器人群信息和群管理相关接口 88 | - common: SDK公共操作集合 89 | - event: 封装事件订阅、卡片action回调、机器人接收消息回调的接口 90 | - message: 封装机器人发送消息的接口,支持发送文本、图片、富文本、群名片、卡片消息,支持批量发送消息,提供简单的构造富文本、卡片消息的接口。 91 | - protocol: 开放平台相关协议、SDK自定义协议 92 | - generatecode: 框架代码生成工具,当前只支持生成gin框架的代码 93 | 94 | # SDK 使用说明 95 | ## 日志初始化 96 | SDK日志初始化函数 `func InitLogger(log LogInterface, option interface{})` 97 | 98 | SDK提供默认的日志实现和默认的日志参数,通过如下调用使用默认日志实现: `common.InitLogger(common.NewCommonLogger(), common.DefaultOption())` 99 | 100 | 开发者可以通过实现`LogInterface`接口,来使用自定义日志库。 101 | 102 | ## SDK初始化 103 | SDK使用前需要执行初始化操作,具体操作步骤: 104 | 1. 获取应用相关配置信息(AppID、AppSecret、VerifyToken、EncryptKey 等),为了数据安全,不建议开发者在代码中明文写入这些信息,您可以选择从数据库读取、远程配置系统、环境变量中获取; 105 | 2. 调用`appconfig.Init(conf)`函数,初始化应用配置; 106 | 3. 如果是 Independent Software Vendor App (ISVApp), 需要实现读取和保存 AppTicket 的接口。框架提供使用redis读取和保存 AppTicket 的接口实现,你也可以实现`TicketManager`接口,来使自定义 AppTicket 的读写方式; 107 | 4. 根据业务逻辑注册事件回调处理函数; 108 | 109 | ### 示例代码 110 | 1. 从redis读取配置信息: 111 | [示例代码](./demo/sdk_init/sdk_init.go) 112 | 2. 从环境变量读取配置信息: 113 | ```golang 114 | conf := &appconfig.AppConfig{ 115 | AppID: os.Getenv("AppID"),//从飞书开放平台-凭证与基础信息中获取 116 | AppType: protocol.InternalApp, //apptype只有两种,Independent Software Vendor App 和 Internal App 117 | AppSecret: os.Getenv("AppSecret"),//从飞书开放平台的凭证与基础信息中获取 118 | VerifyToken: os.Getenv("VerifyToken"),//从飞书开放平台的事件订阅中获取,开放平台事件订阅相关,如果没有使用事件订阅,可以填空字符串 119 | EncryptKey: os.Getenv("EncryptKey"),//从飞书开放平台的事件订阅中获取,开放平台事件订阅相关,如果没有使用事件订阅,可以填空字符串 120 | } 121 | ``` 122 | 123 | ## 存储初始化 124 | SDK存储读写接口`type DBClient interface`。在SDK中,其只要用于: ISV 应用的 app ticket 读写操作;身份认证操作的session数据读写操作;敏感信息如 app secret的读取。 125 | 126 | 接口提供默认的redis实现`DefaultRedisClient`,使用前需要做初始化操作,初始化示例代码 127 | ```golang 128 | redisClient := &common.DefaultRedisClient{} 129 | err := redisClient.InitDB(map[string]string{"addr": "127.0.0.1:6379"}) 130 | if err != nil { 131 | return fmt.Errorf("init redis-client error[%v]", err) 132 | } 133 | ``` 134 | 135 | 开发者可以通过实现 DBClient 接口,来使用自定义存储。 136 | 137 | # 生成 Gin 框架代码 138 | ## 配置文件示例 139 | ```yml 140 | ServiceInfo: 141 | Path: github.com/larksuite/demo # GOPATH相对路径,或者使用go module方式时的module name 142 | GenCodePath: # 生成代码的绝对路径;若为空,代码会生成到配置项Path对应的GOPATH路径下 143 | EventWebhook: /webhook/event 144 | CardWebhook: /webhook/card 145 | AppID: cli_12345 # 应用ID 146 | Description: test_demo # 应用描述信息 147 | IsISVApp: false # ISV 应用标志,默认为非ISV应用 148 | EventList: 149 | - EventName: Message # 必须 150 | # - EventName: AppTicket # 按需使用,ISV应用 必须订阅 151 | # - EventName: Approval # 按需使用 152 | # - EventName: LeaveApproval # 按需使用 153 | # - EventName: WorkApproval # 按需使用 154 | # - EventName: ShiftApproval # 按需使用 155 | # - EventName: RemedyApproval # 按需使用 156 | # - EventName: TripApproval # 按需使用 157 | # - EventName: AppOpen # 按需使用 158 | # - EventName: ContactUser # 按需使用 159 | # - EventName: ContactDept # 按需使用 160 | # - EventName: ContactScope # 按需使用 161 | # - EventName: RemoveBot # 按需使用 162 | # - EventName: AddBot # 按需使用 163 | # - EventName: P2PChatCreate # 按需使用 164 | # - EventName: AppStatusChange # 按需使用 165 | # - EventName: UserToChat # 按需使用 166 | # - EventName: ChatDisband # 按需使用 167 | # - EventName: GroupSettingUpdate # 按需使用 168 | # - EventName: OrderPaid # 按需使用 169 | # - EventName: CreateWidgetInstance # 按需使用 170 | # - EventName: DeleteWidgetInstance # 按需使用 171 | # - EventName: MessageRead # 按需使用 172 | CommandList: 173 | - Cmd: Default # 必须 174 | Description: 表示默认命令,群聊只@机器人而不输入任何其他内容,或收到未定义的命令时 175 | # - Cmd: Help 176 | # Description: 向机器人发送消息,前缀带有help 177 | # - Cmd: Show 178 | # Description: 向机器人发送消息,前缀带有show 179 | CardActionList: 180 | - MethodName: create 181 | - MethodName: delete 182 | - MethodName: update 183 | ``` 184 | 185 | ## 生成代码命令 186 | ```shell 187 | # cd projectPath 188 | go build 189 | ./botframework-go -f ./generatecode/demo.yml 190 | ``` 191 | 192 | ## 生成代码规则说明 193 | - 首次生成代码时,会依据配置文件生成全部代码文件; 194 | - 之后若修改配置文件(修改代码路径之外的其他选项),在原始的路径上重新生成代码时,只会强制更新`./handler/regist.go`文件,其他文件不会更新,以避免覆盖用户自定义代码。 195 | - `./handler/regist.go`文件,会被强制更新,用户不应该在该文件中加入自定义代码。 196 | 197 | # 示例说明 198 | - [管理 AppAccessToken](./docs/zh/app_access_token.md) 199 | - [管理 TenantAccessToken](./docs/zh/tenant_access_token.md) 200 | - [订阅事件回调](./docs/zh/webhook_event.md) 201 | - [卡片交互事件回调](./docs/zh/webhook_card.md) 202 | - [发送消息](./docs/zh/send_message.md) 203 | - [构造卡片消息](./docs/zh/send_card.md) 204 | - [身份认证/小程序登录](./docs/zh/authentication.md) 205 | -------------------------------------------------------------------------------- /SDK/appconfig/appconfig.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package appconfig 6 | 7 | import ( 8 | "fmt" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | var ( 14 | appConfMap = make(map[string]AppConfig) 15 | 16 | appTokenMap = make(map[string]*AppTokenManager) 17 | ) 18 | 19 | const ( 20 | ExpireInterval = 300 // 300 seconds 21 | ) 22 | 23 | type AppConfig struct { 24 | AppID string `json:"app_id"` 25 | AppSecret string `json:"app_secret"` 26 | VerifyToken string `json:"verify_token"` 27 | EncryptKey string `json:"encrypt_key"` 28 | AppType string `json:"app_type"` 29 | } 30 | 31 | type AppTokenManager struct { 32 | AppAccessToken *AppAccessTokenCache 33 | TenantAccessToken map[string]*TenantAccessTokenCache //tenantKey->tenantAccessToken 34 | rwMuApp sync.RWMutex 35 | rwMuTenant sync.RWMutex 36 | } 37 | 38 | type AppAccessTokenCache struct { 39 | Token string 40 | Expire int64 41 | } 42 | 43 | type TenantAccessTokenCache struct { 44 | Token string 45 | Expire int64 46 | } 47 | 48 | func (a *AppTokenManager) GetAppAccessToken() (string, error) { 49 | a.rwMuApp.RLock() 50 | defer a.rwMuApp.RUnlock() 51 | 52 | if a.AppAccessToken != nil && a.AppAccessToken.Token != "" && a.AppAccessToken.Expire > time.Now().Unix() { 53 | return a.AppAccessToken.Token, nil 54 | } 55 | 56 | return "", fmt.Errorf("cannot find app access token") 57 | } 58 | 59 | func (a *AppTokenManager) SetAppAccessToken(appAccessToken string, expireSecond int) error { 60 | a.rwMuApp.Lock() 61 | defer a.rwMuApp.Unlock() 62 | 63 | if a.AppAccessToken == nil { 64 | a.AppAccessToken = new(AppAccessTokenCache) 65 | } 66 | a.AppAccessToken.Token = appAccessToken 67 | a.AppAccessToken.Expire = time.Now().Unix() + int64(expireSecond-ExpireInterval) 68 | 69 | return nil 70 | } 71 | 72 | func (a *AppTokenManager) DisableAppAccessToken() error { 73 | a.rwMuApp.Lock() 74 | defer a.rwMuApp.Unlock() 75 | 76 | if a.AppAccessToken != nil { 77 | a.AppAccessToken.Expire = 0 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (a *AppTokenManager) GetTenantAccessToken(tenantKey string) (string, error) { 84 | a.rwMuTenant.RLock() 85 | defer a.rwMuTenant.RUnlock() 86 | 87 | tcToken, ok := a.TenantAccessToken[tenantKey] 88 | if ok && tcToken != nil && tcToken.Token != "" && tcToken.Expire > time.Now().Unix() { 89 | return tcToken.Token, nil 90 | } 91 | 92 | return "", fmt.Errorf("cannot find tenant access token") 93 | } 94 | 95 | func (a *AppTokenManager) SetTenantAccessToken(tenantKey string, tenantAccessToken string, expireSecond int) error { 96 | a.rwMuTenant.Lock() 97 | defer a.rwMuTenant.Unlock() 98 | 99 | if a.TenantAccessToken == nil { 100 | a.TenantAccessToken = make(map[string]*TenantAccessTokenCache) 101 | } 102 | if a.TenantAccessToken[tenantKey] == nil { 103 | a.TenantAccessToken[tenantKey] = new(TenantAccessTokenCache) 104 | } 105 | 106 | a.TenantAccessToken[tenantKey].Token = tenantAccessToken 107 | a.TenantAccessToken[tenantKey].Expire = time.Now().Unix() + int64(expireSecond-ExpireInterval) 108 | 109 | return nil 110 | } 111 | 112 | func (a *AppTokenManager) DisableTenantAccessToken(tenantKey string) error { 113 | a.rwMuTenant.Lock() 114 | defer a.rwMuTenant.Unlock() 115 | 116 | if a.TenantAccessToken != nil && a.TenantAccessToken[tenantKey] != nil { 117 | a.TenantAccessToken[tenantKey].Expire = 0 118 | } 119 | 120 | return nil 121 | } 122 | 123 | func Init(appConfs ...AppConfig) { 124 | for _, v := range appConfs { 125 | appConfMap[v.AppID] = v 126 | 127 | appTokenMap[v.AppID] = &AppTokenManager{ 128 | AppAccessToken: &AppAccessTokenCache{}, 129 | TenantAccessToken: make(map[string]*TenantAccessTokenCache), 130 | } 131 | } 132 | } 133 | 134 | func GetConfig(appID string) (AppConfig, error) { 135 | appConf, ok := appConfMap[appID] 136 | if !ok { 137 | return AppConfig{}, fmt.Errorf("getAppConfig: cannot find appConfig, appid[%s]confSize[%d]", appID, len(appConfMap)) 138 | } 139 | 140 | return appConf, nil 141 | } 142 | 143 | func GetTokenManager(appID string) (*AppTokenManager, error) { 144 | tokenManager, ok := appTokenMap[appID] 145 | if !ok { 146 | return nil, fmt.Errorf("getAppToken: cannot find tokenManager, appid[%s]tokenSize[%d]", appID, len(appTokenMap)) 147 | } 148 | 149 | return tokenManager, nil 150 | } 151 | -------------------------------------------------------------------------------- /SDK/auth/default_appticket_manager.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package auth 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/larksuite/botframework-go/SDK/common" 11 | ) 12 | 13 | // DefaultAppTicketManager 14 | type DefaultAppTicketManager struct { 15 | Client common.DBClient 16 | } 17 | 18 | // NewDefaultAppTicketManager demo: 19 | // client := &common.DefaultRedisClient{} 20 | // err := client.InitDB(map[string]string{"addr": "127.0.0.1:6379"}) 21 | // if err != nil { 22 | // return fmt.Errorf("init redis error[%v]", err) 23 | // } 24 | // manager := auth.NewDefaultAppTicketManager(client) 25 | func NewDefaultAppTicketManager(client common.DBClient) *DefaultAppTicketManager { 26 | r := &DefaultAppTicketManager{ 27 | Client: client, 28 | } 29 | return r 30 | } 31 | 32 | func (a *DefaultAppTicketManager) SetAppTicket(ctx context.Context, appID, appTicket string) error { 33 | return a.Client.Set("appticket:"+appID, appTicket, 0) 34 | } 35 | 36 | func (a *DefaultAppTicketManager) GetAppTicket(ctx context.Context, appID string) (string, error) { 37 | return a.Client.Get("appticket:" + appID) 38 | } 39 | -------------------------------------------------------------------------------- /SDK/authentication/auth_mini_program.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package authentication 6 | 7 | import ( 8 | "context" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/larksuite/botframework-go/SDK/auth" 13 | "github.com/larksuite/botframework-go/SDK/common" 14 | "github.com/larksuite/botframework-go/SDK/protocol" 15 | ) 16 | 17 | type AuthMiniProgram struct { 18 | Manager SessionManager 19 | ValidPeriod time.Duration 20 | AuthCookieLevel CookieDomainLevel 21 | } 22 | 23 | // NewAuthMiniProgram demo: 24 | // client := &common.DefaultRedisClient{} 25 | // err := client.InitDB(map[string]string{"addr": "127.0.0.1:6379"}) 26 | // if err != nil { 27 | // return fmt.Errorf("init redis error[%v]", err) 28 | // } 29 | // manager := authentication.NewDefaultSessionManager("DojK2hs*790(", client) 30 | // minaAuth := authentication.NewAuthMiniProgram(manager, time.Hour*24*7) 31 | func NewAuthMiniProgram(manager SessionManager, validPeriod time.Duration) *AuthMiniProgram { 32 | mina := &AuthMiniProgram{} 33 | mina.Init(manager, validPeriod) 34 | 35 | return mina 36 | } 37 | 38 | func (a *AuthMiniProgram) Init(manager SessionManager, validPeriod time.Duration) { 39 | a.Manager = manager 40 | a.ValidPeriod = validPeriod 41 | 42 | a.SetCookieDomainLevel(DomainLevelZero) // DomainLevelZero is default auth cookie level 43 | } 44 | 45 | func (a *AuthMiniProgram) Login(ctx context.Context, code string, appID string, requestHost string) (map[string]*http.Cookie, error) { 46 | // check params 47 | if code == "" || appID == "" { 48 | return nil, common.ErrValidateParams.ErrorWithExtStr("miniProgram login input param is empty") 49 | } 50 | 51 | // get app_access_token 52 | appAccessToken, err := auth.GetAppAccessToken(ctx, appID) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | rsp, err := MiniProgramValidateByAppToken(code, appAccessToken) 58 | if err != nil { 59 | if rsp.Code == protocol.ErrMinaAppAccessTokenInvalid { 60 | auth.DisableAppToken(ctx, appID) 61 | } 62 | return nil, err 63 | } 64 | 65 | sessionName := a.Manager.GenerateSessionKeyName(appID) 66 | 67 | authUser := TransMPAuthUser(rsp) 68 | 69 | sessionKey, err := a.Manager.SetAuthUserInfo(authUser, a.GetValidPeriod()) 70 | if err != nil { 71 | return nil, common.ErrMinaSetAuth.ErrorWithExtErr(err) 72 | } 73 | 74 | // cookie 75 | mapCookie := make(map[string]*http.Cookie) 76 | mapCookie[sessionName] = &http.Cookie{ 77 | Name: sessionName, 78 | Value: sessionKey, 79 | Expires: time.Now().Add(a.GetValidPeriod()), 80 | MaxAge: int(a.GetValidPeriod().Seconds()), 81 | Path: "/", 82 | Domain: GetAuthCookieDomain(requestHost, a.GetCookieDomainLevel()), 83 | } 84 | 85 | return mapCookie, nil 86 | } 87 | 88 | func (a *AuthMiniProgram) Auth(ctx context.Context, sessionKey string) error { 89 | // check params 90 | if sessionKey == "" { 91 | return common.ErrAuthParams.ErrorWithExtStr("miniProgram Auth input param is empty") 92 | } 93 | 94 | authUser, err := a.Manager.GetAuthUserInfo(sessionKey) 95 | if err != nil { 96 | return common.ErrMinaGetAuth.ErrorWithExtErr(err) 97 | } 98 | 99 | if len(authUser.User.OpenID) == 0 { 100 | return common.ErrMinaSessionInvalid.Error() 101 | } 102 | 103 | return nil 104 | } 105 | 106 | func (a *AuthMiniProgram) Logout(ctx context.Context, appID string, requestHost string) (map[string]*http.Cookie, error) { 107 | // check params 108 | if appID == "" { 109 | return nil, common.ErrLogoutParams.ErrorWithExtStr("miniProgram Auth input param is empty") 110 | } 111 | 112 | sessionName := a.Manager.GenerateSessionKeyName(appID) 113 | 114 | mapCookie := make(map[string]*http.Cookie) 115 | mapCookie[sessionName] = &http.Cookie{ 116 | Name: sessionName, 117 | Value: "", 118 | Expires: time.Now().AddDate(0, 0, -1), 119 | MaxAge: -1, 120 | Path: "/", 121 | Domain: GetAuthCookieDomain(requestHost, a.GetCookieDomainLevel()), 122 | } 123 | 124 | return mapCookie, nil 125 | 126 | } 127 | 128 | func (a *AuthMiniProgram) SetCookieDomainLevel(cookieLevel CookieDomainLevel) { 129 | a.AuthCookieLevel = cookieLevel 130 | } 131 | 132 | func (a *AuthMiniProgram) GetCookieDomainLevel() CookieDomainLevel { 133 | return a.AuthCookieLevel 134 | } 135 | 136 | func (a *AuthMiniProgram) GetValidPeriod() time.Duration { 137 | return a.ValidPeriod 138 | } 139 | 140 | func (a *AuthMiniProgram) GetSessionManager() SessionManager { 141 | return a.Manager 142 | } 143 | 144 | func TransMPAuthUser(rsp *protocol.MiniProgramLoginByAppTokenResponse) *AuthUserInfo { 145 | authUser := &AuthUserInfo{} 146 | 147 | authUser.Token.AccessToken = rsp.Data.AccessToken 148 | authUser.Token.TokenType = rsp.Data.TokenType 149 | authUser.Token.ExpiresIn = rsp.Data.ExpiresIn 150 | authUser.Token.RefreshToken = rsp.Data.RefreshToken 151 | 152 | authUser.User.TenantKey = rsp.Data.TenantKey 153 | authUser.User.OpenID = rsp.Data.OpenID 154 | 155 | authUser.Extra = map[string]string{ 156 | "union_id": rsp.Data.UnionID, 157 | "session_key": rsp.Data.SessionKey, 158 | } 159 | 160 | return authUser 161 | } 162 | -------------------------------------------------------------------------------- /SDK/authentication/authentication.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package authentication 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "net/http" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type Authentication interface { 16 | Init(manager SessionManager, validPeriod time.Duration) 17 | 18 | Login(ctx context.Context, code string, appID string, requestHost string) (map[string]*http.Cookie, error) 19 | Auth(ctx context.Context, sessionKey string) error 20 | Logout(ctx context.Context, appID string, requestHost string) (map[string]*http.Cookie, error) 21 | 22 | // auth cookie level, DomainLevelZero is default 23 | SetCookieDomainLevel(cookieLevel CookieDomainLevel) 24 | GetCookieDomainLevel() CookieDomainLevel 25 | 26 | // get valid period 27 | GetValidPeriod() time.Duration 28 | 29 | // get session manager 30 | GetSessionManager() SessionManager 31 | } 32 | 33 | type CookieDomainLevel int 34 | 35 | const ( 36 | DomainLevelZero CookieDomainLevel = 0 //current host 37 | DomainLevelOne CookieDomainLevel = 1 //First-level domain name 38 | DomainLevelTwo CookieDomainLevel = 2 //Second-level domain name 39 | ) 40 | 41 | func GetAuthCookieDomain(requestHost string, cookieLevel CookieDomainLevel) string { 42 | if requestHost == "" { 43 | return "" 44 | } 45 | 46 | names := strings.Split(requestHost, ".") 47 | namesLen := len(names) 48 | 49 | switch cookieLevel { 50 | case DomainLevelOne: 51 | if namesLen <= 2 { 52 | return requestHost 53 | } else { 54 | return fmt.Sprintf("%s.%s", names[namesLen-2], names[namesLen-1]) 55 | } 56 | case DomainLevelTwo: 57 | if namesLen <= 3 { 58 | return requestHost 59 | } else { 60 | return fmt.Sprintf("%s.%s.%s", names[namesLen-3], names[namesLen-2], names[namesLen-1]) 61 | } 62 | default: 63 | return "" 64 | } 65 | } 66 | 67 | type SessionManager interface { 68 | SetEncryptKey(encryptKey string) 69 | GetEncryptKey() string 70 | 71 | GenerateSessionKeyName(appID string) string 72 | GenerateSessionKey() string 73 | 74 | SetAuthUserInfo(authUser *AuthUserInfo, validPeriod time.Duration) (string, error) // return sessionKey, err 75 | GetAuthUserInfo(sessionKey string) (*AuthUserInfo, error) 76 | } 77 | 78 | type AuthUserInfo struct { 79 | Token TokenInfo 80 | User UserInfo 81 | Extra map[string]string 82 | } 83 | 84 | type TokenInfo struct { 85 | AccessToken string `json:"access_token"` // user_access_token 86 | TokenType string `json:"token_type"` 87 | ExpiresIn int `json:"expires_in"` 88 | RefreshToken string `json:"refresh_token"` 89 | } 90 | 91 | type UserInfo struct { 92 | TenantKey string `json:"tenant_key"` 93 | OpenID string `json:"open_id"` 94 | EmployeeID string `json:"employee_id"` 95 | } 96 | -------------------------------------------------------------------------------- /SDK/authentication/default_session_manager.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package authentication 6 | 7 | import ( 8 | "fmt" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/larksuite/botframework-go/SDK/common" 14 | uuid "github.com/satori/go.uuid" 15 | ) 16 | 17 | type defaultSessionManager struct { 18 | EncryptKey string 19 | Client common.DBClient 20 | } 21 | 22 | // NewDefaultSessionManager demo: 23 | // client := &common.DefaultRedisClient{} 24 | // err := client.InitDB(map[string]string{"addr": "127.0.0.1:6379"}) 25 | // if err != nil { 26 | // return fmt.Errorf("init redis error[%v]", err) 27 | // } 28 | // manager := authentication.NewDefaultSessionManager("DojK2hs*790(", client) 29 | func NewDefaultSessionManager(encryptKey string, client common.DBClient) *defaultSessionManager { 30 | manager := &defaultSessionManager{ 31 | EncryptKey: encryptKey, 32 | Client: client, 33 | } 34 | 35 | return manager 36 | } 37 | 38 | func (d *defaultSessionManager) SetEncryptKey(encryptKey string) { 39 | d.EncryptKey = encryptKey 40 | } 41 | 42 | func (d *defaultSessionManager) GetEncryptKey() string { 43 | return d.EncryptKey 44 | } 45 | 46 | func (d *defaultSessionManager) GenerateSessionKeyName(appID string) string { 47 | prefix := "cli_" 48 | if strings.HasPrefix(appID, prefix) { 49 | appID = appID[len(prefix):] 50 | } 51 | 52 | return fmt.Sprintf("bframewk-session-%s", appID) 53 | } 54 | 55 | func (d *defaultSessionManager) GenerateSessionKey() string { 56 | newUUID := uuid.NewV4() 57 | return fmt.Sprintf("bframewk-w04j5mfw-%s-%s", newUUID.String(), strconv.FormatInt(time.Now().Unix(), 36)) 58 | } 59 | 60 | func (d *defaultSessionManager) SetAuthUserInfo(authUser *AuthUserInfo, validPeriod time.Duration) (string, error) { 61 | sessionKey := d.GenerateSessionKey() 62 | 63 | sessionValue, err := common.EncryptAes(authUser, sessionKey+d.GetEncryptKey()) 64 | if err != nil { 65 | return "", fmt.Errorf("auth encrypt err[%v]", err) 66 | } 67 | 68 | err = d.Client.Set(sessionKey, sessionValue, validPeriod) 69 | if err != nil { 70 | return "", fmt.Errorf("set auth err[%v]", err) 71 | } 72 | 73 | return sessionKey, nil 74 | } 75 | 76 | func (d *defaultSessionManager) GetAuthUserInfo(sessionKey string) (*AuthUserInfo, error) { 77 | sessionValue, err := d.Client.Get(sessionKey) 78 | if err != nil { 79 | return nil, fmt.Errorf("get auth err[%v]", err) 80 | } 81 | 82 | authInfo := &AuthUserInfo{} 83 | 84 | err = common.DecryptAes(sessionValue, sessionKey+d.GetEncryptKey(), authInfo) 85 | if err != nil { 86 | return nil, fmt.Errorf("auth decrypt err[%v]", err) 87 | } 88 | 89 | return authInfo, nil 90 | } 91 | -------------------------------------------------------------------------------- /SDK/authentication/mini_program.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package authentication 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | 11 | "github.com/larksuite/botframework-go/SDK/common" 12 | "github.com/larksuite/botframework-go/SDK/protocol" 13 | ) 14 | 15 | // Code Exchange Token 16 | func MiniProgramValidateByAppToken(code string, appAccessToken string) (*protocol.MiniProgramLoginByAppTokenResponse, error) { 17 | // check params 18 | if code == "" || appAccessToken == "" { 19 | return nil, common.ErrValidateParams.ErrorWithExtStr("code/appAccessToken is empty") 20 | } 21 | 22 | request := &protocol.MiniProgramLoginByAppTokenRequest{ 23 | Code: code, 24 | } 25 | 26 | rspBytes, statusCode, err := common.DoHttpPostOApi(protocol.MPValidateByAppTokenPath, common.NewHeaderToken(appAccessToken), request) 27 | if err != nil { 28 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 29 | } 30 | 31 | rspData := &protocol.MiniProgramLoginByAppTokenResponse{} 32 | err = json.Unmarshal(rspBytes, rspData) 33 | if err != nil { 34 | return nil, common.ErrJsonUnmarshal.ErrorWithExtErr( 35 | fmt.Errorf("jsonUnmarshalError[%v] httpStatusCode[%d] httpBody[%s]", err, statusCode, string(rspBytes))) 36 | } 37 | 38 | if rspData.Code != 0 { 39 | return rspData, common.ErrOpenApiReturnError.ErrorWithExtStr(fmt.Sprintf("[code:%d msg:%s]", rspData.Code, rspData.Msg)) 40 | } 41 | 42 | return rspData, nil 43 | } 44 | 45 | // Code Exchange Token 46 | func MiniProgramValidateByIDSecret(code string, appID string, appSecret string) (*protocol.MiniProgramLoginByIDSecretResponse, error) { 47 | // check params 48 | if code == "" || appID == "" || appSecret == "" { 49 | return nil, common.ErrValidateParams.ErrorWithExtStr("code/appID/appSecret is empty") 50 | } 51 | 52 | rspBytes, statusCode, err := common.DoHttpGetOApi(protocol.MPValidateByIDSecretPath, map[string]string{}, 53 | protocol.GenMiniProgramLoginByIDSecretRequest(code, appID, appSecret)) 54 | 55 | if err != nil { 56 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 57 | } 58 | 59 | rspData := &protocol.MiniProgramLoginByIDSecretResponse{} 60 | err = json.Unmarshal(rspBytes, rspData) 61 | if err != nil { 62 | return nil, common.ErrJsonUnmarshal.ErrorWithExtErr( 63 | fmt.Errorf("jsonUnmarshalError[%v] httpStatusCode[%d] httpBody[%s]", err, statusCode, string(rspBytes))) 64 | } 65 | 66 | if rspData.Code != 0 { 67 | return rspData, common.ErrOpenApiReturnError.ErrorWithExtStr(fmt.Sprintf("[code:%d msg:%s]", rspData.Code, rspData.Msg)) 68 | } 69 | 70 | return rspData, nil 71 | } 72 | -------------------------------------------------------------------------------- /SDK/authentication/open_sso.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package authentication 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "net/url" 12 | 13 | "github.com/larksuite/botframework-go/SDK/common" 14 | "github.com/larksuite/botframework-go/SDK/protocol" 15 | ) 16 | 17 | // generate open sso authentication URL 18 | func OpenSSOGenerateAuthURL(redirectURL string, appID string, state string) string { 19 | m := make(url.Values) 20 | m.Set("redirect_uri", redirectURL) 21 | m.Set("app_id", appID) 22 | m.Set("state", state) 23 | 24 | reqURL := common.GetOpenPlatformHost() + string(protocol.OpenSSOGetCodePath) + "?" + m.Encode() 25 | 26 | return reqURL 27 | } 28 | 29 | func OpenSSOCodeValidateByAppToken(ctx context.Context, code string, appAccessToken string) (*protocol.OpenSSOTokenResponse, error) { 30 | tokenReq := &protocol.OpenSSOTokenRequest{ 31 | AppAccessToken: appAccessToken, 32 | GrantType: protocol.GrantTypeAuthCode, 33 | Code: code, 34 | } 35 | 36 | return openSSOValidate(ctx, tokenReq) 37 | } 38 | 39 | func OpenSSOCodeValidateByIDSecret(ctx context.Context, code string, appID, appSecret string) (*protocol.OpenSSOTokenResponse, error) { 40 | tokenReq := &protocol.OpenSSOTokenRequest{ 41 | AppID: appID, 42 | AppSecret: appSecret, 43 | GrantType: protocol.GrantTypeAuthCode, 44 | Code: code, 45 | } 46 | 47 | return openSSOValidate(ctx, tokenReq) 48 | } 49 | 50 | func OpenSSORefreshTokenByAppToken(ctx context.Context, refreshToken string, appAccessToken string) (*protocol.OpenSSOTokenResponse, error) { 51 | tokenReq := &protocol.OpenSSOTokenRequest{ 52 | AppAccessToken: appAccessToken, 53 | GrantType: protocol.GrantTypeRefreshToken, 54 | RefreshToken: refreshToken, 55 | } 56 | 57 | return openSSOValidate(ctx, tokenReq) 58 | } 59 | 60 | func OpenSSORefreshTokenByIDSecret(ctx context.Context, refreshToken string, appID, appSecret string) (*protocol.OpenSSOTokenResponse, error) { 61 | tokenReq := &protocol.OpenSSOTokenRequest{ 62 | AppID: appID, 63 | AppSecret: appSecret, 64 | GrantType: protocol.GrantTypeRefreshToken, 65 | RefreshToken: refreshToken, 66 | } 67 | 68 | return openSSOValidate(ctx, tokenReq) 69 | } 70 | 71 | func openSSOValidate(ctx context.Context, tokenReq *protocol.OpenSSOTokenRequest) (*protocol.OpenSSOTokenResponse, error) { 72 | // check params 73 | if (tokenReq.AppID == "" || tokenReq.AppSecret == "") && tokenReq.AppAccessToken == "" { 74 | return nil, common.ErrValidateParams.ErrorWithExtStr("(app_id+app_secret)/app_access_token are empty") 75 | } 76 | if tokenReq.GrantType != protocol.GrantTypeAuthCode && tokenReq.GrantType != protocol.GrantTypeRefreshToken { 77 | return nil, common.ErrValidateParams.ErrorWithExtStr("grant_type is invalid") 78 | } 79 | if tokenReq.GrantType == protocol.GrantTypeAuthCode && tokenReq.Code == "" { 80 | return nil, common.ErrValidateParams.ErrorWithExtStr("code is empty") 81 | } 82 | if tokenReq.GrantType == protocol.GrantTypeRefreshToken && tokenReq.RefreshToken == "" { 83 | return nil, common.ErrValidateParams.ErrorWithExtStr("refresh_token is empty") 84 | } 85 | 86 | rspBytes, statusCode, err := common.DoHttpPostOApi(protocol.OpenSSOValidatePath, common.NewHeaderJson(), tokenReq) 87 | if err != nil { 88 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 89 | } 90 | 91 | rspData := &protocol.OpenSSOTokenResponse{} 92 | err = json.Unmarshal(rspBytes, rspData) 93 | if err != nil { 94 | return nil, common.ErrJsonUnmarshal.ErrorWithExtErr( 95 | fmt.Errorf("jsonUnmarshalError[%v] httpStatusCode[%d] httpBody[%s]", err, statusCode, string(rspBytes))) 96 | } 97 | 98 | if rspData.Code != 0 { 99 | return rspData, common.ErrOpenApiReturnError.ErrorWithExtStr(fmt.Sprintf("[code:%d msg:%s]", rspData.Code, rspData.Msg)) 100 | } 101 | 102 | return rspData, nil 103 | } 104 | -------------------------------------------------------------------------------- /SDK/chat/chat.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package chat 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | 12 | "github.com/larksuite/botframework-go/SDK/auth" 13 | "github.com/larksuite/botframework-go/SDK/common" 14 | "github.com/larksuite/botframework-go/SDK/protocol" 15 | ) 16 | 17 | func GetChatInfo(ctx context.Context, tenantKey, appID string, chatID string) (*protocol.GetGroupInfoResponse, error) { 18 | // check params 19 | if appID == "" || chatID == "" { 20 | return nil, common.ErrChatParams.ErrorWithExtStr("param is empty or is nil") 21 | } 22 | 23 | accessToken, err := auth.GetTenantAccessToken(ctx, tenantKey, appID) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | rspBytes, statusCode, err := common.DoHttpGetOApi(protocol.GetChatInfoPath, common.NewHeaderToken(accessToken), 29 | protocol.GenGetGroupInfoRequest(chatID)) 30 | 31 | if err != nil { 32 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 33 | } 34 | 35 | rspData := &protocol.GetGroupInfoResponse{} 36 | err = json.Unmarshal(rspBytes, rspData) 37 | if err != nil { 38 | return nil, common.ErrJsonUnmarshal.ErrorWithExtErr( 39 | fmt.Errorf("jsonUnmarshalError[%v] httpStatusCode[%d] httpBody[%s]", err, statusCode, string(rspBytes))) 40 | } 41 | 42 | if rspData.Code != 0 { 43 | auth.CheckAndDisableTenantToken(ctx, appID, tenantKey, rspData.Code) 44 | return rspData, common.ErrOpenApiReturnError.ErrorWithExtStr(fmt.Sprintf("[code:%d msg:%s]", rspData.Code, rspData.Msg)) 45 | } 46 | 47 | return rspData, nil 48 | } 49 | 50 | func GetChatList(ctx context.Context, tenantKey, appID string, pageSize int, pageToken string) (*protocol.GetGroupListResponse, error) { 51 | // check params 52 | if appID == "" || pageSize <= 0 || pageSize > 200 { 53 | return nil, common.ErrChatParams.ErrorWithExtStr("param is empty or is invalid") 54 | } 55 | 56 | accessToken, err := auth.GetTenantAccessToken(ctx, tenantKey, appID) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | rspBytes, statusCode, err := common.DoHttpGetOApi(protocol.GetChatListPath, common.NewHeaderToken(accessToken), 62 | protocol.GenGetGroupListRequest(pageSize, pageToken)) 63 | 64 | if err != nil { 65 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 66 | } 67 | 68 | rspData := &protocol.GetGroupListResponse{} 69 | err = json.Unmarshal(rspBytes, rspData) 70 | if err != nil { 71 | return nil, common.ErrJsonUnmarshal.ErrorWithExtErr( 72 | fmt.Errorf("jsonUnmarshalError[%v] httpStatusCode[%d] httpBody[%s]", err, statusCode, string(rspBytes))) 73 | } 74 | 75 | if rspData.Code != 0 { 76 | auth.CheckAndDisableTenantToken(ctx, appID, tenantKey, rspData.Code) 77 | return rspData, common.ErrOpenApiReturnError.ErrorWithExtStr(fmt.Sprintf("[code:%d msg:%s]", rspData.Code, rspData.Msg)) 78 | } 79 | 80 | return rspData, nil 81 | } 82 | 83 | func CreateChat(ctx context.Context, tenantKey, appID string, request *protocol.CreateChatRequest) (*protocol.CreateChatResponse, error) { 84 | // check params 85 | if appID == "" || request == nil { 86 | return nil, common.ErrCreateChatParams.ErrorWithExtStr("param is empty or is invalid") 87 | } 88 | 89 | accessToken, err := auth.GetTenantAccessToken(ctx, tenantKey, appID) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | rspBytes, statusCode, err := common.DoHttpPostOApi(protocol.CreateChatPath, common.NewHeaderToken(accessToken), request) 95 | if err != nil { 96 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 97 | } 98 | 99 | rspData := &protocol.CreateChatResponse{} 100 | err = json.Unmarshal(rspBytes, rspData) 101 | if err != nil { 102 | return nil, common.ErrJsonUnmarshal.ErrorWithExtErr( 103 | fmt.Errorf("jsonUnmarshalError[%v] httpStatusCode[%d] httpBody[%s]", err, statusCode, string(rspBytes))) 104 | } 105 | 106 | if rspData.Code != 0 { 107 | auth.CheckAndDisableTenantToken(ctx, appID, tenantKey, rspData.Code) 108 | return rspData, common.ErrOpenApiReturnError.ErrorWithExtStr(fmt.Sprintf("[code:%d msg:%s]", rspData.Code, rspData.Msg)) 109 | } 110 | 111 | return rspData, nil 112 | } 113 | 114 | func UpdateChatInfo(ctx context.Context, tenantKey, appID string, request *protocol.UpdateChatInfoRequest) (*protocol.UpdateChatInfoResponse, error) { 115 | // check params 116 | if appID == "" || request == nil { 117 | return nil, common.ErrUpdateChatInfoParams.ErrorWithExtStr("param is empty or is invalid") 118 | } 119 | 120 | accessToken, err := auth.GetTenantAccessToken(ctx, tenantKey, appID) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | rspBytes, statusCode, err := common.DoHttpPostOApi(protocol.UpdateChatInfoPath, common.NewHeaderToken(accessToken), request) 126 | if err != nil { 127 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 128 | } 129 | 130 | rspData := &protocol.UpdateChatInfoResponse{} 131 | err = json.Unmarshal(rspBytes, rspData) 132 | if err != nil { 133 | return nil, common.ErrJsonUnmarshal.ErrorWithExtErr( 134 | fmt.Errorf("jsonUnmarshalError[%v] httpStatusCode[%d] httpBody[%s]", err, statusCode, string(rspBytes))) 135 | } 136 | 137 | if rspData.Code != 0 { 138 | auth.CheckAndDisableTenantToken(ctx, appID, tenantKey, rspData.Code) 139 | return rspData, common.ErrOpenApiReturnError.ErrorWithExtStr(fmt.Sprintf("[code:%d msg:%s]", rspData.Code, rspData.Msg)) 140 | } 141 | 142 | return rspData, nil 143 | } 144 | 145 | func AddUserToChat(ctx context.Context, tenantKey, appID string, request *protocol.AddUserToChatRequest) (*protocol.AddUserToChatResponse, error) { 146 | // check params 147 | if appID == "" || request == nil { 148 | return nil, common.ErrAddUserToChatParams.ErrorWithExtStr("param is empty or is invalid") 149 | } 150 | 151 | accessToken, err := auth.GetTenantAccessToken(ctx, tenantKey, appID) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | rspBytes, statusCode, err := common.DoHttpPostOApi(protocol.AddUserToChatPath, common.NewHeaderToken(accessToken), request) 157 | if err != nil { 158 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 159 | } 160 | 161 | rspData := &protocol.AddUserToChatResponse{} 162 | err = json.Unmarshal(rspBytes, rspData) 163 | if err != nil { 164 | return nil, common.ErrJsonUnmarshal.ErrorWithExtErr( 165 | fmt.Errorf("jsonUnmarshalError[%v] httpStatusCode[%d] httpBody[%s]", err, statusCode, string(rspBytes))) 166 | } 167 | 168 | if rspData.Code != 0 { 169 | auth.CheckAndDisableTenantToken(ctx, appID, tenantKey, rspData.Code) 170 | return rspData, common.ErrOpenApiReturnError.ErrorWithExtStr(fmt.Sprintf("[code:%d msg:%s]", rspData.Code, rspData.Msg)) 171 | } 172 | 173 | return rspData, nil 174 | } 175 | 176 | func DeleteUserFromChat(ctx context.Context, tenantKey, appID string, request *protocol.DeleteUserFromChatRequest) (*protocol.DeleteUserFromChatResponse, error) { 177 | // check params 178 | if appID == "" || request == nil { 179 | return nil, common.ErrDeleteUserFromChatParams.ErrorWithExtStr("param is empty or is invalid") 180 | } 181 | 182 | accessToken, err := auth.GetTenantAccessToken(ctx, tenantKey, appID) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | rspBytes, statusCode, err := common.DoHttpPostOApi(protocol.DeleteUserFromChatPath, common.NewHeaderToken(accessToken), request) 188 | if err != nil { 189 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 190 | } 191 | 192 | rspData := &protocol.DeleteUserFromChatResponse{} 193 | err = json.Unmarshal(rspBytes, rspData) 194 | if err != nil { 195 | return nil, common.ErrJsonUnmarshal.ErrorWithExtErr( 196 | fmt.Errorf("jsonUnmarshalError[%v] httpStatusCode[%d] httpBody[%s]", err, statusCode, string(rspBytes))) 197 | } 198 | 199 | if rspData.Code != 0 { 200 | auth.CheckAndDisableTenantToken(ctx, appID, tenantKey, rspData.Code) 201 | return rspData, common.ErrOpenApiReturnError.ErrorWithExtStr(fmt.Sprintf("[code:%d msg:%s]", rspData.Code, rspData.Msg)) 202 | } 203 | 204 | return rspData, nil 205 | } 206 | 207 | func DisbandChat(ctx context.Context, tenantKey, appID string, request *protocol.DisbandChatRequest) (*protocol.DisbandChatResponse, error) { 208 | // check params 209 | if appID == "" || request == nil { 210 | return nil, common.ErrDisbandChatParams.ErrorWithExtStr("param is empty or is invalid") 211 | } 212 | 213 | accessToken, err := auth.GetTenantAccessToken(ctx, tenantKey, appID) 214 | if err != nil { 215 | return nil, err 216 | } 217 | 218 | rspBytes, statusCode, err := common.DoHttpPostOApi(protocol.DisbandChatPath, common.NewHeaderToken(accessToken), request) 219 | if err != nil { 220 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 221 | } 222 | 223 | rspData := &protocol.DisbandChatResponse{} 224 | err = json.Unmarshal(rspBytes, rspData) 225 | if err != nil { 226 | return nil, common.ErrJsonUnmarshal.ErrorWithExtErr( 227 | fmt.Errorf("jsonUnmarshalError[%v] httpStatusCode[%d] httpBody[%s]", err, statusCode, string(rspBytes))) 228 | } 229 | 230 | if rspData.Code != 0 { 231 | auth.CheckAndDisableTenantToken(ctx, appID, tenantKey, rspData.Code) 232 | return rspData, common.ErrOpenApiReturnError.ErrorWithExtStr(fmt.Sprintf("[code:%d msg:%s]", rspData.Code, rspData.Msg)) 233 | } 234 | 235 | return rspData, nil 236 | } 237 | -------------------------------------------------------------------------------- /SDK/chat/chat_ex.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package chat 6 | 7 | import ( 8 | "context" 9 | ) 10 | 11 | func CheckUserIDInGroup(ctx context.Context, tenantKey, appID, chatID, userID string) (bool, error) { 12 | rspData, err := GetChatInfo(ctx, tenantKey, appID, chatID) 13 | if err != nil { 14 | return false, err 15 | } 16 | 17 | for _, v := range rspData.Data.Members { 18 | if userID == v.UserID { 19 | return true, nil 20 | } 21 | } 22 | 23 | return false, nil 24 | } 25 | 26 | func CheckOpenIDInGroup(ctx context.Context, tenantKey, appID, chatID, openID string) (bool, error) { 27 | rspData, err := GetChatInfo(ctx, tenantKey, appID, chatID) 28 | if err != nil { 29 | return false, err 30 | } 31 | 32 | for _, v := range rspData.Data.Members { 33 | if openID == v.OpenID { 34 | return true, nil 35 | } 36 | } 37 | 38 | return false, nil 39 | } 40 | 41 | func CheckBotInGroup(ctx context.Context, tenantKey, appID, chatID string) (bool, error) { 42 | const MaxGetPage int = 1000 // avoid to fall into an endless loop 43 | 44 | pageToken := "" 45 | for page := 1; page < MaxGetPage; page++ { 46 | rspData, err := GetChatList(ctx, tenantKey, appID, 100, pageToken) 47 | if err != nil { 48 | return false, err 49 | } 50 | 51 | for _, v := range rspData.Data.Groups { 52 | if chatID == v.ChatID { 53 | return true, nil 54 | } 55 | } 56 | 57 | if !rspData.Data.HasMore { 58 | return false, nil 59 | } 60 | 61 | pageToken = rspData.Data.PageToken 62 | } 63 | 64 | return false, nil 65 | } 66 | 67 | func CheckUserIDBotInSameGroup(ctx context.Context, tenantKey, appID, chatID, userID string) (bool, error) { 68 | isBotInGroup, err := CheckBotInGroup(ctx, tenantKey, appID, chatID) 69 | if err != nil { 70 | return false, err 71 | } 72 | isUserIDInGroup, err := CheckUserIDInGroup(ctx, tenantKey, appID, chatID, userID) 73 | if err != nil { 74 | return false, err 75 | } 76 | 77 | if isBotInGroup && isUserIDInGroup { 78 | return true, nil 79 | } 80 | return false, nil 81 | } 82 | 83 | func CheckOpenIDBotInSameGroup(ctx context.Context, tenantKey, appID, chatID, openID string) (bool, error) { 84 | isBotInGroup, err := CheckBotInGroup(ctx, tenantKey, appID, chatID) 85 | if err != nil { 86 | return false, err 87 | } 88 | isOpenIDInGroup, err := CheckOpenIDInGroup(ctx, tenantKey, appID, chatID, openID) 89 | if err != nil { 90 | return false, err 91 | } 92 | 93 | if isBotInGroup && isOpenIDInGroup { 94 | return true, nil 95 | } 96 | return false, nil 97 | } 98 | -------------------------------------------------------------------------------- /SDK/chat/chat_ex_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package chat_test 6 | 7 | import ( 8 | "context" 9 | "os" 10 | "sync" 11 | "testing" 12 | 13 | "github.com/larksuite/botframework-go/SDK/appconfig" 14 | "github.com/larksuite/botframework-go/SDK/chat" 15 | ) 16 | 17 | var ( 18 | once sync.Once 19 | appConf *appconfig.AppConfig 20 | tenantKey string 21 | chatID string 22 | openID string 23 | userID string 24 | ) 25 | 26 | func InitTestParams() { 27 | once.Do(func() { 28 | appConf = &appconfig.AppConfig{ 29 | AppID: os.Getenv("appid"), 30 | AppSecret: os.Getenv("appsecret"), 31 | VerifyToken: os.Getenv("verifytoken"), 32 | EncryptKey: os.Getenv("encryptkey"), 33 | AppType: os.Getenv("apptype"), 34 | } 35 | 36 | tenantKey = os.Getenv("tenantkey") 37 | chatID = os.Getenv("chatid") 38 | openID = os.Getenv("openid") 39 | userID = os.Getenv("userid") 40 | 41 | appconfig.Init(*appConf) 42 | }) 43 | } 44 | 45 | func TestCheckUserInGroup(t *testing.T) { 46 | c := context.Background() 47 | InitTestParams() 48 | 49 | inGroup, err := chat.CheckOpenIDInGroup(c, tenantKey, appConf.AppID, chatID, openID) 50 | if err != nil { 51 | t.Errorf("CheckOpenIDInGroup failed: %v", err) 52 | } else { 53 | t.Logf("CheckOpenIDInGroup: %v", inGroup) 54 | } 55 | 56 | inGroup, err = chat.CheckUserIDInGroup(c, tenantKey, appConf.AppID, chatID, userID) 57 | if err != nil { 58 | t.Errorf("CheckUserIDInGroup failed: %v", err) 59 | } else { 60 | t.Logf("CheckUserIDInGroup: %v", inGroup) 61 | } 62 | } 63 | 64 | func TestCheckBotInGroup(t *testing.T) { 65 | c := context.Background() 66 | InitTestParams() 67 | 68 | inGroup, err := chat.CheckBotInGroup(c, tenantKey, appConf.AppID, chatID) 69 | if err != nil { 70 | t.Errorf("CheckBotInGroup failed: %v", err) 71 | } else { 72 | t.Logf("CheckBotInGroup: %v", inGroup) 73 | } 74 | } 75 | 76 | func TestCheckUserBotInSameGroup(t *testing.T) { 77 | c := context.Background() 78 | InitTestParams() 79 | 80 | inGroup, err := chat.CheckOpenIDBotInSameGroup(c, tenantKey, appConf.AppID, chatID, openID) 81 | if err != nil { 82 | t.Errorf("CheckOpenIDBotInSameGroup failed: %v", err) 83 | } else { 84 | t.Logf("CheckOpenIDBotInSameGroup: %v", inGroup) 85 | } 86 | 87 | inGroup, err = chat.CheckUserIDBotInSameGroup(c, tenantKey, appConf.AppID, chatID, userID) 88 | if err != nil { 89 | t.Errorf("CheckUserIDBotInSameGroup failed: %v", err) 90 | } else { 91 | t.Logf("CheckUserIDBotInSameGroup: %v", inGroup) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /SDK/chat/chat_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package chat_test 6 | 7 | import ( 8 | "context" 9 | "testing" 10 | 11 | "github.com/larksuite/botframework-go/SDK/appconfig" 12 | "github.com/larksuite/botframework-go/SDK/chat" 13 | "github.com/larksuite/botframework-go/SDK/protocol" 14 | ) 15 | 16 | func TestGetChatInfo(t *testing.T) { 17 | c := context.Background() 18 | InitTestParams() 19 | 20 | appconfig.Init(*appConf) 21 | resp, err := chat.GetChatInfo(c, tenantKey, appConf.AppID, chatID) 22 | if err != nil { 23 | t.Errorf("GetChatInfo failed: %v", err) 24 | } else { 25 | t.Logf("GetChatInfo chatInfo[%+v]", resp) 26 | } 27 | } 28 | 29 | func TestGetChatList(t *testing.T) { 30 | ctx := context.Background() 31 | InitTestParams() 32 | 33 | appconfig.Init(*appConf) 34 | resp, err := chat.GetChatList(ctx, tenantKey, appConf.AppID, 100, "") 35 | if err != nil { 36 | t.Errorf("GetChatList failed: %v", err) 37 | } else { 38 | t.Logf("GetChatList hasMore[%v]pageToken[%s]groupNum[%d]", resp.Data.HasMore, resp.Data.PageToken, len(resp.Data.Groups)) 39 | for k, v := range resp.Data.Groups { 40 | t.Logf("GetChatList chatList %d:%+v\n", k+1, *v) 41 | } 42 | } 43 | } 44 | 45 | func TestUpdateChatInfo(t *testing.T) { 46 | ctx := context.Background() 47 | InitTestParams() 48 | 49 | appconfig.Init(*appConf) 50 | 51 | newName := "Update Name" 52 | request := &protocol.UpdateChatInfoRequest{ 53 | ChatID: chatID, 54 | OwnerUserID: nil, 55 | OwnerOpenID: nil, 56 | Name: &newName, 57 | ChatI18nNames: nil, 58 | } 59 | 60 | resp, err := chat.UpdateChatInfo(ctx, tenantKey, appConf.AppID, request) 61 | if err != nil { 62 | t.Errorf("UpdateChatInfo failed: %v", err) 63 | } else { 64 | t.Logf("UpdateChatInfo Success, chatid: [%+v]", resp) 65 | } 66 | } 67 | 68 | func TestCreateChat(t *testing.T) { 69 | ctx := context.Background() 70 | InitTestParams() 71 | 72 | appconfig.Init(*appConf) 73 | 74 | userids := []string{} 75 | openids := []string{openID} 76 | request := &protocol.CreateChatRequest{ 77 | Name: "test group", 78 | Description: "test group description", 79 | UserIDs: userids, 80 | OpenIDs: openids, 81 | ChatI18nNames: nil, 82 | } 83 | 84 | resp, err := chat.CreateChat(ctx, tenantKey, appConf.AppID, request) 85 | if err != nil { 86 | t.Errorf("CreateChat failed: %v", err) 87 | } else { 88 | t.Logf("CreateChat Success, chatid: [%+v]", resp) 89 | } 90 | } 91 | 92 | func TestAddUserToChat(t *testing.T) { 93 | ctx := context.Background() 94 | InitTestParams() 95 | 96 | appconfig.Init(*appConf) 97 | 98 | userids := []string{} 99 | openids := []string{openID} 100 | request := &protocol.AddUserToChatRequest{ 101 | ChatID: chatID, 102 | UserIDs: userids, 103 | OpenIDs: openids, 104 | } 105 | 106 | resp, err := chat.AddUserToChat(ctx, tenantKey, appConf.AppID, request) 107 | if err != nil { 108 | t.Errorf("AddUserToChat failed: %v", err) 109 | } else { 110 | t.Logf("AddUserToChat Success [%+v]", resp) 111 | } 112 | } 113 | 114 | func TestDeleteUserFromChat(t *testing.T) { 115 | ctx := context.Background() 116 | InitTestParams() 117 | 118 | appconfig.Init(*appConf) 119 | 120 | userids := []string{} 121 | openids := []string{openID} 122 | request := &protocol.DeleteUserFromChatRequest{ 123 | ChatID: chatID, 124 | UserIDs: userids, 125 | OpenIDs: openids, 126 | } 127 | 128 | resp, err := chat.DeleteUserFromChat(ctx, tenantKey, appConf.AppID, request) 129 | if err != nil { 130 | t.Errorf("DeleteUserFromChat failed: %v", err) 131 | } else { 132 | t.Logf("DeleteUserFromChat Success [%+v]", resp) 133 | } 134 | } 135 | 136 | func TestDisbandChat(t *testing.T) { 137 | ctx := context.Background() 138 | InitTestParams() 139 | 140 | appconfig.Init(*appConf) 141 | 142 | request := &protocol.DisbandChatRequest{ 143 | ChatID: chatID, 144 | } 145 | 146 | resp, err := chat.DisbandChat(ctx, tenantKey, appConf.AppID, request) 147 | if err != nil { 148 | t.Errorf("DisbandChat failed: %v", err) 149 | } else { 150 | t.Logf("DisbandChat Success [%+v]", resp) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /SDK/common/crypto_simple.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | var ( 8 | aesEncryptKey = "" 9 | ) 10 | 11 | // to encrypt/decrypt information more conveniently 12 | 13 | func InitAESEncryptKey(encryptKey string) { 14 | aesEncryptKey = encryptKey 15 | } 16 | 17 | func GetAESEncryptKey() string { 18 | return aesEncryptKey 19 | } 20 | 21 | func AESEncrypt(origData string) string { 22 | if aesEncryptKey == "" { 23 | return origData 24 | } 25 | 26 | data, _ := EncryptAESCBCBase64(origData, aesEncryptKey) 27 | return data 28 | } 29 | 30 | func AESDecrypt(encryptedData string) string { 31 | if aesEncryptKey == "" { 32 | return encryptedData 33 | } 34 | 35 | data, _ := DecryptAESCBCBase64(encryptedData, aesEncryptKey) 36 | return data 37 | } 38 | -------------------------------------------------------------------------------- /SDK/common/crypto_util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | import ( 8 | "bytes" 9 | "crypto/aes" 10 | "crypto/cipher" 11 | "crypto/sha256" 12 | "encoding/base64" 13 | "encoding/json" 14 | "errors" 15 | "fmt" 16 | "reflect" 17 | ) 18 | 19 | // EncryptAESCBCBase64 AES CBC base64URLEncoding 20 | func EncryptAESCBCBase64(originalData string, key string) (string, error) { 21 | hashKey := getHashKey(key) 22 | data := []byte(originalData) 23 | block, err := aes.NewCipher(hashKey) 24 | if err != nil { 25 | return "", err 26 | } 27 | blockSize := block.BlockSize() 28 | data = PKCS5Padding(data, blockSize) 29 | blockMode := cipher.NewCBCEncrypter(block, hashKey[:blockSize]) 30 | crypted := make([]byte, len(data)) 31 | blockMode.CryptBlocks(crypted, []byte(data)) 32 | return base64.URLEncoding.EncodeToString(crypted), nil 33 | } 34 | 35 | // DecryptAESCBCBase64 AES CBC base64URLEncoding 36 | func DecryptAESCBCBase64(encryptedData string, key string) (string, error) { 37 | hashKey := getHashKey(key) 38 | crypted, err := base64.URLEncoding.DecodeString(encryptedData) 39 | if err != nil { 40 | return "", err 41 | } 42 | block, err := aes.NewCipher(hashKey) 43 | if err != nil { 44 | return "", err 45 | } 46 | blockMode := cipher.NewCBCDecrypter(block, hashKey) 47 | originalData := make([]byte, len(crypted)) 48 | blockMode.CryptBlocks(originalData, crypted) 49 | originalData, err = PKCS5UnPadding(originalData) 50 | if err != nil { 51 | return "", err 52 | } 53 | return string(originalData), nil 54 | } 55 | 56 | // EncryptAes aes encrypt 57 | func EncryptAes(original interface{}, key string) (string, error) { 58 | bEncrypt, err := json.Marshal(original) 59 | if err != nil { 60 | return "", fmt.Errorf("original Marshal failed. err:%v", err) 61 | } 62 | 63 | return EncryptAESCBCBase64(string(bEncrypt), key) 64 | } 65 | 66 | // DecryptAes aes decypt 67 | func DecryptAes(encryptedData string, key string, original interface{}) error { 68 | rv := reflect.ValueOf(original) 69 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 70 | return errors.New("original isnot Ptr, or is nil") 71 | } 72 | 73 | originalData, err := DecryptAESCBCBase64(encryptedData, key) 74 | if err != nil { 75 | return fmt.Errorf("aes decypt failed. err:%v", err) 76 | } 77 | 78 | err = json.Unmarshal([]byte(originalData), &original) 79 | if err != nil { 80 | return fmt.Errorf("encryptedData Unmarshal failed. err:%v", err) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | // PKCS5Padding PKCS5 87 | func PKCS5Padding(ciphertext []byte, blockSize int) []byte { 88 | padding := blockSize - len(ciphertext)%blockSize 89 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 90 | return append(ciphertext, padtext...) 91 | } 92 | 93 | // PKCS5UnPadding PKCS5 94 | func PKCS5UnPadding(originalData []byte) ([]byte, error) { 95 | length := len(originalData) 96 | if length <= 0 { 97 | return originalData, errors.New("length error") 98 | } 99 | unpadding := int(originalData[length-1]) 100 | if length < unpadding { 101 | return originalData, errors.New("length lower unpadding") 102 | } 103 | return originalData[:(length - unpadding)], nil 104 | } 105 | 106 | func getHashKey(input string) []byte { 107 | h := sha256.New() 108 | h.Write([]byte(input)) 109 | buf := h.Sum(nil) 110 | return buf[:16] 111 | } 112 | -------------------------------------------------------------------------------- /SDK/common/db_client_interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | import "time" 8 | 9 | type DBClient interface { 10 | InitDB(mapParams map[string]string) error 11 | 12 | Set(key string, value interface{}, expiration time.Duration) error 13 | Get(key string) (string, error) 14 | } 15 | -------------------------------------------------------------------------------- /SDK/common/db_client_redis.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | 11 | "github.com/go-redis/redis" 12 | ) 13 | 14 | // Just for the convenience of implementing the default-session-manager/default-appticket-manager 15 | 16 | type DefaultRedisClient struct { 17 | Client *redis.Client 18 | } 19 | 20 | func (d *DefaultRedisClient) InitDB(mapParams map[string]string) error { 21 | d.Client = redis.NewClient(&redis.Options{ 22 | Addr: mapParams["addr"], // addr = host:port , demo: "127.0.0.1:6379" 23 | }) 24 | 25 | _, err := d.Client.Ping().Result() 26 | if err != nil { 27 | return fmt.Errorf("init db error[%v]", err) 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func (d *DefaultRedisClient) Set(key string, value interface{}, expiration time.Duration) error { 34 | if d.Client == nil { 35 | return fmt.Errorf("db_client isnot initialized, key[%s]", key) 36 | } 37 | 38 | _, err := d.Client.Set(key, value, expiration).Result() 39 | if err != nil { 40 | return fmt.Errorf("set value error[%v], key[%s]", err, key) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func (d *DefaultRedisClient) Get(key string) (string, error) { 47 | if d.Client == nil { 48 | return "", fmt.Errorf("db_client isnot initialized, key[%s]", key) 49 | } 50 | 51 | value, err := d.Client.Get(key).Result() 52 | if err != nil { 53 | return "", fmt.Errorf("get value error[%v], key[%s]", err, key) 54 | } 55 | 56 | return value, nil 57 | } 58 | -------------------------------------------------------------------------------- /SDK/common/env.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | var ( 8 | isFeishuOrLark bool = true 9 | 10 | hostFeishu string = "https://open.feishu.cn" 11 | hostLark string = "https://open.larksuite.com" 12 | ) 13 | 14 | func SetFeishu() { 15 | isFeishuOrLark = true 16 | } 17 | 18 | func SetLark() { 19 | isFeishuOrLark = false 20 | } 21 | 22 | func ReplaceFeishuHost(host string) { 23 | hostFeishu = host 24 | } 25 | 26 | func ReplaceLarkHost(host string) { 27 | hostLark = host 28 | } 29 | 30 | // GetOpenPlatformHost outside mainland China use https://open.larksuite.com , in Mainland China use https://open.feishu.cn 31 | func GetOpenPlatformHost() string { 32 | if isFeishuOrLark { 33 | return hostFeishu 34 | } 35 | 36 | return hostLark 37 | } 38 | -------------------------------------------------------------------------------- /SDK/common/error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | import "fmt" 8 | 9 | type ErrCodeMsg struct { 10 | Code int 11 | Message string 12 | } 13 | 14 | func (e ErrCodeMsg) Error() error { 15 | return fmt.Errorf("code=%d, msg=%s", e.Code, e.Message) 16 | } 17 | 18 | func (e ErrCodeMsg) ErrorWithExtErr(err error) error { 19 | return fmt.Errorf("code=%d, msg=%s, extError=%v", e.Code, e.Message, err) 20 | } 21 | 22 | func (e ErrCodeMsg) ErrorWithExtStr(str string) error { 23 | return fmt.Errorf("code=%d, msg=%s, extError=%s", e.Code, e.Message, str) 24 | } 25 | 26 | func (e ErrCodeMsg) String() string { 27 | return fmt.Sprintf("code=%d, msg=%s", e.Code, e.Message) 28 | } 29 | 30 | func (e ErrCodeMsg) StringWithExtErr(extErr error) string { 31 | return fmt.Sprintf("code=%d, msg=%s, extError=%v", e.Code, e.Message, extErr) 32 | } 33 | 34 | var ( 35 | Success = &ErrCodeMsg{Code: 0, Message: "success"} 36 | 37 | // 1. common 1000 - 1999 38 | ErrJsonMarshal = &ErrCodeMsg{Code: 1000, Message: "json marshal error"} 39 | ErrJsonUnmarshal = &ErrCodeMsg{Code: 1001, Message: "json unmarshal error"} 40 | ErrOpenApiFailed = &ErrCodeMsg{Code: 1002, Message: "open api failed"} 41 | ErrOpenApiReturnError = &ErrCodeMsg{Code: 1003, Message: "open api return error"} 42 | ErrAppConfNotFound = &ErrCodeMsg{Code: 1004, Message: "app config not found"} 43 | ErrHttpCode = &ErrCodeMsg{Code: 1005, Message: "http return not 200 "} 44 | 45 | // 2. auth 2000 - 2999 46 | ErrAppTokenNotFound = &ErrCodeMsg{Code: 2000, Message: "auth app token not found"} 47 | ErrAppTicketNotFound = &ErrCodeMsg{Code: 2001, Message: "auth app ticket not found"} 48 | ErrGetInternalTenantAccessToken = &ErrCodeMsg{Code: 2002, Message: "auth get internal tenant access token error"} 49 | ErrGetISVTenantAccessToken = &ErrCodeMsg{Code: 2003, Message: "auth get ISV tenant access token error"} 50 | ErrGetAppAccessToken = &ErrCodeMsg{Code: 2004, Message: "auth get app access token error"} 51 | ErrGetInternalAppAccessToken = &ErrCodeMsg{Code: 2005, Message: "auth get internal app access token error"} 52 | ErrGetISVAppAccessToken = &ErrCodeMsg{Code: 2006, Message: "auth get ISV app access token error"} 53 | ErrRespDataIsNil = &ErrCodeMsg{Code: 2007, Message: "auth response data is nil"} 54 | ErrTicketManagerNotInit = &ErrCodeMsg{Code: 2008, Message: "auth ticket manager not init"} 55 | ErrSetAppTicketFailed = &ErrCodeMsg{Code: 2009, Message: "auth set app ticket failed"} 56 | 57 | // 3. message 3000 - 3999 58 | ErrSendMsgParams = &ErrCodeMsg{Code: 3000, Message: "send msg params error"} 59 | ErrPostFormParams = &ErrCodeMsg{Code: 3001, Message: "postform params error"} 60 | ErrImageParams = &ErrCodeMsg{Code: 3002, Message: "get_imagekey params error"} 61 | ErrGenBinImageFailed = &ErrCodeMsg{Code: 3003, Message: "generate binary image error"} 62 | ErrGetImageBinDataParams = &ErrCodeMsg{Code: 3004, Message: "get_image_bin_data params error"} 63 | ErrCardUpdateParams = &ErrCodeMsg{Code: 3100, Message: "update card params error"} 64 | 65 | // 4. chat and bot 4000 - 4999 66 | ErrChatParams = &ErrCodeMsg{Code: 4000, Message: "chat params error"} 67 | ErrCreateChatParams = &ErrCodeMsg{Code: 4001, Message: "create chat params error"} 68 | ErrUpdateChatInfoParams = &ErrCodeMsg{Code: 4002, Message: "update chat info params error"} 69 | ErrAddUserToChatParams = &ErrCodeMsg{Code: 4003, Message: "add user to chat params error"} 70 | ErrDeleteUserFromChatParams = &ErrCodeMsg{Code: 4004, Message: "delete user from chat params error"} 71 | ErrDisbandChatParams = &ErrCodeMsg{Code: 4005, Message: "disband chat params error"} 72 | ErrGetBotInfoParams = &ErrCodeMsg{Code: 4006, Message: "getBotInfo params error"} 73 | ErrAddBotToChatParams = &ErrCodeMsg{Code: 4007, Message: "addBotToChat params error"} 74 | ErrRemoveBotFromChatParams = &ErrCodeMsg{Code: 4008, Message: "removeBotFromChat params error"} 75 | 76 | // 5. event 5000 - 5999 77 | ErrEventTypeRegister = &ErrCodeMsg{Code: 5000, Message: "event type register handler error"} 78 | ErrEventManagerNotInit = &ErrCodeMsg{Code: 5001, Message: "event not init"} 79 | ErrEventParams = &ErrCodeMsg{Code: 5002, Message: "event params error"} 80 | ErrEventDecrypt = &ErrCodeMsg{Code: 5003, Message: "event decrypt error"} 81 | ErrEventGetBase = &ErrCodeMsg{Code: 5004, Message: "event get event base error"} 82 | ErrEventVeriToken = &ErrCodeMsg{Code: 5005, Message: "event veri token error"} 83 | ErrEventTypeUnknown = &ErrCodeMsg{Code: 5006, Message: "event unknown callback type"} 84 | ErrEventGetJsonEvent = &ErrCodeMsg{Code: 5007, Message: "event get event body error"} 85 | ErrEventGetJsonType = &ErrCodeMsg{Code: 5008, Message: "event get event type error"} 86 | ErrEventGetJsonAppID = &ErrCodeMsg{Code: 5009, Message: "event get app id error"} 87 | ErrEventAppIDNotMatch = &ErrCodeMsg{Code: 5010, Message: "event app id not match error"} 88 | ErrEventAppIDUnregistered = &ErrCodeMsg{Code: 5011, Message: "event appid_handler unregistered"} 89 | ErrEventTypeUnregistered = &ErrCodeMsg{Code: 5012, Message: "event notification_type_handler unregistered"} 90 | ErrEventHandlerIsNil = &ErrCodeMsg{Code: 5013, Message: "event handler not found"} 91 | ErrEventHandlerFailed = &ErrCodeMsg{Code: 5014, Message: "event handle function error"} 92 | 93 | ErrBotRecvMsgRegister = &ErrCodeMsg{Code: 5100, Message: "botRecvMsg registered error"} 94 | ErrBotRecvMsgMsgTypeJson = &ErrCodeMsg{Code: 5101, Message: "botRecvMsg get msg_type error"} 95 | ErrBotRecvMsgAppIDJson = &ErrCodeMsg{Code: 5102, Message: "botRecvMsg get app_id error"} 96 | ErrBotRecvMsgHandlerNoFound = &ErrCodeMsg{Code: 5103, Message: "botRecvMsg cannot find handler"} 97 | ErrBotRecvMsgHandlerFailed = &ErrCodeMsg{Code: 5104, Message: "botRecvMsg call handler failed"} 98 | 99 | ErrCardParams = &ErrCodeMsg{Code: 5200, Message: "card action callback params error"} 100 | ErrCardMethodRegister = &ErrCodeMsg{Code: 5201, Message: "card action method has not registered yet"} 101 | ErrCardManagerNotInit = &ErrCodeMsg{Code: 5202, Message: "card action handler need be init"} 102 | ErrCardVeriTokenInvalid = &ErrCodeMsg{Code: 5203, Message: "card action callback veri token invalid"} 103 | ErrCardSignatureInvalid = &ErrCodeMsg{Code: 5204, Message: "card action callback signature invalid"} 104 | ErrCardWithoutMethod = &ErrCodeMsg{Code: 5205, Message: "card action callback has no method"} 105 | ErrCardWithoutSessionID = &ErrCodeMsg{Code: 5206, Message: "card action callback has no session id"} 106 | ErrCardMetaInvalid = &ErrCodeMsg{Code: 5207, Message: "card action callback meta invalid"} 107 | ErrCardHandlerIsNil = &ErrCodeMsg{Code: 5208, Message: "card action handler not found"} 108 | ErrCardHandlerFailed = &ErrCodeMsg{Code: 5209, Message: "card action handler failed"} 109 | 110 | // 6. authentication 6000 - 6999 111 | ErrValidateParams = &ErrCodeMsg{Code: 6000, Message: "authentication-login params error"} 112 | ErrAuthParams = &ErrCodeMsg{Code: 6001, Message: "authentication-auth params error"} 113 | ErrMinaCodeGetParams = &ErrCodeMsg{Code: 6002, Message: "mini-program codeToSession get params error"} 114 | ErrMinaSetAuth = &ErrCodeMsg{Code: 6003, Message: "mini-program set auth-user-info error"} 115 | ErrMinaGetAuth = &ErrCodeMsg{Code: 6004, Message: "mini-program get auth-user-info error"} 116 | ErrMinaSessionInvalid = &ErrCodeMsg{Code: 6005, Message: "mini-program session invalid"} 117 | ErrLogoutParams = &ErrCodeMsg{Code: 6006, Message: "authentication-logout params error"} 118 | ) 119 | -------------------------------------------------------------------------------- /SDK/common/http_util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "net/url" 14 | 15 | "github.com/larksuite/botframework-go/SDK/protocol" 16 | ) 17 | 18 | // Common HTTP methods. 19 | const ( 20 | HTTPMethodGet = "GET" 21 | HTTPMethodHead = "HEAD" 22 | HTTPMethodPost = "POST" 23 | HTTPMethodPut = "PUT" 24 | HTTPMethodPatch = "PATCH" 25 | HTTPMethodDelete = "DELETE" 26 | HTTPMethodConnect = "CONNECT" 27 | HTTPMethodOptions = "OPTIONS" 28 | HTTPMethodTrace = "TRACE" 29 | ) 30 | 31 | // Common HTTP status code. 32 | const ( 33 | HTTPCodeOK = 200 34 | ) 35 | 36 | // DoHttpPostOApi open platform POST http 37 | func DoHttpPostOApi(path protocol.OpenApiPath, headers map[string]string, data interface{}) ([]byte, int, error) { 38 | reqBody := new(bytes.Buffer) 39 | err := json.NewEncoder(reqBody).Encode(data) 40 | if err != nil { 41 | return nil, 0, fmt.Errorf("jsonEncodeError[%v]", err) 42 | } 43 | 44 | reqURL := GetOpenPlatformHost() + string(path) 45 | 46 | return DoHttp(HTTPMethodPost, reqURL, headers, reqBody) 47 | } 48 | 49 | // DoHttpGetOApi open platform GET http 50 | func DoHttpGetOApi(path protocol.OpenApiPath, headers map[string]string, params map[string]string) ([]byte, int, error) { 51 | reqURL := GetOpenPlatformHost() + string(path) 52 | 53 | if params != nil && len(params) > 0 { 54 | m := make(url.Values) 55 | for k, v := range params { 56 | m.Set(k, v) 57 | } 58 | 59 | reqURL = reqURL + "?" + m.Encode() 60 | } 61 | 62 | reqBody := new(bytes.Buffer) 63 | return DoHttp(HTTPMethodGet, reqURL, headers, reqBody) 64 | } 65 | 66 | // DoHttpPutOApi open platform PUT http 67 | func DoHttpPutOApi(path protocol.OpenApiPath, headers map[string]string, data interface{}) ([]byte, int, error) { 68 | reqBody := new(bytes.Buffer) 69 | err := json.NewEncoder(reqBody).Encode(data) 70 | if err != nil { 71 | return nil, 0, fmt.Errorf("jsonEncodeError[%v]", err) 72 | } 73 | 74 | reqURL := GetOpenPlatformHost() + string(path) 75 | 76 | return DoHttp(http.MethodPut, reqURL, headers, reqBody) 77 | } 78 | 79 | // DoHttpPatchApi open platform PATCH http 80 | func DoHttpPatchApi(path protocol.OpenApiPath, headers map[string]string, data interface{}) ([]byte, int, error) { 81 | reqBody := new(bytes.Buffer) 82 | err := json.NewEncoder(reqBody).Encode(data) 83 | if err != nil { 84 | return nil, 0, fmt.Errorf("jsonEncodeError[%v]", err) 85 | } 86 | 87 | reqURL := GetOpenPlatformHost() + string(path) 88 | 89 | return DoHttp(http.MethodPatch, reqURL, headers, reqBody) 90 | } 91 | 92 | // DoHttpDeleteOApi open platform DELETE http 93 | func DoHttpDeleteOApi(path protocol.OpenApiPath, headers map[string]string, params map[string]string) ([]byte, int, error) { 94 | reqURL := GetOpenPlatformHost() + string(path) 95 | 96 | if params != nil && len(params) > 0 { 97 | m := make(url.Values) 98 | for k, v := range params { 99 | m.Set(k, v) 100 | } 101 | 102 | reqURL = reqURL + "?" + m.Encode() 103 | } 104 | 105 | reqBody := new(bytes.Buffer) 106 | return DoHttp(http.MethodDelete, reqURL, headers, reqBody) 107 | } 108 | 109 | func DoHttp(method string, url string, headers map[string]string, body *bytes.Buffer) ([]byte, int, error) { 110 | req, err := http.NewRequest(method, url, body) 111 | if err != nil { 112 | return nil, 0, fmt.Errorf("httpNewRequestError[%v]", err) 113 | } 114 | 115 | // http header 116 | if headers == nil { 117 | headers = map[string]string{"Content-Type": "application/json"} 118 | } 119 | for k, v := range headers { 120 | req.Header.Set(k, v) 121 | } 122 | 123 | client := &http.Client{} 124 | resp, err := client.Do(req) 125 | if resp != nil && resp.Body != nil { 126 | defer resp.Body.Close() 127 | } 128 | 129 | if err != nil { 130 | return nil, 0, fmt.Errorf("httpDoError[%v]", err) 131 | } 132 | 133 | respBody, err := ioutil.ReadAll(resp.Body) 134 | if err != nil { 135 | return nil, resp.StatusCode, fmt.Errorf("readRespBodyError[%v]", err) 136 | } 137 | 138 | return respBody, resp.StatusCode, nil 139 | } 140 | 141 | func NewHeaderToken(accessToken string) map[string]string { 142 | header := make(map[string]string) 143 | header["Authorization"] = fmt.Sprintf("Bearer %s", accessToken) 144 | header["Content-Type"] = "application/json" 145 | return header 146 | } 147 | 148 | func NewHeaderJson() map[string]string { 149 | header := make(map[string]string) 150 | header["Content-Type"] = "application/json" 151 | return header 152 | } 153 | -------------------------------------------------------------------------------- /SDK/common/log_interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | import ( 8 | "context" 9 | ) 10 | 11 | type LogInterface interface { 12 | Init(option interface{}) 13 | Flush() 14 | GetLogger(ctx context.Context) LogEntry 15 | } 16 | 17 | type LogEntry interface { 18 | Debugf(format string, args ...interface{}) 19 | Infof(format string, args ...interface{}) 20 | Warnf(format string, args ...interface{}) 21 | Errorf(format string, args ...interface{}) 22 | Fatalf(format string, args ...interface{}) 23 | 24 | Debug(args ...interface{}) 25 | Info(args ...interface{}) 26 | Warn(args ...interface{}) 27 | Error(args ...interface{}) 28 | Fatal(args ...interface{}) 29 | 30 | Debugln(args ...interface{}) 31 | Infoln(args ...interface{}) 32 | Warnln(args ...interface{}) 33 | Errorln(args ...interface{}) 34 | Fatalln(args ...interface{}) 35 | } 36 | 37 | var ( 38 | logger LogInterface 39 | ) 40 | 41 | // default demo: common.InitLogger(common.NewCommonLogger(), common.DefaultOption()) 42 | func InitLogger(log LogInterface, option interface{}) { 43 | logger = log 44 | logger.Init(option) 45 | } 46 | 47 | func FlushLogger() { 48 | if logger == nil { 49 | logger = NewCommonLogger() 50 | logger.Init(DefaultOption()) 51 | } 52 | logger.Flush() 53 | } 54 | 55 | func Logger(ctx context.Context) LogEntry { 56 | if logger == nil { 57 | logger = NewCommonLogger() 58 | logger.Init(DefaultOption()) 59 | } 60 | 61 | return logger.GetLogger(ctx) 62 | } 63 | -------------------------------------------------------------------------------- /SDK/common/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "io/ioutil" 11 | 12 | "github.com/rifflock/lfshook" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type CommonLoggerOption struct { 17 | Level logrus.Level 18 | TimestampFormat string 19 | FullTimestamp bool 20 | HighSpeedMode bool 21 | OnFile bool 22 | FilePathMap lfshook.PathMap 23 | } 24 | 25 | func DefaultOption() *CommonLoggerOption { 26 | return &CommonLoggerOption{ 27 | Level: logrus.DebugLevel, 28 | TimestampFormat: "2006-01-02 15:04:05.000", 29 | FullTimestamp: true, 30 | HighSpeedMode: false, 31 | OnFile: false, 32 | FilePathMap: lfshook.PathMap{ 33 | logrus.DebugLevel: "./default.log", 34 | logrus.InfoLevel: "./default.log", 35 | logrus.WarnLevel: "./default.log", 36 | logrus.ErrorLevel: "./default.log", 37 | }, 38 | } 39 | } 40 | 41 | type CommonLogger struct { 42 | KeyMap map[string]string 43 | } 44 | 45 | func NewCommonLogger() *CommonLogger { 46 | log := &CommonLogger{} 47 | log.RegistFieldName("request_id", "request_id") 48 | 49 | return log 50 | } 51 | 52 | func (l *CommonLogger) Init(op interface{}) { 53 | option, ok := op.(*CommonLoggerOption) 54 | if !ok { 55 | option = DefaultOption() 56 | } 57 | 58 | logrus.SetLevel(option.Level) 59 | 60 | customFormatter := new(logrus.TextFormatter) 61 | customFormatter.TimestampFormat = option.TimestampFormat 62 | customFormatter.FullTimestamp = option.FullTimestamp 63 | logrus.SetFormatter(customFormatter) 64 | 65 | if option.HighSpeedMode { 66 | logrus.StandardLogger().SetNoLock() 67 | logrus.SetOutput(ioutil.Discard) 68 | } 69 | 70 | if option.OnFile { 71 | fileHook := lfshook.NewHook(option.FilePathMap, customFormatter) 72 | logrus.AddHook(fileHook) 73 | } 74 | } 75 | 76 | // FlushLogger Flush 77 | func (l *CommonLogger) Flush() { 78 | 79 | } 80 | 81 | func (l *CommonLogger) GetLogger(ctx context.Context) LogEntry { 82 | fields := logrus.Fields{} 83 | if ctx != nil { 84 | for ctxKey, logKey := range l.KeyMap { 85 | if contextValue := ctx.Value(ctxKey); contextValue != nil { 86 | fields[logKey] = fmt.Sprint(contextValue) 87 | } 88 | } 89 | } 90 | 91 | return logrus.WithFields(fields) 92 | } 93 | 94 | func (l *CommonLogger) RegistFieldName(ctxFieldName, logFieldName string) (oldLogFieldName string) { 95 | if l.KeyMap == nil { 96 | l.KeyMap = make(map[string]string) 97 | } 98 | 99 | if logFieldName, ok := l.KeyMap[ctxFieldName]; ok { 100 | return logFieldName 101 | } 102 | 103 | l.KeyMap[ctxFieldName] = logFieldName 104 | return "" 105 | } 106 | -------------------------------------------------------------------------------- /SDK/common/util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | import ( 8 | "context" 9 | "crypto/md5" 10 | "encoding/hex" 11 | "io" 12 | "runtime" 13 | "runtime/debug" 14 | ) 15 | 16 | func RecoverPanic(ctx context.Context) { 17 | if err := recover(); err != nil { 18 | pc, file, line, _ := runtime.Caller(3) 19 | f := runtime.FuncForPC(pc) 20 | Logger(ctx).Errorf("Recover: funcName=%v, file=%s, line=%d, panic:%v, stack info:%v", 21 | f.Name(), file, line, err, string(debug.Stack())) 22 | } 23 | } 24 | 25 | func GetMd5(src io.Reader) string { 26 | hash := md5.New() 27 | io.Copy(hash, src) 28 | return hex.EncodeToString(hash.Sum(nil)) 29 | } 30 | 31 | func GetMd5ByBytes(src []byte) string { 32 | hash := md5.New() 33 | hash.Write(src) 34 | return hex.EncodeToString(hash.Sum(nil)) 35 | } 36 | -------------------------------------------------------------------------------- /SDK/event/bot_recv_msg.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package event 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "strings" 12 | 13 | "github.com/bitly/go-simplejson" 14 | "github.com/larksuite/botframework-go/SDK/common" 15 | "github.com/larksuite/botframework-go/SDK/protocol" 16 | ) 17 | 18 | type HandlerBotMsg func(ctx context.Context, msg *protocol.BotRecvMsg) error 19 | 20 | // CommandHandlerManager cmd --> handler 21 | type CommandHandlerManager struct { 22 | mapHandler map[string]map[string]HandlerBotMsg 23 | } 24 | 25 | func (p *CommandHandlerManager) Set(appID string, cmdName string, handler HandlerBotMsg) { 26 | if handler == nil { 27 | return 28 | } 29 | 30 | if _, ok := p.mapHandler[appID]; !ok { 31 | p.mapHandler[appID] = make(map[string]HandlerBotMsg, 0) 32 | } 33 | 34 | cmdName = strings.ToLower(cmdName) 35 | p.mapHandler[appID][cmdName] = handler 36 | } 37 | 38 | func (p *CommandHandlerManager) Get(appID string, cmdName string) (HandlerBotMsg, error) { 39 | cmdName = strings.ToLower(cmdName) 40 | 41 | if _, ok := p.mapHandler[appID]; !ok { 42 | return nil, fmt.Errorf("botRecvMsg appid[%s] has not been registered", appID) 43 | } 44 | if _, ok := p.mapHandler[appID][cmdName]; !ok { 45 | return nil, fmt.Errorf("botRecvMsg cmdName[%s] has not been registered in appid[%s]", cmdName, appID) 46 | } 47 | 48 | return p.mapHandler[appID][cmdName], nil 49 | } 50 | 51 | var cmdHandler *CommandHandlerManager 52 | 53 | func init() { 54 | cmdHandler = &CommandHandlerManager{mapHandler: make(map[string]map[string]HandlerBotMsg, 0)} 55 | } 56 | 57 | // BotRecvMsgRegister appid+cmd --> handler 58 | func BotRecvMsgRegister(appID string, cmdName string, handler HandlerBotMsg) error { 59 | if appID == "" || cmdName == "" { 60 | return common.ErrBotRecvMsgRegister.ErrorWithExtStr("appID/cmdName is empty") 61 | } 62 | if handler == nil { 63 | return common.ErrBotRecvMsgRegister.ErrorWithExtStr("action handler is nil") 64 | } 65 | 66 | cmdHandler.Set(appID, cmdName, handler) 67 | return nil 68 | } 69 | 70 | // BotRecvMsgHandler callback botRecvMsg 71 | func BotRecvMsgHandler(ctx context.Context, data []byte) error { 72 | // get msg_type and app_id 73 | jsonMsg, err := simplejson.NewJson(data) 74 | if err != nil { 75 | return common.ErrJsonUnmarshal.ErrorWithExtErr(err) 76 | } 77 | msgType, err := jsonMsg.Get("msg_type").String() 78 | if err != nil { 79 | return common.ErrBotRecvMsgMsgTypeJson.ErrorWithExtErr(err) 80 | } 81 | appID, err := jsonMsg.Get("app_id").String() 82 | if err != nil || appID == "" { 83 | return common.ErrBotRecvMsgAppIDJson.ErrorWithExtErr(err) 84 | } 85 | 86 | var msg protocol.BotRecvMsg 87 | msg.AppID = appID 88 | msg.MsgType = msgType 89 | 90 | var textWithoutAtBot string 91 | switch msgType { 92 | case protocol.EventMsgTypeText: 93 | msgEvent := &protocol.TextMsgEvent{} 94 | err := json.Unmarshal(data, msgEvent) 95 | if err != nil { 96 | return common.ErrJsonUnmarshal.ErrorWithExtErr(err) 97 | } 98 | msg.TenantKey = msgEvent.TenantKey 99 | msg.RootID = msgEvent.RootID 100 | msg.ParentID = msgEvent.ParentID 101 | msg.OpenChatID = msgEvent.OpenChatID 102 | msg.ChatType = msgEvent.ChatType 103 | msg.OpenID = msgEvent.OpenID 104 | msg.OpenMessageID = msgEvent.OpenMessageID 105 | msg.OriData = msgEvent 106 | 107 | textWithoutAtBot = msgEvent.TextWithoutAtBot 108 | case protocol.EventMsgTypePost: 109 | msgEvent := &protocol.PostMsgEvent{} 110 | err := json.Unmarshal(data, msgEvent) 111 | if err != nil { 112 | return common.ErrJsonUnmarshal.ErrorWithExtErr(err) 113 | } 114 | msg.TenantKey = msgEvent.TenantKey 115 | msg.RootID = msgEvent.RootID 116 | msg.ParentID = msgEvent.ParentID 117 | msg.OpenChatID = msgEvent.OpenChatID 118 | msg.ChatType = msgEvent.ChatType 119 | msg.OpenID = msgEvent.OpenID 120 | msg.OpenMessageID = msgEvent.OpenMessageID 121 | msg.OriData = msgEvent 122 | 123 | textWithoutAtBot = "" 124 | case protocol.EventMsgTypeImage: 125 | msgEvent := &protocol.ImageMsgEvent{} 126 | err := json.Unmarshal(data, msgEvent) 127 | if err != nil { 128 | return common.ErrJsonUnmarshal.ErrorWithExtErr(err) 129 | } 130 | msg.TenantKey = msgEvent.TenantKey 131 | msg.RootID = msgEvent.RootID 132 | msg.ParentID = msgEvent.ParentID 133 | msg.OpenChatID = msgEvent.OpenChatID 134 | msg.ChatType = msgEvent.ChatType 135 | msg.OpenID = msgEvent.OpenID 136 | msg.OpenMessageID = msgEvent.OpenMessageID 137 | msg.OriData = msgEvent 138 | 139 | textWithoutAtBot = "" 140 | case protocol.EventMsgTypeMergeForward: 141 | msgEvent := &protocol.MergeForwardMsgEvent{} 142 | err := json.Unmarshal(data, msgEvent) 143 | if err != nil { 144 | return common.ErrJsonUnmarshal.ErrorWithExtErr(err) 145 | } 146 | msg.TenantKey = msgEvent.TenantKey 147 | msg.RootID = msgEvent.RootID 148 | msg.ParentID = msgEvent.ParentID 149 | msg.OpenChatID = msgEvent.OpenChatID 150 | msg.ChatType = msgEvent.ChatType 151 | msg.OpenID = msgEvent.OpenID 152 | msg.OpenMessageID = msgEvent.OpenMessageID 153 | msg.OriData = msgEvent 154 | 155 | textWithoutAtBot = "" 156 | default: 157 | textWithoutAtBot = "" 158 | } 159 | 160 | msg.TextParam = textWithoutAtBot 161 | 162 | //get cmd 163 | var cmd string 164 | s := strings.Split(strings.Trim(msg.TextParam, " "), " ") 165 | if len(s) > 1 { 166 | cmd = strings.ToLower(s[0]) 167 | msg.TextParam = strings.Trim(strings.Join(s[1:], " "), " ") 168 | } else if len(s) > 0 { 169 | cmd = strings.ToLower(s[0]) 170 | msg.TextParam = "" 171 | } else { 172 | cmd = protocol.CmdDefault 173 | } 174 | 175 | //get handler 176 | handler, err := cmdHandler.Get(appID, cmd) 177 | if err != nil { 178 | if protocol.CmdDefault != cmd { 179 | cmd = protocol.CmdDefault 180 | msg.TextParam = textWithoutAtBot 181 | 182 | handler, err = cmdHandler.Get(appID, cmd) 183 | } 184 | 185 | if err != nil { 186 | return common.ErrBotRecvMsgHandlerNoFound.ErrorWithExtErr(err) 187 | } 188 | } 189 | 190 | err = handler(ctx, &msg) 191 | if err != nil { 192 | return common.ErrBotRecvMsgHandlerFailed.ErrorWithExtErr(err) 193 | } 194 | 195 | return nil 196 | } 197 | -------------------------------------------------------------------------------- /SDK/event/card.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package event 6 | 7 | import ( 8 | "context" 9 | "crypto/sha1" 10 | "encoding/json" 11 | "fmt" 12 | "strings" 13 | 14 | "github.com/larksuite/botframework-go/SDK/appconfig" 15 | "github.com/larksuite/botframework-go/SDK/common" 16 | "github.com/larksuite/botframework-go/SDK/protocol" 17 | ) 18 | 19 | type ActionMethod func(ctx context.Context, cardCallback *protocol.CardCallbackForm) (*protocol.CardForm, error) 20 | 21 | type ActionHandlerManager struct { 22 | mapHandler map[string]map[string]ActionMethod // appID => method => func 23 | ignoreSign map[string]bool 24 | } 25 | 26 | func (a *ActionHandlerManager) Set(appID string, method string, v ActionMethod) { 27 | if v == nil { 28 | return 29 | } 30 | 31 | if m, ok := a.mapHandler[appID]; !ok || m == nil { 32 | a.mapHandler[appID] = make(map[string]ActionMethod, 0) 33 | } 34 | 35 | a.mapHandler[appID][method] = v 36 | } 37 | 38 | func (a *ActionHandlerManager) Get(appID string, method string) (ActionMethod, error) { 39 | if _, ok := a.mapHandler[appID]; !ok { 40 | return nil, fmt.Errorf("getCardActionHandler appid[%s] has not been registered", appID) 41 | } 42 | if _, ok := a.mapHandler[appID][method]; !ok { 43 | return nil, fmt.Errorf("getCardActionHandler method[%s] has not been registered in appid[%s]", method, appID) 44 | } 45 | 46 | return a.mapHandler[appID][method], nil 47 | } 48 | 49 | var cardHandler *ActionHandlerManager 50 | 51 | func init() { 52 | cardHandler = &ActionHandlerManager{ 53 | mapHandler: make(map[string]map[string]ActionMethod, 0), 54 | ignoreSign: make(map[string]bool, 0), 55 | } 56 | } 57 | 58 | func IgnoreSign(appid string, ignore bool) { 59 | cardHandler.ignoreSign[appid] = ignore 60 | } 61 | 62 | func CardRegister(appID string, method string, handler ActionMethod) error { 63 | if appID == "" || method == "" { 64 | return common.ErrCardMethodRegister.ErrorWithExtStr("method/appID is empty") 65 | } 66 | if handler == nil { 67 | return common.ErrCardMethodRegister.ErrorWithExtStr("action handler is nil") 68 | } 69 | 70 | cardHandler.Set(appID, method, handler) 71 | 72 | return nil 73 | } 74 | 75 | func CardCallBack(ctx context.Context, appID string, header map[string]string, body []byte) (*protocol.CardForm, string, error) { 76 | // check params 77 | if appID == "" || len(header) == 0 || len(body) == 0 { 78 | return nil, "", common.ErrCardParams.ErrorWithExtStr("callBack params is empty") 79 | } 80 | 81 | // check init 82 | if cardHandler == nil { 83 | return nil, "", common.ErrCardManagerNotInit.Error() 84 | } 85 | 86 | // get app config 87 | appConf, err := appconfig.GetConfig(appID) 88 | if err != nil { 89 | return nil, "", common.ErrAppConfNotFound.ErrorWithExtErr(err) 90 | } 91 | 92 | // challenge event 93 | f := &protocol.CardChallenge{} 94 | err = json.Unmarshal(body, f) 95 | if err != nil { 96 | return nil, "", common.ErrJsonUnmarshal.ErrorWithExtErr(fmt.Errorf("challenge error[%v]", err)) 97 | } 98 | 99 | // is challenge callback 100 | if len(f.Challenge) != 0 { 101 | if f.Token != appConf.VerifyToken { 102 | return nil, "", common.ErrCardVeriTokenInvalid.Error() 103 | } 104 | 105 | return nil, f.Challenge, nil 106 | } 107 | 108 | // action callback 109 | callback := &protocol.CardCallbackForm{} 110 | err = json.Unmarshal(body, callback) 111 | if err != nil { 112 | return nil, "", common.ErrJsonUnmarshal.ErrorWithExtErr(fmt.Errorf("card callback error[%v]", err)) 113 | } 114 | 115 | // check signature 116 | if !cardHandler.ignoreSign[appID] { 117 | err = verifySignature(ctx, appConf.VerifyToken, header, body) 118 | if err != nil { 119 | return nil, "", common.ErrCardSignatureInvalid.Error() 120 | } 121 | } 122 | 123 | var ok bool 124 | var method string 125 | if method, ok = callback.Action.Value["method"]; !ok { 126 | return nil, "", common.ErrCardWithoutMethod.ErrorWithExtErr(err) 127 | } 128 | if _, ok := callback.Action.Value["sid"]; !ok { 129 | return nil, "", common.ErrCardWithoutSessionID.ErrorWithExtErr(err) 130 | } 131 | 132 | // check meta 133 | meta := &protocol.Meta{} 134 | if metaData, ok := callback.Action.Value["meta"]; !ok { 135 | meta = nil 136 | } else { 137 | err = json.Unmarshal([]byte(metaData), meta) 138 | if err != nil { 139 | return nil, "", common.ErrCardMetaInvalid.ErrorWithExtErr(fmt.Errorf("meta jsonUnmarshalError[%v]", err)) 140 | } 141 | } 142 | 143 | handler, err := cardHandler.Get(appID, method) 144 | if err != nil { 145 | return nil, "", common.ErrCardMethodRegister.ErrorWithExtErr(err) 146 | } 147 | if handler == nil { 148 | return nil, "", common.ErrCardHandlerIsNil.ErrorWithExtStr(fmt.Sprintf("method[%s]", method)) 149 | } 150 | 151 | card, err := handler(ctx, callback) 152 | if err != nil { 153 | return nil, "", common.ErrCardHandlerFailed.ErrorWithExtErr(err) 154 | } 155 | 156 | return card, "", nil 157 | } 158 | 159 | // check action Signature 160 | func verifySignature(ctx context.Context, verifyToken string, header map[string]string, body []byte) error { 161 | timestamp := header["X-Lark-Request-Timestamp"] 162 | nonce := header["X-Lark-Request-Nonce"] 163 | sig := header["X-Lark-Signature"] 164 | 165 | targetSig := genPostRequestSignature(nonce, timestamp, string(body), verifyToken) 166 | if sig == targetSig { 167 | return nil 168 | } 169 | 170 | return fmt.Errorf("signature invalid") 171 | } 172 | 173 | func genPostRequestSignature(nonce string, timestamp string, body string, token string) string { 174 | var b strings.Builder 175 | b.WriteString(timestamp) 176 | b.WriteString(nonce) 177 | b.WriteString(token) 178 | b.WriteString(body) 179 | 180 | bs := []byte(b.String()) 181 | h := sha1.New() 182 | h.Write(bs) 183 | bs = h.Sum(nil) 184 | 185 | return fmt.Sprintf("%x", bs) 186 | } 187 | -------------------------------------------------------------------------------- /SDK/event/event.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package event 6 | 7 | import ( 8 | "context" 9 | "crypto/aes" 10 | "crypto/cipher" 11 | "crypto/sha256" 12 | "encoding/base64" 13 | "encoding/json" 14 | "errors" 15 | "fmt" 16 | "strings" 17 | 18 | "github.com/bitly/go-simplejson" 19 | "github.com/larksuite/botframework-go/SDK/appconfig" 20 | "github.com/larksuite/botframework-go/SDK/auth" 21 | "github.com/larksuite/botframework-go/SDK/common" 22 | "github.com/larksuite/botframework-go/SDK/protocol" 23 | ) 24 | 25 | type EventHandler func(ctx context.Context, eventBody []byte) error 26 | 27 | type EventHandlerManager struct { 28 | mapHandler map[string]map[string]EventHandler //[app_id][eventType][function] 29 | } 30 | 31 | func (a *EventHandlerManager) Set(appID, eventTypeList string, eventHandler EventHandler) { 32 | if eventHandler == nil { 33 | return 34 | } 35 | 36 | if m, ok := a.mapHandler[appID]; !ok || m == nil { 37 | a.mapHandler[appID] = make(map[string]EventHandler, 0) 38 | } 39 | 40 | s := strings.Split(strings.Trim(eventTypeList, " "), ",") 41 | for _, eventType := range s { 42 | a.mapHandler[appID][eventType] = eventHandler 43 | } 44 | } 45 | 46 | func (a *EventHandlerManager) Get(appID string, eventType string) (EventHandler, error) { 47 | if _, ok := a.mapHandler[appID]; !ok { 48 | return nil, common.ErrEventAppIDUnregistered.ErrorWithExtStr(fmt.Sprintf("appid[%s]eventType[%s]", appID, eventType)) 49 | } 50 | if _, ok := a.mapHandler[appID][eventType]; !ok { 51 | return nil, common.ErrEventTypeUnregistered.ErrorWithExtStr(fmt.Sprintf("appid[%s]eventType[%s]", appID, eventType)) 52 | } 53 | if a.mapHandler[appID][eventType] == nil { 54 | return nil, common.ErrEventHandlerIsNil.ErrorWithExtStr(fmt.Sprintf("appid[%s]eventType[%s]", appID, eventType)) 55 | } 56 | 57 | return a.mapHandler[appID][eventType], nil 58 | } 59 | 60 | var eventManager *EventHandlerManager 61 | 62 | func init() { 63 | eventManager = &EventHandlerManager{ 64 | mapHandler: make(map[string]map[string]EventHandler, 0), 65 | } 66 | } 67 | 68 | func EventRegister(appID, eventTypeList string, eventHandler EventHandler) error { 69 | // 参数校验 70 | if appID == "" || eventTypeList == "" || eventHandler == nil { 71 | return common.ErrEventTypeRegister.ErrorWithExtStr( 72 | fmt.Sprintf("params is empty or nil. AppID[%s]EventType[%s]HandlerIsNil[%t]", appID, eventTypeList, eventHandler == nil)) 73 | } 74 | 75 | eventManager.Set(appID, eventTypeList, eventHandler) 76 | 77 | return nil 78 | } 79 | 80 | func EventCallback(ctx context.Context, body string, appID string) (string, error) { 81 | // check params 82 | if body == "" || appID == "" { 83 | return "", common.ErrEventParams.Error() 84 | } 85 | 86 | // get app config 87 | appConf, err := appconfig.GetConfig(appID) 88 | if err != nil { 89 | return "", common.ErrAppConfNotFound.ErrorWithExtErr(err) 90 | } 91 | 92 | // decrypt data 93 | var content string 94 | if appConf.EncryptKey != "" { 95 | content, err = eventDataDecrypter(body, appConf.EncryptKey) 96 | if err != nil { 97 | return "", common.ErrEventDecrypt.ErrorWithExtErr(err) 98 | } 99 | } else { 100 | content = body 101 | } 102 | 103 | var callbackBase protocol.CallbackBase 104 | err = json.Unmarshal([]byte(content), &callbackBase) 105 | if err != nil { 106 | return "", common.ErrEventGetBase.ErrorWithExtErr(err) 107 | } 108 | 109 | // check token 110 | if callbackBase.Token != appConf.VerifyToken { 111 | return "", common.ErrEventVeriToken.ErrorWithExtStr( 112 | fmt.Sprintf("eventVTokenSize[%d]confVTokenSize[%d]", len(callbackBase.Token), len(appConf.VerifyToken))) 113 | } 114 | 115 | // dispatch event type 116 | switch callbackBase.Type { 117 | case protocol.EventChallenge: 118 | if appConf.AppType == protocol.ISVApp { 119 | auth.ReSendAppTicket(ctx, appConf.AppID, appConf.AppSecret) 120 | } 121 | 122 | return callbackBase.Challenge, nil 123 | case protocol.EventCallback: 124 | err = eventCallbackHandler(ctx, appID, content) 125 | if err != nil { 126 | return "", err 127 | } 128 | default: 129 | return "", common.ErrEventTypeUnknown.ErrorWithExtStr(fmt.Sprintf("appid[%s]type[%s]", appID, callbackBase.Type)) 130 | } 131 | 132 | return "", nil 133 | } 134 | 135 | func eventCallbackHandler(ctx context.Context, appID, content string) error { 136 | // get type and app_id 137 | jsonBody, err := simplejson.NewJson([]byte(content)) 138 | if err != nil { 139 | return common.ErrJsonUnmarshal.ErrorWithExtErr(err) 140 | } 141 | jsonEvent := jsonBody.Get("event") 142 | if jsonEvent.Interface() == nil { 143 | return common.ErrEventGetJsonEvent.Error() 144 | } 145 | eventType, err := jsonEvent.Get("type").String() 146 | if err != nil { 147 | return common.ErrEventGetJsonType.ErrorWithExtErr(err) 148 | } 149 | eventAppID, err := jsonEvent.Get("app_id").String() 150 | if err != nil { 151 | return common.ErrEventGetJsonAppID.ErrorWithExtErr(err) 152 | } 153 | 154 | // check params 155 | if eventAppID != appID { 156 | return common.ErrEventAppIDNotMatch.Error() 157 | } 158 | 159 | // dispatch event type 160 | handler, err := eventManager.Get(appID, eventType) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | byteEvent, err := jsonEvent.MarshalJSON() 166 | if err != nil { 167 | return common.ErrJsonMarshal.ErrorWithExtErr(err) 168 | } 169 | 170 | err = handler(ctx, byteEvent) 171 | if err != nil { 172 | return common.ErrEventHandlerFailed.ErrorWithExtErr(err) 173 | } 174 | 175 | return nil 176 | } 177 | 178 | func eventDataDecrypter(encryptData, keyStr string) (string, error) { 179 | type AESMsg struct { 180 | Encrypt string `json:"encrypt"` 181 | } 182 | var encrypt AESMsg 183 | err := json.Unmarshal([]byte(encryptData), &encrypt) 184 | if err != nil { 185 | return "", fmt.Errorf("dataDecrypter jsonUnmarshalError[%v]", err) 186 | } 187 | 188 | buf, err := base64.StdEncoding.DecodeString(encrypt.Encrypt) 189 | if err != nil { 190 | return "", fmt.Errorf("base64StdEncode Error[%v]", err) 191 | } 192 | 193 | key := sha256.Sum256([]byte(keyStr)) 194 | 195 | block, err := aes.NewCipher(key[:sha256.Size]) 196 | if err != nil { 197 | return "", fmt.Errorf("AESNewCipher Error[%v]", err) 198 | } 199 | 200 | if len(buf) < aes.BlockSize { 201 | return "", errors.New("ciphertext too short") 202 | } 203 | iv := buf[:aes.BlockSize] 204 | buf = buf[aes.BlockSize:] 205 | 206 | // CBC mode always works in whole blocks. 207 | if len(buf)%aes.BlockSize != 0 { 208 | return "", errors.New("ciphertext is not a multiple of the block size") 209 | } 210 | 211 | mode := cipher.NewCBCDecrypter(block, iv) 212 | mode.CryptBlocks(buf, buf) 213 | 214 | n := strings.Index(string(buf), "{") 215 | if n == -1 { 216 | n = 0 217 | } 218 | m := strings.LastIndex(string(buf), "}") 219 | if m == -1 { 220 | m = len(buf) - 1 221 | } 222 | 223 | return string(buf[n : m+1]), nil 224 | } 225 | -------------------------------------------------------------------------------- /SDK/message/image.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package message 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "mime/multipart" 15 | "net/http" 16 | "os" 17 | "strings" 18 | 19 | lru "github.com/hashicorp/golang-lru" 20 | "github.com/larksuite/botframework-go/SDK/auth" 21 | "github.com/larksuite/botframework-go/SDK/common" 22 | "github.com/larksuite/botframework-go/SDK/protocol" 23 | ) 24 | 25 | const ( 26 | localCacheSize = 1000000 27 | ) 28 | 29 | var ( 30 | LruCache *lru.Cache 31 | ) 32 | 33 | func init() { 34 | var err error 35 | LruCache, err = lru.New(localCacheSize) 36 | if err != nil { 37 | panic(err) 38 | } 39 | } 40 | 41 | // GetImageKey: get imagekey, image_type = message 42 | func GetImageKey(ctx context.Context, tenantKey, appID, url, path string) (string, error) { 43 | if url == "" && path == "" { 44 | return "", common.ErrImageParams.Error() 45 | } 46 | 47 | // get from cache 48 | var cacheKey string 49 | if path != "" { 50 | cacheKey = path 51 | } else { 52 | cacheKey = url 53 | } 54 | 55 | if v, ok := LruCache.Get(cacheKey); ok { 56 | imageKey := v.(string) 57 | if imageKey != "" { 58 | return imageKey, nil 59 | } 60 | LruCache.Remove(cacheKey) 61 | } 62 | 63 | // upload image 64 | imageType := protocol.MessageImageType 65 | var body *bytes.Buffer 66 | var contentType string 67 | var err error 68 | if path != "" { 69 | body, contentType, err = GenBinaryImageByPath(path, imageType) 70 | if err != nil { 71 | return "", common.ErrGenBinImageFailed.ErrorWithExtErr(err) 72 | } 73 | } else { 74 | body, contentType, err = GenBinaryImageByUrl(url, imageType) 75 | if err != nil { 76 | return "", common.ErrGenBinImageFailed.ErrorWithExtErr(err) 77 | } 78 | } 79 | 80 | rspData, err := UploadImage(ctx, tenantKey, appID, body, contentType) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | addLruCache(cacheKey, rspData.Data.ImageKey) 86 | 87 | return rspData.Data.ImageKey, nil 88 | } 89 | 90 | func GetImageBinData(ctx context.Context, tenantKey, appID, imageKey string) ([]byte, error) { 91 | if appID == "" || imageKey == "" { 92 | return nil, common.ErrGetImageBinDataParams.ErrorWithExtStr("param is empty") 93 | } 94 | 95 | accessToken, err := auth.GetTenantAccessToken(ctx, tenantKey, appID) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | rspBytes, httpCode, err := common.DoHttpGetOApi(protocol.GetImagePath, 101 | map[string]string{"Authorization": fmt.Sprintf("Bearer %s", accessToken)}, 102 | map[string]string{"image_key": imageKey}, 103 | ) 104 | if err != nil { 105 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 106 | } 107 | 108 | if httpCode != common.HTTPCodeOK { 109 | return nil, common.ErrHttpCode.ErrorWithExtStr(fmt.Sprintf("httpCode[%d]httpRspBody[%s]", httpCode, string(rspBytes))) 110 | } 111 | 112 | return rspBytes, nil 113 | } 114 | 115 | func UploadImage(ctx context.Context, tenantKey, appID string, body *bytes.Buffer, contentType string) (*protocol.UpLoadImageResponse, error) { 116 | accessToken, err := auth.GetTenantAccessToken(ctx, tenantKey, appID) 117 | if err != nil { 118 | return nil, err 119 | } 120 | authorization := fmt.Sprintf("Bearer %s", accessToken) 121 | header := map[string]string{"Authorization": authorization, "Content-Type": contentType} 122 | 123 | reqURL := common.GetOpenPlatformHost() + string(protocol.UploadImagePath) 124 | rspBytes, _, err := common.DoHttp(common.HTTPMethodPost, reqURL, header, body) 125 | if err != nil { 126 | return nil, common.ErrOpenApiFailed.ErrorWithExtErr(err) 127 | } 128 | 129 | rspData := &protocol.UpLoadImageResponse{} 130 | err = json.Unmarshal(rspBytes, &rspData) 131 | if err != nil { 132 | return nil, common.ErrJsonUnmarshal.ErrorWithExtErr(err) 133 | } 134 | 135 | if rspData.Code != 0 { 136 | auth.CheckAndDisableTenantToken(ctx, appID, tenantKey, rspData.Code) 137 | return rspData, common.ErrOpenApiReturnError.ErrorWithExtStr(fmt.Sprintf("[code:%d msg:%s]", rspData.Code, rspData.Msg)) 138 | } 139 | 140 | return rspData, nil 141 | 142 | } 143 | 144 | func GenBinaryImageByPath(path string, imageType protocol.ImageType) (*bytes.Buffer, string, error) { 145 | file, err := os.Open(path) 146 | if err != nil { 147 | return nil, "", fmt.Errorf("open file error[%v]", err) 148 | } 149 | defer file.Close() 150 | 151 | buffer := &bytes.Buffer{} 152 | writer := multipart.NewWriter(buffer) 153 | imageFile, err := writer.CreateFormFile("image", path) 154 | if err != nil { 155 | return nil, "", fmt.Errorf("create form file error[%v]", err) 156 | } 157 | _, err = io.Copy(imageFile, file) 158 | if err != nil { 159 | return nil, "", fmt.Errorf("io copy error[%v]", err) 160 | } 161 | 162 | writer.WriteField("image_type", string(imageType)) 163 | err = writer.Close() 164 | if err != nil { 165 | return nil, "", fmt.Errorf("writer close error[%v]", err) 166 | } 167 | contentType := writer.FormDataContentType() 168 | 169 | return buffer, contentType, nil 170 | } 171 | 172 | func GenBinaryImageByUrl(url string, imageType protocol.ImageType) (*bytes.Buffer, string, error) { 173 | imageBytes, err := downloadImage(url) 174 | if err != nil { 175 | return nil, "", fmt.Errorf("download image error[%v]", err) 176 | } 177 | 178 | path := strings.Split(url, "/") 179 | name := path[0] 180 | if len(path) > 1 { 181 | name = path[len(path)-1] 182 | } 183 | 184 | buffer := &bytes.Buffer{} 185 | writer := multipart.NewWriter(buffer) 186 | part, err := writer.CreateFormFile("image", name) 187 | if err != nil { 188 | return nil, "", fmt.Errorf("create form file error[%v]", err) 189 | } 190 | _, err = io.Copy(part, bytes.NewReader(imageBytes)) 191 | if err != nil { 192 | return nil, "", fmt.Errorf("io copy error[%v]", err) 193 | } 194 | 195 | writer.WriteField("image_type", string(imageType)) 196 | err = writer.Close() 197 | if err != nil { 198 | return nil, "", fmt.Errorf("writer close error[%v]", err) 199 | } 200 | contentType := writer.FormDataContentType() 201 | 202 | return buffer, contentType, nil 203 | } 204 | 205 | func downloadImage(url string) ([]byte, error) { 206 | resp, err := http.Get(url) 207 | if err != nil { 208 | return nil, fmt.Errorf("http get error[%v]", err) 209 | } 210 | defer resp.Body.Close() 211 | return ioutil.ReadAll(resp.Body) 212 | } 213 | 214 | func addLruCache(key string, value interface{}) { 215 | if value != "" { 216 | LruCache.Add(key, value) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /SDK/message/image_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package message_test 6 | 7 | import ( 8 | "context" 9 | "io/ioutil" 10 | "os" 11 | "sync" 12 | "testing" 13 | 14 | "github.com/larksuite/botframework-go/SDK/appconfig" 15 | "github.com/larksuite/botframework-go/SDK/message" 16 | ) 17 | 18 | var ( 19 | once sync.Once 20 | appConf *appconfig.AppConfig 21 | tenantKey string 22 | chatID string 23 | openID string 24 | userID string 25 | imageKey string 26 | messageID string 27 | ) 28 | 29 | func InitTestParams() { 30 | once.Do(func() { 31 | appConf = &appconfig.AppConfig{ 32 | AppID: os.Getenv("appid"), 33 | AppSecret: os.Getenv("appsecret"), 34 | VerifyToken: os.Getenv("verifytoken"), 35 | EncryptKey: os.Getenv("encryptkey"), 36 | AppType: os.Getenv("apptype"), 37 | } 38 | 39 | tenantKey = os.Getenv("tenantkey") 40 | chatID = os.Getenv("chatid") 41 | openID = os.Getenv("openid") 42 | userID = os.Getenv("userid") 43 | 44 | imageKey = os.Getenv("imagekey") 45 | 46 | messageID = os.Getenv("messageid") 47 | 48 | appconfig.Init(*appConf) 49 | }) 50 | } 51 | 52 | func TestGetImageKey(t *testing.T) { 53 | c := context.Background() 54 | InitTestParams() 55 | 56 | // by path 57 | path := "../../demo/source/lark0.jpg" 58 | key, err := message.GetImageKey(c, tenantKey, appConf.AppID, "", path) 59 | if err != nil { 60 | t.Errorf("GetImageKeyByPath failed: %v", err) 61 | } else { 62 | t.Logf("GetImageKeyByPath: image_key = %s", key) 63 | } 64 | 65 | // by url 66 | url := "https://s0.pstatp.com/ee/lark-open/web/static/apply.226f11cb.png" 67 | key, err = message.GetImageKey(c, tenantKey, appConf.AppID, url, "") 68 | if err != nil { 69 | t.Errorf("GetImageKeyByURL failed: %v", err) 70 | } else { 71 | t.Logf("GetImageKeyByURL: image_key = %s", key) 72 | } 73 | } 74 | 75 | func TestGetImageBinData(t *testing.T) { 76 | c := context.Background() 77 | InitTestParams() 78 | 79 | data, err := message.GetImageBinData(c, tenantKey, appConf.AppID, imageKey) 80 | if err != nil { 81 | t.Errorf("GetImageBinData failed: %v", err) 82 | } else { 83 | t.Logf("GetImageBinData: succ") 84 | } 85 | 86 | err = ioutil.WriteFile("./temp.jpg", data, 0644) 87 | if err != nil { 88 | t.Errorf("WriteFile failed: %v", err) 89 | } else { 90 | t.Logf("WriteFile: succ") 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /SDK/message/richtext_builder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package message 6 | 7 | import ( 8 | "github.com/larksuite/botframework-go/SDK/protocol" 9 | ) 10 | 11 | func NewRichTextForm(title *string, content *protocol.RichTextContent) *protocol.RichTextForm { 12 | richForm := &protocol.RichTextForm{} 13 | richForm.Title = *title 14 | richForm.Content = content 15 | return richForm 16 | } 17 | 18 | func NewRichTextContent() *protocol.RichTextContent { 19 | return &protocol.RichTextContent{} 20 | } 21 | 22 | func NewRichTextElementForm() *protocol.RichTextElementForm { 23 | return &protocol.RichTextElementForm{} 24 | } 25 | 26 | func NewTextTag(text string, unURLEncode bool, lines int32) *protocol.RichTextElementForm { 27 | var textTag protocol.RichTextElementForm 28 | textTag.Tag = "text" 29 | textTag.Text = &text 30 | textTag.UnEscape = unURLEncode 31 | textTag.Lines = &lines 32 | return &textTag 33 | } 34 | 35 | func NewATag(text string, unURLEncode bool, href string) *protocol.RichTextElementForm { 36 | var textTag protocol.RichTextElementForm 37 | textTag.Tag = "a" 38 | textTag.Text = &text 39 | textTag.UnEscape = unURLEncode 40 | textTag.Href = href 41 | return &textTag 42 | } 43 | 44 | func NewAtTag(text, userID string) *protocol.RichTextElementForm { 45 | var atTag protocol.RichTextElementForm 46 | atTag.Tag = "at" 47 | atTag.Text = &text 48 | atTag.UserID = userID 49 | return &atTag 50 | } 51 | 52 | // go can get imageKey by call the function "GetImageKey" 53 | func NewImageTag(imageKey string, height int32, width int32) *protocol.RichTextElementForm { 54 | 55 | var imageTag protocol.RichTextElementForm 56 | imageTag.Tag = "img" 57 | imageTag.ImageKey = imageKey 58 | imageTag.Height = height 59 | imageTag.Width = width 60 | return &imageTag 61 | } 62 | 63 | func checkPostContent(content map[protocol.Language]*protocol.RichTextForm) bool { 64 | for _, v := range content { 65 | if !checkRichTextContent(*v) { 66 | return false 67 | } 68 | } 69 | return true 70 | } 71 | 72 | func checkRichTextContent(richTextForm protocol.RichTextForm) bool { 73 | for _, RichTextElementForms := range *richTextForm.Content { 74 | for _, richTextElementForm := range RichTextElementForms { 75 | if richTextElementForm.Tag == "img" && richTextElementForm.ImageKey == "" { 76 | return false 77 | } 78 | } 79 | } 80 | return true 81 | } 82 | -------------------------------------------------------------------------------- /SDK/message/richtext_builder_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package message_test 6 | 7 | import ( 8 | "encoding/json" 9 | "testing" 10 | 11 | "github.com/larksuite/botframework-go/SDK/message" 12 | "github.com/larksuite/botframework-go/SDK/protocol" 13 | ) 14 | 15 | func TestBuildRichtextReq(t *testing.T) { 16 | postForm := make(map[protocol.Language]*protocol.RichTextForm) 17 | 18 | // en-us 19 | titleUS := "this is a title" 20 | contentUS := message.NewRichTextContent() 21 | 22 | // first line 23 | contentUS.AddElementBlock( 24 | message.NewTextTag("first line :", true, 1), 25 | message.NewATag("hyperlinks", true, "https://www.feishu.cn"), 26 | message.NewAtTag("username", "userid"), 27 | ) 28 | 29 | // second line 30 | contentUS.AddElementBlock( 31 | message.NewTextTag("second line :", true, 1), 32 | message.NewTextTag("text test", true, 1), 33 | ) 34 | 35 | postForm[protocol.EnUS] = message.NewRichTextForm(&titleUS, contentUS) 36 | 37 | // zh-cn 38 | titleCN := "这是一个标题" 39 | contentCN := message.NewRichTextContent() 40 | 41 | // first line 42 | contentCN.AddElementBlock( 43 | message.NewTextTag("第一行 :", true, 1), 44 | message.NewATag("超链接", true, "https://www.feishu.cn"), 45 | message.NewAtTag("username", "userid"), 46 | ) 47 | 48 | // second line 49 | contentCN.AddElementBlock( 50 | message.NewTextTag("第二行 :", true, 1), 51 | message.NewTextTag("文本测试", true, 1), 52 | ) 53 | 54 | postForm[protocol.ZhCN] = message.NewRichTextForm(&titleCN, contentCN) 55 | 56 | post := map[string]*protocol.RichTextForm{} 57 | for k, v := range postForm { 58 | post[k.String()] = v 59 | } 60 | 61 | bytes, err := json.Marshal(post) 62 | if err != nil { 63 | t.Error(err) 64 | return 65 | } 66 | t.Log(string(bytes)) 67 | } 68 | -------------------------------------------------------------------------------- /SDK/protocol/authentication.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package protocol 6 | 7 | type MiniProgramLoginByAppTokenRequest struct { 8 | Code string `json:"code"` 9 | } 10 | 11 | type MiniProgramLoginByAppTokenResponse struct { 12 | Code int `json:"code"` 13 | Msg string `json:"msg"` 14 | 15 | Data struct { 16 | MiniProgramUserToken 17 | } `json:"data"` 18 | } 19 | 20 | func GenMiniProgramLoginByIDSecretRequest(code, appID, appSecret string) map[string]string { 21 | return map[string]string{ 22 | "code": code, 23 | "appid": appID, 24 | "secret": appSecret, 25 | } 26 | } 27 | 28 | type MiniProgramLoginByIDSecretResponse struct { 29 | Code int `json:"error"` 30 | Msg string `json:"message"` 31 | 32 | MiniProgramUserToken 33 | } 34 | 35 | type MiniProgramUserToken struct { 36 | AccessToken string `json:"access_token"` // user_access_token 37 | TokenType string `json:"token_type"` 38 | ExpiresIn int `json:"expires_in"` 39 | RefreshToken string `json:"refresh_token"` 40 | TenantKey string `json:"tenant_key"` 41 | OpenID string `json:"open_id"` 42 | UnionID string `json:"union_id"` 43 | SessionKey string `json:"session_key"` 44 | } 45 | 46 | const ( 47 | GrantTypeAuthCode = "authorization_code" 48 | GrantTypeRefreshToken = "refresh_token" 49 | ) 50 | 51 | type OpenSSOTokenRequest struct { 52 | //(app_id + app_secret)/app_access_token, two ways to choose one. app_access_token is recommended 53 | AppID string `json:"app_id"` 54 | AppSecret string `json:"app_secret"` 55 | AppAccessToken string `json:"app_access_token"` 56 | GrantType string `json:"grant_type"` // authorization_code / refresh_token 57 | Code string `json:"code"` // required when GrantType=authorization_code 58 | RefreshToken string `json:"refresh_token"` // required when GrantType=refresh_token 59 | } 60 | 61 | type OpenSSOTokenResponse struct { 62 | Code int `json:"code"` 63 | Msg string `json:"message"` 64 | 65 | UserTokenInfo 66 | } 67 | 68 | type UserTokenInfo struct { 69 | AccessToken string `json:"access_token"` // user_access_token 70 | TokenType string `json:"token_type"` 71 | ExpiresIn int `json:"expires_in"` 72 | RefreshToken string `json:"refresh_token"` 73 | TenantKey string `json:"tenant_key"` 74 | OpenID string `json:"open_id"` 75 | EmployeeID string `json:"employee_id"` 76 | Name string `json:"name"` 77 | ENName string `json:"en_name"` 78 | AvatarURL string `json:"avatar_url"` 79 | } 80 | -------------------------------------------------------------------------------- /SDK/protocol/authorization.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package protocol 6 | 7 | const ( 8 | ISVApp = "isv" 9 | InternalApp = "internal" 10 | ) 11 | 12 | // GetTenantAccessToken Internal App request 13 | type GetTenantAccessTokenInternalReq struct { 14 | AppID string `json:"app_id"` 15 | AppSecret string `json:"app_secret"` 16 | } 17 | 18 | // GetTenantAccessToken ISV App request 19 | type GetTenantAccessTokenISVReq struct { 20 | AppAccessToken string `json:"app_access_token"` 21 | TenantKey string `json:"tenant_key"` 22 | } 23 | 24 | // GetTenantAccessToken Internal/ISV response 25 | type GetTenantAccessTokenResp struct { 26 | BaseResponse 27 | Expire int `json:"expire"` 28 | TenantAccessToken string `json:"tenant_access_token"` 29 | } 30 | 31 | // GetAppAccessToken Internal request 32 | type GetAppAccessTokenInternalReq struct { 33 | AppID string `json:"app_id"` 34 | AppSecret string `json:"app_secret"` 35 | } 36 | 37 | // GetAppAccessToken Internal response 38 | type GetAppAccessTokenInternalResp struct { 39 | BaseResponse 40 | Expire int `json:"expire"` 41 | AppAccessToken string `json:"app_access_token"` 42 | TenantAccessToken string `json:"tenant_access_token"` 43 | } 44 | 45 | // GetAppAccessToken ISV request 46 | type GetAppAccessTokenIsvReq struct { 47 | AppID string `json:"app_id"` 48 | AppSecret string `json:"app_secret"` 49 | AppTicket string `json:"app_ticket"` 50 | } 51 | 52 | // GetAppAccessToken ISV response 53 | type GetAppAccessTokenIsvResp struct { 54 | BaseResponse 55 | Expire int `json:"expire"` 56 | AppAccessToken string `json:"app_access_token"` 57 | } 58 | 59 | // AppTicketReq request 60 | type AppTicketReq struct { 61 | AppID string `json:"app_id"` 62 | AppSecret string `json:"app_secret"` 63 | } 64 | 65 | // AppTicketResp response 66 | type AppTicketResp struct { 67 | BaseResponse 68 | } 69 | -------------------------------------------------------------------------------- /SDK/protocol/bot_recv_msg.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package protocol 6 | 7 | const ( 8 | CmdDefault = "default" 9 | DescDefault = "defalut cmd" 10 | ) 11 | 12 | type BotRecvMsg struct { 13 | AppID string 14 | TenantKey string 15 | MsgType string 16 | TextParam string 17 | RootID string 18 | ParentID string 19 | OpenChatID string 20 | ChatType string 21 | OpenID string 22 | OpenMessageID string 23 | OriData interface{} 24 | } 25 | -------------------------------------------------------------------------------- /SDK/protocol/card.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package protocol 6 | 7 | // card meta 8 | const ( 9 | VER = "1.0.0" 10 | ) 11 | 12 | // card meta info 13 | type Meta struct { 14 | SDKVersion string `json:"sdk_version,omitempty" validate:"omitempty"` 15 | } 16 | 17 | func NewMeta() *Meta { 18 | meta := &Meta{} 19 | meta.SDKVersion = VER 20 | return meta 21 | } 22 | 23 | // card callback 24 | type CardChallenge struct { 25 | AppID string `json:"appid,omitempty" validate:"omitempty"` 26 | Challenge string `json:"challenge,omitempty" validate:"omitempty"` 27 | Token string `json:"token,omitempty" validate:"omitempty"` 28 | Type string `json:"type,omitempty" validate:"omitempty"` 29 | } 30 | 31 | type CardCallbackBase struct { 32 | OpenID string `json:"open_id,omitempty" validate:"omitempty"` 33 | UserID string `json:"user_id,omitempty" validate:"omitempty"` 34 | OpenMessageID string `json:"open_message_id,omitempty" validate:"omitempty"` 35 | TenantKey string `json:"tenant_key,omitempty" validate:"omitempty"` 36 | Token *string `json:"token,omitempty" validate:"omitempty"` 37 | Timezone *string `json:"timezone,omitempty" validate:"omitempty"` 38 | } 39 | 40 | type CardCallbackForm struct { 41 | CardCallbackBase 42 | Action CardCallBackAction `json:"action,omitempty" validate:"omitempty"` 43 | } 44 | 45 | type CardCallBackAction struct { 46 | Value map[string]string `json:"value,omitempty" validate:"omitempty"` 47 | Tag *string `json:"tag,omitempty" validate:"omitempty"` 48 | Option *string `json:"option,omitempty" validate:"omitempty"` 49 | Timezone *string `json:"timezone,omitempty" validate:"omitempty"` 50 | } 51 | -------------------------------------------------------------------------------- /SDK/protocol/chat.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package protocol 6 | 7 | import "fmt" 8 | 9 | func GenGetGroupListRequest(pageSize int, pageToken string) map[string]string { 10 | return map[string]string{ 11 | "page_size": fmt.Sprintf("%d", pageSize), 12 | "page_token": pageToken, 13 | } 14 | } 15 | 16 | type GetGroupListResponse struct { 17 | BaseResponse 18 | Data struct { 19 | Groups []*Group `json:"groups,omitempty" validate:"omitempty"` 20 | HasMore bool `json:"has_more,omitempty" validate:"omitempty"` 21 | PageToken string `json:"page_token,omitempty" validate:"omitempty"` 22 | } `json:"data,omitempty" validate:"omitempty"` 23 | } 24 | 25 | type Group struct { 26 | Avatar string `json:"avatar,omitempty" validate:"omitempty"` 27 | ChatID string `json:"chat_id,omitempty" validate:"omitempty"` 28 | Description string `json:"description,omitempty" validate:"omitempty"` 29 | Name string `json:"name,omitempty" validate:"omitempty"` 30 | OwnerOpenID string `json:"owner_open_id,omitempty" validate:"omitempty"` 31 | OwnerUserID string `json:"owner_user_id,omitempty" validate:"omitempty"` 32 | } 33 | 34 | func GenGetGroupInfoRequest(chatID string) map[string]string { 35 | return map[string]string{ 36 | "chat_id": chatID, 37 | } 38 | } 39 | 40 | type GetGroupInfoResponse struct { 41 | BaseResponse 42 | 43 | Data struct { 44 | Group 45 | 46 | ChatI18nNames I18nNames `json:"i18n_names,omitempty" validate:"omitempty"` 47 | Members []UserIDInfo `json:"members,omitempty" validate:"omitempty"` 48 | } `json:"data,omitempty" validate:"omitempty"` 49 | } 50 | 51 | type UpdateChatInfoRequest struct { 52 | ChatID string `json:"chat_id,omitempty" validate:"omitempty"` 53 | OwnerUserID *string `json:"owner_user_id,omitempty" validate:"omitempty"` 54 | OwnerOpenID *string `json:"owner_open_id,omitempty" validate:"omitempty"` 55 | Name *string `json:"name,omitempty" validate:"omitempty"` 56 | ChatI18nNames *I18nNames `json:"i18n_names,omitempty" validate:"omitempty"` 57 | } 58 | 59 | type UpdateChatInfoResponse struct { 60 | BaseResponse 61 | Data struct { 62 | ChatID string `json:"chat_id,omitempty" validate:"omitempty"` 63 | } `json:"data,omitempty" validate:"omitempty"` 64 | } 65 | 66 | type CreateChatRequest struct { 67 | Name string `json:"name,omitempty" validate:"omitempty"` 68 | Description string `json:"description,omitempty" validate:"omitempty"` 69 | UserIDs []string `json:"user_ids,omitempty" validate:"omitempty"` 70 | OpenIDs []string `json:"open_ids,omitempty" validate:"omitempty"` 71 | ChatI18nNames *I18nNames `json:"i18n_names,omitempty" validate:"omitempty"` 72 | } 73 | 74 | type CreateChatResponse struct { 75 | BaseResponse 76 | Data struct { 77 | ChatID string `json:"chat_id,omitempty" validate:"omitempty"` 78 | InvalidOpenIDs []string `json:"invalid_open_ids,omitempty" validate:"omitempty"` 79 | InvalidUserIDs []string `json:"invalid_user_ids,omitempty" validate:"omitempty"` 80 | } `json:"data,omitempty" validate:"omitempty"` 81 | } 82 | 83 | type AddUserToChatRequest struct { 84 | ChatID string `json:"chat_id,omitempty" validate:"omitempty"` 85 | UserIDs []string `json:"user_ids,omitempty" validate:"omitempty"` 86 | OpenIDs []string `json:"open_ids,omitempty" validate:"omitempty"` 87 | } 88 | 89 | type AddUserToChatResponse struct { 90 | BaseResponse 91 | Data struct { 92 | InvalidOpenIDs []string `json:"invalid_open_ids,omitempty" validate:"omitempty"` 93 | InvalidUserIDs []string `json:"invalid_user_ids,omitempty" validate:"omitempty"` 94 | } `json:"data,omitempty" validate:"omitempty"` 95 | } 96 | 97 | type DeleteUserFromChatRequest struct { 98 | ChatID string `json:"chat_id,omitempty" validate:"omitempty"` 99 | UserIDs []string `json:"user_ids,omitempty" validate:"omitempty"` 100 | OpenIDs []string `json:"open_ids,omitempty" validate:"omitempty"` 101 | } 102 | 103 | type DeleteUserFromChatResponse struct { 104 | BaseResponse 105 | Data struct { 106 | InvalidOpenIDs []string `json:"invalid_open_ids,omitempty" validate:"omitempty"` 107 | InvalidUserIDs []string `json:"invalid_user_ids,omitempty" validate:"omitempty"` 108 | } `json:"data,omitempty" validate:"omitempty"` 109 | } 110 | 111 | type DisbandChatRequest struct { 112 | ChatID string `json:"chat_id,omitempty" validate:"omitempty"` 113 | } 114 | 115 | type DisbandChatResponse struct { 116 | BaseResponse 117 | } 118 | -------------------------------------------------------------------------------- /SDK/protocol/openapi.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package protocol 6 | 7 | type OpenApiPath string 8 | 9 | const ( 10 | GetAppAccessTokenInternalPath OpenApiPath = "/open-apis/auth/v3/app_access_token/internal/" 11 | GetAppAccessTokenIsvPath OpenApiPath = "/open-apis/auth/v3/app_access_token/" 12 | GetTenantAccessTokenInternalPath OpenApiPath = "/open-apis/auth/v3/tenant_access_token/internal/" 13 | GetTenantAccessTokenIsvPath OpenApiPath = "/open-apis/auth/v3/tenant_access_token/" 14 | ResendAppTicketPath OpenApiPath = "/open-apis/auth/v3/app_ticket/resend" 15 | SendMessagePath OpenApiPath = "/open-apis/message/v4/send/" 16 | SendMessageBatchPath OpenApiPath = "/open-apis/message/v4/batch_send/" 17 | UploadImagePath OpenApiPath = "/open-apis/image/v4/put/" 18 | GetImagePath OpenApiPath = "/open-apis/image/v4/get" 19 | GetChatInfoPath OpenApiPath = "/open-apis/chat/v4/info/" 20 | GetChatListPath OpenApiPath = "/open-apis/chat/v4/list/" 21 | UpdateChatInfoPath OpenApiPath = "/open-apis/chat/v4/update/" 22 | CreateChatPath OpenApiPath = "/open-apis/chat/v4/create/" 23 | AddUserToChatPath OpenApiPath = "/open-apis/chat/v4/chatter/add/" 24 | DeleteUserFromChatPath OpenApiPath = "/open-apis/chat/v4/chatter/delete/" 25 | DisbandChatPath OpenApiPath = "/open-apis/chat/v4/disband/" 26 | CardUpdatePath OpenApiPath = "/open-apis/interactive/v1/card/update" 27 | MPValidateByAppTokenPath OpenApiPath = "/open-apis/mina/v2/tokenLoginValidate" //mini programe login validate, ExchangeToken 28 | MPValidateByIDSecretPath OpenApiPath = "/open-apis/mina/loginValidate" //mini programe login validate, ExchangeToken 29 | OpenSSOValidatePath OpenApiPath = "/connect/qrconnect/oauth2/access_token/" //open sso login validate, ExchangeToken/RefreshToken 30 | OpenSSOGetCodePath OpenApiPath = "/connect/qrconnect/page/sso/" //open sso, GetCode 31 | ) 32 | 33 | type BaseResponse struct { 34 | Code int `json:"code"` 35 | Msg string `json:"msg"` 36 | } 37 | 38 | const ( 39 | ErrAppTicketNil = 10003 40 | ErrAppTicketInvalid = 10012 41 | ErrTenantAccessTokenInvalid = 99991663 42 | ErrAppAccessTokenInvalid = 99991664 43 | ErrMinaAppAccessTokenInvalid = 10202 44 | ) 45 | -------------------------------------------------------------------------------- /demo/bot_receive_and_response/bot_receive_and_response.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/larksuite/botframework-go/SDK/common" 15 | "github.com/larksuite/botframework-go/SDK/event" 16 | "github.com/larksuite/botframework-go/SDK/message" 17 | "github.com/larksuite/botframework-go/SDK/protocol" 18 | "github.com/larksuite/botframework-go/demo/sdk_init" 19 | ) 20 | 21 | const ( 22 | appID = "" 23 | tenantKey = "" 24 | ) 25 | 26 | func main() { 27 | r := gin.Default() 28 | 29 | common.InitLogger(common.NewCommonLogger(), common.DefaultOption()) 30 | defer common.FlushLogger() 31 | 32 | //Necessary steps,init app configuration and regist 33 | err := InitInfoAndRegist() 34 | if err != nil { 35 | common.Logger(context.TODO()).Errorf("InitError[%v]", err) 36 | return 37 | } 38 | 39 | r.POST("/webhook/event", EventCallback) //open platform event callback 40 | r.POST("/webhook/card", CardCallback) //card action callback 41 | 42 | r.Run(":8089") 43 | } 44 | 45 | //init app configuration and regist 46 | func InitInfoAndRegist() error { 47 | err := sdk_init.InitInfo() 48 | if err != nil { 49 | common.Logger(context.TODO()).Errorf("InitError[%v]", err) 50 | return err 51 | } 52 | RegistHandler(appID) 53 | return nil 54 | } 55 | 56 | func RegistHandler(appID string) { 57 | event.EventRegister(appID, protocol.EventTypeMessage, EventMessage) //necessary function.process events and distribute them 58 | event.BotRecvMsgRegister(appID, "help", BotRecvMsgHelp) //response "help" 59 | event.BotRecvMsgRegister(appID, "card", BotRecvMsgCard) //response "card" 60 | event.CardRegister(appID, "clickbutton", ActionClickButton) //response when clicking this button 61 | } 62 | 63 | // EventCallback open platform event 64 | func EventCallback(c *gin.Context) { 65 | 66 | body, err := ioutil.ReadAll(c.Request.Body) 67 | if err != nil || len(body) == 0 { 68 | common.Logger(c).Errorf("eventReqParamsError: readHttpBodyError err[%v]bodyLen[%d]", err, len(body)) 69 | c.JSON(500, gin.H{"codemsg": common.ErrEventParams.String()}) 70 | return 71 | } 72 | 73 | appID := appID 74 | challenge, err := event.EventCallback(c, string(body), appID) 75 | common.Logger(c).Infof("eventInfo: challenge[%s] err[%v]", challenge, err) 76 | if err != nil { 77 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 78 | } else if "" != challenge { 79 | c.JSON(200, gin.H{"challenge": challenge}) 80 | } else { 81 | c.JSON(200, gin.H{"codemsg": common.Success.String()}) 82 | } 83 | } 84 | 85 | // CardCallback card action callback 86 | func CardCallback(c *gin.Context) { 87 | 88 | body, err := ioutil.ReadAll(c.Request.Body) 89 | if err != nil || len(body) == 0 { 90 | common.Logger(c).Errorf("eventReqParamsError: readHttpBodyError err[%v]bodyLen[%d]", err, len(body)) 91 | c.JSON(500, gin.H{"codemsg": common.ErrCardParams.String()}) 92 | return 93 | } 94 | 95 | // for verify signature 96 | header := map[string]string{ 97 | "X-Lark-Request-Timestamp": c.Request.Header.Get("X-Lark-Request-Timestamp"), 98 | "X-Lark-Request-Nonce": c.Request.Header.Get("X-Lark-Request-Nonce"), 99 | "X-Lark-Signature": c.Request.Header.Get("X-Lark-Signature"), 100 | } 101 | 102 | appID := appID 103 | card, challenge, err := event.CardCallBack(c, appID, header, body) 104 | common.Logger(c).Infof("cardInfo: challenge[%s] err[%v]", challenge, err) 105 | if err != nil { 106 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 107 | } else if "" != challenge { 108 | c.JSON(200, gin.H{"challenge": challenge}) 109 | } else { 110 | data, err := json.Marshal(card) 111 | if err != nil { 112 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 113 | } else { 114 | c.String(200, string(data)) 115 | } 116 | } 117 | } 118 | 119 | // event-Message 120 | func EventMessage(ctx context.Context, eventBody []byte) error { 121 | return event.BotRecvMsgHandler(ctx, eventBody) 122 | } 123 | 124 | func BotRecvMsgHelp(ctx context.Context, msg *protocol.BotRecvMsg) error { 125 | user := &protocol.UserInfo{ 126 | ID: msg.OpenChatID, 127 | Type: protocol.UserTypeChatID, 128 | } 129 | message.SendTextMessage(ctx, tenantKey, appID, user, "", "hello,this is help") 130 | return nil 131 | } 132 | 133 | func BotRecvMsgCard(ctx context.Context, msg *protocol.BotRecvMsg) error { 134 | user := &protocol.UserInfo{ 135 | ID: msg.OpenChatID, 136 | Type: protocol.UserTypeChatID, 137 | } 138 | 139 | //build card 140 | builder := &message.CardBuilder{} 141 | //add config 142 | config := protocol.ConfigForm{ 143 | MinVersion: protocol.VersionForm{}, 144 | WideScreenMode: true, 145 | } 146 | builder.SetConfig(config) 147 | 148 | //add header 149 | content := "this is a card" 150 | line := 1 151 | title := protocol.TextForm{ 152 | Tag: protocol.PLAIN_TEXT_E, 153 | Content: &content, 154 | Lines: &line, 155 | } 156 | builder.AddHeader(title, "") 157 | 158 | //add button 159 | button1 := make(map[string]string, 0) 160 | button1["key"] = "buttonValue" 161 | builder.AddActionBlock([]protocol.ActionElement{ 162 | message.NewButton(message.NewMDText("callback button", nil, nil, nil), nil, nil, button1, protocol.DANGER, nil, "clickbutton"), 163 | }) 164 | 165 | card, err := builder.BuildForm() 166 | if err != nil { 167 | common.Logger(ctx).Errorf("card build failed error[%v]", err) 168 | return fmt.Errorf("card build failed error[%v]", err) 169 | } 170 | 171 | //add params to use message.SendCardMessage 172 | resp, err := message.SendCardMessage(ctx, tenantKey, appID, user, "", *card, false) 173 | if err != nil { 174 | common.Logger(ctx).Errorf("send message failed error[%v]", err) 175 | return fmt.Errorf("send message failed error[%v]", err) 176 | } 177 | 178 | common.Logger(ctx).Errorf("code[%d],msg[%s],openMessageID[%s]", resp.Code, resp.Msg, resp.Data.MessageID) 179 | return nil 180 | } 181 | 182 | // methodName-clickbutton 183 | func ActionClickButton(ctx context.Context, callback *protocol.CardCallbackForm) (*protocol.CardForm, error) { 184 | method, _ := callback.Action.Value["method"] 185 | sessionID, _ := callback.Action.Value["sid"] 186 | common.Logger(ctx).Infof("cardActionCallBack: method[%s]sessionID[%s]", method, sessionID) 187 | 188 | //update card 189 | //build a new card 190 | builder := &message.CardBuilder{} 191 | //add config 192 | config := protocol.ConfigForm{ 193 | MinVersion: protocol.VersionForm{}, 194 | WideScreenMode: true, 195 | } 196 | builder.SetConfig(config) 197 | 198 | //add header 199 | content := "this is a card" 200 | line := 1 201 | title := protocol.TextForm{ 202 | Tag: protocol.PLAIN_TEXT_E, 203 | Content: &content, 204 | Lines: &line, 205 | } 206 | builder.AddHeader(title, "") 207 | 208 | //add button 209 | button1 := make(map[string]string, 0) 210 | button1["key"] = "buttonValue" 211 | builder.AddActionBlock([]protocol.ActionElement{ 212 | message.NewButton(message.NewMDText("clicked button", nil, nil, nil), nil, nil, button1, protocol.UNKNOWN, nil, "clickbutton"), 213 | }) 214 | 215 | card, err := builder.BuildForm() 216 | if err != nil { 217 | common.Logger(ctx).Errorf("card update failed error[%v]", err) 218 | return &protocol.CardForm{}, fmt.Errorf("card update failed error[%v]", err) 219 | } 220 | 221 | return card, nil 222 | } 223 | -------------------------------------------------------------------------------- /demo/sdk_init/sdk_init.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sdk_init 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/larksuite/botframework-go/SDK/appconfig" 11 | "github.com/larksuite/botframework-go/SDK/auth" 12 | "github.com/larksuite/botframework-go/SDK/common" 13 | "github.com/larksuite/botframework-go/SDK/protocol" 14 | ) 15 | 16 | //Initialization function 17 | func InitInfo() error { 18 | //init log 19 | common.InitLogger(common.NewCommonLogger(), common.DefaultOption()) 20 | defer common.FlushLogger() 21 | 22 | //init redis-client 23 | redisClient := &common.DefaultRedisClient{} 24 | err := redisClient.InitDB(map[string]string{"addr": "127.0.0.1:6379"}) 25 | if err != nil { 26 | return fmt.Errorf("init redis-client error[%v]", err) 27 | } 28 | 29 | // get app info 30 | conf, err := GetAppConf(redisClient) 31 | if err != nil { 32 | return fmt.Errorf("get conf error[%v]", err) 33 | } 34 | // init app info 35 | appconfig.Init(*conf) 36 | 37 | //Independent Software Vendor App(ISVApp) has to get APPTicket,if your AppType is not ISV,you can ignore this 38 | // ISVApp Set TicketManager 39 | if conf.AppType == protocol.ISVApp { 40 | // ISVApp need to implement the TicketManager interface 41 | // It is recommended to set/get your app-ticket in redis 42 | 43 | err := auth.InitISVAppTicketManager(auth.NewDefaultAppTicketManager(redisClient)) 44 | if err != nil { 45 | return fmt.Errorf("Authorization Initialize Error[%v]", err) 46 | } 47 | } 48 | 49 | //If you need to register an/a event/card callback,you can do it here. 50 | //You can reference this simple example or detailed introduction from file(4.webhook-event和5.webhook-card) 51 | 52 | // regist open platform event handler 53 | // event.EventRegister(appID, protocol.EventTypeMessage, EventMessage) 54 | 55 | // regist bot recv message handler 56 | //event.BotRecvMsgRegister(appID, "help", BotRecvMsgHelp) 57 | 58 | // regist card action handler 59 | //event.CardRegister(appID, "testcard", ActionTestCard) 60 | 61 | return nil 62 | } 63 | 64 | // get appinfo(app_id、app_secret、veri_token、encrypt_key) from redis/mysql or remote config system 65 | // redis/mysql or remote config system is recommended 66 | func GetAppConf(client common.DBClient) (*appconfig.AppConfig, error) { 67 | //Read information from redis 68 | appID, err := client.Get("AppID") 69 | if err != nil { 70 | return nil, fmt.Errorf("get AppID failed[%v]", err) 71 | } 72 | appSecret, err := client.Get("AppSecret") 73 | if err != nil { 74 | return nil, fmt.Errorf("get AppSecret failed[%v]", err) 75 | } 76 | verifyToken, err := client.Get("VerifyToken") 77 | if err != nil { 78 | return nil, fmt.Errorf("get VerifyToken failed[%v]", err) 79 | } 80 | encryptKey, err := client.Get("EncryptKey") 81 | if err != nil { 82 | return nil, fmt.Errorf("get EncryptKey failed[%v]", err) 83 | } 84 | 85 | // Initialize app config 86 | // Clear text in code is not recommended. 87 | conf := &appconfig.AppConfig{ 88 | AppID: appID, //get it from lark-voucher and basic information。 89 | AppType: protocol.InternalApp, //AppType only has two types: Independent Software Vendor App(ISVApp) or Internal App. 90 | AppSecret: appSecret, //get it from lark-voucher and basic information. 91 | VerifyToken: verifyToken, //get it from lark-event subscriptions. 92 | EncryptKey: encryptKey, //get it from lark-event subscriptions. 93 | } 94 | 95 | return conf, nil 96 | } 97 | -------------------------------------------------------------------------------- /demo/send_card/send_card.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/larksuite/botframework-go/SDK/common" 13 | "github.com/larksuite/botframework-go/SDK/message" 14 | "github.com/larksuite/botframework-go/SDK/protocol" 15 | "github.com/larksuite/botframework-go/demo/sdk_init" 16 | ) 17 | 18 | func main() { 19 | //init log 20 | common.InitLogger(common.NewCommonLogger(), common.DefaultOption()) 21 | defer common.FlushLogger() 22 | 23 | //param 24 | ctx := context.TODO() 25 | 26 | //Necessary step: init app configuration 27 | err := sdk_init.InitInfo() 28 | if err != nil { 29 | common.Logger(ctx).Errorf("InitError[%v]", err) 30 | return 31 | } 32 | 33 | //params 34 | chatID := "" //p2p or group chat ID 35 | tenantKey := "" //tenantKey of your company 36 | appID := "" //APP ID 37 | 38 | //send card 39 | sendCard(chatID, tenantKey, appID) 40 | } 41 | 42 | func sendCard(chatID, tenantKey, appID string) error { 43 | user := &protocol.UserInfo{ 44 | ID: chatID, 45 | Type: protocol.UserTypeChatID, 46 | } 47 | 48 | ctx := context.TODO() 49 | 50 | //build card 51 | builder := &message.CardBuilder{} 52 | 53 | //add config 54 | config := protocol.ConfigForm{ 55 | MinVersion: protocol.VersionForm{}, 56 | WideScreenMode: true, 57 | } 58 | builder.SetConfig(config) 59 | 60 | //add header 61 | content := "this is a card" 62 | line := 1 63 | title := protocol.TextForm{ 64 | Tag: protocol.PLAIN_TEXT_E, 65 | Content: &content, 66 | Lines: &line, 67 | } 68 | builder.AddHeader(title, "") 69 | 70 | //add dividing line 71 | builder.AddHRBlock() 72 | 73 | //set href 74 | urlhref := make(map[string]protocol.URLForm, 0) 75 | uHost := "https://www.larksuite.com/" 76 | urlhref["link"] = protocol.URLForm{Url: &uHost} 77 | 78 | //add content block,suck as title、content and extra(null here). 79 | builder.AddDIVBlock(message.NewMDText("[lark]($link)", nil, nil, urlhref), []protocol.FieldForm{ 80 | *message.NewField(false, message.NewMDText("**boldText**", nil, nil, nil)), 81 | *message.NewField(false, message.NewMDText("text", nil, nil, nil)), 82 | }, nil) 83 | 84 | builder.AddHRBlock() 85 | 86 | //add image block 87 | ImageContent := "Description when your mouse is over the picture" 88 | ImageTag := protocol.TextForm{ 89 | Tag: protocol.PLAIN_TEXT_E, 90 | Content: &ImageContent, 91 | Lines: nil, 92 | } 93 | 94 | //get imageKey.You can get detailed introduction about imageKey from file(6.2) 95 | imageURL := "https://s0.pstatp.com/ee/lark-open/web/static/apply.226f11cb.png" 96 | imageKey, err := message.GetImageKey(ctx, tenantKey, appID, imageURL, "") 97 | if err != nil { 98 | common.Logger(ctx).Errorf("get imageKey failed[%v]", err) 99 | } 100 | builder.AddImageBlock(message.NewMDText("image title", nil, nil, nil), ImageTag, imageKey) 101 | 102 | builder.AddHRBlock() 103 | 104 | //add button 105 | payload1 := make(map[string]string, 0) 106 | payload1["key"] = "value" 107 | builder.AddActionBlock([]protocol.ActionElement{ 108 | message.NewButton(message.NewMDText("lark button", nil, nil, nil), &uHost, nil, payload1, protocol.PRIMARY, nil, "testcard"), 109 | message.NewButton(message.NewMDText("simple button", nil, nil, nil), nil, nil, payload1, protocol.DANGER, nil, "testcard"), 110 | }) 111 | builder.AddHRBlock() 112 | 113 | //add menu 114 | //SelectPersonMenu 115 | builder.AddActionBlock([]protocol.ActionElement{ 116 | message.NewSelectPersonMenu(message.NewMDText("SelectPersonMenu", nil, nil, nil), nil, []protocol.OptionForm{}, nil, nil, "testcard"), 117 | }, 118 | ) 119 | 120 | //SelectStaticMenu 121 | optiontextcontent1 := "option1" 122 | optiontext1 := protocol.TextForm{ 123 | Tag: protocol.PLAIN_TEXT_E, 124 | Content: &optiontextcontent1, 125 | Lines: nil, 126 | } 127 | optiontextcontent2 := "option2" 128 | optiontext2 := protocol.TextForm{ 129 | Tag: protocol.PLAIN_TEXT_E, 130 | Content: &optiontextcontent2, 131 | Lines: nil, 132 | } 133 | value1 := "value1" 134 | value2 := "value2" 135 | builder.AddActionBlock([]protocol.ActionElement{ 136 | message.NewSelectStaticMenu(message.NewMDText("SelectStaticMenu", nil, nil, nil), nil, []protocol.OptionForm{ 137 | message.NewOption(optiontext1, value1), 138 | message.NewOption(optiontext2, value2), 139 | }, &value1, nil, "testcard"), //option1 is default option 140 | }, 141 | ) 142 | builder.AddHRBlock() 143 | 144 | //add overflow 145 | overFlowcontent1 := "option1" 146 | overFlowtext1 := protocol.TextForm{ 147 | Tag: protocol.PLAIN_TEXT_E, 148 | Content: &overFlowcontent1, 149 | Lines: nil, 150 | } 151 | overFlowcontent2 := "option2" 152 | overFlowtext2 := protocol.TextForm{ 153 | Tag: protocol.PLAIN_TEXT_E, 154 | Content: &overFlowcontent2, 155 | Lines: nil, 156 | } 157 | builder.AddActionBlock([]protocol.ActionElement{ 158 | message.NewOverflowMenu(nil, []protocol.OptionForm{ 159 | message.NewOption(overFlowtext1, value1), 160 | message.NewOption(overFlowtext2, value2), 161 | }, nil, "testcard"), 162 | }, 163 | ) 164 | builder.AddHRBlock() 165 | 166 | //add datapicker 167 | timePicker := time.Now().Format("2006-01-02") 168 | builder.AddActionBlock([]protocol.ActionElement{ 169 | message.NewPickerDate(message.NewMDText("dataPicker", nil, nil, nil), nil, nil, &timePicker, "testcard"), 170 | }, 171 | ) 172 | builder.AddHRBlock() 173 | 174 | //add note 175 | noteTextContent := "noteTextContent" 176 | notetext := protocol.TextForm{ 177 | Tag: protocol.PLAIN_TEXT_E, 178 | Content: ¬eTextContent, 179 | Lines: nil, 180 | } 181 | noteImageContent := "noteImageContent" 182 | noteImage := protocol.ImageForm{ 183 | Tag: protocol.IMG_BLOCK, 184 | ImageKey: imageKey, 185 | ALT: protocol.TextForm{ 186 | Tag: protocol.PLAIN_TEXT_E, 187 | Content: ¬eImageContent, 188 | Lines: nil, 189 | }, 190 | } 191 | builder.AddNoteBlock([]protocol.BaseElement{ 192 | ¬etext, 193 | ¬eImage, 194 | }) 195 | 196 | card, err := builder.BuildForm() 197 | if err != nil { 198 | common.Logger(ctx).Errorf("buildForm failed[%v]", err) 199 | return fmt.Errorf("buildForm failed[%v]", err) 200 | } 201 | 202 | //add params to use message.SendCardMessage 203 | resp, err := message.SendCardMessage(ctx, tenantKey, appID, user, "", *card, false) 204 | if err != nil { 205 | common.Logger(ctx).Errorf("send card failed[%v]", err) 206 | return fmt.Errorf("send card failed[%v]", err) 207 | } 208 | 209 | common.Logger(ctx).Info("code:[%d],msg:[%s],openMessageID:[%s]", resp.Code, resp.Msg, resp.Data.MessageID) 210 | return nil 211 | } 212 | -------------------------------------------------------------------------------- /demo/send_message/send_message.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | 11 | "github.com/larksuite/botframework-go/SDK/common" 12 | "github.com/larksuite/botframework-go/SDK/message" 13 | "github.com/larksuite/botframework-go/SDK/protocol" 14 | "github.com/larksuite/botframework-go/demo/sdk_init" 15 | ) 16 | 17 | func main() { 18 | //init log 19 | common.InitLogger(common.NewCommonLogger(), common.DefaultOption()) 20 | defer common.FlushLogger() 21 | 22 | //param 23 | ctx := context.TODO() 24 | 25 | //Necessary step: init app configuration 26 | err := sdk_init.InitInfo() 27 | if err != nil { 28 | common.Logger(ctx).Errorf("init app config failed[%v]", err) 29 | return 30 | } 31 | 32 | //params 33 | chatID := "" //p2p or group chat ID 34 | tenantKey := "" //tenantKey of your company 35 | appID := "" //APP ID 36 | 37 | //send text 38 | sendTextMessage(chatID, tenantKey, appID) 39 | 40 | //send image 41 | sendImageMessage(chatID, tenantKey, appID) 42 | 43 | //send rich text 44 | //param 45 | userID := "" // @this user 46 | sendRichTextMessage(chatID, tenantKey, appID, userID) 47 | 48 | //send group card 49 | //params 50 | openID := "" //User_ID in this app.Group card will be sent to this user who has this openID. 51 | sharChatID := "" //group's chatid 52 | 53 | sendShareChatMessage(openID, tenantKey, appID, sharChatID) 54 | 55 | //send card 56 | //you can find the example in demo/send_card/send_card.go 57 | } 58 | 59 | //send text 60 | func sendTextMessage(chatID, tenantKey, appID string) error { 61 | user := &protocol.UserInfo{ 62 | ID: chatID, 63 | Type: protocol.UserTypeChatID, 64 | } 65 | 66 | ctx := context.TODO() 67 | 68 | _, err := message.SendTextMessage(ctx, tenantKey, appID, user, "", "Always One, Always Agile") 69 | if err != nil { 70 | common.Logger(ctx).Errorf("send text failed[%v]", err) 71 | return fmt.Errorf("send text failed[%v]", err) 72 | } 73 | 74 | return nil 75 | } 76 | 77 | //send image 78 | func sendImageMessage(chatID, tenantKey, appID string) error { 79 | user := &protocol.UserInfo{ 80 | ID: chatID, 81 | Type: protocol.UserTypeChatID, 82 | } 83 | 84 | ctx := context.TODO() 85 | 86 | //send image(use imageurl) 87 | imageURL := "https://s0.pstatp.com/ee/lark-open/web/static/apply.226f11cb.png" 88 | _, err := message.SendImageMessage(ctx, tenantKey, appID, user, "", imageURL, "", "") 89 | if err != nil { 90 | common.Logger(ctx).Errorf("send image failed[%v]", err) 91 | return fmt.Errorf("send image failed[%v]", err) 92 | } 93 | return nil 94 | } 95 | 96 | //send rich text 97 | func sendRichTextMessage(chatID, tenantKey, appID, userID string) error { 98 | user := &protocol.UserInfo{ 99 | ID: chatID, 100 | Type: protocol.UserTypeChatID, 101 | } 102 | 103 | ctx := context.TODO() 104 | 105 | //add content of richtext 106 | 107 | //zh-cn 108 | titleCN := "这是一个标题" 109 | contentCN := message.NewRichTextContent() 110 | // first line 111 | contentCN.AddElementBlock( 112 | message.NewTextTag("第一行 :", true, 1), 113 | message.NewATag("超链接", true, "https://www.feishu.cn"), 114 | message.NewAtTag("用户名", userID), 115 | ) 116 | // second line 117 | contentCN.AddElementBlock( 118 | message.NewTextTag("第二行 :", true, 1), 119 | message.NewTextTag("文本测试", true, 1), 120 | ) 121 | 122 | //en-us 123 | titleUS := "this is a title" 124 | contentUS := message.NewRichTextContent() 125 | // first line 126 | contentUS.AddElementBlock( 127 | message.NewTextTag("first line :", true, 1), 128 | message.NewAtTag("username", userID), 129 | ) 130 | // second line 131 | contentUS.AddElementBlock( 132 | message.NewTextTag("second line :", true, 1), 133 | message.NewTextTag("text test", true, 1), 134 | ) 135 | 136 | postForm := make(map[protocol.Language]*protocol.RichTextForm) 137 | postForm[protocol.ZhCN] = message.NewRichTextForm(&titleCN, contentCN) 138 | postForm[protocol.EnUS] = message.NewRichTextForm(&titleUS, contentUS) 139 | 140 | //send rich text 141 | _, err := message.SendRichTextMessage(ctx, tenantKey, appID, user, "", postForm) 142 | if err != nil { 143 | common.Logger(ctx).Errorf("send rich text failed[%v]", err) 144 | return fmt.Errorf("send rich text failed[%v]", err) 145 | } 146 | return nil 147 | } 148 | 149 | //send group shared card 150 | func sendShareChatMessage(openID, tenantKey, appID, shareChatID string) error { 151 | user := &protocol.UserInfo{ 152 | ID: openID, 153 | Type: protocol.UserTypeOpenID, 154 | } 155 | 156 | ctx := context.TODO() 157 | 158 | //send group shared card(last param means group chat id, and this message will be sent to this user by openid) 159 | _, err := message.SendShareChatMessage(ctx, tenantKey, appID, user, "", shareChatID) 160 | if err != nil { 161 | common.Logger(ctx).Errorf("send group card failed[%v]", err) 162 | return fmt.Errorf("send group card failed[%v]", err) 163 | } 164 | return nil 165 | } 166 | -------------------------------------------------------------------------------- /demo/source/lark0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/botframework-go/ff14b99e324b2528f5385d5cfdfe2ae21c0730c8/demo/source/lark0.jpg -------------------------------------------------------------------------------- /demo/source/lark1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/botframework-go/ff14b99e324b2528f5385d5cfdfe2ae21c0730c8/demo/source/lark1.jpg -------------------------------------------------------------------------------- /demo/source/lark2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/botframework-go/ff14b99e324b2528f5385d5cfdfe2ae21c0730c8/demo/source/lark2.jpg -------------------------------------------------------------------------------- /docs/us/app_access_token.md: -------------------------------------------------------------------------------- 1 | # get app_access_token 2 | You can get it in two steps. 3 | 1. Initialize SDK configuration. 4 | 2. Use GetAppAccessToken function in SDK/auth/authorization.go to get app_access_token. 5 | 6 | ## Initialize SDK configuration 7 | Please refer to 8 | [SDK Init doc](../../README.md) 9 | [SDK Init code](../../demo/sdk_init/sdk_init.go) 10 | 11 | ## get app_access_token 12 | You can use GetAppAccessToken function in SDK/auth/authorization.go to get app_access_token easily. 13 | Of course, you should initialize SDK configuration first. 14 | ```go 15 | func GetAppAccessToken(ctx context.Context, appID string)(string,error){ 16 | accessToken, err := auth.GetAppAccessToken(ctx, appID) 17 | if err != nil { 18 | return "", fmt.Errorf("get AppAccessToken failed[%v]", err) 19 | } 20 | return accessToken, nil 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/us/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | `SDK/authentication/authentication.go`: `Authentication` define the authentication interface, `SessionManager` define the user session get/set interface. 3 | 4 | `SDK/authentication/default_session_manager.go`: SDK provides default seesion access implementation. 5 | demo: 6 | ```golang 7 | client := &common.DefaultRedisClient{} 8 | client.InitDB(map[string]string{"addr": "127.0.0.1:6379"}) 9 | manager := authentication.NewDefaultSessionManager("DojK2hs*790(", client) 10 | ``` 11 | 12 | `SDK/authentication/mini_program.go`: Mini-Program authentication related function 13 | `SDK/authentication/open_sso.go`: Open-SSO authentication related function 14 | 15 | `SDK/authentication/auth_mini_program.go`: Mini-Program Login/CheckLogin/Logout 16 | 17 | ## Mini Program Authentication Demo 18 | init 19 | ```golang 20 | var minaAuth *authentication.AuthMiniProgram 21 | 22 | func InitMinaAuth() error { 23 | client := &common.DefaultRedisClient{} 24 | err := client.InitDB(map[string]string{"addr": "127.0.0.1:6379"}) 25 | if err != nil { 26 | common.Logger(context.TODO()).Errorf("init redis error[%v]", err) 27 | return fmt.Errorf("init redis error[%v]", err) 28 | } 29 | common.Logger(context.TODO()).Infof("init redis succss") 30 | 31 | minaAuth = authentication.NewAuthMiniProgram(authentication.NewDefaultSessionManager("DojK2hs*790k", client), time.Hour*24*7) 32 | 33 | return nil 34 | } 35 | ``` 36 | 37 | Login Demo 38 | ```golang 39 | func MinaLogin(c *gin.Context) { 40 | appID := "cli_9d1ad8ed77f69108" 41 | 42 | params, err := url.ParseQuery(c.Request.URL.RawQuery) 43 | if err != nil { 44 | common.Logger(c).Errorf("minaLogin: getParams[%v]", err) 45 | c.JSON(500, gin.H{"codemsg": common.ErrMinaCodeGetParams.String()}) 46 | return 47 | } 48 | 49 | code := params.Get("code") 50 | if code == "" { 51 | common.Logger(c).Errorf("minaLogin: code is empty") 52 | c.JSON(500, gin.H{"codemsg": common.ErrMinaCodeGetParams.String()}) 53 | return 54 | } 55 | 56 | host := c.Request.Host 57 | 58 | mapCookie, err := minaAuth.Login(c, code, appID, host) 59 | if err != nil { 60 | common.Logger(c).Errorf("minaLogin: codeToSession[%v]", err) 61 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 62 | return 63 | } 64 | 65 | for _, v := range mapCookie { 66 | http.SetCookie(c.Writer, v) 67 | } 68 | 69 | c.JSON(200, gin.H{"code": 0, "msg": ""}) 70 | return 71 | } 72 | ``` 73 | 74 | Check Login Demo 75 | ```golang 76 | func MinaCheckAuth(c *gin.Context) { 77 | appID := "cli_9d1ad8ed77f69108" 78 | 79 | sessionKeyName := minaAuth.GetSessionManager().GenerateSessionKeyName(appID) 80 | sessionKey, err := c.Cookie(sessionKeyName) 81 | if err != nil { 82 | common.Logger(c).Errorf("MinaCheckAuth: getSessionKey error[%v]", err) 83 | c.JSON(500, gin.H{"codemsg": common.ErrMinaSessionInvalid.String()}) 84 | return 85 | } 86 | common.Logger(c).Infof("MinaCheckAuth: sessionKeyName[%s]sessionKey[%s]", sessionKeyName, sessionKey) 87 | 88 | err = minaAuth.Auth(c, sessionKey) 89 | if err != nil { 90 | common.Logger(c).Errorf("MinaCheckAuth: Auth error[%v]", err) 91 | c.JSON(500, gin.H{"codemsg": common.ErrMinaSessionInvalid.String()}) 92 | return 93 | } 94 | 95 | c.JSON(200, gin.H{"code": 0, "msg": ""}) 96 | return 97 | } 98 | ``` 99 | 100 | Logout Demo 101 | ```golang 102 | func MinaLogout(c *gin.Context) { 103 | appID := "cli_9d1ad8ed77f69108" 104 | 105 | mapCookie, err := minaAuth.Logout(c, appID, c.Request.Host) 106 | if err != nil { 107 | common.Logger(c).Errorf("minaLogout: logout error[%v]", err) 108 | c.JSON(500, gin.H{"codemsg": common.ErrMinaSessionInvalid.String()}) 109 | return 110 | } 111 | 112 | for _, v := range mapCookie { 113 | http.SetCookie(c.Writer, v) 114 | common.Logger(c).Infof("minaLogout: cookie[%+v]", v) 115 | } 116 | 117 | c.JSON(200, gin.H{"code": 0, "msg": ""}) 118 | return 119 | } 120 | ``` 121 | -------------------------------------------------------------------------------- /docs/us/send_message.md: -------------------------------------------------------------------------------- 1 | # bot sends all kinds of messages 2 | You can use functions in SDK to send all kinds of messages easily. 3 | 4 | ## text 5 | Example code: 6 | ```go 7 | //send text 8 | func sendTextMessage(chatID, tenantKey, appID string) error { 9 | // Necessary steps. Necessary params. ID and type correspond one by one 10 | user := &protocol.UserInfo{ 11 | ID: chatID, 12 | Type: protocol.UserTypeChatID, 13 | } 14 | 15 | // Necessary steps. Necessary params. 16 | ctx := context.TODO() 17 | 18 | //Penultimate param means rootid,if you want to reply a message,rootid is this message's id.You can use "" in general. 19 | _, err := message.SendTextMessage(ctx, tenantKey, appID, user, "", "在飞书,享高效") 20 | if err != nil { 21 | return fmt.Errorf("send text failed[%v]", err) 22 | } 23 | 24 | return nil 25 | } 26 | ``` 27 | 28 | ## image 29 | There are three ways to send pictures: imageKey, path and url.(priority: imageKey>path>url) 30 | Example code: 31 | ```go 32 | //send image 33 | func sendImageMessage(chatID, tenantKey, appID string) error { 34 | //Necessary steps. Necessary params. ID and type correspond one by one 35 | user := &protocol.UserInfo{ 36 | ID: chatID, 37 | Type: protocol.UserTypeChatID, 38 | } 39 | //Necessary steps. Necessary params. 40 | ctx := context.TODO() 41 | 42 | //send image(use imageurl) 43 | url := "https://s0.pstatp.com/ee/lark-open/web/static/apply.226f11cb.png" 44 | _, err := message.SendImageMessage(ctx, tenantKey, appID, user, "", url, "", "") 45 | if err != nil { 46 | return fmt.Errorf("send image failed[%v]", err) 47 | } 48 | return nil 49 | } 50 | ``` 51 | 52 | If you want to use imageKey to send an image, you should get imageKey first. 53 | Example code: 54 | ```go 55 | user := &protocol.UserInfo{ 56 | ID: chatID, 57 | Type: protocol.UserTypeChatID, 58 | } 59 | ctx := context.TODO() 60 | 61 | url := "https://s0.pstatp.com/ee/lark-open/web/static/apply.226f11cb.png" 62 | imagekey,err := message.GetImageKey(ctx, tenantKey, appID, url, "" ) 63 | if err != nil{ 64 | return fmt.Errorf("get imageKey failed[%v]", err) 65 | } 66 | _, err = message.SendImageMessage(ctx, tenantKey, appID, user, "", "", "", imagekey) 67 | if err != nil { 68 | return fmt.Errorf("send image failed[%v]", err) 69 | } 70 | ``` 71 | 72 | ## rich text 73 | Example code: 74 | ```go 75 | //send rich text 76 | func sendRichTextMessage(chatID, tenantKey, appID, userID string) error { 77 | user := &protocol.UserInfo{ 78 | ID: chatID, 79 | Type: protocol.UserTypeChatID, 80 | } 81 | ctx := context.TODO() 82 | 83 | //add content of richtext 84 | //zh-cn 85 | titleCN := "这是一个标题" 86 | contentCN := message.NewRichTextContent() 87 | // first line 88 | contentCN.AddElementBlock( 89 | message.NewTextTag("第一行 :", true, 1), 90 | message.NewATag("超链接", true, "https://www.feishu.cn"), 91 | message.NewAtTag("用户名", userID), 92 | ) 93 | // second line 94 | contentCN.AddElementBlock( 95 | message.NewTextTag("第二行 :", true, 1), 96 | message.NewTextTag("文本测试", true, 1), 97 | ) 98 | 99 | //en-us 100 | titleUS := "this is a title" 101 | contentUS := message.NewRichTextContent() 102 | // first line 103 | contentUS.AddElementBlock( 104 | message.NewTextTag("first line :", true, 1), 105 | message.NewAtTag("username", userID), 106 | ) 107 | // second line 108 | contentUS.AddElementBlock( 109 | message.NewTextTag("second line :", true, 1), 110 | message.NewTextTag("text test", true, 1), 111 | ) 112 | 113 | postForm := make(map[protocol.Language]*protocol.RichTextForm) 114 | postForm[protocol.ZhCN] = message.NewRichTextForm(&titleCN, contentCN) 115 | postForm[protocol.EnUS] = message.NewRichTextForm(&titleUS, contentUS) 116 | 117 | //send rich text 118 | _, err := message.SendRichTextMessage(ctx, tenantKey, appID, user, "", postForm) 119 | if err != nil { 120 | return fmt.Errorf("send rich text failed[%v]", err) 121 | } 122 | return nil 123 | } 124 | ``` 125 | 126 | ## group card 127 | Example code: 128 | ```go 129 | //send group shared card 130 | func sendShareChatMessage(openID, tenantKey, appID, shareChatID string) error { 131 | user := &protocol.UserInfo{ 132 | ID: openID, 133 | Type: protocol.UserTypeOpenID, 134 | } 135 | 136 | ctx := context.TODO() 137 | 138 | //send group shared card(last param means group chat id, and this message will be sent to this user by openid) 139 | _, err := message.SendShareChatMessage(ctx, tenantKey, appID, user, "", shareChatID) 140 | if err != nil { 141 | return fmt.Errorf("send group card failed[%v]", err) 142 | } 143 | return nil 144 | } 145 | ``` 146 | 147 | ### send message demo 148 | [demo code](../../demo/send_message/send_message.go) 149 | -------------------------------------------------------------------------------- /docs/us/tenant_access_token.md: -------------------------------------------------------------------------------- 1 | # get and manage tenant_access_token 2 | You can get it in two steps. 3 | 1. Initialize SDK configuration. 4 | 2. Use GetTenantAccessToken function in SDK/auth/authorization.go to get tenant_access_token. 5 | 6 | ## Initialize SDK configuration 7 | Please refer to 8 | [SDK Init doc](../../README.md). 9 | [SDK Init code](../../demo/sdk_init/sdk_init.go) 10 | 11 | ## get tenant_access_token 12 | generated code: 13 | If you use generated code, you just need to provide param tenantKey, and SDK will use tenantKey to get tenant_access_token automatically. 14 | For example, when you use SendTextMessage function in SDK/message/message.go. 15 | ```go 16 | func SendTextMessage(ctx context.Context, tenantKey, appID string, 17 | user *protocol.UserInfo, rootID string, 18 | text string) (*protocol.SendMsgResponse, error) { 19 | return sendMsg(ctx, tenantKey, appID, protocol.NewTextMsgReq(user, rootID, text)) 20 | } 21 | ``` 22 | 23 | your own code: 24 | You can use GetTenantAccessToken function in SDK/auth/authorization.go to get tenant_access_token easily. 25 | Of course, you should initialize SDK configuration first. 26 | ```go 27 | func GetTenantAccessToken(ctx context.Context, tenantKey, appID string)(string,error){ 28 | accessToken, err := auth.GetTenantAccessToken(ctx, tenantKey, appID) 29 | if err != nil { 30 | return "", fmt.Errorf("get TenantAccessToken failed[%v]", err) 31 | } 32 | return accessToken, nil 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/us/webhook_card.md: -------------------------------------------------------------------------------- 1 | # webhook-card 2 | 3 | ## bind 4 | generated code: 5 | Code for binding has been generated in main.go. 6 | ```go 7 | r := gin.Default() 8 | 9 | r.POST("/webhook/card", CardCallback) //card action callback 10 | ``` 11 | 12 | ## callback 13 | generated code: 14 | Code for card callback has been generated in callback.go. You can use it just by providing some params. 15 | Example code: 16 | ```go 17 | // CardCallback card action callback 18 | func CardCallback(c *gin.Context) { 19 | body, err := ioutil.ReadAll(c.Request.Body) 20 | if err != nil || len(body) == 0 { 21 | c.JSON(500, gin.H{"codemsg": common.ErrCardParams.String()}) 22 | return 23 | } 24 | // for verify signature 25 | header := map[string]string{ 26 | "X-Lark-Request-Timestamp": c.Request.Header.Get("X-Lark-Request-Timestamp"), 27 | "X-Lark-Request-Nonce": c.Request.Header.Get("X-Lark-Request-Nonce"), 28 | "X-Lark-Signature": c.Request.Header.Get("X-Lark-Signature"), 29 | } 30 | appID := "your appid" 31 | card, challenge, err := event.CardCallBack(c, appID, header, body) 32 | if err != nil { 33 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 34 | } else if "" != challenge { 35 | c.JSON(200, gin.H{"challenge": challenge}) 36 | } else { 37 | data, err := json.Marshal(card) 38 | if err != nil { 39 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 40 | } else { 41 | c.String(200, string(data)) 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | 48 | ## register and callback 49 | Card callback functions were be registered with interaction modules. Here we use code to explain in detail. 50 | Example code: 51 | 1. registe event callback 52 | ```go 53 | func RegistHandler(appID string) { 54 | event.EventRegister(appID, protocol.EventTypeMessage, EventMessage) 55 | 56 | event.BotRecvMsgRegister(appID, "card", BotRecvMsgCard) 57 | 58 | event.CardRegister(appID, "clickbutton", ActionClickButton) 59 | } 60 | ``` 61 | 62 | 2. add BotRecvMsgCard function (send a card) 63 | ```go 64 | func EventMessage(ctx context.Context, eventBody []byte) error { 65 | return event.BotRecvMsgHandler(ctx, eventBody) 66 | } 67 | 68 | func BotRecvMsgCard(ctx context.Context, msg *protocol.BotRecvMsg) error { 69 | user := &protocol.UserInfo{ 70 | ID: msg.OpenChatID, 71 | Type: protocol.UserTypeChatID, 72 | } 73 | 74 | //build card 75 | builder := &message.CardBuilder{} 76 | //add config 77 | config := protocol.ConfigForm{ 78 | MinVersion: protocol.VersionForm{}, 79 | WideScreenMode: true, 80 | } 81 | builder.SetConfig(config) 82 | 83 | //add header 84 | content := "this is a card" 85 | line := 1 86 | title := protocol.TextForm{ 87 | Tag: protocol.PLAIN_TEXT_E, 88 | Content: &content, 89 | Lines: &line, 90 | } 91 | builder.AddHeader(title, "") 92 | 93 | //add button 94 | button1 := make(map[string]string, 0) 95 | button1["key"] = "buttonValue" 96 | builder.AddActionBlock([]protocol.ActionElement{ 97 | message.NewButton(message.NewMDText("callback button", nil, nil, nil), nil, nil, button1, protocol.DANGER, nil, "clickbutton"), 98 | }) 99 | 100 | card, err := builder.BuildForm() 101 | if err != nil { 102 | return fmt.Errorf("card build failed error[%v]", err) 103 | } 104 | 105 | //add params to use message.SendCardMessage 106 | //last param means updateMulti. If the param is true, the card type is shared, all users have the same card. 107 | //If the param is false, the card type is exclusive, usre can only change his own card in general. 108 | _, err := message.SendCardMessage(ctx, tenantKey, appID, user, "", *card, false) 109 | if err != nil { 110 | return fmt.Errorf("send message failed error[%v]", err) 111 | } 112 | 113 | return nil 114 | } 115 | ``` 116 | 117 | 3. add ActionClickButton function(update the card) 118 | ```go 119 | func ActionClickButton(ctx context.Context, callback *protocol.CardCallbackForm) (*protocol.CardForm, error) { 120 | method, _ := callback.Action.Value["method"] 121 | sessionID, _ := callback.Action.Value["sid"] 122 | common.Logger(ctx).Infof("cardActionCallBack: method[%s]sessionID[%s]", method, sessionID) 123 | 124 | //build card 125 | builder := &message.CardBuilder{} 126 | 127 | //add openids (if you want to update other people's card otherwise you can ignore it) 128 | openids := make([]string,1) 129 | openids[0] = "another user's openid" 130 | builder.OpenIDs = openids 131 | 132 | //add config 133 | config := protocol.ConfigForm{ 134 | MinVersion: protocol.VersionForm{}, 135 | WideScreenMode: true, 136 | } 137 | builder.SetConfig(config) 138 | 139 | //add header 140 | content := "this is a card" 141 | line := 1 142 | title := protocol.TextForm{ 143 | Tag: protocol.PLAIN_TEXT_E, 144 | Content: &content, 145 | Lines: &line, 146 | } 147 | builder.AddHeader(title, "") 148 | 149 | //add button 150 | button1 := make(map[string]string, 0) 151 | button1["key"] = "buttonValue" 152 | builder.AddActionBlock([]protocol.ActionElement{ 153 | message.NewButton(message.NewMDText("clicked button", nil, nil, nil), nil, nil, button1, protocol.UNKNOWN, nil, "clickbutton"), 154 | }) 155 | 156 | card, err := builder.BuildForm() 157 | if err != nil { 158 | return &protocol.CardForm{}, fmt.Errorf("card update failed error[%v]", err) 159 | } 160 | 161 | return card, nil 162 | } 163 | ``` 164 | -------------------------------------------------------------------------------- /docs/us/webhook_event.md: -------------------------------------------------------------------------------- 1 | # webhook-event 2 | 3 | ## bind 4 | generated code: 5 | Code for binding has been generated in main.go. 6 | ```go 7 | r := gin.Default() 8 | 9 | r.POST("/webhook/event", EventCallback) //open platform event callback 10 | ``` 11 | 12 | ## call back 13 | generated code: 14 | Code for event callback has been generated in `callback.go`. 15 | You can use it just by providing some params. 16 | ```go 17 | // EventCallback open platform event 18 | func EventCallback(c *gin.Context) { 19 | body, err := ioutil.ReadAll(c.Request.Body) 20 | if err != nil || len(body) == 0 { 21 | c.JSON(500, gin.H{"codemsg": common.ErrEventParams.String()}) 22 | return 23 | } 24 | appID := "your appid" 25 | challenge, err := event.EventCallback(c, string(body), appID) 26 | if err != nil { 27 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 28 | } else if "" != challenge { 29 | c.JSON(200, gin.H{"challenge": challenge}) 30 | } else { 31 | c.JSON(200, gin.H{"codemsg": common.Success.String()}) 32 | } 33 | } 34 | ``` 35 | 36 | your own code: 37 | Use EventCallback function in SDK/event/event.go to analyze http request body. 38 | 39 | ## register 40 | generated code: 41 | You can find RegistHandler function which was in regist.go. There are some response functions. 42 | 43 | Register Example code: 44 | ```go 45 | func RegistHandler(appID string) { 46 | // regist open platform event handler 47 | event.EventRegister(appID, protocol.EventTypeMessage, EventMessage) 48 | event.EventRegister(appID, protocol.EventTypeAppTicket, EventAppTicket) 49 | event.EventRegister(appID, protocol.EventTypeAppOpen, EventAppOpen) 50 | event.EventRegister(appID, protocol.EventTypeAddBot, EventAddBot) 51 | event.EventRegister(appID, protocol.EventTypeP2PChatCreate, EventP2PChatCreate) 52 | 53 | // regist bot recv message handler 54 | event.BotRecvMsgRegister(appID, "default", BotRecvMsgDefault) 55 | event.BotRecvMsgRegister(appID, "help", BotRecvMsgHelp) 56 | 57 | } 58 | ``` 59 | 60 | **create chat firstly example code**: 61 | ```go 62 | //add EventP2PChatCreate function 63 | func EventP2PChatCreate(ctx context.Context, eventBody []byte) error { 64 | request := &protocol.P2PChatCreateEvent{} 65 | err := json.Unmarshal(eventBody, request) 66 | if err != nil { 67 | return err 68 | } 69 | user:=&protocol.UserInfo{ 70 | ID: request.ChatID, 71 | Type: protocol.UserTypeChatID, 72 | } 73 | message.SendTextMessage(ctx,"your tenanKey",request.AppID,user,"","hello,P2PChatCreate") 74 | return nil 75 | } 76 | ``` 77 | 78 | **app_ticket for ISVApp example code**: 79 | ```go 80 | //register AppTicket event 81 | func RegistHandler(appID string) { 82 | event.EventRegister(appID, protocol.EventTypeAppTicket, EventAppTicket) 83 | } 84 | //add EventAppTicket function 85 | func EventAppTicket(ctx context.Context, eventBody []byte) error { 86 | return auth.RefreshAppTicket(ctx, eventBody) 87 | } 88 | ``` 89 | 90 | **BotRecvMsgDefault example code**: 91 | ```go 92 | //send "text that is empty or is not matched" 93 | func BotRecvMsgDefault(ctx context.Context, msg *protocol.BotRecvMsg) error { 94 | user:=&protocol.UserInfo{ 95 | ID: msg.OpenChatID, 96 | Type: protocol.UserTypeChatID, 97 | } 98 | input := msg.TextParam 99 | message.SendTextMessage(ctx,"your tenantKey",msg.AppID,user,"","Text that is empty or isnot matched") 100 | return nil 101 | } 102 | ``` 103 | 104 | **BotRecvMsgHelp example code**: 105 | ```go 106 | //send "this is help" 107 | func BotRecvMsgHelp(ctx context.Context, msg *protocol.BotRecvMsg) error { 108 | user:=&protocol.UserInfo{ 109 | ID: msg.OpenChatID, 110 | Type: protocol.UserTypeChatID, 111 | } 112 | message.SendTextMessage(ctx,"your tenantKey",msg.AppID,user,"","this is help") 113 | return nil 114 | } 115 | ``` 116 | -------------------------------------------------------------------------------- /docs/zh/app_access_token.md: -------------------------------------------------------------------------------- 1 | # app_access_token 的获取 2 | 获取 app_access_token 主要分为两部分。 3 | 1. 首先初始化 SDK 相关配置信息。 4 | 2. 然后调用 SDK/auth/authorization.go 中的 GetAppAccessToken 函数获取 app_access_token 。 5 | 6 | ## 初始化 SDK 相关配置信息 7 | 请参考 8 | [SDK Init 文档](../../README.zh-cn.md) 9 | [SDK Init 示例代码](../../demo/sdk_init/sdk_init.go) 10 | 11 | ## 获取 app_access_token 12 | 通过 SDK/auth/authorization.go 中的 GetAppAccessToken 函数可以直接获取,示例代码如下: 13 | (在使用以下函数前需要先初始化 SDK 相关配置信息) 14 | ```go 15 | func GetAppAccessToken(ctx context.Context, appID string)(string,error){ 16 | accessToken, err := auth.GetAppAccessToken(ctx, appID) 17 | if err != nil { 18 | return "", fmt.Errorf("get AppAccessToken failed[%v]", err) 19 | } 20 | return accessToken, nil 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/zh/authentication.md: -------------------------------------------------------------------------------- 1 | # 身份认证 2 | `SDK/authentication/authentication.go`: `Authentication` 定义身份认证接口,`SessionManager` 定义用户 session 存取接口。 3 | 4 | `SDK/authentication/default_session_manager.go`: SDK 提供默认的 seesion 存取实现,通过`NewDefaultSessionManager`函数获取,代码示例为 5 | ```golang 6 | client := &common.DefaultRedisClient{} 7 | client.InitDB(map[string]string{"addr": "127.0.0.1:6379"}) 8 | manager := authentication.NewDefaultSessionManager("DojK2hs*790(", client) 9 | ``` 10 | 11 | `SDK/authentication/mini_program.go`: SDK 提供了小程序身份认证相关接口 12 | `SDK/authentication/open_sso.go`: SDK 提供了 open sso 身份认证相关接口 13 | `SDK/authentication/auth_mini_program.go`: SDK 已经实现小程序的登录、登录态校验、登出操作 14 | 15 | ## 小程序身份认证示例代码 16 | 初始化 17 | ```golang 18 | var minaAuth *authentication.AuthMiniProgram 19 | 20 | func InitMinaAuth() error { 21 | client := &common.DefaultRedisClient{} 22 | err := client.InitDB(map[string]string{"addr": "127.0.0.1:6379"}) 23 | if err != nil { 24 | common.Logger(context.TODO()).Errorf("init redis error[%v]", err) 25 | return fmt.Errorf("init redis error[%v]", err) 26 | } 27 | common.Logger(context.TODO()).Infof("init redis succss") 28 | 29 | minaAuth = authentication.NewAuthMiniProgram(authentication.NewDefaultSessionManager("DojK2hs*790k", client), time.Hour*24*7) 30 | 31 | return nil 32 | } 33 | ``` 34 | 35 | 登录示例代码 36 | ```golang 37 | func MinaLogin(c *gin.Context) { 38 | appID := "cli_9d1ad8ed77f69108" 39 | 40 | params, err := url.ParseQuery(c.Request.URL.RawQuery) 41 | if err != nil { 42 | common.Logger(c).Errorf("minaLogin: getParams[%v]", err) 43 | c.JSON(500, gin.H{"codemsg": common.ErrMinaCodeGetParams.String()}) 44 | return 45 | } 46 | 47 | code := params.Get("code") 48 | if code == "" { 49 | common.Logger(c).Errorf("minaLogin: code is empty") 50 | c.JSON(500, gin.H{"codemsg": common.ErrMinaCodeGetParams.String()}) 51 | return 52 | } 53 | 54 | host := c.Request.Host 55 | 56 | mapCookie, err := minaAuth.Login(c, code, appID, host) 57 | if err != nil { 58 | common.Logger(c).Errorf("minaLogin: codeToSession[%v]", err) 59 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 60 | return 61 | } 62 | 63 | for _, v := range mapCookie { 64 | http.SetCookie(c.Writer, v) 65 | common.Logger(c).Infof("minaLogin: code[%s]host[%s]cookie[%+v]", code, host, v) 66 | } 67 | 68 | c.JSON(200, gin.H{"code": 0, "msg": ""}) 69 | return 70 | } 71 | ``` 72 | 73 | 校验登录状态示例代码 74 | ```golang 75 | func MinaCheckAuth(c *gin.Context) { 76 | appID := "cli_9d1ad8ed77f69108" 77 | 78 | sessionKeyName := minaAuth.GetSessionManager().GenerateSessionKeyName(appID) 79 | sessionKey, err := c.Cookie(sessionKeyName) 80 | if err != nil { 81 | common.Logger(c).Errorf("MinaCheckAuth: getSessionKey error[%v]", err) 82 | c.JSON(500, gin.H{"codemsg": common.ErrMinaSessionInvalid.String()}) 83 | return 84 | } 85 | common.Logger(c).Infof("MinaCheckAuth: sessionKeyName[%s]sessionKey[%s]", sessionKeyName, sessionKey) 86 | 87 | err = minaAuth.Auth(c, sessionKey) 88 | if err != nil { 89 | common.Logger(c).Errorf("MinaCheckAuth: Auth error[%v]", err) 90 | c.JSON(500, gin.H{"codemsg": common.ErrMinaSessionInvalid.String()}) 91 | return 92 | } 93 | 94 | c.JSON(200, gin.H{"code": 0, "msg": ""}) 95 | return 96 | } 97 | ``` 98 | 99 | 登出示例代码 100 | ```golang 101 | func MinaLogout(c *gin.Context) { 102 | appID := "cli_9d1ad8ed77f69108" 103 | 104 | mapCookie, err := minaAuth.Logout(c, appID, c.Request.Host) 105 | if err != nil { 106 | common.Logger(c).Errorf("minaLogout: logout error[%v]", err) 107 | c.JSON(500, gin.H{"codemsg": common.ErrMinaSessionInvalid.String()}) 108 | return 109 | } 110 | 111 | for _, v := range mapCookie { 112 | http.SetCookie(c.Writer, v) 113 | common.Logger(c).Infof("minaLogout: cookie[%+v]", v) 114 | } 115 | 116 | c.JSON(200, gin.H{"code": 0, "msg": ""}) 117 | return 118 | } 119 | ``` 120 | -------------------------------------------------------------------------------- /docs/zh/send_card.md: -------------------------------------------------------------------------------- 1 | # 消息卡片的构造 2 | 卡片主要分为三个部分: config 、 header 和 elements 。 3 | **config**: 4 | config 主要存储一系列卡片配置。config 模块中只有一个 bool 类型的字段 wide_screen_mode 。表示是否根据屏幕宽度动态调整消息卡片的宽度。默认为 false 值。 5 | **header**: 6 | header 主要配置消息卡片的标题内容。只有一个字段 title ,title 是一个 text 对象,且仅支持 "plain_text" 。 7 | **element**: 8 | element 为一个个模块。卡片的内容正是由一个个模块堆砌起来的。模块又分为内容模块、分割线模块、图片模块、交互模块、备注模块等五种。 9 | 10 | ## 如何构造并发送一张卡片 11 | 这里通过示例代码详细介绍如何构造一张卡片。 12 | 1. 声明一个结构体用来存储卡片的信息 13 | ```go 14 | builder := &message.CardBuilder{} 15 | ``` 16 | 17 | 2. 添加和配置 config 18 | ```go 19 | //add config 20 | config := protocol.ConfigForm{ 21 | MinVersion: protocol.VersionForm{}, 22 | WideScreenMode: true, 23 | } 24 | builder.SetConfig(config) 25 | ``` 26 | 27 | 3. 添加 header 28 | ```go 29 | //add header 30 | content := "this is a card" 31 | line := 1 32 | title := protocol.TextForm{ 33 | Tag: protocol.PLAIN_TEXT_E, 34 | Content: &content, 35 | Lines: &line, 36 | } 37 | builder.AddHeader(title, "") 38 | ``` 39 | 40 | 4. 添加一个简单的模块 41 | ```go 42 | //add button 43 | button1 := make(map[string]string, 0) 44 | button1["key"] = "buttonValue" 45 | builder.AddActionBlock([]protocol.ActionElement{ 46 | message.NewButton(message.NewMDText("callback button", nil, nil, nil), nil, nil, button1, protocol.DANGER, nil, "clickbutton"), 47 | }) 48 | ``` 49 | 50 | 5. 将该结构体转换为卡片类型,并通过 message.SendCardMessage 函数发送卡片 51 | ```go 52 | card, err := builder.BuildForm() 53 | if err != nil { 54 | return fmt.Errorf("card build failed error[%v]", err) 55 | } 56 | _, err = message.SendCardMessage(ctx, tenantKey, appID, user, "", *card, false) 57 | if err != nil { 58 | return fmt.Errorf("send message failed error[%v]", err) 59 | } 60 | ``` 61 | 62 | ## 模块说明 63 | ### 内容模块 64 | 内容模块是用途最灵活的模块,可以在其中组合各类用于展示的内容。 65 | 模块标签为 “div” ,可以单独通过 text 和 field 来展示文本内容,也可以配合一个 image 元素或一个 button , overflow , selectMenu , pickerDate 等互动元素增加内容的丰富性。 66 | ```go 67 | //设置文本的超链接 如果不需要可以不设置 68 | urlhref := make(map[string]protocol.URLForm, 0) 69 | uHost := "https://www.larksuite.com/" 70 | urlhref["link"] = protocol.URLForm{Url: &uHost} 71 | //增加内容模块的标题,内容,附加元素(这里没有添加) 72 | builder.AddDIVBlock(message.NewMDText("[lark]($link)", nil, nil, urlhref), []protocol.FieldForm{ 73 | *message.NewField(false, message.NewMDText("**boldText**", nil, nil, nil)), 74 | *message.NewField(false, message.NewMDText("text", nil, nil, nil)), 75 | }, nil) 76 | ``` 77 | 78 | ### 分割线模块 79 | 分割线模块在模块之间添加一条分割线。 80 | ```go 81 | builder.AddHRBlock() 82 | ``` 83 | 84 | ### 图片模块 85 | 图片模块分为三个部分: title,alt和imageky。 86 | tilte中存放图片的标题。alt中存放鼠标指针悬停在图片上时显示的图片说明。 87 | ```go 88 | //增加图片模块 89 | ImageContent := "Description when your mouse is over the picture" 90 | ImageTag := protocol.TextForm{ 91 | Tag: protocol.PLAIN_TEXT_E, 92 | Content: &ImageContent, 93 | Lines: nil, 94 | } 95 | 96 | //获取imageKey,可以在 docs/zh/send_message.md/机器人发送图片消息 查看imagekey的详细说明 97 | imageURL := "https://s0.pstatp.com/ee/lark-open/web/static/apply.226f11cb.png" 98 | imageKey, err := message.GetImageKey(ctx, tenantKey, appID, imageURL, "") 99 | if err != nil { 100 | return fmt.Errorf("get imageKey failed[%v]", err) 101 | } 102 | builder.AddImageBlock(message.NewMDText("image title", nil, nil, nil), ImageTag, imageKey) 103 | ``` 104 | 105 | ### 备注模块 106 | 备注模块用来展示用于辅助说明或备注的次要信息,支持小尺寸的图片和文本。 107 | ```go 108 | //增加备注模块 109 | noteTextContent := "备注文本内容" 110 | notetext := protocol.TextForm{ 111 | Tag: protocol.PLAIN_TEXT_E, 112 | Content: ¬eTextContent, 113 | Lines: nil, 114 | } 115 | noteImageContent := "备注图片内容" 116 | noteImage := protocol.ImageForm{ 117 | Tag: protocol.IMG_BLOCK, 118 | ImageKey: imageKey, //这里填写正确的imageKey 119 | ALT: protocol.TextForm{ 120 | Tag: protocol.PLAIN_TEXT_E, 121 | Content: ¬eImageContent, 122 | Lines: nil, 123 | }, 124 | } 125 | builder.AddNoteBlock([]protocol.BaseElement{ 126 | ¬etext, 127 | ¬eImage, 128 | }) 129 | ``` 130 | 131 | #### 交互模块 132 | 交互模块用于承载交互元素。 133 | 卡片提供了 4 类交互元素,通过 elements 字段添加交互元素,实现交互功能。 134 | 135 | 回传方式:每个交互元素或交互选项都提供了 value 字段,用户点击交互元素或选项之后,业务方会收到对应的 value 字段值,以此决定后续操作。业务方可知用户行为。 136 | 137 | 跳转方式:button 和 overflow 的选项配置跳转链接,用户点击跳转相应链接,不回传 value 字段值到业务方。业务方不可知用户行为。 138 | 139 | #### button 140 | button属于交互元素的一种,可用于内容块的extra字段和交互块的elements字段。 141 | ```go 142 | button1 := make(map[string]string, 0) 143 | button1["key"] = "value" 144 | url := "https://www.feishu.cn" 145 | builder.AddActionBlock([]protocol.ActionElement{ 146 | message.NewButton(message.NewMDText("button",nil,nil,nil),&url,nil,button1,protocol.PRIMARY,nil,"nil"), 147 | }) 148 | ``` 149 | 150 | #### selectMenu 151 | selectMenu属于交互元素的一种,提供选项菜单的功能,可用于内容块的extra字段和交互块的elements字段。 152 | 选项菜单 153 | ```go 154 | //SelectStaticMenu 155 | optiontextcontent1 := "选项一" 156 | optiontext1 := protocol.TextForm{ 157 | Tag: protocol.PLAIN_TEXT_E, 158 | Content: &optiontextcontent1, 159 | Lines: nil, 160 | } 161 | optiontextcontent2 := "选项二" 162 | optiontext2 := protocol.TextForm{ 163 | Tag: protocol.PLAIN_TEXT_E, 164 | Content: &optiontextcontent2, 165 | Lines: nil, 166 | } 167 | value1 := "value1" 168 | value2 := "value2" 169 | builder.AddActionBlock([]protocol.ActionElement{ 170 | message.NewSelectStaticMenu(message.NewMDText("SelectStaticMenu", nil, nil, nil), nil, []protocol.OptionForm{ 171 | message.NewOption(optiontext1, value1), 172 | message.NewOption(optiontext2, value2), 173 | }, &value1, nil, "testcard"), //option1 is default option 174 | }, 175 | ) 176 | ``` 177 | 178 | 选人菜单 179 | ```go 180 | //SelectPersonMenu 181 | builder.AddActionBlock([]protocol.ActionElement{ 182 | message.NewSelectPersonMenu(message.NewMDText("SelectPersonMenu", nil, nil, nil), nil, []protocol.OptionForm{}, nil, nil, "testcard"), 183 | }, 184 | ) 185 | ``` 186 | 187 | #### overflow 188 | 提供折叠的按钮型菜单。 189 | overflow 属于交互元素的一种,可用于内容块的 extra 字段和交互块的 elements 字段。 190 | 通过 options 字段配置选项,可用于多个按扭的折叠隐藏功能。 191 | ```go 192 | //add overflow 193 | overFlowcontent1 := "选项一" 194 | overFlowtext1 := protocol.TextForm{ 195 | Tag: protocol.PLAIN_TEXT_E, 196 | Content: &overFlowcontent1, 197 | Lines: nil, 198 | } 199 | overFlowcontent2 := "选项二" 200 | overFlowtext2 := protocol.TextForm{ 201 | Tag: protocol.PLAIN_TEXT_E, 202 | Content: &overFlowcontent2, 203 | Lines: nil, 204 | } 205 | builder.AddActionBlock([]protocol.ActionElement{ 206 | message.NewOverflowMenu(nil, []protocol.OptionForm{ 207 | message.NewOption(overFlowtext1, value1), 208 | message.NewOption(overFlowtext2, value2), 209 | }, nil, "testcard"), 210 | }, 211 | ) 212 | ``` 213 | 214 | ##### PickerDate 215 | 提供日期选择的功能。 216 | PickerDate 属于交互元素的一种,可用于内容块的extra字段和交互块的elements字段。 217 | ```go 218 | //add PickerDate 219 | timePicker := time.Now().Format("2006-01-02") 220 | builder.AddActionBlock([]protocol.ActionElement{ 221 | message.NewPickerDate(message.NewMDText("PickerDate", nil, nil, nil), nil, nil, &timePicker, "testcard"), 222 | }, 223 | ) 224 | ``` 225 | 226 | ##### 延迟更新卡片 227 | 业务方使用交互返回的 token 凭证,在30分钟内最多更新两次卡片。 228 | 除正常卡片内容外,还有 open_ids 字段控制更新指定用户的消息卡片,字段值为用户 openId 数组。 229 | 示例代码如下: 230 | ```go 231 | // use message.UpdateCard function to update card 232 | func UpdateCard(token, tenantkey, appid string, openid []string) error { 233 | //token可以在交互模块回调时获得 234 | 235 | //build a new card 236 | builder := &message.CardBuilder{} 237 | //add config 238 | config := protocol.ConfigForm{ 239 | MinVersion: protocol.VersionForm{}, 240 | WideScreenMode: true, 241 | } 242 | builder.SetConfig(config) 243 | 244 | //add header 245 | content := "this is a card" 246 | line := 1 247 | title := protocol.TextForm{ 248 | Tag: protocol.PLAIN_TEXT_E, 249 | Content: &content, 250 | Lines: &line, 251 | } 252 | builder.AddHeader(title, "") 253 | 254 | builder.AddDIVBlock(nil, []protocol.FieldForm{ 255 | *message.NewField(false, message.NewMDText("updatecard by message.UpdateCard", nil, nil, nil)), 256 | }, nil) 257 | 258 | card, err := builder.BuildForm() 259 | if err != nil { 260 | return fmt.Errorf("build card failed error[%v]", err) 261 | } 262 | 263 | // card.OpenIDs can't be nil. 264 | card.OpenIDs = openid 265 | 266 | _, err = message.UpdateCard(context.TODO(), tenantkey, appid, token, *card) 267 | if err != nil { 268 | return fmt.Errorf("card update failed error[%v]", err) 269 | } 270 | return nil 271 | } 272 | ``` 273 | 274 | ### 发送卡片 demo 275 | [demo code](../../demo/send_card/send_card.go) 276 | 该示例代码实现了卡片内增加内容模块、分割线、图片模块、交互模块、备注模块等,可供开发者参考。 277 | -------------------------------------------------------------------------------- /docs/zh/send_message.md: -------------------------------------------------------------------------------- 1 | # 机器人发送各类消息 2 | 可以非常简单的通过调用 SDK 中的一系列函数来发送各类消息。 3 | 4 | ## 机器人发送文本信息 5 | 调用 SDK 中的 message.SendTextMessage 函数即可发送文本消息。 6 | 示例代码如下: 7 | ```go 8 | func sendTextMessage(chatID, tenantKey, appID string) error { 9 | user := &protocol.UserInfo{ 10 | ID: chatID, 11 | Type: protocol.UserTypeChatID, 12 | } 13 | ctx := context.TODO() 14 | 15 | _, err := message.SendTextMessage(ctx, tenantKey, appID, user, "", "在飞书,享高效") 16 | if err != nil { 17 | return fmt.Errorf("send text failed[%v]", err) 18 | } 19 | 20 | return nil 21 | } 22 | ``` 23 | 24 | ## 机器人发送图片信息 25 | 调用 SDK 中的 message.SendImageMessage 函数即可发送图片消息。 26 | 该函数的最后三个参数分别为图片 url 、图片 path 和图片 imageKey 。 27 | 这三个参数填写任意一个即可,如果填写多个,则会按照 imagekey -> path -> url 的优先级使用。 28 | 底层接口实际需要 imagekey ,当使用 path 或 url 时,函数内部会先调用上传图片接口换取 imagekey 。 29 | 示例代码如下: 30 | ```go 31 | //发送图片消息函数 32 | func sendImageMessage(chatID, tenantKey, appID string) error { 33 | user := &protocol.UserInfo{ 34 | ID: chatID, 35 | Type: protocol.UserTypeChatID, 36 | } 37 | ctx := context.TODO() 38 | 39 | //发送图片消息,以url为例 40 | url := "https://s0.pstatp.com/ee/lark-open/web/static/apply.226f11cb.png" 41 | _, err := message.SendImageMessage(ctx, tenantKey, appID, user, "", url, "", "") 42 | if err != nil { 43 | return fmt.Errorf("send image failed[%v]", err) 44 | } 45 | return nil 46 | } 47 | ``` 48 | 49 | 如果想通过 imagekey 发送图片消息,需要先获取图片的 imagkey 。(获取 imagkey 的函数仍然需要 url 或 path 其中一项) 50 | 先上传图片,得到 imagekey ,以后就可以通过该 imagekey 发送图片了。 51 | 实际上 message.SendImageMessage 函数内部也是通过该方式先获取 imagekey 再通过 imagekey 发送图片的。 52 | ```go 53 | user := &protocol.UserInfo{ 54 | ID: chatID, 55 | Type: protocol.UserTypeChatID, 56 | } 57 | ctx := context.TODO() 58 | 59 | url := "https://s0.pstatp.com/ee/lark-open/web/static/apply.226f11cb.png" 60 | imagekey,err := message.GetImageKey(ctx, tenantKey, appID, url, "" ) 61 | if err != nil{ 62 | return fmt.Errorf("get imageKey failed[%v]", err) 63 | } 64 | 65 | _, err = message.SendImageMessage(ctx, tenantKey, appID, user, "", "", "", imagekey) 66 | if err != nil { 67 | return fmt.Errorf("send image failed[%v]", err) 68 | } 69 | ``` 70 | 71 | ## 机器人发送富文本信息 72 | 调用 SDK 中的 message.SendRichTextMessage 函数即可发送富文本消息。 73 | 示例代码如下: 74 | ```go 75 | //发送富文本消息函数 76 | func sendRichTextMessage(chatID, tenantKey, appID, userID string) error { 77 | user := &protocol.UserInfo{ 78 | ID: chatID, 79 | Type: protocol.UserTypeChatID, 80 | } 81 | ctx := context.TODO() 82 | 83 | //构建富文本内容 84 | 85 | //i18n zh-CN 86 | //标题内容自定义,将作为必要参数传入 87 | titleCN := "这是一个标题" 88 | //设置富文本content,将作为必要参数传入 89 | contentCN := message.NewRichTextContent() 90 | //设置富文本具体内容,如果不设置则没有内容,用户自定义即可 91 | contentCN.AddElementBlock( 92 | message.NewTextTag("第一行 :", true, 1), 93 | message.NewATag("超链接", true, "https://www.feishu.cn"), 94 | message.NewAtTag("用户名", userID), 95 | ) 96 | contentCN.AddElementBlock( 97 | message.NewTextTag("第二行 :", true, 1), 98 | message.NewTextTag("文本测试", true, 1), 99 | ) 100 | 101 | //i18n en-US 102 | titleUS := "this is a title" 103 | contentUS := message.NewRichTextContent() 104 | contentUS.AddElementBlock( 105 | message.NewTextTag("first line :", true, 1), 106 | message.NewATag("href", true, "https://www.feishu.cn"), 107 | message.NewAtTag("username", userID), 108 | ) 109 | contentUS.AddElementBlock( 110 | message.NewTextTag("second line :", true, 1), 111 | message.NewTextTag("text test", true, 1), 112 | ) 113 | postForm := make(map[protocol.Language]*protocol.RichTextForm) 114 | postForm[protocol.ZhCN] = message.NewRichTextForm(&titleCN, contentCN) 115 | postForm[protocol.EnUS] = message.NewRichTextForm(&titleUS, contentUS) 116 | 117 | //发送富文本消息 118 | _, err := message.SendRichTextMessage(ctx, tenantKey, appID, user, "", postForm) 119 | if err != nil { 120 | return fmt.Errorf("send rich text failed[%v]", err) 121 | } 122 | return nil 123 | } 124 | ``` 125 | 126 | ## 机器人发送群名片信息 127 | 调用 SDK 中的 message.SendRichTextMessage 函数即可发送群名片消息。 128 | 示例代码如下: 129 | ```go 130 | //发送群名片消息函数 131 | func sendShareChatMessage(openID, tenantKey, appID, shareChatID string) error { 132 | user := &protocol.UserInfo{ 133 | ID: openID, 134 | Type: protocol.UserTypeOpenID, 135 | } 136 | 137 | ctx := context.TODO() 138 | 139 | _, err := message.SendShareChatMessage(ctx, tenantKey, appID, user, "", shareChatID) 140 | if err != nil { 141 | return fmt.Errorf("send group card failed[%v]", err) 142 | } 143 | return nil 144 | } 145 | ``` 146 | 147 | ## 发送卡片消息 148 | 调用 SDK 中的 message.SendCardMessage 函数即可发送卡片消息。 149 | [demo code](../../demo/send_card/send_card.go) 150 | 该示例代码实现了卡片内增加内容模块、分割线、图片模块、交互模块、备注模块等,可供开发者参考。 151 | -------------------------------------------------------------------------------- /docs/zh/tenant_access_token.md: -------------------------------------------------------------------------------- 1 | # tenant_access_token 的获取和管理 2 | 获取 tenant_access_token 主要分为两部分。 3 | 1. 首先初始化 SDK 相关配置信息。 4 | 2. 然后调用 SDK/auth/authorization.go 中的 GetTenantAccessToken 函数获取 tenant_access_token 。 5 | 6 | ## 初始化 SDK 相关配置信息 7 | 请参考[SDK Init](../../README.zh-cn.md). 8 | 9 | ## 获取 tenant_access_token 10 | gin 框架生成代码: 11 | 使用自动生成的代码无需特地获取 tenant_access_token ,在需要使用 tenant_access_token 的地方,如发送文本消息时使用的 SendTextMessage 函数,参数中有 tenantKey 一项,输入正确的 tenantKey 即可。 12 | SDK 会自动使用 tenantKey 去获取 tenant_access_token ,而无需开发者手动获取。 13 | SendTextMessage 函数如下: 14 | ```go 15 | func SendTextMessage(ctx context.Context, tenantKey, appID string, 16 | user *protocol.UserInfo, rootID string, 17 | text string) (*protocol.SendMsgResponse, error) { 18 | return sendMsg(ctx, tenantKey, appID, protocol.NewTextMsgReq(user, rootID, text)) 19 | } 20 | ``` 21 | 22 | 手动编写代码: 23 | 通过 SDK/auth/authorization.go 中的 GetTenantAccessToken 函数可以直接获取,示例代码如下: 24 | (在使用以下函数前需要先初始化 SDK 相关配置信息) 25 | ```go 26 | func GetTenantAccessToken(ctx context.Context, tenantKey, appID string)(string,error){ 27 | accessToken, err := auth.GetTenantAccessToken(ctx, tenantKey, appID) 28 | if err != nil { 29 | return "", fmt.Errorf("get TenantAccessToken failed[%v]", err) 30 | } 31 | return accessToken, nil 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/zh/webhook_card.md: -------------------------------------------------------------------------------- 1 | # webhook-card 2 | 对于开放平台卡片交互回调 3 | 1. 在 feishu 或 lark 开放平台应用信息页面,将应用功能--机器人选项卡配置页面上的消息卡片请求网址 URL 与应用后端服务做关联,即该 URL 的请求最终会请求到应用后端服务。 4 | 2. 应用后端服务调用 SDK 的 event.CardCallBack 函数进行协议解析,将不同的交互操作路由到对应的回调处理函数中,处理相应业务逻辑。 5 | 6 | 本框架支持自动生成基于 gin 框架的应用后端代码,其中已经实现了卡片交互回调的注册和调用操作,可供开发者使用或参考。 7 | 8 | ## 绑定 9 | 将消息卡片请求网址 URL 指向应用后端服务之后 10 | 如果使用 gin 框架,在 main 函数中添加如下代码,其中 CardCallback 为本框架生成的订阅事件回调处理函数。 11 | ```go 12 | r := gin.Default() 13 | 14 | r.POST("/webhook/card", CardCallback) //card action callback 15 | ``` 16 | 17 | ## 回调与解析 18 | 如果使用了自动生成的代码,则回调与解析函数也已经自动生成了,也就是生成代码 callback.go 文件中的 CardCallback 函数。 19 | 只需要将函数中的 appID 替换为正确的应用 ID 即可。 20 | 21 | 下面给出 CardCallback 示例代码。 22 | ```go 23 | // CardCallback card action callback 24 | func CardCallback(c *gin.Context) { 25 | body, err := ioutil.ReadAll(c.Request.Body) 26 | if err != nil || len(body) == 0 { 27 | c.JSON(500, gin.H{"codemsg": common.ErrCardParams.String()}) 28 | return 29 | } 30 | // for verify signature 31 | header := map[string]string{ 32 | "X-Lark-Request-Timestamp": c.Request.Header.Get("X-Lark-Request-Timestamp"), 33 | "X-Lark-Request-Nonce": c.Request.Header.Get("X-Lark-Request-Nonce"), 34 | "X-Lark-Signature": c.Request.Header.Get("X-Lark-Signature"), 35 | } 36 | appID := "your appid" 37 | card, challenge, err := event.CardCallBack(c, appID, header, body) 38 | if err != nil { 39 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 40 | } else if "" != challenge { 41 | c.JSON(200, gin.H{"challenge": challenge}) 42 | } else { 43 | data, err := json.Marshal(card) 44 | if err != nil { 45 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 46 | } else { 47 | c.String(200, string(data)) 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | ## 事件注册和处理 54 | 用户点击卡片上的交互模块之后,应用后端服务将接收到对应的请求,框架将依据注册信息,路由到对应的回调函数执行业务逻辑。 55 | 56 | 以“机器人接收以 card 为首单词的消息后,后端服务向用户发送卡片消息,用户收到卡片消息后点击卡片上的按钮,后端服务接收用户点击操作的信息”这个业务逻辑为例,说明卡片交互事件的处理流程。 57 | 58 | 1. 注册 event 事件。 59 | ```go 60 | func RegistHandler(appID string) { 61 | //机器人接收消息回调注册 62 | event.EventRegister(appID, protocol.EventTypeMessage, EventMessage) 63 | 64 | //以 card 为首单词的消息处理回调函数注册 65 | event.BotRecvMsgRegister(appID, "card", BotRecvMsgCard) 66 | 67 | //卡片交互回调注册:将method clickbutton 和函数ActionClickButton绑定 68 | event.CardRegister(appID, "clickbutton", ActionClickButton) 69 | } 70 | ``` 71 | 72 | 2. 补全 EventMessage 函数和 BotRecvMsgCard 函数。 73 | (如果使用gin框架自动生成的代码则 EventMessage 函数已经生成了) 74 | ```go 75 | func EventMessage(ctx context.Context, eventBody []byte) error { 76 | return event.BotRecvMsgHandler(ctx, eventBody) 77 | } 78 | 79 | func BotRecvMsgCard(ctx context.Context, msg *protocol.BotRecvMsg) error { 80 | user := &protocol.UserInfo{ 81 | ID: msg.OpenChatID, 82 | Type: protocol.UserTypeChatID, 83 | } 84 | 85 | //build card 86 | builder := &message.CardBuilder{} 87 | //add config 88 | config := protocol.ConfigForm{ 89 | MinVersion: protocol.VersionForm{}, 90 | WideScreenMode: true, 91 | } 92 | builder.SetConfig(config) 93 | 94 | //add header 95 | content := "this is a card" 96 | line := 1 97 | title := protocol.TextForm{ 98 | Tag: protocol.PLAIN_TEXT_E, 99 | Content: &content, 100 | Lines: &line, 101 | } 102 | builder.AddHeader(title, "") 103 | 104 | //add button 105 | button1 := make(map[string]string, 0) 106 | button1["key"] = "buttonValue" 107 | builder.AddActionBlock([]protocol.ActionElement{ 108 | message.NewButton(message.NewMDText("callback button", nil, nil, nil), nil, nil, button1, protocol.DANGER, nil, "clickbutton"), 109 | }) 110 | 111 | card, err := builder.BuildForm() 112 | if err != nil { 113 | return fmt.Errorf("card build failed error[%v]", err) 114 | } 115 | 116 | //add params to use message.SendCardMessage 117 | //最后一个参数为卡片的类型,独享或共享。当为false时,卡片为独享卡片,只会更新指定用户看到的卡片(发出更新卡片请求的用户的卡片一定会被更新)。未指定用户的卡片并不会被更新。而当updateMulti为true时,卡片为共享卡片,所有用户的卡片都会被同步更新。 118 | _, err := message.SendCardMessage(ctx, tenantKey, appID, user, "", *card, false) 119 | if err != nil { 120 | return fmt.Errorf("send message failed error[%v]", err) 121 | } 122 | 123 | return nil 124 | } 125 | ``` 126 | 127 | 上面的 BotRecvMsgAsyncButton 函数会在用户发送"card"后,回复一张含有一个名为"callback button"的按钮的卡片。 128 | 点击消息卡片中的按钮,便会发送相应的消息卡片请求。 129 | 130 | 3. 实现响应函数 131 | ```go 132 | func ActionClickButton(ctx context.Context, callback *protocol.CardCallbackForm) (*protocol.CardForm, error) { 133 | method, _ := callback.Action.Value["method"] 134 | sessionID, _ := callback.Action.Value["sid"] 135 | common.Logger(ctx).Infof("cardActionCallBack: method[%s]sessionID[%s]", method, sessionID) 136 | 137 | //build card 138 | builder := &message.CardBuilder{} 139 | 140 | //add openids (如果你想更新其他用户的卡片,可以向OpenIDs参数中加入对应用户的openid,如果不需要,可以不写) 141 | openids := make([]string,1) 142 | openids[0] = "another user's openid" 143 | builder.OpenIDs = openids 144 | 145 | //add config 146 | config := protocol.ConfigForm{ 147 | MinVersion: protocol.VersionForm{}, 148 | WideScreenMode: true, 149 | } 150 | builder.SetConfig(config) 151 | 152 | //add header 153 | content := "this is a card" 154 | line := 1 155 | title := protocol.TextForm{ 156 | Tag: protocol.PLAIN_TEXT_E, 157 | Content: &content, 158 | Lines: &line, 159 | } 160 | builder.AddHeader(title, "") 161 | 162 | //add button 163 | button1 := make(map[string]string, 0) 164 | button1["key"] = "buttonValue" 165 | builder.AddActionBlock([]protocol.ActionElement{ 166 | message.NewButton(message.NewMDText("clicked button", nil, nil, nil), nil, nil, button1, protocol.UNKNOWN, nil, "clickbutton"), 167 | }) 168 | 169 | card, err := builder.BuildForm() 170 | if err != nil { 171 | return &protocol.CardForm{}, fmt.Errorf("card update failed error[%v]", err) 172 | } 173 | 174 | return card, nil 175 | } 176 | ``` 177 | 该响应函数实现: 用户点击按钮后,将卡片上的按钮更新为灰色状态。 178 | -------------------------------------------------------------------------------- /docs/zh/webhook_event.md: -------------------------------------------------------------------------------- 1 | # webhook-event 2 | 对于开放平台订阅事件 3 | 1. 将 feishu 或 lark 开放平台应用事件订阅配置页面上的请求网址URL与应用后端服务做关联,即该 URL 的请求最终会请求到应用后端服务。 4 | 2. 应用后端服务调用 SDK 的 event.EventCallback 函数进行协议解析,将不同的事件路由到对应的事件处理函数中,处理相应业务逻辑。 5 | 6 | 本框架支持自动生成基于 gin 框架的应用后端代码,其中已经实现了事件订阅的相关操作,可供开发者使用或参考。 7 | 8 | ## 绑定回调处理函数 9 | 将应用事件订阅配置页面上的请求网址URL指向应用后端服务之后 10 | 如果使用 gin 框架,在 main 函数中添加如下代码,其中 EventCallback 为本框架生成的订阅事件回调处理函数。 11 | ```go 12 | r := gin.Default() 13 | 14 | r.POST("/webhook/event", EventCallback) //open platform event callback 15 | ``` 16 | 17 | ## 回调与解析 18 | 如果使用了自动生成的代码,则回调与解析函数也已经自动生成了,也就是生成代码 `callback.go` 文件中的 EventCallback 函数。 19 | 只需要将函数中的 appID 替换为正确的应用 ID 即可。 20 | 21 | 下面给出 EventCallback 示例代码。 22 | ```go 23 | // EventCallback open platform event 24 | func EventCallback(c *gin.Context) { 25 | body, err := ioutil.ReadAll(c.Request.Body) 26 | if err != nil || len(body) == 0 { 27 | c.JSON(500, gin.H{"codemsg": common.ErrEventParams.String()}) 28 | return 29 | } 30 | appID := "your appid" 31 | challenge, err := event.EventCallback(c, string(body), appID) 32 | if err != nil { 33 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 34 | } else if "" != challenge { 35 | c.JSON(200, gin.H{"challenge": challenge}) 36 | } else { 37 | c.JSON(200, gin.H{"codemsg": common.Success.String()}) 38 | } 39 | } 40 | ``` 41 | 42 | 手动编写代码: 43 | 主要是通过调用 SDK/event/event.go 中的 EventCallback 函数进行对 http 请求包中的 body 进行解析。 44 | 45 | ## 事件注册和处理 46 | 本框架是基于注册和回调函数来实现事件的分发和处理的。 47 | 开发者需要先注册回调函数,之后 event.EventCallback 函数将解析请求协议并调用对应事件的回调处理函数,来执行对应的业务逻辑。 48 | 49 | 框架自动生成代码: 50 | 关于事件注册,框架会基于配置文件自动生成注册函数和回调处理函数。 51 | 在生成代码的 handler_event/regist.go 文件中有 RegistHandler 函数,函数里注册了一系列相应的事件处理函数。 52 | 简单的示例代码如下: 53 | ```go 54 | func RegistHandler(appID string) { 55 | // regist open platform event handler 56 | event.EventRegister(appID, protocol.EventTypeMessage, EventMessage) 57 | event.EventRegister(appID, protocol.EventTypeAppTicket, EventAppTicket) 58 | event.EventRegister(appID, protocol.EventTypeAppOpen, EventAppOpen) 59 | event.EventRegister(appID, protocol.EventTypeAddBot, EventAddBot) 60 | event.EventRegister(appID, protocol.EventTypeP2PChatCreate, EventP2PChatCreate) 61 | 62 | // regist bot recv message handler 63 | event.BotRecvMsgRegister(appID, "default", BotRecvMsgDefault) 64 | event.BotRecvMsgRegister(appID, "help", BotRecvMsgHelp) 65 | 66 | } 67 | ``` 68 | event.EventRegister 函数用于注册不同种类的订阅事件。 69 | event.BotRecvMsgRegister 函数用于注册机器人接收消息时不同消息首单词的回调函数,它是 protocol.EventTypeMessage 类型事件的细分,方便基于接收消息首单词执行不同的业务操作。 70 | event.BotRecvMsgRegister 函数中第二个参数为关键字,当消息首单词为该关键词时,将指向对应的回调函数,其中当未匹配到任何关键字时,将调用 default 关键字对应的回调函数。 71 | 72 | **用户和机器人会话首次被创建事件回调函数,示例代码如下** 73 | ```go 74 | //EventP2PChatCreate在会话首次被创建时向用户发送"hello,P2PChatCreate"消息 75 | func EventP2PChatCreate(ctx context.Context, eventBody []byte) error { 76 | request := &protocol.P2PChatCreateEvent{} 77 | err := json.Unmarshal(eventBody, request) 78 | if err != nil { 79 | return err 80 | } 81 | user:=&protocol.UserInfo{ 82 | ID: request.ChatID, 83 | Type: protocol.UserTypeChatID, 84 | } 85 | message.SendTextMessage(ctx,"your tenanKey",request.AppID,user,"","hello,P2PChatCreate") 86 | return nil 87 | } 88 | ``` 89 | 90 | **ISVApp 的 app_ticket 事件回调函数,示例代码如下** 91 | ```go 92 | //EventAppTicket函数自动更新AppTicket 93 | func EventAppTicket(ctx context.Context, eventBody []byte) error { 94 | return auth.RefreshAppTicket(ctx, eventBody) 95 | } 96 | ``` 97 | 98 | **BotRecvMsgDefault 函数示例代码如下** 99 | ```go 100 | //(该函数会向用户回复"Text that is empty or is not matched"消息) 101 | func BotRecvMsgDefault(ctx context.Context, msg *protocol.BotRecvMsg) error { 102 | user:=&protocol.UserInfo{ 103 | ID: msg.OpenChatID, 104 | Type: protocol.UserTypeChatID, 105 | } 106 | input := msg.TextParam 107 | message.SendTextMessage(ctx,"your tenantKey",msg.AppID,user,"","Text that is empty or is not matched") 108 | return nil 109 | } 110 | ``` 111 | 112 | **BotRecvMsgHelp 函数示例代码如下** 113 | ```go 114 | //(该函数会向用户回复"this is help"消息) 115 | func BotRecvMsgHelp(ctx context.Context, msg *protocol.BotRecvMsg) error { 116 | user:=&protocol.UserInfo{ 117 | ID: msg.OpenChatID, 118 | Type: protocol.UserTypeChatID, 119 | } 120 | message.SendTextMessage(ctx,"your tenantKey",msg.AppID,user,"","this is help") 121 | return nil 122 | } 123 | ``` 124 | 125 | 手动编写代码: 126 | 利用 event.EventRegister 和 event.BotRecvMsgRegister 函数注册需要处理的订阅事件即可。 127 | 你可以参考 gin 框架自动生成代码 中的 RegistHandler 函数去实现它。 128 | -------------------------------------------------------------------------------- /generatecode/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package generatecode 6 | 7 | type GenCodeInfo struct { 8 | ServiceInfo ServiceInfoConf `yaml:"ServiceInfo"` 9 | EventList []EventConf `yaml:"EventList"` 10 | CommandList []CommandConf `yaml:"CommandList"` 11 | CardActionList []CardConf `yaml:"CardActionList"` 12 | } 13 | 14 | type ServiceInfoConf struct { 15 | Path string `yaml:"Path"` 16 | GenCodePath string `yaml:"GenCodePath"` 17 | EventWebhook string `yaml:"EventWebhook"` 18 | CardWebhook string `yaml:"CardWebhook"` 19 | AppID string `yaml:"AppID"` 20 | Description string `yaml:"Description"` 21 | IsISVApp bool `yaml:"IsISVApp"` 22 | } 23 | 24 | type EventConf struct { 25 | EventName string `yaml:"EventName"` 26 | } 27 | 28 | type CommandConf struct { 29 | Cmd string `yaml:"Cmd"` 30 | Description string `yaml:"Description"` 31 | } 32 | 33 | type CardConf struct { 34 | MethodName string `yaml:"MethodName"` 35 | } 36 | -------------------------------------------------------------------------------- /generatecode/demo.yml: -------------------------------------------------------------------------------- 1 | ServiceInfo: 2 | Path: github.com/larksuite/demo # relative path in GOPATH or go module name 3 | GenCodePath: # Code generation absolute path. If it is empty, the configuration item named "Path" will be used. 4 | EventWebhook: /webhook/event 5 | CardWebhook: /webhook/card 6 | AppID: cli_9d1ad8ed77f69108 # your app id 7 | Description: test_demo # your app description 8 | IsISVApp: false # ISV App flag, false is default 9 | EventList: 10 | - EventName: Message # required 11 | # - EventName: AppTicket # use as needed, ISVApp must 12 | # - EventName: Approval # use as needed 13 | # - EventName: LeaveApproval # use as needed 14 | # - EventName: WorkApproval # use as needed 15 | # - EventName: ShiftApproval # use as needed 16 | # - EventName: RemedyApproval # use as needed 17 | # - EventName: TripApproval # use as needed 18 | # - EventName: AppOpen # use as needed 19 | # - EventName: ContactUser # use as needed 20 | # - EventName: ContactDept # use as needed 21 | # - EventName: ContactScope # use as needed 22 | # - EventName: RemoveBot # use as needed 23 | # - EventName: AddBot # use as needed 24 | # - EventName: P2PChatCreate # use as needed 25 | # - EventName: AppStatusChange # use as needed 26 | # - EventName: UserToChat # use as needed 27 | # - EventName: ChatDisband # use as needed 28 | # - EventName: GroupSettingUpdate # use as needed 29 | # - EventName: OrderPaid # use as needed 30 | # - EventName: CreateWidgetInstance # use as needed 31 | # - EventName: DeleteWidgetInstance # use as needed 32 | CommandList: 33 | - Cmd: default # required 34 | Description: Text that is empty or isnot matched 35 | - Cmd: help 36 | Description: Text that begin with the word help 37 | - Cmd: show 38 | Description: Text that begin with the word show 39 | CardActionList: 40 | - MethodName: create 41 | - MethodName: delete 42 | - MethodName: update 43 | -------------------------------------------------------------------------------- /generatecode/generate_code.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package generatecode 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "fmt" 11 | "html/template" 12 | "io/ioutil" 13 | "os" 14 | 15 | "github.com/larksuite/botframework-go/SDK/common" 16 | ) 17 | 18 | func GenerateCode(ctx context.Context, tplName, tplStr, path, file string, tpl interface{}, isForceUpdate bool) error { 19 | fileName := path + file 20 | 21 | isExist, err := fileExists(fileName) // check if file exists 22 | if err != nil { 23 | return fmt.Errorf("Error checkFileIsExist file[%s]error[%v]", file, err) 24 | } 25 | 26 | if !isForceUpdate { 27 | if isExist { 28 | common.Logger(ctx).Infof("file[$path%-24s]: exist", file) 29 | return nil 30 | } else { 31 | common.Logger(ctx).Infof("file[$path%-24s]: create", file) 32 | } 33 | } else { // Force Update 34 | if isExist { 35 | common.Logger(ctx).Infof("file[$path%-24s]: force update", file) 36 | } else { 37 | common.Logger(ctx).Infof("file[$path%-24s]: create", file) 38 | } 39 | } 40 | 41 | tmpl, err := template.New(tplName).Parse(tplStr) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | var buf bytes.Buffer 47 | err = tmpl.Execute(&buf, tpl) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | err = ioutil.WriteFile(fileName, buf.Bytes(), 0644) 53 | if err != nil { 54 | return fmt.Errorf("write to file err: %v", err.Error()) 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func InitPath(path string) error { 61 | isExist, err := fileExists(path) 62 | if err != nil { 63 | return fmt.Errorf("checkPathExistError path[%s]error[%v]\n", path, err) 64 | } 65 | if !isExist { 66 | err = pathMkdir(path) 67 | if err != nil { 68 | return fmt.Errorf("mkdirPathError path[%s]error[%v]\n", path, err) 69 | } 70 | } 71 | 72 | path = path + "/handler_event" 73 | isExist, err = fileExists(path) 74 | if err != nil { 75 | return fmt.Errorf("checkPathExistError path[%s]error[%v]\n", path, err) 76 | } 77 | if !isExist { 78 | err = pathMkdir(path) 79 | if err != nil { 80 | return fmt.Errorf("mkdirPathError path[%s]error[%v]\n", path, err) 81 | } 82 | } 83 | 84 | return nil 85 | } 86 | 87 | func FormatFuncName(s string) string { 88 | if len(s) < 1 { 89 | return s 90 | } 91 | strArr := []byte(s) 92 | if strArr[0] >= 'a' && strArr[0] <= 'z' { 93 | strArr[0] -= 'a' - 'A' 94 | } 95 | return string(strArr) 96 | } 97 | 98 | func fileExists(path string) (bool, error) { 99 | _, err := os.Stat(path) 100 | if err == nil { 101 | return true, nil 102 | } 103 | if os.IsNotExist(err) { 104 | return false, nil 105 | } 106 | return false, err 107 | } 108 | 109 | func pathMkdir(path string) error { 110 | err := os.Mkdir(path, os.ModePerm) 111 | if err != nil { 112 | return fmt.Errorf("mkdirFailed path[%s]err[%v]", path, err) 113 | } 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /generatecode/template_gin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package generatecode 6 | 7 | type MainTemplate struct { 8 | Path string 9 | GenCodePath string 10 | EventWebhook string 11 | CardWebhook string 12 | AppID string 13 | IsISVApp bool 14 | } 15 | -------------------------------------------------------------------------------- /generatecode/template_handler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package generatecode 6 | 7 | // event template 8 | type EventTemplate struct { 9 | EventList []Event // event 10 | BotCmdList []BotCommand // bot recv msg 11 | CardList []CardAction // bot recv msg 12 | UseJson bool 13 | UseAuth bool 14 | } 15 | 16 | func (tpl *EventTemplate) AddEvent(name string) { 17 | tpl.EventList = append(tpl.EventList, Event{EventName: name}) 18 | } 19 | 20 | func (tpl *EventTemplate) AddBotCommand(cmd, description string) { 21 | tpl.BotCmdList = append(tpl.BotCmdList, BotCommand{Cmd: cmd, FuncName: FormatFuncName(cmd), Description: description}) 22 | } 23 | 24 | func (tpl *EventTemplate) AddCardAction(name string) { 25 | tpl.CardList = append(tpl.CardList, CardAction{MethodName: name, FuncName: FormatFuncName(name)}) 26 | } 27 | 28 | type Event struct { 29 | EventName string 30 | } 31 | 32 | type BotCommand struct { 33 | Cmd string 34 | FuncName string 35 | Description string 36 | } 37 | type CardAction struct { 38 | MethodName string 39 | FuncName string 40 | } 41 | -------------------------------------------------------------------------------- /generatecode/tpl_card.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package generatecode 6 | 7 | var TplCard = `package handler_event 8 | 9 | import ( 10 | "context" 11 | "encoding/json" 12 | 13 | "github.com/larksuite/botframework-go/SDK/common" 14 | "github.com/larksuite/botframework-go/SDK/protocol" 15 | ) 16 | {{range .CardList}} 17 | // methodName-{{.MethodName}} 18 | func Action{{.FuncName}}(ctx context.Context, callback *protocol.CardCallbackForm) (*protocol.CardForm, error) { 19 | method, _ := callback.Action.Value["method"] 20 | sessionID, _ := callback.Action.Value["sid"] 21 | common.Logger(ctx).Infof("cardActionCallBack: method[%s]sessionID[%s]", method, sessionID) 22 | 23 | // get meta 24 | meta := &protocol.Meta{} 25 | if metaData, ok := callback.Action.Value["meta"]; ok { 26 | _ = json.Unmarshal([]byte(metaData), meta) 27 | } 28 | 29 | // NOTE your business code 30 | 31 | card := &protocol.CardForm{} 32 | 33 | return card, nil 34 | } 35 | {{end}} 36 | ` 37 | -------------------------------------------------------------------------------- /generatecode/tpl_event.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package generatecode 6 | 7 | var TplEvent = `package handler_event 8 | 9 | import ( 10 | "context" 11 | {{if .UseJson}}"encoding/json" 12 | "fmt"{{end}} 13 | {{if .UseAuth}}"github.com/larksuite/botframework-go/SDK/auth"{{end}} 14 | "github.com/larksuite/botframework-go/SDK/event" 15 | "github.com/larksuite/botframework-go/SDK/protocol" 16 | ) 17 | {{range .EventList}} 18 | // event-{{.EventName}} 19 | func Event{{.EventName}}(ctx context.Context, eventBody []byte) error { 20 | {{if eq .EventName "Message"}} 21 | return event.BotRecvMsgHandler(ctx, eventBody){{else if eq .EventName "AppTicket"}} 22 | return auth.RefreshAppTicket(ctx, eventBody){{else}} 23 | request := &protocol.{{.EventName}}Event{} 24 | err := json.Unmarshal(eventBody, request) 25 | if err != nil { 26 | return fmt.Errorf("jsonUnmarshalError[%v]", err) 27 | } 28 | 29 | // NOTE your business code 30 | 31 | return nil{{end}} 32 | } 33 | {{end}} 34 | // handler-bot receive message {{range .BotCmdList}} 35 | // cmd-{{.Cmd}} description: {{.Description}} 36 | func BotRecvMsg{{.FuncName}}(ctx context.Context, msg *protocol.BotRecvMsg) error { 37 | // NOTE your business code 38 | 39 | return nil 40 | } 41 | {{end}} 42 | ` 43 | -------------------------------------------------------------------------------- /generatecode/tpl_gin_callback.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package generatecode 6 | 7 | var TplGinCallback = `package main 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "io/ioutil" 13 | 14 | "github.com/gin-gonic/gin" 15 | "github.com/larksuite/botframework-go/SDK/common" 16 | "github.com/larksuite/botframework-go/SDK/event" 17 | ) 18 | 19 | // EventCallback open platform event 20 | func EventCallback(c *gin.Context) { 21 | 22 | body, err := ioutil.ReadAll(c.Request.Body) 23 | if err != nil || len(body) == 0 { 24 | common.Logger(c).Errorf("eventReqParamsError: readHttpBodyError err[%v]bodyLen[%d]", err, len(body)) 25 | c.JSON(500, gin.H{"codemsg": common.ErrEventParams.String()}) 26 | return 27 | } 28 | 29 | appID := "{{.AppID}}" 30 | challenge, err := event.EventCallback(c, string(body), appID) 31 | common.Logger(c).Infof("eventInfo: challenge[%s] err[%v]", challenge, err) 32 | if err != nil { 33 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 34 | } else if "" != challenge { 35 | c.JSON(200, gin.H{"challenge": challenge}) 36 | } else { 37 | c.JSON(200, gin.H{"codemsg": common.Success.String()}) 38 | } 39 | } 40 | 41 | // CardCallback card action callback 42 | func CardCallback(c *gin.Context) { 43 | 44 | body, err := ioutil.ReadAll(c.Request.Body) 45 | if err != nil || len(body) == 0 { 46 | common.Logger(c).Errorf("eventReqParamsError: readHttpBodyError err[%v]bodyLen[%d]", err, len(body)) 47 | c.JSON(500, gin.H{"codemsg": common.ErrCardParams.String()}) 48 | return 49 | } 50 | 51 | // for verify signature 52 | header := map[string]string{ 53 | "X-Lark-Request-Timestamp": c.Request.Header.Get("X-Lark-Request-Timestamp"), 54 | "X-Lark-Request-Nonce": c.Request.Header.Get("X-Lark-Request-Nonce"), 55 | "X-Lark-Signature": c.Request.Header.Get("X-Lark-Signature"), 56 | } 57 | 58 | appID := "{{.AppID}}" 59 | card, challenge, err := event.CardCallBack(c, appID, header, body) 60 | common.Logger(c).Infof("cardInfo: challenge[%s] err[%v]", challenge, err) 61 | if err != nil { 62 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 63 | } else if "" != challenge { 64 | c.JSON(200, gin.H{"challenge": challenge}) 65 | } else { 66 | data, err := json.Marshal(card) 67 | if err != nil { 68 | c.JSON(500, gin.H{"codemsg": fmt.Sprintf("%v", err)}) 69 | } else { 70 | c.String(200, string(data)) 71 | } 72 | } 73 | } 74 | ` 75 | -------------------------------------------------------------------------------- /generatecode/tpl_gin_main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package generatecode 6 | 7 | var TplGinMain = `package main 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/larksuite/botframework-go/SDK/appconfig" 15 | "github.com/larksuite/botframework-go/SDK/auth" 16 | "github.com/larksuite/botframework-go/SDK/common" 17 | "github.com/larksuite/botframework-go/SDK/protocol" 18 | "{{.Path}}/handler_event" 19 | ) 20 | 21 | func main() { 22 | r := gin.Default() 23 | 24 | common.InitLogger(common.NewCommonLogger(), common.DefaultOption()) 25 | defer common.FlushLogger() 26 | 27 | err := InitInfo() 28 | if err != nil { 29 | common.Logger(context.TODO()).Errorf("InitError[%v]", err) 30 | return 31 | } 32 | 33 | r.POST("{{.EventWebhook}}", EventCallback) //open platform event callback 34 | r.POST("{{.CardWebhook}}", CardCallback) //card action callback 35 | 36 | // NOTE your business code 37 | 38 | r.Run(":8089") 39 | } 40 | 41 | func InitInfo() error { 42 | // Initialize app config 43 | conf := appconfig.AppConfig{ 44 | AppID: "{{.AppID}}", 45 | AppType: {{if .IsISVApp}}protocol.ISVApp{{else}}protocol.InternalApp{{end}}, // Independent Software Vendor App / Internal App 46 | 47 | // NOTE your business code 48 | // get appinfo(app_secret、veri_token、encrypt_key) from redis/mysql or remote config system 49 | // redis/mysql or remote config system is recommended 50 | 51 | // AppSecret: redis.Get("{{.AppID}}" + "AppSecret"), 52 | // VerifyToken: redis.Get("{{.AppID}}" + "VerifyToken"), 53 | // EncryptKey: redis.Get("{{.AppID}}" + "EncryptKey"), 54 | } 55 | 56 | appconfig.Init(conf) 57 | 58 | // ISVApp Set TicketManager 59 | if conf.AppType == protocol.ISVApp { 60 | // ISVApp need to implement the TicketManager interface 61 | // It is recommended to set/get your app-ticket in redis 62 | 63 | redisClient := &common.DefaultRedisClient{} 64 | err := redisClient.InitDB(map[string]string{"addr": "127.0.0.1:6379"}) 65 | if err != nil { 66 | return fmt.Errorf("init redis-client error[%v]", err) 67 | } 68 | 69 | err = auth.InitISVAppTicketManager(auth.NewDefaultAppTicketManager(redisClient)) 70 | if err != nil { 71 | return fmt.Errorf("Authorization Initialize Error[%v]", err) 72 | } 73 | } 74 | 75 | // regist handler 76 | handler_event.RegistHandler(conf.AppID) 77 | 78 | return nil 79 | } 80 | ` 81 | -------------------------------------------------------------------------------- /generatecode/tpl_regist.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package generatecode 6 | 7 | var TplRegist = `package handler_event 8 | 9 | import ( 10 | "github.com/larksuite/botframework-go/SDK/event" 11 | {{if .EventList}}"github.com/larksuite/botframework-go/SDK/protocol"{{end}} 12 | ) 13 | 14 | // If the code is first generated, all code files are generated from the configuration file. 15 | // 16 | // If you modify the configuration file later, and regenerate the code on the original path, 17 | // only the ./handler_event/regist.go will be forced updated, other files are not updated to avoid overwriting user-defined code. 18 | // 19 | // The ./handler_event/regist.go file will be forced update, you should not write your business code in the file. 20 | 21 | // RegistHandler: regist handler 22 | func RegistHandler(appID string) { 23 | 24 | // regist open platform event handler {{range .EventList}} 25 | event.EventRegister(appID, protocol.EventType{{.EventName}}, Event{{.EventName}}){{end}} 26 | 27 | // regist bot recv message handler {{range .BotCmdList}} 28 | event.BotRecvMsgRegister(appID, "{{.Cmd}}", BotRecvMsg{{.FuncName}}){{end}} 29 | 30 | // regist card action handler {{range .CardList}} 31 | event.CardRegister(appID, "{{.MethodName}}", Action{{.FuncName}}){{end}} 32 | } 33 | ` 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/larksuite/botframework-go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/bitly/go-simplejson v0.5.0 7 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect 8 | github.com/gin-gonic/gin v1.4.0 9 | github.com/go-redis/redis v6.15.6+incompatible 10 | github.com/google/uuid v1.1.1 11 | github.com/hashicorp/golang-lru v0.5.3 12 | github.com/jinzhu/configor v1.1.1 13 | github.com/kr/pretty v0.1.0 // indirect 14 | github.com/onsi/ginkgo v1.10.2 // indirect 15 | github.com/onsi/gomega v1.7.0 // indirect 16 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 17 | github.com/satori/go.uuid v1.2.0 18 | github.com/sirupsen/logrus v1.4.2 19 | ) 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Bytedance Inc. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "flag" 10 | "os" 11 | "strings" 12 | 13 | "github.com/jinzhu/configor" 14 | "github.com/larksuite/botframework-go/SDK/common" 15 | "github.com/larksuite/botframework-go/SDK/protocol" 16 | "github.com/larksuite/botframework-go/generatecode" 17 | ) 18 | 19 | var ( 20 | help bool 21 | filePath string 22 | ) 23 | 24 | func init() { 25 | flag.BoolVar(&help, "h", false, "this help") 26 | flag.StringVar(&filePath, "f", "", "config file path") 27 | } 28 | 29 | func main() { 30 | flag.Parse() 31 | 32 | if help { 33 | flag.Usage() 34 | return 35 | } 36 | if filePath == "" { 37 | flag.Usage() 38 | return 39 | } 40 | 41 | common.InitLogger(common.NewCommonLogger(), common.DefaultOption()) 42 | defer common.FlushLogger() 43 | ctx := context.Background() 44 | 45 | // read config 46 | config := &generatecode.GenCodeInfo{} 47 | err := configor.Load(config, filePath) 48 | if err != nil { 49 | common.Logger(ctx).Errorf("genConfigError error[%v]confPath[%s]", err, filePath) 50 | return 51 | } 52 | 53 | // check config 54 | if config.ServiceInfo.Path == "" || 55 | config.ServiceInfo.EventWebhook == "" || 56 | config.ServiceInfo.CardWebhook == "" || 57 | config.ServiceInfo.AppID == "" { 58 | 59 | common.Logger(ctx).Errorf("config error, Path/EventWebhook/CardWebhookAppID can not be empty ") 60 | return 61 | } 62 | 63 | // generate code 64 | GenCodeGin(ctx, config) 65 | } 66 | 67 | func GenCodeGin(ctx context.Context, config *generatecode.GenCodeInfo) { 68 | mainTpl := generatecode.MainTemplate{ 69 | Path: config.ServiceInfo.Path, 70 | GenCodePath: config.ServiceInfo.GenCodePath, 71 | EventWebhook: config.ServiceInfo.EventWebhook, 72 | CardWebhook: config.ServiceInfo.CardWebhook, 73 | AppID: config.ServiceInfo.AppID, 74 | IsISVApp: config.ServiceInfo.IsISVApp, 75 | } 76 | 77 | var path string 78 | if mainTpl.GenCodePath != "" { 79 | path = mainTpl.GenCodePath 80 | } else { 81 | GOPATH := os.Getenv("GOPATH") 82 | path = GOPATH + "/src/" + mainTpl.Path 83 | } 84 | 85 | // init path 86 | err := generatecode.InitPath(path) 87 | if err != nil { 88 | common.Logger(ctx).Errorf("initPathError[%v]", err) 89 | return 90 | } 91 | 92 | common.Logger(ctx).Infof("path=%s", path) 93 | // generatecode main、callback 94 | err = generatecode.GenerateCode(ctx, "tplmain", generatecode.TplGinMain, path, "/main.go", mainTpl, false) 95 | if err != nil { 96 | common.Logger(ctx).Errorf("generateCodeError[%v]", err) 97 | return 98 | } 99 | 100 | err = generatecode.GenerateCode(ctx, "tplcallback", generatecode.TplGinCallback, path, "/callback.go", mainTpl, false) 101 | if err != nil { 102 | common.Logger(ctx).Errorf("generateCodeError[%v]", err) 103 | return 104 | } 105 | 106 | // generatecode handler_event 107 | eventTpl := &generatecode.EventTemplate{} 108 | for _, v := range config.EventList { 109 | if v.EventName == "AppTicket" { 110 | eventTpl.UseAuth = true 111 | } else if v.EventName == "Message" { 112 | 113 | } else { 114 | eventTpl.UseJson = true 115 | } 116 | eventTpl.AddEvent(v.EventName) 117 | } 118 | 119 | isHasDefault := false 120 | for _, v := range config.CommandList { 121 | if protocol.CmdDefault == strings.ToLower(v.Cmd) { 122 | isHasDefault = true 123 | } 124 | eventTpl.AddBotCommand(v.Cmd, v.Description) 125 | } 126 | if !isHasDefault { 127 | eventTpl.AddBotCommand(protocol.CmdDefault, protocol.DescDefault) 128 | } 129 | 130 | for _, v := range config.CardActionList { 131 | eventTpl.AddCardAction(v.MethodName) 132 | } 133 | 134 | err = generatecode.GenerateCode(ctx, "tplregist", generatecode.TplRegist, path, "/handler_event/regist.go", eventTpl, true) 135 | if err != nil { 136 | common.Logger(ctx).Errorf("generateCodeError[%v]", err) 137 | return 138 | } 139 | 140 | err = generatecode.GenerateCode(ctx, "tplevent", generatecode.TplEvent, path, "/handler_event/event.go", eventTpl, false) 141 | if err != nil { 142 | common.Logger(ctx).Errorf("generateCodeError[%v]", err) 143 | return 144 | } 145 | 146 | err = generatecode.GenerateCode(ctx, "tplCard", generatecode.TplCard, path, "/handler_event/card.go", eventTpl, false) 147 | if err != nil { 148 | common.Logger(ctx).Errorf("generateCodeError[%v]", err) 149 | return 150 | } 151 | 152 | common.Logger(ctx).Infof("Success. code in path=%s", path) 153 | return 154 | } 155 | --------------------------------------------------------------------------------