├── .gitignore ├── LICENSE ├── README.md ├── README.zh.md ├── api_process.jpg ├── api_process_zh.jpg ├── build.sh ├── doc ├── ISV.APP.README.zh.md ├── app_type.en.png └── app_type.zh.png └── packages ├── allcore ├── package.json ├── src │ ├── index.ts │ └── lark.ts └── tsconfig.json ├── api ├── package.json ├── src │ ├── api.ts │ ├── core │ │ ├── constants │ │ │ └── constants.ts │ │ ├── errors │ │ │ └── errors.ts │ │ ├── handlers │ │ │ ├── accessToken.ts │ │ │ └── handlers.ts │ │ ├── request │ │ │ ├── formData.ts │ │ │ └── request.ts │ │ ├── response │ │ │ ├── error.ts │ │ │ └── response.ts │ │ ├── token │ │ │ └── token.ts │ │ └── tools │ │ │ └── file.ts │ └── index.ts └── tsconfig.json ├── card ├── package.json ├── src │ ├── card.ts │ ├── handlers │ │ ├── err.ts │ │ └── handlers.ts │ ├── http │ │ ├── http.ts │ │ ├── native │ │ │ └── requestListener.ts │ │ └── server │ │ │ └── server.ts │ ├── index.ts │ └── model │ │ └── card.ts └── tsconfig.json ├── core ├── package.json ├── src │ ├── config │ │ ├── config.ts │ │ ├── getConfig.ts │ │ └── settings.ts │ ├── constants │ │ └── constants.ts │ ├── context.ts │ ├── errors │ │ └── errors.ts │ ├── http │ │ ├── handle.ts │ │ ├── model.ts │ │ ├── requestListener.ts │ │ └── startServer.ts │ ├── index.ts │ ├── log │ │ └── log.ts │ ├── store │ │ ├── keys.ts │ │ └── store.ts │ ├── tools │ │ └── decrypt.ts │ └── version.ts └── tsconfig.json ├── event ├── package.json ├── src │ ├── app │ │ └── v1 │ │ │ └── appTicket.ts │ ├── core │ │ ├── handlers │ │ │ ├── err.ts │ │ │ └── handlers.ts │ │ └── model │ │ │ └── event.ts │ ├── event.ts │ ├── http │ │ ├── http.ts │ │ ├── native │ │ │ └── native.ts │ │ └── server │ │ │ └── server.ts │ └── index.ts └── tsconfig.json └── sample ├── package.json ├── src ├── api │ ├── api.js │ ├── batchAPIReqCall.js │ ├── helpdesk.js │ ├── imageDownload.js │ ├── imageUpload.js │ ├── onlineApi.ts │ └── test.png ├── card │ ├── express.js │ └── httpServer.js ├── config │ └── config.ts ├── event │ ├── express.js │ └── httpServer.js └── tools │ └── downFile.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | .idea 3 | package-lock.json 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lark Technologies Pte. Ltd. 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 | [**飞书,点这里**](README.zh.md) | Larksuite(Overseas) 2 | 3 | - 如果使用的是飞书,请看 [**飞书,点这里**](README.zh.md) ,飞书与Larksuite使用的域名不一样,引用的文档地址也是不同的。(If you are using FeiShu, please see [**飞书,点这里**](README.zh.md) , Feishu and larksuite use different domain names and reference different document addresses.) 4 | 5 | # [Deprecated]LarkSuite open api SDK 6 | 7 | **Please use the new version of node-sdk: https://github.com/larksuite/node-sdk** 8 | ## Overview 9 | 10 | --- 11 | 12 | - Larksuite open platform facilitates the integration of enterprise applications and larksuite, making collaboration and 13 | management more efficient. 14 | 15 | - Larksuite development interface SDK, convenient call server API and subscribe server events, such as: Message & group, 16 | address book, calendar, docs and others can visit [larksuite open platform document](https://open.larksuite.cn/document) ,Take a look at [REFERENCE]. 17 | 18 | ## Problem feedback 19 | 20 | If you encounter any problems during usage, please let us know by submitting [Github Issues](https://github.com/larksuite/oapi-sdk-nodejs/issues). We will deal with these Issues and get back to you as soon as possible. 21 | 22 | 23 | ## installation 24 | 25 | ```shell script 26 | npm i @larksuiteoapi/allcore@1.0.14 27 | ``` 28 | 29 | ## Explanation of terms 30 | - Larksuite: the overseas name of lark, which mainly provides services for overseas enterprises and has an independent [domain name address](https://www.larksuite.com/) . 31 | - Development documents: reference to the open interface of the open platform **developers must see, and can use search to query documents efficiently** . [more information](https://open.feishu.cn/document/) . 32 | - Developer background: the management background for developers to develop applications, [more introduction](https://open.larksuite.cn/app/) . 33 | - Cutome APP: the application can only be installed and used in the enterprise,[more introduction](https://open.larksuite.com/document/ukzMxEjL5MTMx4SOzETM/uEjNwYjLxYDM24SM2AjN) . 34 | - Marketplace App:The app will be displayed in [App Directory](https://app.larksuite.com/) Display, each enterprise can choose to install. 35 | 36 | ![App type](doc/app_type.en.png) 37 | 38 | 39 | ## Quick use 40 | 41 | --- 42 | 43 | ### Call API 44 | 45 | #### Example of using `Custom App` to access [send text message](https://open.larksuite.com/document/uMzMyEjLzMjMx4yMzITM/ugDN0EjL4QDNx4CO0QTM) API 46 | - Since the SDK has encapsulated the app_access_token、tenant_access_token So when calling the business API, you don't need to get the app_access_token、tenant_access_token. If the business interface needs to use user_access_token, which needs to be set(lark.api.setUserAccessToken("UserAccessToken")), Please refer to README.md -> How to build a request(Request) 47 | - For more use examples, please see: [packages/sample/src/api](packages/sample/src/api) 48 | 49 | ```javascript 50 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 51 | const lark = require("@larksuiteoapi/allcore"); // javascript 52 | 53 | // Configuration of custom app, parameter description: 54 | // appID、appSecret: "Developer Console" -> "Credentials"(App ID、App Secret) 55 | // verificationToken、encryptKey:"Developer Console" -> "Event Subscriptions"(Verification Token、Encrypt Key) 56 | // helpDeskID、helpDeskToken, Help Desk token:https://open.feishu.cn/document/ukTMukTMukTM/ugDOyYjL4gjM24CO4IjN 57 | const appSettings = lark.newInternalAppSettings({ 58 | appID: "App ID", 59 | appSecret: "App Secret", 60 | encryptKey: "Encrypt Key", // Not required. Required when subscribing to events 61 | verificationToken: "Verification Token", // Not required, required to subscribe to event and message cards 62 | helpDeskID: "HelpDesk ID", // Not required, required when using the help Desk API 63 | helpDeskToken: "HelpDesk Token", // Not required, required when using the help Desk API 64 | }) 65 | 66 | // Currently, you are visiting larksuite, which uses default storage and default log (error level). For more optional configurations, see readme.md -> How to Build an overall Configuration (Config). 67 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 68 | loggerLevel: lark.LoggerLevel.ERROR, 69 | }) 70 | 71 | // The content of the sent message 72 | const body = { 73 | open_id: "user open id", 74 | msg_type: "text", 75 | content: { 76 | text: "test send message", 77 | }, 78 | } 79 | // Build request 80 | const req = lark.api.newRequest("/open-apis/message/v4/send", "POST", lark.api.AccessTokenType.Tenant, body) 81 | // Send request 82 | lark.api.sendRequest(conf, req).then(r => { 83 | // Print the requestId of the request 84 | console.log(r.getRequestID()) 85 | // Print the response status of the request 86 | console.log(r.getHTTPStatusCode()) 87 | console.log(r) // r = response.body 88 | }).catch(e => { 89 | // Error handling of request 90 | console.log(e) 91 | }) 92 | ``` 93 | 94 | ### Subscribe to events 95 | 96 | - [Subscribe to events](https://open.larksuite.com/document/uMzMyEjLzMjMx4yMzITM/uETM4QjLxEDO04SMxgDN) , to understand 97 | the process and precautions of subscribing to events. 98 | - For more use examples, please refer to [packages/sample/src/event](packages/sample/src/event)(including: use in combination with express) 99 | 100 | #### Example of using `Custom App` to subscribe [App First Enabled](https://open.larksuite.com/document/uMzMyEjLzMjMx4yMzITM/uYjMyYjL2IjM24iNyIjN) event. 101 | 102 | ```javascript 103 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 104 | const lark = require("@larksuiteoapi/allcore"); // javascript 105 | 106 | // Configuration of custom app, parameter description: 107 | // appID、appSecret: "Developer Console" -> "Credentials"(App ID、App Secret) 108 | // verificationToken、encryptKey:"Developer Console" -> "Event Subscriptions"(Verification Token、Encrypt Key) 109 | // helpDeskID、helpDeskToken, Help Desk token:https://open.feishu.cn/document/ukTMukTMukTM/ugDOyYjL4gjM24CO4IjN 110 | const appSettings = lark.newInternalAppSettings({ 111 | appID: "App ID", 112 | appSecret: "App Secret", 113 | encryptKey: "Encrypt Key", 114 | verificationToken: "Verification Token", 115 | }) 116 | 117 | // Currently, you are visiting larksuite, which uses default storage and default log (error level). For more optional configurations, see readme.md -> How to Build an overall Configuration (Config). 118 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 119 | loggerLevel: lark.LoggerLevel.ERROR, 120 | }) 121 | 122 | // Set the application event handler to be enabled for the first time 123 | lark.event.setTypeHandler(conf, "app_open", (ctx, event) => { 124 | // Print the request ID of the request 125 | console.log(ctx.getRequestID()); 126 | // Print event 127 | console.log(event); 128 | }) 129 | 130 | // Start the httpserver, "Developer Console" -> "Event Subscriptions", setting Request URL:https://domain 131 | // startup event http server, port: 8089 132 | lark.event.startServer(conf, 8089) 133 | 134 | ``` 135 | 136 | ### Processing message card callbacks 137 | 138 | - [Message Card Development Process](https://open.larksuite.com/document/uMzMyEjLzMjMx4yMzITM/ukzM3QjL5MzN04SOzcDN) , to 139 | understand the process and precautions of processing message cards 140 | - For more use examples, please refer to [packages/sample/src/card](packages/sample/src/card)(including: use in combination with express) 141 | 142 | #### Example of using `Custom App` to handling message card callback. 143 | 144 | ```javascript 145 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 146 | const lark = require("@larksuiteoapi/allcore"); // javascript 147 | 148 | // Configuration of custom app, parameter description: 149 | // appID、appSecret: "Developer Console" -> "Credentials"(App ID、App Secret) 150 | // verificationToken、encryptKey:"Developer Console" -> "Event Subscriptions"(Verification Token、Encrypt Key) 151 | // helpDeskID、helpDeskToken, Help Desk token:https://open.feishu.cn/document/ukTMukTMukTM/ugDOyYjL4gjM24CO4IjN 152 | const appSettings = lark.newInternalAppSettings({ 153 | appID: "App ID", 154 | appSecret: "App Secret", 155 | encryptKey: "Encrypt Key", // Not required. Required when subscribing to events 156 | verificationToken: "Verification Token", 157 | }) 158 | 159 | // Currently, you are visiting larksuite, which uses default storage and default log (error level). For more optional configurations, see readme.md -> How to Build an overall Configuration (Config). 160 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 161 | loggerLevel: lark.LoggerLevel.ERROR, 162 | }) 163 | 164 | // Set the handler of the message card 165 | // Return value: can be "", JSON string of new message card 166 | lark.card.setHandler(conf, (ctx, card) => { 167 | // 打印消息卡片 168 | console.log(card) 169 | console.log(card.action) 170 | return "{\"config\":{\"wide_screen_mode\":true},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"div\",\"text\":{\"tag\":\"lark_md\",\"content\":\"[飞书](https://www.feishu.cn)整合即时沟通、日历、音视频会议、云文档、云盘、工作台等功能于一体,成就组织和个人,更高效、更愉悦。\"}}]}}"; 171 | }) 172 | 173 | // Start the httpserver, "Developer Console" -> "Features" -> "Bot", setting Message Card Request URL: https://domain 174 | // startup event http server, port: 8089 175 | lark.event.startServer(conf, 8089) 176 | ``` 177 | 178 | ## How to build app settings(AppSettings) 179 | 180 | ```javascript 181 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 182 | const lark = require("@larksuiteoapi/allcore"); // javascript 183 | 184 | // To prevent application information leakage, in the configuration environment variables, the variables (4) are described as follows: 185 | // APP_ID: "Developer Console" -> "Credentials"(App ID) 186 | // APP_Secret: "Developer Console" -> "Credentials"(App Secret) 187 | // VERIFICATION_Token: VerificationToken、EncryptKey:"Developer Console" -> "Event Subscriptions"(Verification Token) 188 | // ENCRYPT_Key: VerificationToken、EncryptKey:"Developer Console" -> "Event Subscriptions"(Encrypt Key) 189 | // HELP_DESK_ID: Service desk setup center -> ID 190 | // HELP_DESK_TOKEN: Service desk setup center -> 令牌 191 | // The configuration of "Custom App" is obtained through environment variables 192 | const appSettings = lark.getInternalAppSettingsByEnv() 193 | // The configuration of "Marketplace App" is obtained through environment variables 194 | const appSettings = lark.getISVAppSettingsByEnv() 195 | 196 | 197 | // Parameter description: 198 | // appID、appSecret: "Developer Console" -> "Credentials"(App ID、App Secret) 199 | // verificationToken、encryptKey:"Developer Console" -> "Event Subscriptions"(Verification Token、Encrypt Key) 200 | // helpDeskID、helpDeskToken, Help Desk token:https://open.feishu.cn/document/ukTMukTMukTM/ugDOyYjL4gjM24CO4IjN 201 | // The Configuration of custom app, parameter description: 202 | const appSettings = lark.newInternalAppSettings({ 203 | appID: "App ID", 204 | appSecret: "App Secret", 205 | encryptKey: "Encrypt Key", // Not required. Required when subscribing to events 206 | verificationToken: "Verification Token", // Not required, required to subscribe to event and message cards 207 | helpDeskID: "HelpDesk ID", // Not required, required when using the help Desk API 208 | helpDeskToken: "HelpDesk Token", // Not required, required when using the help Desk API 209 | }) 210 | // The configuration of "Marketplace App" 211 | const appSettings = lark.newISVAppSettings({ 212 | appID: "App ID", 213 | appSecret: "App Secret", 214 | encryptKey: "Encrypt Key", // Not required. Required when subscribing to events 215 | verificationToken: "Verification Token", // Not required, required to subscribe to event and message cards 216 | helpDeskID: "HelpDesk ID", // Not required, required when using the help Desk API 217 | helpDeskToken: "HelpDesk Token", // Not required, required when using the help Desk API 218 | }) 219 | 220 | ``` 221 | 222 | ## How to build overall configuration(Config) 223 | 224 | - Visit Larksuite, Feishu or others 225 | - App settings 226 | - The implementation of logger is used to output the logs generated in the process of SDK processing, which is 227 | convenient for troubleshooting. 228 | - You can use the log implementation of the business system, see the sample 229 | code: [packages/core/src/log/log.ts](packages/core/src/log/log.ts) ConsoleLogger 230 | - The implementation of store is used to save the access credentials (app/tenant_access_token), temporary voucher ( 231 | app_ticket) 232 | - Redis is recommended. Please see the example code: [sample/config/redis_store.go](packages/sample/src/config/config.ts) RedisStore 233 | - It can reduce the times of obtaining access credentials and prevent the frequency limit of calling access 234 | credentials interface. 235 | - "Marketplace App", accept open platform distributed `app_ticket` will be saved to the storage, so the 236 | implementation of the storage interface (store) needs to support distributed storage. 237 | 238 | ```javascript 239 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 240 | const lark = require("@larksuiteoapi/allcore"); 241 | 242 | // It is recommended to use Redis to implement the storage interface (Store) to reduce the number of accesses to the AccessToken interface 243 | // Parameter description: 244 | // domain: URL Domain address. Value range: lark.Domain.FeiShu / lark.Domain.LarkSuite / Other URL domain name address 245 | // appSettings: application configuration 246 | // opts: configuration options 247 | // opts.logger: [log interface](core/log/log.go), default: lark.ConsoleLogger 248 | // opts.loggerlevel: log level. Default: ERROR level (lark.LoggerLevel.ERROR) 249 | // opts.store: [storage port](core/store/store.go), used to store app_ticket/app_access_token/tenant_access_token. Default: lark.DefaultStore 250 | lark.newConfig(domain: Domain, appSettings: AppSettings, opts: ConfigOpts): Config 251 | 252 | // Use example: 253 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 254 | loggerLevel: lark.LoggerLevel.ERROR, 255 | logger: new lark.ConsoleLogger(), 256 | store: new lark.DefaultStore(), 257 | }) 258 | ``` 259 | 260 | ## How to build a request(Request) 261 | 262 | - For more examples, see[packages/sample/src/api](packages/sample/src/api)(including: file upload and download) 263 | 264 | ```javascript 265 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 266 | const lark = require("@larksuiteoapi/allcore"); 267 | 268 | // Create request 269 | // httpPath: API path 270 | // such as: https://domain/open-apis/contact/v3/users/:user_id 271 | // support: the path of the domain name after, httpPath: "/open apis/contact/v3/users/:user_id" (recommended) 272 | // support: the full path, httpPath: "https://domain/open-apis/contact/v3/users/:user_id" 273 | // support: httpPath: "contact/v3/users/:user_id" 274 | // httpMethod: GET/POST/PUT/BATCH/DELETE 275 | // accessTokenType: What kind of token access the API uses, value range: lark.api.AccessTokenType.App/Tenant/User, for example: lark.api.AccessTokenType.Tenant 276 | // input : The request body (may be formdata (for example: file upload)), if the request body is not needed (for example, some GET requests), pass: undefined 277 | let req = lark.api.newRequest(httpPath: string, httpMethod: string, accessTokenType: AccessTokenType, input: any) 278 | 279 | // Request method , SDK version requirements: 1.0.9 and above 280 | 281 | setPathParams(pathParams: { [key: string]: any }) // Set the URL Path parameter (with: prefix) value 282 | // Use example: 283 | req.setPathParams({"user_id":4}) // when httpPath="users/:user_id", the requested URL="https://{domain}/open-apis/users/4" 284 | 285 | 286 | setQueryParams(queryParams: { [key: string]: any }) // Set the URL qeury 287 | // Use example: 288 | req.setQueryParams({"age":4,"types":[1,2]}) // it will be appended to the url?age=4&types=1&types=2 289 | 290 | 291 | setTenantKey(tenantKey: string) // to `ISV application` status, indication `tenant_access_token` access API, you need to set 292 | // Use example: 293 | req.setTenantKey("68daYsd") // Set TenantKey to "68daysd" 294 | 295 | 296 | setUserAccessToken(userAccessToken: string) // use of` user_access_token` access API, you need to set 297 | // Use example: 298 | req.setUserAccessToken("u-7f1bcd13fc57d46bac21793a18e560") // Set the user access token to "u-7f1bcd13fc57d46bac21793a18e560" 299 | 300 | 301 | setTimeoutOfMs(timeoutOfMs: number) // set the http request, timeout time in milliseconds 302 | // Use example: 303 | req.setTimeoutOfMs(5000) // Set the request timeout to 5000 Ms 304 | 305 | 306 | setIsResponseStream() // set whether the response is a stream, such as downloading a file, at this time: the output value is of Buffer type 307 | // Use example: 308 | req.setIsResponseStream() // set the response is a stream 309 | 310 | 311 | setResponseStream(responseStream: stream.Writable) // Set whether the response body is a stream. For example, when downloading a file, the response stream will be written to the responsestream 312 | // Use example: 313 | req.setResponseStream(fs.createWriteStream("./test.1.png")) // Write the response stream to the "./test.1.png" file 314 | 315 | 316 | setNeedHelpDeskAuth() // If it is a HelpDesk API, you need to set the HelpDesk token 317 | // Use example: 318 | req.setNeedHelpDeskAuth() // Sets whether the request requires a HelpDesk token 319 | 320 | 321 | ``` 322 | 323 | ## How to send a request 324 | - Since the SDK has encapsulated the app_access_token、tenant_access_token So when calling the business API, you don't need to get the app_access_token、tenant_access_token. If the business interface needs to use user_access_token, which needs to be set(lark.api.setUserAccessToken("UserAccessToken")), Please refer to README.md -> How to build a request(Request) 325 | - For more use examples, please see: [packages/sample/src/api](packages/sample/src/api)(including: file upload and download) 326 | 327 | ```javascript 328 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 329 | const lark = require("@larksuiteoapi/allcore"); 330 | 331 | // Parameter Description: 332 | // conf:Overall configuration(Config) 333 | // req:Request(Request) 334 | // resp: http response body json 335 | // err:Send request happen error 336 | async lark.api.sendRequest(conf: lark.core.Config, req: lark.api.Request) 337 | 338 | ``` 339 | 340 | ## lark.core.Context common methods 341 | 342 | ```javascript 343 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 344 | const lark = require("@larksuiteoapi/allcore"); 345 | 346 | // In the handler of event subscription and message card callback, you can lark.core.Context Get config from 347 | const conf = lark.core.getConfigByCtx(ctx: lark.core.Context) 348 | 349 | ``` 350 | 351 | ## Download File Tool 352 | 353 | - Download files via network request 354 | - For more use examples, please see: [packages/sample/src/tools/downFile.js](packages/sample/src/tools/downFile.js) 355 | 356 | ```javascript 357 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 358 | const lark = require("@larksuiteoapi/allcore"); 359 | 360 | // Get the file content 361 | // Parameter Description: 362 | // url:The HTTP address of the file 363 | // timeoutOfMs:Time the request timed out in milliseconds 364 | // Return value Description: 365 | // resp: http response body binary 366 | // err:Send request happen error 367 | async lark.api.downloadFile(url: string, timeoutOfMs: number) 368 | 369 | ``` 370 | 371 | ## License 372 | 373 | --- 374 | 375 | - MIT 376 | 377 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | [**README of Larksuite(Overseas)**](README.md) | 飞书 2 | 3 | # [Deprecated]飞书开放接口SDK 4 | 5 | **请使用官方新版node-sdk:https://github.com/larksuite/node-sdk** 6 | 7 | ## 概述 8 | 9 | --- 10 | 11 | - 飞书开放平台,便于企业应用与飞书集成,让协同与管理更加高效,[概述](https://open.feishu.cn/document/uQjL04CN/ucDOz4yN4MjL3gzM) 12 | 13 | - 飞书开发接口SDK,便捷调用服务端API与订阅服务端事件,例如:消息&群组、通讯录、日历、视频会议、云文档、 OKR等具体可以访问 [飞书开放平台文档](https://open.feishu.cn/document/) 看看【服务端 14 | API】。 15 | 16 | ## 问题反馈 17 | 18 | 如有任何SDK使用相关问题,请提交 [Github Issues](https://github.com/larksuite/oapi-sdk-nodejs/issues), 我们会在收到 Issues 的第一时间处理,并尽快给您答复。 19 | 20 | ## 安装方法 21 | 22 | ```shell script 23 | npm i @larksuiteoapi/allcore@1.0.14 24 | ``` 25 | 26 | ## 术语解释 27 | 28 | - 飞书(FeiShu):Lark在中国的称呼,主要为国内的企业提供服务,拥有独立的[域名地址](https://www.feishu.cn)。 29 | - LarkSuite:Lark在海外的称呼,主要为海外的企业提供服务,拥有独立的[域名地址](https://www.larksuite.com/) 。 30 | - 开发文档:开放平台的开放接口的参考,**开发者必看,可以使用搜索功能,高效的查询文档**。[更多介绍说明](https://open.feishu.cn/document/) 。 31 | - 开发者后台:开发者开发应用的管理后台,[更多介绍说明](https://open.feishu.cn/app/) 。 32 | - 企业自建应用:应用仅仅可在本企业内安装使用,[更多介绍说明](https://open.feishu.cn/document/uQjL04CN/ukzM04SOzQjL5MDN) 。 33 | - 应用商店应用:应用会在 [应用目录](https://app.feishu.cn/?lang=zh-CN) 34 | 展示,各个企业可以选择安装,[更多介绍说明](https://open.feishu.cn/document/uQjL04CN/ugTO5UjL4kTO14CO5kTN) 。 35 | 36 | ![App type](doc/app_type.zh.png) 37 | 38 | ## 快速使用 39 | 40 | --- 41 | 42 | ### 调用服务端API 43 | 44 | - [如何调用服务端API](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/guide-to-use-server-api),了解调用服务端API的过程及注意事项。 45 | - 由于SDK已经封装了app_access_token、tenant_access_token的获取,所以在调业务API的时候,不需要去获取app_access_token、tenant_access_token。如果业务接口需要使用user_access_token,需要进行设置(lark.api.setUserAccessToken(" 46 | UserAccessToken")),具体请看 README.zh.md -> 如何构建请求(Request) 47 | 48 | - 更多使用示例,请看[packages/sample/src/api](packages/sample/src/api) 49 | 50 | #### [使用`应用商店应用`调用 服务端API 示例](doc/ISV.APP.README.zh.md) 51 | 52 | #### 使用`企业自建应用`访问 修改用户部分信息API 示例 53 | 54 | ```javascript 55 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 56 | const lark = require("@larksuiteoapi/allcore"); 57 | 58 | // 企业自建应用的配置 59 | // appID、appSecret: "开发者后台" -> "凭证与基础信息" -> 应用凭证(App ID、App Secret) 60 | // verificationToken、encryptKey:"开发者后台" -> "事件订阅" -> 事件订阅(Verification Token、Encrypt Key)。 61 | // helpDeskID、helpDeskToken, 服务台 token:https://open.feishu.cn/document/ukTMukTMukTM/ugDOyYjL4gjM24CO4IjN 62 | const appSettings = lark.newInternalAppSettings({ 63 | appID: "App ID", 64 | appSecret: "App Secret", 65 | encryptKey: "Encrypt Key", // 非必需,订阅事件时必需 66 | verificationToken: "Verification Token", // 非必需,订阅事件、消息卡片时必需 67 | helpDeskID: "HelpDesk ID", // 非必需,使用服务台API时必需 68 | helpDeskToken: "HelpDesk Token", // 非必需,使用服务台API时必需 69 | }) 70 | 71 | // 当前访问的是飞书,使用默认本地内存存储、默认控制台日志输出(Error级别),更多可选配置,请看:README.zh.md -> 如何构建整体配置(Config)。 72 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 73 | loggerLevel: lark.LoggerLevel.ERROR, 74 | }) 75 | 76 | // 发送消息的内容 77 | const body = { 78 | open_id: "user open id", 79 | msg_type: "text", 80 | content: { 81 | text: "test send message", 82 | }, 83 | } 84 | // 构建请求 85 | const req = lark.api.newRequest("/open-apis/message/v4/send", "POST", lark.api.AccessTokenType.Tenant, body) 86 | // 发送请求 87 | lark.api.sendRequest(conf, req).then(r => { 88 | // 打印请求的RequestID 89 | console.log(r.getRequestID()) 90 | // 打印请求的响应状态吗 91 | console.log(r.getHTTPStatusCode()) 92 | console.log(r) // r = response.body 93 | }).catch(e => { 94 | // 请求的error处理 95 | console.log(e) 96 | }) 97 | ``` 98 | 99 | ### 订阅服务端事件 100 | 101 | - [订阅事件概述](https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM) ,了解订阅事件的过程及注意事项。 102 | - 更多使用示例,请看[packages/sample/src/event](packages/sample/src/event)(含:结合express的使用) 103 | 104 | #### 使用`企业自建应用` 订阅 [首次启用应用事件](https://open.feishu.cn/document/ukTMukTMukTM/uQTNxYjL0UTM24CN1EjN) 示例 105 | 106 | ```javascript 107 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 108 | const lark = require("@larksuiteoapi/allcore"); 109 | 110 | // 企业自建应用的配置 111 | // appID、appSecret: "开发者后台" -> "凭证与基础信息" -> 应用凭证(App ID、App Secret) 112 | // verificationToken、encryptKey:"开发者后台" -> "事件订阅" -> 事件订阅(Verification Token、Encrypt Key)。 113 | const appSettings = lark.newInternalAppSettings({ 114 | appID: "App ID", 115 | appSecret: "App Secret", 116 | encryptKey: "Encrypt Key", 117 | verificationToken: "Verification Token", 118 | }) 119 | 120 | // 当前访问的是飞书,使用默认本地内存存储、默认控制台日志输出(Error级别),更多可选配置,请看:README.zh.md -> 如何构建整体配置(Config)。 121 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 122 | loggerLevel: lark.LoggerLevel.ERROR, 123 | }) 124 | 125 | // 设置 首次启用应用 事件的处理者 126 | lark.event.setTypeHandler(conf, "app_open", (ctx, event) => { 127 | // 打印请求的Request ID 128 | console.log(ctx.getRequestID()); 129 | // 打印事件 130 | console.log(event); 131 | }) 132 | 133 | // 启动httpServer,"开发者后台" -> "事件订阅" 请求网址 URL:https://domain 134 | // startup event http server, port: 8089 135 | lark.event.startServer(conf, 8089) 136 | 137 | ``` 138 | 139 | ### 处理消息卡片回调 140 | 141 | - [消息卡片开发流程](https://open.feishu.cn/document/ukTMukTMukTM/uAzMxEjLwMTMx4CMzETM) ,了解订阅事件的过程及注意事项 142 | - 更多使用示例,请看[packages/sample/src/card](packages/sample/src/card)(含:结合express的使用) 143 | 144 | #### 使用`企业自建应用`处理消息卡片回调示例 145 | 146 | ```javascript 147 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 148 | const lark = require("@larksuiteoapi/allcore"); 149 | 150 | // 企业自建应用的配置 151 | // appID、appSecret: "开发者后台" -> "凭证与基础信息" -> 应用凭证(App ID、App Secret) 152 | // verificationToken、encryptKey:"开发者后台" -> "事件订阅" -> 事件订阅(Verification Token、Encrypt Key)。 153 | const appSettings = lark.newInternalAppSettings({ 154 | appID: "App ID", 155 | appSecret: "App Secret", 156 | encryptKey: "Encrypt Key", // 非必需,订阅事件时必需 157 | verificationToken: "Verification Token", 158 | }) 159 | 160 | // 当前访问的是飞书,使用默认本地内存存储、默认控制台日志输出(Error级别),更多可选配置,请看:README.zh.md -> 如何构建整体配置(Config)。 161 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 162 | loggerLevel: lark.LoggerLevel.ERROR, 163 | }) 164 | 165 | // 设置 消息卡片 的处理者 166 | // 返回值:可以为""、新的消息卡片的Json字符串 167 | lark.card.setHandler(conf, (ctx, card) => { 168 | // 打印消息卡片 169 | console.log(card) 170 | console.log(card.action) 171 | return "{\"config\":{\"wide_screen_mode\":true},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"div\",\"text\":{\"tag\":\"lark_md\",\"content\":\"[飞书](https://www.feishu.cn)整合即时沟通、日历、音视频会议、云文档、云盘、工作台等功能于一体,成就组织和个人,更高效、更愉悦。\"}}]}}"; 172 | }) 173 | 174 | // 设置 "开发者后台" -> "应用功能" -> "机器人" 消息卡片请求网址:https://domain 175 | // startup event http server, port: 8089 176 | lark.event.startServer(conf, 8089) 177 | ``` 178 | 179 | ## 如何构建应用配置(AppSettings) 180 | 181 | ```javascript 182 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 183 | const lark = require("@larksuiteoapi/allcore"); 184 | 185 | // 防止应用信息泄漏,配置环境变量中,变量(4个)说明: 186 | // APP_ID:"开发者后台" -> "凭证与基础信息" -> 应用凭证 App ID 187 | // APP_SECRET:"开发者后台" -> "凭证与基础信息" -> 应用凭证 App Secret 188 | // VERIFICATION_TOKEN:"开发者后台" -> "事件订阅" -> 事件订阅 Verification Token 189 | // ENCRYPT_KEY:"开发者后台" -> "事件订阅" -> 事件订阅 Encrypt Key 190 | // HELP_DESK_ID: 服务台设置中心 -> ID 191 | // HELP_DESK_TOKEN: 服务台设置中心 -> 令牌 192 | // 企业自建应用的配置,通过环境变量获取应用配置 193 | const appSettings = lark.getInternalAppSettingsByEnv() 194 | // 应用商店应用的配置,通过环境变量获取应用配置 195 | const appSettings = lark.getISVAppSettingsByEnv() 196 | 197 | 198 | // 参数说明: 199 | // appID、appSecret: "开发者后台" -> "凭证与基础信息" -> 应用凭证(App ID、App Secret) 200 | // verificationToken、encryptKey:"开发者后台" -> "事件订阅" -> 事件订阅(Verification Token、Encrypt Key) 201 | // helpDeskID、helpDeskToken:服务台设置中心 -> ID、令牌, 服务台 token:https://open.feishu.cn/document/ukTMukTMukTM/ugDOyYjL4gjM24CO4IjN 202 | // 企业自建应用的配置 203 | const appSettings = lark.newInternalAppSettings({ 204 | appID: "App ID", 205 | appSecret: "App Secret", 206 | encryptKey: "Encrypt Key", // 非必需,订阅事件时必需 207 | verificationToken: "Verification Token", // 非必需,订阅事件、消息卡片时必需 208 | helpDeskID: "HelpDesk ID", // 非必需,使用服务台API时必需 209 | helpDeskToken: "HelpDesk Token", // 非必需,使用服务台API时必需 210 | }) 211 | // 应用商店应用的配置 212 | const appSettings = lark.newISVAppSettings({ 213 | appID: "App ID", 214 | appSecret: "App Secret", 215 | encryptKey: "Encrypt Key", // 非必需,订阅事件时必需 216 | verificationToken: "Verification Token", // 非必需,订阅事件、消息卡片时必需 217 | helpDeskID: "HelpDesk ID", // 非必需,使用服务台API时必需 218 | helpDeskToken: "HelpDesk Token", // 非必需,使用服务台API时必需 219 | }) 220 | 221 | ``` 222 | 223 | ## 如何构建整体配置(Config) 224 | 225 | - 访问 飞书、LarkSuite 或者 其他URL域名 226 | - 应用的配置 227 | - 日志接口(Logger)的实现,用于输出SDK处理过程中产生的日志,便于排查问题。 228 | - 可以使用业务系统的日志实现,请看示例代码:[packages/core/src/log/log.ts](packages/core/src/log/log.ts) ConsoleLogger 229 | - 存储接口(Store)的实现,用于保存访问凭证(app/tenant_access_token)、临时凭证(app_ticket) 230 | - 推荐使用Redis实现,请看示例代码:[sample/config/redis_store.go](packages/sample/src/config/config.ts) RedisStore 231 | - 减少获取 访问凭证 的次数,防止调用访问凭证 接口被限频。 232 | - 应用商品应用,接受开放平台下发的app_ticket,会保存到存储中,所以存储接口(Store)的实现的实现需要支持分布式存储。 233 | 234 | ```javascript 235 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 236 | const lark = require("@larksuiteoapi/allcore"); 237 | 238 | // 推荐使用Redis实现存储接口(Store),减少访问获取AccessToken接口的次数 239 | // 参数说明: 240 | // domain:URL域名地址,值范围:lark.Domain.FeiShu / lark.Domain.LarkSuite / 其他URL域名地址 241 | // appSettings:应用配置 242 | // opts: 配置选项 243 | // opts.logger: [日志接口](core/log/log.go),默认:lark.ConsoleLogger 244 | // opts.loggerLevel: 日志级别,默认:错误级别(lark.LoggerLevel.ERROR) 245 | // opts.store: [存储接口](core/store/store.go),用来存储 app_ticket/app_access_token/tenant_access_token。默认:lark.DefaultStore 246 | lark.newConfig(domain: Domain, appSettings: AppSettings, opts: ConfigOpts): Config 247 | 248 | // 例如: 249 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 250 | loggerLevel: lark.LoggerLevel.ERROR, 251 | logger: new lark.ConsoleLogger(), 252 | store: new lark.DefaultStore(), 253 | }) 254 | 255 | ``` 256 | 257 | ## 如何构建请求(Request) 258 | 259 | - 更多使用示例,请看:[packages/sample/src/api](packages/sample/src/api)(含:文件的上传与下载) 260 | 261 | ```javascript 262 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 263 | const lark = require("@larksuiteoapi/allcore"); 264 | 265 | // 创建请求 266 | // httpPath:API路径 267 | // 例如:https://domain/open-apis/contact/v3/users/:user_id 268 | // 支持:域名之后的路径,则 httpPath:"/open-apis/contact/v3/users/:user_id"(推荐) 269 | // 支持:全路径,则 httpPath:"https://domain/open-apis/contact/v3/users/:user_id" 270 | // 支持:/open-apis/ 之后的路径,则 httpPath:"contact/v3/users/:user_id" 271 | // accessTokenType:API使用哪种token访问,取值范围:lark.api.AccessTokenType.App/Tenant/User,例如:lark.api.AccessTokenType.Tenant 272 | // input:请求体(可能是lark.api.FormData()(例如:文件上传)),如果不需要请求体(例如一些GET请求),则传:undefined 273 | const req = lark.api.newRequest(httpPath:string, httpMethod:string, accessTokenType:AccessTokenType, input:any) 274 | 275 | // Request 的方法,SDK版本要求:1.0.9及以上 276 | 277 | setPathParams(pathParams:{[key:string]:any}) // 设置URL Path参数(有:前缀)值 278 | // 使用示例: 279 | req.setPathParams({"user_id": 4}) // 当 httpPath = "/open-apis/contact/v3/users/:user_id" 时,请求的URL="https://{domain}/open-apis/contact/v3/users/4" 280 | 281 | 282 | setQueryParams(queryParams:{[key:string]:any}) // 设置 URL qeury 283 | // 使用示例: 284 | req.setQueryParams({"age": 4, "types": [1, 2]}) // 会在url追加?age=4&types=1&types=2 285 | 286 | 287 | setTenantKey(tenantKey:string) // 以`应用商店应用`身份,表示使用`tenant_access_token`访问API,需要设置 288 | // 使用示例: 289 | req.setTenantKey("68daYsd") // 设置TenantKey 为 "68daYsd" 290 | 291 | 292 | setUserAccessToken(userAccessToken:string) // 表示使用`user_access_token`访问API,需要设置 293 | // 使用示例: 294 | req.setUserAccessToken("u-7f1bcd13fc57d46bac21793a18e560") // 设置 User access token 为 "u-7f1bcd13fc57d46bac21793a18e560" 295 | 296 | 297 | setTimeoutOfMs(timeoutOfMs:number) // 设置http请求,超时时间毫秒值 298 | // 使用示例: 299 | req.setTimeoutOfMs(5000) // 设置请求超时时间为 5000 毫秒 300 | 301 | 302 | setIsResponseStream() // 设置响应体的是否是流,例如下载文件,这时:output值是Buffer类型 303 | // 使用示例: 304 | req.setIsResponseStream() // 设置响应体是流 305 | 306 | 307 | setResponseStream(responseStream:stream.Writable) // 设置响应体的是否是流,例如下载文件,这时会把响应流写入 responseStream 308 | // 使用示例: 309 | req.setResponseStream(fs.createWriteStream("./test.1.png")) // 把响应流写入"./test.1.png"文件中 310 | 311 | setNeedHelpDeskAuth() // 如果是服务台 API,需要设置 HelpDesk token 312 | // 使用示例: 313 | req.setNeedHelpDeskAuth() // 设置请求是否需要 HelpDesk token 314 | 315 | ``` 316 | 317 | ## 如何发送请求 318 | 319 | - 由于SDK已经封装了app_access_token、tenant_access_token的获取,所以在调业务API的时候,不需要去获取app_access_token、tenant_access_token。如果业务接口需要使用user_access_token,需要进行设置(lark.api.setUserAccessToken(" 320 | UserAccessToken")),具体请看 README.zh.md -> 如何构建请求(Request) 321 | - 更多使用示例,请看:[packages/sample/src/api](packages/sample/src/api)(含:文件的上传与下载) 322 | 323 | ```javascript 324 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 325 | const lark = require("@larksuiteoapi/allcore"); 326 | 327 | // 参数说明: 328 | // conf:整体的配置(Config) 329 | // req:请求(Request) 330 | // 返回值说明: 331 | // resp: http response body json 332 | // err:发送请求,出现的错误 333 | async lark.api.sendRequest(conf:lark.core.Config, req:lark.api.Request) 334 | 335 | ``` 336 | 337 | ## lark.core.Context 常用方法 338 | 339 | ```javascript 340 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 341 | const lark = require("@larksuiteoapi/allcore"); 342 | 343 | // 在事件订阅与消息卡片回调的处理者中,可以从lark.core.Context中获取 Config 344 | const conf = lark.core.getConfigByCtx(ctx:lark.core.Context) 345 | 346 | ``` 347 | 348 | ## 下载文件工具 349 | 350 | - 通过网络请求下载文件 351 | - 更多使用示例,请看:[packages/sample/src/tools/downFile.js](packages/sample/src/tools/downFile.js) 352 | 353 | ```javascript 354 | // import * as lark from "@larksuiteoapi/allcore"; // typescript 355 | const lark = require("@larksuiteoapi/allcore"); 356 | 357 | // 参数说明: 358 | // url:文件的HTTP地址 359 | // timeoutOfMs:请求超时的时间(毫秒) 360 | // 返回值说明: 361 | // resp: http response body binary 362 | // err:发送请求,出现的错误 363 | async lark.api.downloadFile(url:string, timeoutOfMs:number) 364 | 365 | ``` 366 | 367 | ## License 368 | 369 | --- 370 | 371 | - MIT 372 | 373 | ## 联系我们 374 | 375 | --- 376 | 377 | - 飞书:[服务端SDK](https://open.feishu.cn/document/ukTMukTMukTM/uETO1YjLxkTN24SM5UjN) 页面右上角【这篇文档是否对你有帮助?】提交反馈 378 | 379 | -------------------------------------------------------------------------------- /api_process.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/oapi-sdk-nodejs/75fd98c46680664ba138e4f043125ae93438bdc8/api_process.jpg -------------------------------------------------------------------------------- /api_process_zh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/oapi-sdk-nodejs/75fd98c46680664ba138e4f043125ae93438bdc8/api_process_zh.jpg -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | cd packages 2 | 3 | cd core 4 | npm install 5 | npm link 6 | 7 | cd ../api 8 | npm link @larksuiteoapi/core 9 | npm install 10 | npm link 11 | 12 | cd ../event 13 | npm link @larksuiteoapi/core 14 | npm install 15 | npm link 16 | 17 | cd ../card 18 | npm link @larksuiteoapi/core 19 | npm install 20 | npm link 21 | 22 | cd ../allcore 23 | npm link @larksuiteoapi/core 24 | npm link @larksuiteoapi/api 25 | npm link @larksuiteoapi/event 26 | npm link @larksuiteoapi/card 27 | npm install 28 | npm link 29 | 30 | cd ../sample 31 | npm link @larksuiteoapi/allcore 32 | npm install 33 | -------------------------------------------------------------------------------- /doc/ISV.APP.README.zh.md: -------------------------------------------------------------------------------- 1 | # 使用应用商店应用调用服务端API 2 | 3 | --- 4 | 5 | - 如何获取app_access_token,请看:[获取app_access_token](https://open.feishu.cn/document/ukTMukTMukTM/uEjNz4SM2MjLxYzM) (应用商店应用) 6 | - 与企业自建应用相比,应用商店应用的获取app_access_token的流程复杂一些。 7 | - 需要开放平台下发的app_ticket,通过订阅事件接收。SDK已经封装了app_ticket事件的处理,只需要启动事件订阅服务。 8 | - 使用SDK调用服务端API时,如果当前还没有收到开发平台下发的app_ticket,会报错且向开放平台申请下发app_ticket,可以尽快的收到开发平台下发的app_ticket,保证再次调用服务端API的正常。 9 | - 使用SDK调用服务端API时,需要使用tenant_access_token访问凭证时,需要 tenant_key ,来表示当前是哪个租户使用这个应用调用服务端API。 10 | - tenant_key,租户安装启用了这个应用,开放平台发送的服务端事件,事件内容中都含有tenant_key。 11 | 12 | ### 使用`应用商店应用`访问 [发送文本消息API](https://open.feishu.cn/document/ukTMukTMukTM/uUjNz4SN2MjL1YzM) 示例 13 | 14 | - 第一步:启动启动事件订阅服务,用于接收`app_ticket`。 15 | 16 | ```javascript 17 | const lark = require("@larksuiteoapi/allcore"); 18 | 19 | // 应用商店应用的配置 20 | // appID、appSecret: "开发者后台" -> "凭证与基础信息" -> 应用凭证(App ID、App Secret) 21 | // verificationToken、encryptKey:"开发者后台" -> "事件订阅" -> 事件订阅(Verification Token、Encrypt Key)。 22 | // helpDeskID、helpDeskToken, 服务台 token:https://open.feishu.cn/document/ukTMukTMukTM/ugDOyYjL4gjM24CO4IjN 23 | const appSettings = lark.newISVAppSettings({ 24 | appID: "App ID", 25 | appSecret: "App Secret", 26 | encryptKey: "Encrypt Key", 27 | verificationToken: "Verification Token", 28 | helpDeskID: "HelpDesk ID", // 非必需,使用服务台API时必需 29 | helpDeskToken: "HelpDesk Token", // 非必需,使用服务台API时必需 30 | }) 31 | 32 | // 当前访问的是飞书,使用默认存储、默认日志(Error级别),更多可选配置,请看:README.zh.md -> 如何构建整体配置(Config)。 33 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 34 | loggerLevel: lark.LoggerLevel.ERROR, 35 | store: 需要使用分布式缓存实现, // 例如: packages/sample/src/config/config.ts 的 RedisStore 36 | }) 37 | 38 | // 启动httpServer,"开发者后台" -> "事件订阅" 请求网址 URL:https://domain 39 | // startup event http server, port: 8089 40 | lark.event.startServer(conf, 8089) 41 | 42 | ``` 43 | 44 | - 第二步:调用服务端接口。 45 | 46 | ```javascript 47 | const lark = require("@larksuiteoapi/allcore"); 48 | 49 | // 应用商店应用的配置 50 | // appID、appSecret: "开发者后台" -> "凭证与基础信息" -> 应用凭证(App ID、App Secret) 51 | // verificationToken、encryptKey:"开发者后台" -> "事件订阅" -> 事件订阅(Verification Token、Encrypt Key)。 52 | // helpDeskID、helpDeskToken, 服务台 token:https://open.feishu.cn/document/ukTMukTMukTM/ugDOyYjL4gjM24CO4IjN 53 | const appSettings = lark.newISVAppSettings({ 54 | appID: "App ID", 55 | appSecret: "App Secret", 56 | encryptKey: "Encrypt Key", 57 | verificationToken: "Verification Token", 58 | helpDeskID: "HelpDesk ID", // 非必需,使用服务台API时必需 59 | helpDeskToken: "HelpDesk Token", // 非必需,使用服务台API时必需 60 | }) 61 | 62 | // 当前访问的是飞书,使用默认存储、默认日志(Error级别),更多可选配置,请看:README.zh.md -> 如何构建整体配置(Config)。 63 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 64 | loggerLevel: lark.LoggerLevel.ERROR, 65 | store: 需要使用分布式缓存实现, // 例如: packages/sample/src/config/config.ts 的 RedisStore 66 | }) 67 | 68 | // 发送消息的内容 69 | const body = { 70 | open_id: "user open id", 71 | msg_type: "text", 72 | content: { 73 | text: "test send message", 74 | }, 75 | } 76 | // 构建请求 && 设置企业标识 Tenant key 77 | const req = lark.api.newRequest("/open-apis/message/v4/send", "POST", lark.api.AccessTokenType.Tenant, body, lark.api.setTenantKey("Tenant key")) 78 | // 发送请求 79 | lark.api.sendRequest(conf, req).then(r => { 80 | // 打印请求的RequestID 81 | console.log(r.getRequestID()) 82 | // 打印请求的响应状态吗 83 | console.log(r.getHTTPStatusCode()) 84 | console.log(r) // r = response.body 85 | }).catch(e => { 86 | // 请求的error处理 87 | console.log(e) 88 | }) 89 | ``` 90 | -------------------------------------------------------------------------------- /doc/app_type.en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/oapi-sdk-nodejs/75fd98c46680664ba138e4f043125ae93438bdc8/doc/app_type.en.png -------------------------------------------------------------------------------- /doc/app_type.zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/oapi-sdk-nodejs/75fd98c46680664ba138e4f043125ae93438bdc8/doc/app_type.zh.png -------------------------------------------------------------------------------- /packages/allcore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@larksuiteoapi/allcore", 3 | "version": "1.0.14", 4 | "description": "larksuite open all core sdk", 5 | "keywords": [ 6 | "feishu", 7 | "larksuite" 8 | ], 9 | "author": { 10 | "name": "Lark Technologies Pte. Ltd" 11 | }, 12 | "main": "dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "files": [ 15 | "dist/**/*" 16 | ], 17 | "engines": { 18 | "node": ">= 8.9.0", 19 | "npm": ">= 5.5.1" 20 | }, 21 | "repository": "larksuite/oapi-sdk-nodejs", 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "license": "MIT", 26 | "homepage": "https://github.com/larksuite/oapi-sdk-nodejs", 27 | "bugs": { 28 | "url": "https://github.com/larksuite/oapi-sdk-nodejs/issues" 29 | }, 30 | "scripts": { 31 | "prepare": "npm run build", 32 | "build": "npm run build:clean && tsc", 33 | "build:clean": "rm -rf ./dist" 34 | }, 35 | "dependencies": { 36 | "@larksuiteoapi/api": "1.0.14", 37 | "@larksuiteoapi/card": "1.0.14", 38 | "@larksuiteoapi/core": "1.0.14", 39 | "@larksuiteoapi/event": "1.0.14" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "^14.6.4", 43 | "@types/node-fetch": "^2.5.7", 44 | "ts-node": "^9.0.0", 45 | "typescript": "^3.9.7" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/allcore/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as core from "@larksuiteoapi/core" 2 | export * as api from "@larksuiteoapi/api" 3 | export * as event from "@larksuiteoapi/event" 4 | export * as card from "@larksuiteoapi/card" 5 | export * from './lark'; 6 | -------------------------------------------------------------------------------- /packages/allcore/src/lark.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@larksuiteoapi/core"; 2 | 3 | export const getInternalAppSettingsByEnv = core.getInternalAppSettingsByEnv 4 | export const getISVAppSettingsByEnv = core.getISVAppSettingsByEnv 5 | 6 | export const newISVAppSettings = core.newISVAppSettingsWithOpts 7 | export const newInternalAppSettings = core.newInternalAppSettingsWithOpts 8 | 9 | export const newConfig = core.newConfigWithOpts 10 | export const Domain = core.Domain 11 | export const LoggerLevel = core.LoggerLevel 12 | export const ConsoleLogger = core.ConsoleLogger 13 | export const DefaultStore = core.DefaultStore 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/allcore/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "strict": false, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "*": [ 18 | "./types/*" 19 | ] 20 | }, 21 | "esModuleInterop": true 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ], 26 | "exclude": [ 27 | "src/**/*.test.ts", 28 | "src/**/*.js" 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@larksuiteoapi/api", 3 | "version": "1.0.14", 4 | "description": "larksuite open api sdk", 5 | "keywords": [ 6 | "feishu", 7 | "larksuite" 8 | ], 9 | "author": { 10 | "name": "Lark Technologies Pte. Ltd" 11 | }, 12 | "main": "dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "files": [ 15 | "dist/**/*" 16 | ], 17 | "engines": { 18 | "node": ">= 8.9.0", 19 | "npm": ">= 5.5.1" 20 | }, 21 | "repository": "larksuite/oapi-sdk-nodejs", 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "license": "MIT", 26 | "homepage": "https://github.com/larksuite/oapi-sdk-nodejs", 27 | "bugs": { 28 | "url": "https://github.com/larksuite/oapi-sdk-nodejs/issues" 29 | }, 30 | "scripts": { 31 | "prepare": "npm run build", 32 | "build": "npm run build:clean && tsc", 33 | "build:clean": "rm -rf ./dist" 34 | }, 35 | "dependencies": { 36 | "@larksuiteoapi/core": "1.0.14", 37 | "form-data": "^3.0.0", 38 | "node-fetch": "^2.6.0", 39 | "tempy": "^0.7.1" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "^14.6.4", 43 | "@types/node-fetch": "^2.5.7", 44 | "ts-node": "^9.0.0", 45 | "typescript": "^3.9.7" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/api/src/api.ts: -------------------------------------------------------------------------------- 1 | import {Request} from "./core/request/request"; 2 | import {handle} from "./core/handlers/handlers"; 3 | import {newErr, ErrCode, instanceOfError} from "./core/response/error"; 4 | import {Config, Context} from "@larksuiteoapi/core"; 5 | import {Response} from "./core/response/response"; 6 | 7 | export const send = async (ctx: Context, conf: Config, req: Request) => { 8 | let response 9 | try { 10 | response = await _sendRequest(ctx, conf, req) 11 | } catch (e) { 12 | if (instanceOfError(e)) { 13 | throw e 14 | } 15 | throw newErr(e) 16 | } 17 | if (response.code == ErrCode.Ok) { 18 | return response.data 19 | } 20 | throw response 21 | } 22 | 23 | export const sendRequest = async (conf: Config, req: Request) => { 24 | return _sendRequest(new Context(), conf, req) 25 | } 26 | 27 | 28 | const _sendRequest = async (ctx: Context, conf: Config, req: Request) => { 29 | conf.withContext(ctx) 30 | req.withContext(ctx) 31 | return handle(ctx, req) 32 | } 33 | 34 | export class ReqCallDone { 35 | ctx: Context 36 | reqCall: ReqCaller 37 | result: T 38 | err: any 39 | 40 | constructor(reqCall: ReqCaller) { 41 | this.reqCall = reqCall 42 | this.ctx = reqCall.ctx 43 | } 44 | } 45 | 46 | export interface ReqCaller { 47 | ctx: Context 48 | 49 | do(): Promise | T 50 | } 51 | 52 | export class ReqCall implements ReqCaller { 53 | ctx: Context 54 | conf: Config 55 | req: Request 56 | 57 | constructor(ctx: Context, conf: Config, req: Request) { 58 | this.ctx = ctx; 59 | this.conf = conf; 60 | this.req = req; 61 | } 62 | 63 | do() { 64 | return send(this.ctx, this.conf, this.req); 65 | } 66 | } 67 | 68 | export type ErrorCallback = (reqCall: ReqCaller, err: any) => Promise | void 69 | 70 | export class BatchReqCall { 71 | private readonly reqCalls: ReqCaller[] 72 | private readonly errorCallback: ErrorCallback 73 | readonly reqCallDos: ReqCallDone[] 74 | 75 | constructor(errorCallback: ErrorCallback, ...reqCalls: ReqCaller[]) { 76 | this.reqCalls = reqCalls 77 | this.reqCallDos = [] 78 | for (let v of this.reqCalls) { 79 | this.reqCallDos.push(new ReqCallDone(v)) 80 | } 81 | this.errorCallback = errorCallback 82 | } 83 | 84 | do = async () => { 85 | await Promise.all(this.reqCalls.map((reqCall: ReqCaller, index: number) => { 86 | return reqCall.do().then(result => { 87 | this.reqCallDos[index].result = result 88 | }).catch(e => { 89 | this.reqCallDos[index].err = e 90 | this.errorCallback(reqCall, e) 91 | }) 92 | })) 93 | return this 94 | } 95 | } 96 | 97 | 98 | export class APIReqCallResult { 99 | reqCall: IReqCall 100 | response: Response 101 | err: any 102 | 103 | constructor(reqCall: IReqCall) { 104 | this.reqCall = reqCall 105 | } 106 | } 107 | 108 | export interface IReqCall { 109 | do(): Promise> 110 | } 111 | 112 | export class APIReqCall implements IReqCall { 113 | conf: Config 114 | req: Request 115 | 116 | constructor(conf: Config, req: Request) { 117 | this.conf = conf; 118 | this.req = req; 119 | } 120 | 121 | do() { 122 | return sendRequest(this.conf, this.req); 123 | } 124 | } 125 | 126 | export class BatchAPIReqCall { 127 | private readonly reqCalls: IReqCall[] 128 | readonly reqCallResults: APIReqCallResult[] 129 | 130 | constructor(...reqCalls: ReqCaller[]) { 131 | this.reqCalls = reqCalls 132 | this.reqCallResults = [] 133 | for (let v of this.reqCalls) { 134 | this.reqCallResults.push(new APIReqCallResult(v)) 135 | } 136 | } 137 | 138 | do = async () => { 139 | if (this.reqCalls.length == 0) { 140 | return this 141 | } 142 | await Promise.all(this.reqCalls.map((reqCall: IReqCall, index: number) => { 143 | return reqCall.do().then(result => { 144 | this.reqCallResults[index].response = result 145 | }).catch(e => { 146 | this.reqCallResults[index].err = e 147 | }) 148 | })) 149 | return this 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /packages/api/src/core/constants/constants.ts: -------------------------------------------------------------------------------- 1 | export const OAPIRootPath = "open-apis" 2 | 3 | export enum URL { 4 | AppAccessTokenInternalUrlPath = "auth/v3/app_access_token/internal", 5 | AppAccessTokenIsvUrlPath = "auth/v3/app_access_token", 6 | TenantAccessTokenInternalUrlPath = "auth/v3/tenant_access_token/internal", 7 | TenantAccessTokenIsvUrlPath = "auth/v3/tenant_access_token", 8 | ApplyAppTicketPath = "auth/v3/app_ticket/resend" 9 | } 10 | -------------------------------------------------------------------------------- /packages/api/src/core/errors/errors.ts: -------------------------------------------------------------------------------- 1 | import {Response} from "../response/response" 2 | import {Err} from "../response/error"; 3 | 4 | export const throwAccessTokenTypeIsInValidErr = () => { 5 | throw new Error("access token type invalid") 6 | } 7 | 8 | export const throwHelpDeskAuthorizationIsEmptyErr = () =>{ 9 | throw new Error("help desk API, please set the helpdesk information of AppSettings") 10 | } 11 | 12 | export const throwTenantKeyIsEmptyErr = () => { 13 | throw new Error("tenant key is empty") 14 | } 15 | export const throwUserAccessTokenKeyIsEmptyErr = () => { 16 | throw new Error("user access token is empty") 17 | } 18 | 19 | export class AppTicketIsEmptyErr extends Error { 20 | name: string = "AppTicketIsEmptyErr" 21 | message: string = "app ticket is empty" 22 | } 23 | 24 | export const throwAppTicketIsEmptyErr = () => { 25 | throw new AppTicketIsEmptyErr() 26 | } 27 | 28 | export class AccessTokenObtainErr extends Error { 29 | name: string = "AccessTokenObtainErr" 30 | response: Response 31 | code: number 32 | msg: string 33 | error: Err 34 | 35 | constructor(message: string, response: Response) { 36 | super(message); 37 | this.response = response 38 | this.code = response.code; 39 | this.msg = response.msg; 40 | this.error = response.error; 41 | } 42 | } -------------------------------------------------------------------------------- /packages/api/src/core/handlers/accessToken.ts: -------------------------------------------------------------------------------- 1 | import * as common from "@larksuiteoapi/core"; 2 | import { 3 | AppAccessToken, 4 | GetInternalAccessTokenReq, 5 | GetISVAppAccessTokenReq, 6 | GetISVTenantAccessTokenReq, 7 | TenantAccessToken 8 | } from "../token/token"; 9 | import * as request from "../request/request" 10 | import {handle} from "./handlers"; 11 | import * as util from "util" 12 | import {URL} from "../constants/constants"; 13 | import { 14 | getAppAccessTokenKey, 15 | getAppTicketKey, 16 | AppType, 17 | getConfigByCtx, 18 | getTenantAccessTokenKey 19 | } from "@larksuiteoapi/core"; 20 | import {throwAppTicketIsEmptyErr, AccessTokenObtainErr} from "../errors/errors"; 21 | import {ErrCode} from "../response/error"; 22 | 23 | const expiryDelta = 3 * 60 24 | 25 | // get internal app access token 26 | export const getInternalAppAccessToken = async (ctx: common.Context) => { 27 | let accessToken: AppAccessToken 28 | let conf = getConfigByCtx(ctx) 29 | let req = request.newRequestByAuth(URL.AppAccessTokenInternalUrlPath, "POST", 30 | new GetInternalAccessTokenReq(conf.getAppSettings().appID, conf.getAppSettings().appSecret), accessToken) 31 | let response = await handle(ctx, req) 32 | if (response.code != ErrCode.Ok) { 33 | throw new AccessTokenObtainErr("obtain internal app access token,failure information:" + req.response.toString(), req.response) 34 | } 35 | return response.data 36 | } 37 | 38 | // get internal tenant access token 39 | export const getInternalTenantAccessToken = async (ctx: common.Context) => { 40 | let accessToken: TenantAccessToken 41 | let conf = getConfigByCtx(ctx) 42 | let req = request.newRequestByAuth(URL.TenantAccessTokenInternalUrlPath, "POST", 43 | new GetInternalAccessTokenReq(conf.getAppSettings().appID, conf.getAppSettings().appSecret), accessToken) 44 | let response = await handle(ctx, req) 45 | if (response.code != ErrCode.Ok) { 46 | throw new AccessTokenObtainErr("obtain internal tenant access token,failure information:" + req.response.toString(), req.response) 47 | } 48 | return response.data 49 | } 50 | 51 | const getAppTicket = async (ctx: common.Context) => { 52 | let conf = getConfigByCtx(ctx) 53 | return conf.getStore().get(getAppTicketKey(conf.getAppSettings().appID)) 54 | } 55 | 56 | // get isv app access token 57 | export const getIsvAppAccessToken = async (ctx: common.Context) => { 58 | let appTicket = await getAppTicket(ctx) 59 | if (!appTicket) { 60 | throwAppTicketIsEmptyErr() 61 | } 62 | let accessToken: AppAccessToken 63 | let conf = getConfigByCtx(ctx) 64 | let req = request.newRequestByAuth(URL.AppAccessTokenIsvUrlPath, "POST", 65 | new GetISVAppAccessTokenReq(conf.getAppSettings().appID, conf.getAppSettings().appSecret, appTicket), accessToken) 66 | let response = await handle(ctx, req) 67 | if (response.code != ErrCode.Ok) { 68 | throw new AccessTokenObtainErr("obtain ISV app access token,failure information:" + req.response.toString(), req.response) 69 | } 70 | return response.data 71 | } 72 | 73 | export const setAppAccessTokenToStore = async (ctx: common.Context, appAccessToken: AppAccessToken) => { 74 | let conf = getConfigByCtx(ctx) 75 | try { 76 | await conf.getStore().put(getAppAccessTokenKey(conf.getAppSettings().appID), appAccessToken.app_access_token, appAccessToken.expire - expiryDelta) 77 | } catch (e) { 78 | conf.getLogger().error(e) 79 | } 80 | } 81 | 82 | // get isv tenant access token 83 | export const getIsvTenantAccessToken = async (ctx: common.Context) => { 84 | let appAccessToken = await getIsvAppAccessToken(ctx) 85 | let info = request.getInfoByCtx(ctx) 86 | let tenantAccessToken: TenantAccessToken 87 | let req = request.newRequestByAuth(URL.TenantAccessTokenIsvUrlPath, "POST", 88 | new GetISVTenantAccessTokenReq(appAccessToken.app_access_token, info.tenantKey), tenantAccessToken) 89 | let response = await handle(ctx, req) 90 | if (response.code != ErrCode.Ok) { 91 | throw new AccessTokenObtainErr("obtain ISV tenant access token,failure information:" + req.response.toString(), req.response) 92 | } 93 | tenantAccessToken = response.data 94 | let res: [AppAccessToken, TenantAccessToken] = [appAccessToken, tenantAccessToken] 95 | return res 96 | } 97 | 98 | export const setTenantAccessTokenToStore = async (ctx: common.Context, tenantAccessToken: TenantAccessToken) => { 99 | let conf = getConfigByCtx(ctx) 100 | let info = request.getInfoByCtx(ctx) 101 | try { 102 | await conf.getStore().put(getTenantAccessTokenKey(conf.getAppSettings().appID, info.tenantKey), 103 | tenantAccessToken.tenant_access_token, tenantAccessToken.expire - expiryDelta) 104 | } catch (e) { 105 | conf.getLogger().error(e) 106 | } 107 | } 108 | 109 | const setAuthorizationToHeader = (headers: {}, token: string): void => { 110 | headers["Authorization"] = util.format("Bearer %s", token) 111 | } 112 | 113 | export const setAppAccessToken = async (ctx: common.Context, headers: {}) => { 114 | let conf = getConfigByCtx(ctx) 115 | let info = request.getInfoByCtx(ctx) 116 | // from store get app access token 117 | if (!info.retryable) { 118 | let tok = await conf.getStore().get(getAppAccessTokenKey(conf.getAppSettings().appID)) 119 | if (tok) { 120 | setAuthorizationToHeader(headers, tok) 121 | return 122 | } 123 | } 124 | let appAccessToken: AppAccessToken 125 | if (conf.getAppSettings().appType == AppType.Internal) { 126 | appAccessToken = await getInternalAppAccessToken(ctx) 127 | } else { 128 | appAccessToken = await getIsvAppAccessToken(ctx) 129 | } 130 | await setAppAccessTokenToStore(ctx, appAccessToken) 131 | setAuthorizationToHeader(headers, appAccessToken.app_access_token) 132 | } 133 | 134 | export const setTenantAccessToken = async (ctx: common.Context, headers: {}) => { 135 | let conf = getConfigByCtx(ctx) 136 | let info = request.getInfoByCtx(ctx) 137 | // from store get tenant access token 138 | if (!info.retryable) { 139 | let tenantKey = info.tenantKey || "" 140 | let tok = await conf.getStore().get(getTenantAccessTokenKey(conf.getAppSettings().appID, tenantKey)) 141 | if (tok) { 142 | setAuthorizationToHeader(headers, tok) 143 | return 144 | } 145 | } 146 | if (conf.getAppSettings().appType == AppType.Internal) { 147 | let tenantAccessToken = await getInternalTenantAccessToken(ctx) 148 | await setTenantAccessTokenToStore(ctx, tenantAccessToken) 149 | setAuthorizationToHeader(headers, tenantAccessToken.tenant_access_token) 150 | } else { 151 | let accessToken = await getIsvTenantAccessToken(ctx) 152 | await setAppAccessTokenToStore(ctx, accessToken[0]) 153 | await setTenantAccessTokenToStore(ctx, accessToken[1]) 154 | setAuthorizationToHeader(headers, accessToken[1].tenant_access_token) 155 | } 156 | } 157 | 158 | export const setUserAccessToken = async (ctx: common.Context, headers: {}) => { 159 | let info = request.getInfoByCtx(ctx) 160 | if (info.userAccessToken) { 161 | setAuthorizationToHeader(headers, info.userAccessToken) 162 | return 163 | } 164 | } -------------------------------------------------------------------------------- /packages/api/src/core/handlers/handlers.ts: -------------------------------------------------------------------------------- 1 | import * as util from "util" 2 | import * as request from "../request/request" 3 | import * as formdata from "../request/formData" 4 | import * as response from "../response/response" 5 | 6 | import { 7 | AppTicketIsEmptyErr, 8 | throwAccessTokenTypeIsInValidErr, throwHelpDeskAuthorizationIsEmptyErr, 9 | throwTenantKeyIsEmptyErr, 10 | throwUserAccessTokenKeyIsEmptyErr 11 | } from "../errors/errors"; 12 | import fetch from 'node-fetch'; 13 | import FormData from "form-data"; 14 | import {Error, ErrCode, newErrorOfInvalidResp} from "../response/error"; 15 | import {ApplyAppTicketReq} from "../token/token"; 16 | import {setAppAccessToken, setTenantAccessToken, setUserAccessToken} from "./accessToken"; 17 | import {URL} from "../constants/constants"; 18 | import { 19 | AppType, 20 | ContentType, 21 | ContentTypeJson, 22 | Context, 23 | DefaultContentType, 24 | getConfigByCtx, HTTPKeyStatusCode, 25 | SdkVersion, HTTPHeaderKey 26 | } from "@larksuiteoapi/core"; 27 | import * as fs from "fs"; 28 | import tempy from "tempy"; 29 | import * as stream from "stream"; 30 | import path from "path"; 31 | 32 | const defaultMaxRetryCount = 1 33 | 34 | let defaultHTTPRequestHeader = new Map() 35 | 36 | defaultHTTPRequestHeader.set("User-Agent", util.format("oapi-sdk-nodejs/%s", SdkVersion)) 37 | 38 | type handler = (ctx: Context, req: request.Request) => Promise 39 | 40 | const initFunc = async (ctx: Context, req: request.Request) => { 41 | let conf = getConfigByCtx(ctx) 42 | req.init(conf.getDomain()) 43 | } 44 | 45 | const validateFunc = async (ctx: Context, req: request.Request) => { 46 | if (req.accessTokenType == request.AccessTokenType.None) { 47 | return 48 | } 49 | if (!req.accessibleTokenTypeSet.has(req.accessTokenType)) { 50 | throwAccessTokenTypeIsInValidErr() 51 | } 52 | if (getConfigByCtx(ctx).getAppSettings().appType === AppType.ISV) { 53 | if (req.accessTokenType === request.AccessTokenType.Tenant && !req.tenantKey) { 54 | throwTenantKeyIsEmptyErr() 55 | } 56 | if (req.accessTokenType === request.AccessTokenType.User && !req.userAccessToken) { 57 | throwUserAccessTokenKeyIsEmptyErr() 58 | } 59 | } 60 | } 61 | 62 | const reqBodyFromFormData = (ctx: Context, req: request.Request): void => { 63 | const form = new FormData(); 64 | let fd = req.input as formdata.FormData 65 | Array.from(fd.getParams().entries()).forEach(([key, value]) => { 66 | form.append(key, value) 67 | }) 68 | let hasStream = false 69 | for (let file of fd.getFiles()) { 70 | let opt = { 71 | filename: file.getName() 72 | } 73 | if (file.getType()) { 74 | opt["contentType"] = file.getType() 75 | } 76 | if (file.isStream()) { 77 | hasStream = true 78 | } 79 | form.append(file.getFieldName(), file.getContent(), opt) 80 | } 81 | Object.entries(form.getHeaders()).forEach(([key, value]) => { 82 | req.httpRequestOpts.headers[key] = value 83 | }) 84 | if (hasStream) { 85 | let filePath = tempy.directory({ 86 | prefix: "larksuiteoapi-", 87 | }) 88 | filePath = path.join(filePath, "formdata") 89 | form.pipe(fs.createWriteStream(filePath)) 90 | req.httpRequestOpts.bodySource = { 91 | isStream: true, 92 | filePath: filePath, 93 | } 94 | getConfigByCtx(ctx).getLogger().debug(util.format("[build]request:%s, formdata:%s", req, filePath)) 95 | } else { 96 | req.httpRequestOpts.body = form.getBuffer() 97 | getConfigByCtx(ctx).getLogger().debug(util.format("[build]request:%s, formdata:", req), req.httpRequestOpts.body) 98 | } 99 | } 100 | 101 | const reqBodyFromInput = (ctx: Context, req: request.Request): void => { 102 | req.httpRequestOpts.headers[ContentType] = DefaultContentType 103 | let input: string 104 | if (typeof req.input == "string") { 105 | input = req.input 106 | } else { 107 | input = JSON.stringify(req.input) 108 | } 109 | req.httpRequestOpts.body = input 110 | getConfigByCtx(ctx).getLogger().debug(util.format("[build]request:%s, body:", req), req.httpRequestOpts.body) 111 | } 112 | 113 | const buildFunc = async (ctx: Context, req: request.Request) => { 114 | if (!req.retryable) { 115 | let conf = getConfigByCtx(ctx) 116 | let opts = { 117 | method: req.httpMethod, 118 | timeout: req.timeout, 119 | headers: {}, 120 | } 121 | Array.from(defaultHTTPRequestHeader).forEach(([key, value]) => { 122 | opts.headers[key.toLowerCase()] = value 123 | }) 124 | req.httpRequestOpts = opts 125 | if (req.input) { 126 | if (req.input instanceof formdata.FormData) { 127 | reqBodyFromFormData(ctx, req) 128 | } else { 129 | reqBodyFromInput(ctx, req) 130 | } 131 | } else { 132 | conf.getLogger().debug(util.format("[build]request:%s, not body", req)) 133 | } 134 | } 135 | } 136 | 137 | const signFunc = async (ctx: Context, req: request.Request) => { 138 | switch (req.accessTokenType) { 139 | case request.AccessTokenType.App: 140 | await setAppAccessToken(ctx, req.httpRequestOpts.headers) 141 | break 142 | case request.AccessTokenType.Tenant: 143 | await setTenantAccessToken(ctx, req.httpRequestOpts.headers) 144 | break 145 | case request.AccessTokenType.User: 146 | await setUserAccessToken(ctx, req.httpRequestOpts.headers) 147 | break 148 | } 149 | if (req.needHelpDeskAuth) { 150 | let helpDeskAuthorization = getConfigByCtx(ctx).getHelpDeskAuthorization() 151 | if (!helpDeskAuthorization) { 152 | throwHelpDeskAuthorizationIsEmptyErr() 153 | } 154 | req.httpRequestOpts.headers["X-Lark-Helpdesk-Authorization"] = helpDeskAuthorization 155 | } 156 | } 157 | 158 | const validateResponseFunc = async (_: Context, req: request.Request) => { 159 | let resp = req.httpResponse 160 | let contentType = resp.headers.get(ContentType.toLowerCase()) 161 | if (req.isResponseStream) { 162 | if (resp.ok) { 163 | req.isResponseStreamReal = true; 164 | return 165 | } 166 | if (contentType && contentType.indexOf(ContentTypeJson) > -1) { 167 | req.isResponseStreamReal = false; 168 | return 169 | } 170 | let content = await resp.buffer() 171 | throw newErrorOfInvalidResp(util.format("response is stream, but status code %d is not 200, content-type: %s, body: %s", resp.status, contentType, content)) 172 | } 173 | if (!contentType || contentType.indexOf(ContentTypeJson) === -1) { 174 | let content = await resp.buffer() 175 | throw newErrorOfInvalidResp(util.format("status code: %d, content-type %s is not %s, body: %s", resp.status, contentType, ContentTypeJson, content)) 176 | } 177 | } 178 | 179 | export const unmarshalResponseFunc = async (ctx: Context, req: request.Request) => { 180 | let resp = req.httpResponse 181 | if (req.isResponseStreamReal) { 182 | if (req.output && req.output instanceof stream.Writable) { 183 | resp.body.pipe(req.output) 184 | req.response.data = req.output 185 | return 186 | } 187 | let body = await resp.buffer() 188 | req.output = body as any 189 | req.response.data = req.output 190 | return 191 | } 192 | let json = await resp.json() 193 | getConfigByCtx(ctx).getLogger().debug(util.format("[unmarshalResponse] request:%s, response:body:", 194 | req), JSON.stringify(json)) 195 | req.retryable = retryable(json.code) 196 | Object.entries(json).forEach(([k, v]) => { 197 | req.response[k] = v; 198 | }) 199 | if (req.isNotDataField) { 200 | req.response.data = json 201 | } 202 | } 203 | 204 | const retryable = (code: number): boolean => { 205 | let b = false 206 | switch (code) { 207 | case ErrCode.AccessTokenInvalid: 208 | case ErrCode.AppAccessTokenInvalid: 209 | case ErrCode.TenantAccessTokenInvalid: 210 | b = true 211 | break 212 | } 213 | return b 214 | } 215 | 216 | // apply app ticket 217 | export const applyAppTicket = async (ctx: Context) => { 218 | let conf = getConfigByCtx(ctx) 219 | let req = request.newRequestByAuth(URL.ApplyAppTicketPath, "POST", 220 | new ApplyAppTicketReq(conf.getAppSettings().appID, conf.getAppSettings().appSecret), {}) 221 | await handle(ctx, req) 222 | } 223 | 224 | const deleteTmpFile = async (ctx: Context, req: request.Request) => { 225 | let conf = getConfigByCtx(ctx) 226 | let bodySource = req.httpRequestOpts.bodySource 227 | if (bodySource && bodySource.isStream) { 228 | try { 229 | fs.unlinkSync(bodySource.filePath) 230 | } catch (err) { 231 | conf.getLogger().info(util.format("[complement] request:%s, delete tmp file(%s) err: ", req.toString()), bodySource.filePath, err) 232 | } 233 | } 234 | } 235 | 236 | const complement = async (ctx: Context, req: request.Request, err: Error) => { 237 | await deleteTmpFile(ctx, req) 238 | if (err) { 239 | if (err instanceof AppTicketIsEmptyErr) { 240 | await applyAppTicket(ctx) 241 | } 242 | throw err 243 | } 244 | if (req.response && req.response.code == ErrCode.AppTicketInvalid) { 245 | await applyAppTicket(ctx) 246 | return 247 | } 248 | } 249 | 250 | export class Handlers { 251 | init: handler 252 | validate: handler 253 | build: handler 254 | sign: handler 255 | validateResponse: handler 256 | unmarshalResponse: handler 257 | 258 | constructor(init: handler, validate: handler, build: handler, sign: handler, validateResponse: handler, 259 | unmarshalResponse: handler) { 260 | this.init = init 261 | this.validate = validate 262 | this.build = build 263 | this.sign = sign 264 | this.validateResponse = validateResponse 265 | this.unmarshalResponse = unmarshalResponse 266 | } 267 | 268 | private _send = async (ctx: Context, req: request.Request) => { 269 | await this.build(ctx, req) 270 | await this.sign(ctx, req) 271 | let bodySource = req.httpRequestOpts.bodySource 272 | if (bodySource && bodySource.isStream) { 273 | req.httpRequestOpts.body = fs.createReadStream(bodySource.filePath) 274 | } 275 | req.httpResponse = await fetch(req.url(), req.httpRequestOpts) 276 | let header: { [key: string]: any } = {} 277 | req.httpResponse.headers.forEach((value, name) => { 278 | header[name.toLowerCase()] = value 279 | }) 280 | ctx.set(HTTPHeaderKey, header) 281 | ctx.set(HTTPKeyStatusCode, req.httpResponse.status) 282 | req.response = new response.Response(ctx) 283 | await this.validateResponse(ctx, req) 284 | await this.unmarshalResponse(ctx, req) 285 | } 286 | 287 | send = async (ctx: Context, req: request.Request) => { 288 | let i = 0 289 | let conf = getConfigByCtx(ctx) 290 | do { 291 | i++ 292 | if (req.retryable) { 293 | conf.getLogger().debug(util.format("[retry] request:%s, response: ", req.toString()), req.response.toString()) 294 | } 295 | await this._send(ctx, req) 296 | } while (req.retryable && i <= defaultMaxRetryCount) 297 | } 298 | } 299 | 300 | export const Default = new Handlers(initFunc, validateFunc, buildFunc, signFunc, validateResponseFunc, unmarshalResponseFunc) 301 | 302 | export const handle = async (ctx: Context, req: request.Request) => { 303 | let err: Error 304 | try { 305 | await Default.init(ctx, req) 306 | await Default.validate(ctx, req) 307 | await Default.send(ctx, req) 308 | } catch (e) { 309 | err = e 310 | } finally { 311 | await complement(ctx, req, err) 312 | } 313 | return req.response 314 | } 315 | 316 | 317 | 318 | 319 | 320 | 321 | -------------------------------------------------------------------------------- /packages/api/src/core/request/formData.ts: -------------------------------------------------------------------------------- 1 | const stream = require('stream'); 2 | 3 | export class FormData { 4 | private params: Map 5 | private files: File[] 6 | 7 | constructor() { 8 | this.params = new Map() 9 | this.files = [] 10 | } 11 | 12 | setField(k: string, v: string | number | boolean): FormData { 13 | this.params.set(k, v) 14 | return this 15 | } 16 | 17 | addField(k: string, v: string | number | boolean): FormData { 18 | this.params.set(k, v) 19 | return this 20 | } 21 | 22 | addFile(k: string, file: File): FormData { 23 | file.setFieldName(k) 24 | this.files.push(file) 25 | return this 26 | } 27 | 28 | appendFile(file: File): FormData { 29 | this.files.push(file) 30 | return this 31 | } 32 | 33 | getParams(): Map { 34 | return this.params 35 | } 36 | 37 | getFiles(): File[] { 38 | return this.files 39 | } 40 | } 41 | 42 | export class File { 43 | private fieldName: string = "file" 44 | private name: string = "unknown" 45 | private type: string 46 | private content: any 47 | private stream: boolean 48 | 49 | getFieldName(): string { 50 | return this.fieldName 51 | } 52 | 53 | setFieldName(fieldName: string): File { 54 | this.fieldName = fieldName 55 | return this 56 | } 57 | 58 | setName(name: string): File { 59 | this.name = name 60 | return this 61 | } 62 | 63 | getName(): string { 64 | return this.name 65 | } 66 | 67 | setType(type: string): File { 68 | this.type = type 69 | return this 70 | } 71 | 72 | getType(): string { 73 | return this.type 74 | } 75 | 76 | setContent(content: any): File { 77 | if (content instanceof stream.Readable) { 78 | this.stream = true 79 | } 80 | this.content = content 81 | return this 82 | } 83 | 84 | getContent(): any { 85 | return this.content 86 | } 87 | 88 | isStream(): boolean { 89 | return this.stream 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /packages/api/src/core/request/request.ts: -------------------------------------------------------------------------------- 1 | import * as querystring from "querystring" 2 | import * as util from "util" 3 | import * as fetch from 'node-fetch' 4 | import {OAPIRootPath} from "../constants/constants"; 5 | import {Response} from "../response/response" 6 | import {Context, Domain} from "@larksuiteoapi/core"; 7 | import * as stream from "stream"; 8 | 9 | const ctxKeyRequestInfo = "x-request-info" 10 | 11 | export type OptFn = (opt: Opt) => void 12 | 13 | export enum AccessTokenType { 14 | None = "none_access_token", 15 | App = "app_access_token", 16 | Tenant = "tenant_access_token", 17 | User = "user_access_token", 18 | } 19 | 20 | export class Opt { 21 | isNotDataField: boolean 22 | pathParams: { [key: string]: any } 23 | queryParams: { [key: string]: any } 24 | userAccessToken: string 25 | tenantKey: string 26 | timeoutOfMs: number 27 | isResponseStream: boolean 28 | responseStream: any 29 | needHelpDeskAuth: boolean 30 | } 31 | 32 | export class Info { 33 | domain: string // request http domain 34 | httpPath: string // request http path 35 | httpMethod: string // request http method 36 | queryParams: string // request query 37 | input: any // request body 38 | accessibleTokenTypeSet: Set // request accessible token type 39 | accessTokenType: AccessTokenType // request access token type 40 | tenantKey: string 41 | userAccessToken: string // user access token 42 | isNotDataField: boolean = false // response body is not data field 43 | isResponseStream: boolean = false 44 | isResponseStreamReal: boolean = false 45 | output: T // response body data 46 | retryable: boolean = false 47 | needHelpDeskAuth: boolean = false // need helpdesk token 48 | timeout: number // http request time out 49 | optFns: OptFn[] = [] 50 | 51 | withContext(ctx: Context): void { 52 | ctx.set(ctxKeyRequestInfo, this) 53 | } 54 | } 55 | 56 | export const getInfoByCtx = (ctx: Context): Info => { 57 | return ctx.get(ctxKeyRequestInfo) 58 | } 59 | 60 | export const setTimeoutOfMs = function (timeoutOfMs: number): (opt: Opt) => void { 61 | return function (opt: Opt) { 62 | opt.timeoutOfMs = timeoutOfMs 63 | } 64 | } 65 | 66 | export const setUserAccessToken = function (userAccessToken: string): (opt: Opt) => void { 67 | return function (opt: Opt) { 68 | opt.userAccessToken = userAccessToken 69 | } 70 | } 71 | 72 | export const setTenantKey = function (tenantKey: string): (opt: Opt) => void { 73 | return function (opt: Opt) { 74 | opt.tenantKey = tenantKey 75 | } 76 | } 77 | 78 | export const setPathParams = function (pathParams: { [key: string]: any }): (opt: Opt) => void { 79 | return function (opt: Opt) { 80 | opt.pathParams = pathParams 81 | } 82 | } 83 | 84 | export const setQueryParams = function (queryParams: { [key: string]: any }): (opt: Opt) => void { 85 | return function (opt: Opt) { 86 | opt.queryParams = queryParams 87 | } 88 | } 89 | 90 | export const setIsNotDataField = function (): (opt: Opt) => void { 91 | return function (opt: Opt) { 92 | opt.isNotDataField = true 93 | } 94 | } 95 | 96 | export const setIsResponseStream = function (): (opt: Opt) => void { 97 | return function (opt: Opt) { 98 | opt.isResponseStream = true 99 | } 100 | } 101 | 102 | export const setNeedHelpDeskAuth = function (): (opt: Opt) => void { 103 | return function (opt: Opt) { 104 | opt.needHelpDeskAuth = true 105 | } 106 | } 107 | 108 | export const setResponseStream = function (responseStream: stream.Writable): (opt: Opt) => void { 109 | return function (opt: Opt) { 110 | opt.isResponseStream = true 111 | opt.responseStream = responseStream 112 | } 113 | } 114 | 115 | export interface HTTPRequestOpts { 116 | method: string 117 | timeout: number 118 | body?: any 119 | headers: {} 120 | bodySource?: { 121 | isStream: boolean; 122 | filePath: string; 123 | } 124 | } 125 | 126 | export class Request extends Info { 127 | httpRequestOpts: HTTPRequestOpts 128 | httpResponse: fetch.Response 129 | response: Response 130 | 131 | url(): string { 132 | let path = this.httpPath 133 | if (this.httpPath.indexOf("http") != 0) { 134 | if (this.httpPath.indexOf("/open-apis") == 0) { 135 | path = util.format("%s%s", this.domain, this.httpPath) 136 | } else { 137 | path = util.format("%s/%s/%s", this.domain, OAPIRootPath, this.httpPath) 138 | } 139 | } 140 | if (this.queryParams) { 141 | path = util.format("%s?%s", path, this.queryParams) 142 | } 143 | return path 144 | } 145 | 146 | fullUrl(domain: Domain): string { 147 | return util.format("%s%s", domain, this.url()) 148 | } 149 | 150 | setPathParams(pathParams: { [key: string]: any }) { 151 | return this.optFns.push(setPathParams(pathParams)) 152 | } 153 | 154 | setQueryParams(queryParams: { [key: string]: any }) { 155 | return this.optFns.push(setQueryParams(queryParams)) 156 | } 157 | 158 | setTimeoutOfMs(timeoutOfMs: number) { 159 | return this.optFns.push(setTimeoutOfMs(timeoutOfMs)) 160 | } 161 | 162 | setTenantKey(tenantKey: string) { 163 | return this.optFns.push(setTenantKey(tenantKey)) 164 | } 165 | 166 | setUserAccessToken(userAccessToken: string) { 167 | return this.optFns.push(setUserAccessToken(userAccessToken)) 168 | } 169 | 170 | setIsNotDataField() { 171 | return this.optFns.push(setIsNotDataField()) 172 | } 173 | 174 | setIsResponseStream() { 175 | return this.optFns.push(setIsResponseStream()) 176 | } 177 | 178 | setResponseStream(responseStream: stream.Writable) { 179 | return this.optFns.push(setResponseStream(responseStream)) 180 | } 181 | 182 | setNeedHelpDeskAuth() { 183 | return this.optFns.push(setNeedHelpDeskAuth()) 184 | } 185 | 186 | toString(): string { 187 | return util.format("%s %s %s", this.httpMethod, this.url(), this.accessTokenType) 188 | } 189 | 190 | init(domain: string) { 191 | this.domain = domain 192 | let opt = new Opt() 193 | for (let v of this.optFns) { 194 | v(opt) 195 | } 196 | this.isNotDataField = opt.isNotDataField 197 | this.isResponseStream = opt.isResponseStream 198 | if (opt.responseStream) { 199 | this.isResponseStream = true 200 | this.output = opt.responseStream 201 | } 202 | if (opt.tenantKey) { 203 | if (this.accessibleTokenTypeSet.has(AccessTokenType.Tenant)) { 204 | this.accessTokenType = AccessTokenType.Tenant 205 | this.tenantKey = opt.tenantKey 206 | } 207 | } 208 | this.tenantKey = opt.tenantKey || "" 209 | if (opt.userAccessToken) { 210 | if (this.accessibleTokenTypeSet.has(AccessTokenType.User)) { 211 | this.accessTokenType = AccessTokenType.User 212 | this.userAccessToken = opt.userAccessToken || "" 213 | } 214 | } 215 | this.needHelpDeskAuth = opt.needHelpDeskAuth 216 | this.timeout = opt.timeoutOfMs || 30000 217 | if (opt.queryParams) { 218 | this.queryParams = querystring.stringify(opt.queryParams) 219 | } 220 | if (opt.pathParams) { 221 | this.httpPath = resolvePath(this.httpPath, opt.pathParams) 222 | } 223 | } 224 | } 225 | 226 | export const newRequestByAuth = (httpPath: string, httpMethod: string, input: any, output: T): Request => { 227 | let r = new Request() 228 | r.httpPath = httpPath 229 | r.httpMethod = httpMethod 230 | r.input = input 231 | r.output = output 232 | r.accessibleTokenTypeSet = new Set() 233 | r.accessTokenType = AccessTokenType.None 234 | r.optFns = [setIsNotDataField()] 235 | return r 236 | } 237 | 238 | const resolvePath = (path: string, pathVar: { [key: string]: any }): string => { 239 | let tmpPath = path 240 | let newPath = "" 241 | while (true) { 242 | let i = tmpPath.indexOf(":") 243 | if (i === -1) { 244 | newPath += tmpPath 245 | break 246 | } 247 | newPath += tmpPath.substring(0, i) 248 | let subPath = tmpPath.substring(i) 249 | let j = subPath.indexOf("/") 250 | if (j === -1) { 251 | j = subPath.length 252 | } 253 | let varName = subPath.substring(1, j) 254 | let v = pathVar[varName] 255 | if (v === undefined) { 256 | throw new Error(util.format("path:%s, param name:%s not find value", path, varName)) 257 | } 258 | newPath += v 259 | if (j === subPath.length) { 260 | break 261 | } 262 | tmpPath = subPath.substring(j) 263 | } 264 | return newPath 265 | } 266 | 267 | export const newRequestOfTs = (httpPath: string, httpMethod: string, accessTokenTypes: AccessTokenType[], 268 | input: any, output: T, ...optFns: OptFn[]): Request => { 269 | let accessibleTokenTypeSet = new Set() 270 | let accessTokenType = accessTokenTypes[0] 271 | for (let v of accessTokenTypes) { 272 | accessibleTokenTypeSet.add(v) 273 | if (v == AccessTokenType.Tenant) { 274 | accessTokenType = v 275 | } 276 | } 277 | let r = new Request() 278 | r.httpPath = httpPath 279 | r.httpMethod = httpMethod 280 | r.accessibleTokenTypeSet = accessibleTokenTypeSet 281 | r.input = input 282 | r.output = output 283 | r.accessTokenType = accessTokenType 284 | r.optFns = optFns 285 | return r 286 | } 287 | 288 | export const newRequest = (httpPath: string, httpMethod: string, accessTokenType: AccessTokenType, 289 | input: any, ...optFns: OptFn[]): Request => { 290 | let output: any 291 | return newRequestOfTs(httpPath, httpMethod, [accessTokenType], input, output, ...optFns) 292 | } 293 | 294 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /packages/api/src/core/response/error.ts: -------------------------------------------------------------------------------- 1 | export enum ErrCode { 2 | Native = -1, 3 | Ok = 0, 4 | AppTicketInvalid = 10012, 5 | AccessTokenInvalid = 99991671, 6 | AppAccessTokenInvalid = 99991664, 7 | TenantAccessTokenInvalid = 99991663, 8 | UserAccessTokenInvalid = 99991668, 9 | UserRefreshTokenInvalid = 99991669, 10 | } 11 | 12 | export interface Detail { 13 | key: string 14 | value: string 15 | 16 | [propName: string]: any 17 | } 18 | 19 | export interface PermissionViolation { 20 | type: string 21 | subject: string 22 | description: string 23 | 24 | [propName: string]: any 25 | } 26 | 27 | export interface FieldViolation { 28 | field: string 29 | value: string 30 | description: string 31 | 32 | [propName: string]: any 33 | } 34 | 35 | export interface Help { 36 | url: string 37 | description: string 38 | 39 | [propName: string]: any 40 | } 41 | 42 | export interface Err { 43 | details?: Detail[] 44 | permission_violations?: PermissionViolation[] 45 | field_violations?: FieldViolation[] 46 | helps?: Help[] 47 | 48 | [propName: string]: any 49 | } 50 | 51 | 52 | export interface Error { 53 | code: number 54 | msg: string 55 | error?: Err 56 | 57 | [propName: string]: any 58 | } 59 | 60 | export const instanceOfError = (object: any): object is Error => { 61 | return "code" in object && "msg" in object; 62 | } 63 | 64 | export const newErr = (e: any): Error => { 65 | let err = { 66 | code: ErrCode.Native, 67 | msg: e.toString(), 68 | } 69 | return err 70 | } 71 | 72 | export const newErrorOfInvalidResp = (msg: string): Error => { 73 | let err = { 74 | code: ErrCode.Native, 75 | msg: msg, 76 | } 77 | return err 78 | } -------------------------------------------------------------------------------- /packages/api/src/core/response/response.ts: -------------------------------------------------------------------------------- 1 | import {Err} from "./error"; 2 | import {Context} from "@larksuiteoapi/core"; 3 | import util from "util"; 4 | 5 | export class Response { 6 | private readonly context: Context 7 | code: number 8 | msg: string 9 | error: Err 10 | data: T 11 | 12 | [propName: string]: any 13 | 14 | 15 | constructor(context: Context) { 16 | this.context = context; 17 | this.code = 0 18 | this.msg = "" 19 | } 20 | 21 | getHeader(): { [key: string]: any } { 22 | return this.context.getHeader() 23 | } 24 | 25 | getRequestID(): string { 26 | return this.context.getRequestID() 27 | } 28 | 29 | getHTTPStatusCode(): number { 30 | return this.context.getHTTPStatusCode() 31 | } 32 | 33 | toString(): string { 34 | return util.format("http_status_code:%s, request_id:%s, response:{'code':%d, 'msg':%s, data omit...}", this.getHTTPStatusCode(), this.getRequestID(), this.code, this.msg) 35 | } 36 | } -------------------------------------------------------------------------------- /packages/api/src/core/token/token.ts: -------------------------------------------------------------------------------- 1 | export class GetISVTenantAccessTokenReq { 2 | app_access_token: string 3 | tenant_key: string 4 | 5 | constructor(app_access_token: string, tenant_key: string) { 6 | this.app_access_token = app_access_token 7 | this.tenant_key = tenant_key 8 | } 9 | } 10 | 11 | export interface TenantAccessToken { 12 | expire: number 13 | tenant_access_token: string 14 | 15 | [propName: string]: any 16 | } 17 | 18 | export class GetInternalAccessTokenReq { 19 | app_id: string 20 | app_secret: string 21 | 22 | constructor(app_id: string, app_secret: string) { 23 | this.app_id = app_id 24 | this.app_secret = app_secret 25 | } 26 | } 27 | 28 | export class GetISVAppAccessTokenReq { 29 | app_id: string 30 | app_secret: string 31 | app_ticket: string 32 | 33 | constructor(app_id: string, app_secret: string, app_ticket: string) { 34 | this.app_id = app_id 35 | this.app_secret = app_secret 36 | this.app_ticket = app_ticket 37 | } 38 | } 39 | 40 | export interface AppAccessToken { 41 | expire: number 42 | app_access_token: string 43 | 44 | [propName: string]: any 45 | } 46 | 47 | export class ApplyAppTicketReq { 48 | app_id: string 49 | app_secret: string 50 | 51 | constructor(app_id: string, app_secret: string) { 52 | this.app_id = app_id 53 | this.app_secret = app_secret 54 | } 55 | } -------------------------------------------------------------------------------- /packages/api/src/core/tools/file.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | 3 | export const downloadFile = async (url: string, timeoutOfMs: number) => { 4 | let httpResponse = await fetch(url, { 5 | timeout: timeoutOfMs 6 | }) 7 | return await httpResponse.buffer() 8 | } -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./core/response/error" 2 | export * from "./api" 3 | export * from "./core/request/request" 4 | export * from "./core/response/response" 5 | export * from "./core/request/formData" 6 | export * from "./core/tools/file" 7 | export * from "./core/token/token" 8 | export * from "./core/constants/constants" -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "strict": false, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "*": [ 18 | "./types/*" 19 | ] 20 | }, 21 | "esModuleInterop": true 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ], 26 | "exclude": [ 27 | "src/**/*.test.ts", 28 | "src/**/*.js" 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/card/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@larksuiteoapi/card", 3 | "version": "1.0.14", 4 | "description": "larksuite open api card sdk", 5 | "keywords": [ 6 | "feishu", 7 | "larksuite" 8 | ], 9 | "author": { 10 | "name": "Lark Technologies Pte. Ltd" 11 | }, 12 | "main": "dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "files": [ 15 | "dist/**/*" 16 | ], 17 | "engines": { 18 | "node": ">= 8.9.0", 19 | "npm": ">= 5.5.1" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "license": "MIT", 25 | "repository": "larksuite/oapi-sdk-nodejs", 26 | "homepage": "https://github.com/larksuite/oapi-sdk-nodejs", 27 | "bugs": { 28 | "url": "https://github.com/larksuite/oapi-sdk-nodejs/issues" 29 | }, 30 | "scripts": { 31 | "prepare": "npm run build", 32 | "build": "npm run build:clean && tsc", 33 | "build:clean": "rm -rf ./dist" 34 | }, 35 | "dependencies": { 36 | "@larksuiteoapi/core": "1.0.14" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^14.6.4", 40 | "ts-node": "^9.0.0", 41 | "typescript": "^3.9.7" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/card/src/card.ts: -------------------------------------------------------------------------------- 1 | import {Config, Context} from "@larksuiteoapi/core"; 2 | import {Card} from "./model/card"; 3 | 4 | export type Handler = (ctx: Context, card: Card) => Promise | any | undefined 5 | 6 | const appID2Handler = new Map() 7 | 8 | export const setHandler = (conf: Config, handler: Handler): void => { 9 | appID2Handler.set(conf.getAppSettings().appID, handler) 10 | } 11 | 12 | export const getHandler = (conf: Config): Handler => { 13 | return appID2Handler.get(conf.getAppSettings().appID) 14 | } -------------------------------------------------------------------------------- /packages/card/src/handlers/err.ts: -------------------------------------------------------------------------------- 1 | export class NotFoundHandlerErr implements Error { 2 | name: string = "NotFoundHandlerErr" 3 | message: string = "card, not found handler" 4 | 5 | toString(): string { 6 | return this.message 7 | } 8 | } 9 | 10 | export class SignatureErr implements Error { 11 | name: string = "SignatureErr" 12 | message: string = "signature error" 13 | 14 | toString(): string { 15 | return this.message 16 | } 17 | } -------------------------------------------------------------------------------- /packages/card/src/handlers/handlers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallbackType, 3 | ContentType, 4 | Context, 5 | DefaultContentType, 6 | getConfigByCtx, 7 | HTTPHeaderKey, 8 | Response, 9 | throwTokenInvalidErr 10 | } from "@larksuiteoapi/core"; 11 | import {Card, Header, HeaderKey, HTTPCard} from "../model/card"; 12 | import {NotFoundHandlerErr, SignatureErr} from "./err"; 13 | import * as util from 'util' 14 | import {getHandler} from "../card"; 15 | 16 | const crypto = require('crypto') 17 | const responseFormat = `{"codemsg":"%s"}` 18 | const challengeResponseFormat = `{"challenge":"%s"}` 19 | 20 | 21 | type handler = (ctx: Context, httpCard: HTTPCard) => Promise 22 | 23 | class Handlers { 24 | init: handler 25 | validate: handler 26 | unmarshal: handler 27 | handler: handler 28 | complement: handler 29 | 30 | constructor(init: handler, validate: handler, unmarshal: handler, handler: handler, complement: handler) { 31 | this.init = init 32 | this.validate = validate 33 | this.unmarshal = unmarshal 34 | this.handler = handler 35 | this.complement = complement 36 | } 37 | } 38 | 39 | const initFunc = async (ctx: Context, httpCard: HTTPCard) => { 40 | let request = httpCard.request 41 | ctx.set(HTTPHeaderKey, request.headers) 42 | httpCard.header = { 43 | timestamp: request.headers[HeaderKey.LarkRequestTimestamp.toLowerCase()], 44 | nonce: request.headers[HeaderKey.LarkRequestRequestNonce.toLowerCase()], 45 | signature: request.headers[HeaderKey.LarkSignature.toLowerCase()], 46 | refresh_token: request.headers[HeaderKey.LarkRefreshToken.toLowerCase()], 47 | } 48 | } 49 | 50 | const validateFunc = async (ctx: Context, httpCard: HTTPCard) => { 51 | let conf = getConfigByCtx(ctx) 52 | let body = httpCard.request.body 53 | let json: object 54 | if (typeof body == "string") { 55 | if (httpCard.header.signature) { 56 | verify(httpCard.header, conf.getAppSettings().verificationToken, body) 57 | } 58 | json = JSON.parse(body) 59 | } else { 60 | json = body 61 | } 62 | conf.getLogger().debug("[validate] card:", json) 63 | httpCard.input = json 64 | } 65 | 66 | const unmarshalFunc = async (ctx: Context, httpCard: HTTPCard) => { 67 | let conf = getConfigByCtx(ctx) 68 | httpCard.type = httpCard.input["type"] as CallbackType 69 | httpCard.challenge = httpCard.input["challenge"] 70 | if (httpCard.type == CallbackType.Challenge) { 71 | let appSettings = conf.getAppSettings() 72 | if (appSettings.verificationToken != httpCard.input["token"]) { 73 | throwTokenInvalidErr() 74 | } 75 | } 76 | } 77 | 78 | const verify = (header: Header, verifyToken: string, body: string): void => { 79 | let targetSig = signature(header, verifyToken, body) 80 | if (header.signature != targetSig) { 81 | throw new SignatureErr() 82 | } 83 | } 84 | 85 | const signature = (header: Header, verifyToken: string, body: string): string => { 86 | let r = header.timestamp + header.nonce + verifyToken + body 87 | let hash = crypto.createHash('sha1') 88 | hash.update(r) 89 | return hash.digest('hex') 90 | } 91 | 92 | const handlerFunc = async (ctx: Context, httpCard: HTTPCard) => { 93 | if (httpCard.type == CallbackType.Challenge) { 94 | return 95 | } 96 | let conf = getConfigByCtx(ctx) 97 | let h = getHandler(conf) 98 | if (!h) { 99 | throw new NotFoundHandlerErr(); 100 | } 101 | httpCard.output = await h(ctx, httpCard.input) 102 | } 103 | 104 | const writeHTTPResponse = (httpCard: HTTPCard, statusCode: number, body: string) => { 105 | let response = new Response() 106 | response.statusCode = statusCode 107 | response.headers[ContentType.toLowerCase()] = DefaultContentType 108 | response.body = body 109 | httpCard.response = response 110 | } 111 | 112 | const complementFunc = async (ctx: Context, httpCard: HTTPCard) => { 113 | let conf = getConfigByCtx(ctx) 114 | let err = httpCard.err 115 | if (err) { 116 | if (err instanceof NotFoundHandlerErr) { 117 | conf.getLogger().error(err) 118 | writeHTTPResponse(httpCard, 500, util.format(responseFormat, err)) 119 | return; 120 | } 121 | conf.getLogger().error(err) 122 | writeHTTPResponse(httpCard, 500, util.format(responseFormat, err)) 123 | return; 124 | } 125 | if (httpCard.type == CallbackType.Challenge) { 126 | writeHTTPResponse(httpCard, 200, util.format(challengeResponseFormat, httpCard.challenge)) 127 | return 128 | } 129 | if (httpCard.output) { 130 | let output = "" 131 | if (typeof httpCard.output == "string") { 132 | output = httpCard.output 133 | } else { 134 | output = JSON.stringify(httpCard.output) 135 | } 136 | writeHTTPResponse(httpCard, 200, output) 137 | return 138 | } 139 | writeHTTPResponse(httpCard, 200, util.format(responseFormat, "success")) 140 | } 141 | 142 | const defaultHandlers = new Handlers(initFunc, validateFunc, unmarshalFunc, handlerFunc, complementFunc) 143 | 144 | export const handle = async (ctx: Context, httpCard: HTTPCard) => { 145 | try { 146 | await defaultHandlers.init(ctx, httpCard) 147 | await defaultHandlers.validate(ctx, httpCard) 148 | await defaultHandlers.unmarshal(ctx, httpCard) 149 | await defaultHandlers.handler(ctx, httpCard) 150 | } catch (e) { 151 | httpCard.err = e 152 | } finally { 153 | await defaultHandlers.complement(ctx, httpCard) 154 | } 155 | } 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /packages/card/src/http/http.ts: -------------------------------------------------------------------------------- 1 | import {Config, Context, Request} from "@larksuiteoapi/core"; 2 | import {HTTPCard} from "../model/card"; 3 | import {handle} from "../handlers/handlers"; 4 | 5 | 6 | export const httpHandle = async (conf: Config, request: Request, err: any) => { 7 | let httpCard = new HTTPCard() 8 | if (err) { 9 | httpCard.err = err 10 | } 11 | httpCard.request = request 12 | let ctx = new Context() 13 | conf.withContext(ctx) 14 | await handle(ctx, httpCard) 15 | return httpCard.response 16 | } -------------------------------------------------------------------------------- /packages/card/src/http/native/requestListener.ts: -------------------------------------------------------------------------------- 1 | import {RequestListener} from "http"; 2 | import {httpHandle} from "../http"; 3 | import {Config, requestListener as requestListener1} from "@larksuiteoapi/core"; 4 | 5 | export const requestListener = (conf: Config): RequestListener => { 6 | return requestListener1(conf, httpHandle) 7 | } -------------------------------------------------------------------------------- /packages/card/src/http/server/server.ts: -------------------------------------------------------------------------------- 1 | import {requestListener} from "../native/requestListener"; 2 | import {Config, startServer as startServer1} from "@larksuiteoapi/core"; 3 | 4 | export const startServer = (conf: Config, port: number): void => { 5 | startServer1(requestListener(conf), port) 6 | } -------------------------------------------------------------------------------- /packages/card/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./card" 2 | export * from "./model/card" 3 | export * from "./http/native/requestListener" 4 | export * from "./http/server/server" 5 | export * from "./http/http" -------------------------------------------------------------------------------- /packages/card/src/model/card.ts: -------------------------------------------------------------------------------- 1 | import {CallbackType, Request, Response} from "@larksuiteoapi/core"; 2 | 3 | export enum HeaderKey { 4 | LarkRequestTimestamp = "X-Lark-Request-Timestamp", 5 | LarkRequestRequestNonce = "X-Lark-Request-Nonce", 6 | LarkSignature = "X-Lark-Signature", 7 | LarkRefreshToken = "X-Refresh-Token", 8 | } 9 | 10 | export interface Header { 11 | timestamp: string 12 | nonce: string 13 | signature: string 14 | refresh_token: string 15 | } 16 | 17 | export class HTTPCard { 18 | header: Header 19 | request: Request 20 | response: Response 21 | input: object 22 | output: any 23 | type: CallbackType 24 | challenge: string 25 | err: any 26 | } 27 | 28 | export interface Action { 29 | value?: object 30 | tag?: string 31 | option?: string 32 | timezone?: string 33 | 34 | [propName: string]: any 35 | } 36 | 37 | export interface Base { 38 | open_id?: string 39 | user_id?: string 40 | open_message_id?: string 41 | tenant_key?: string 42 | token?: string 43 | timezone?: string 44 | } 45 | 46 | export interface Card extends Base { 47 | action?: Action 48 | 49 | [propName: string]: any 50 | } 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /packages/card/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "strict": false, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "*": [ 18 | "./types/*" 19 | ] 20 | }, 21 | "esModuleInterop": true 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ], 26 | "exclude": [ 27 | "src/**/*.test.ts", 28 | "src/**/*.js" 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@larksuiteoapi/core", 3 | "version": "1.0.14", 4 | "description": "larksuite open api core sdk", 5 | "keywords": [ 6 | "feishu", 7 | "larksuite" 8 | ], 9 | "author": { 10 | "name": "Lark Technologies Pte. Ltd" 11 | }, 12 | "main": "dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "files": [ 15 | "dist/**/*" 16 | ], 17 | "engines": { 18 | "node": ">= 8.9.0", 19 | "npm": ">= 5.5.1" 20 | }, 21 | "repository": "larksuite/oapi-sdk-nodejs", 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "license": "MIT", 26 | "homepage": "https://github.com/larksuite/oapi-sdk-nodejs", 27 | "bugs": { 28 | "url": "https://github.com/larksuite/oapi-sdk-nodejs/issues" 29 | }, 30 | "scripts": { 31 | "prepare": "npm run build", 32 | "build": "npm run build:clean && tsc", 33 | "build:clean": "rm -rf ./dist" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^14.14.31", 37 | "ts-node": "^9.0.0", 38 | "typescript": "^3.9.7" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/config/config.ts: -------------------------------------------------------------------------------- 1 | import {Domain} from "../constants/constants"; 2 | import {ConsoleLogger, Logger, LoggerLevel, LoggerProxy} from "../log/log"; 3 | import {DefaultStore, Store} from "../store/store"; 4 | import {AppSettings} from "./settings"; 5 | import {Context} from "../context"; 6 | 7 | const ctxKeyConfig = "ctxKeyConfig" 8 | 9 | export interface ConfigOpts { 10 | logger?: Logger 11 | loggerLevel?: LoggerLevel 12 | store?: Store 13 | } 14 | 15 | export class Config { 16 | private readonly domain: Domain 17 | private readonly logger: Logger 18 | private readonly store: Store 19 | private readonly appSettings: AppSettings 20 | private readonly helpDeskAuthorization: string 21 | 22 | constructor(domain: Domain, appSettings: AppSettings, opts: ConfigOpts) { 23 | const loggerLevel = opts.loggerLevel ? opts.loggerLevel : LoggerLevel.ERROR 24 | const logger = opts.logger ? opts.logger : new ConsoleLogger() 25 | const store = opts.store ? opts.store : new DefaultStore() 26 | this.domain = domain 27 | this.appSettings = appSettings 28 | this.logger = new LoggerProxy(loggerLevel, logger) 29 | this.store = store 30 | this.helpDeskAuthorization = appSettings.helpDeskAuthorization() 31 | } 32 | 33 | withContext(ctx: Context): void { 34 | ctx.set(ctxKeyConfig, this) 35 | } 36 | 37 | getDomain(): Domain { 38 | return this.domain 39 | } 40 | 41 | getAppSettings(): AppSettings { 42 | return this.appSettings 43 | } 44 | 45 | getLogger(): Logger { 46 | return this.logger 47 | } 48 | 49 | getStore(): Store { 50 | return this.store 51 | } 52 | 53 | getHelpDeskAuthorization(): string { 54 | return this.helpDeskAuthorization 55 | } 56 | 57 | } 58 | 59 | export function newTestConfig(domain: Domain, appSettings: AppSettings): Config { 60 | return newConfig(domain, appSettings, new ConsoleLogger(), LoggerLevel.DEBUG, new DefaultStore()) 61 | } 62 | 63 | export function newConfigWithOpts(domain: Domain, appSettings: AppSettings, opts: ConfigOpts): Config { 64 | opts = opts ? opts : {} 65 | return new Config(domain, appSettings, opts) 66 | } 67 | 68 | export function newConfig(domain: Domain, appSettings: AppSettings, logger: Logger, loggerLevel: LoggerLevel, 69 | store: Store): Config { 70 | return new Config(domain, appSettings, { 71 | loggerLevel: loggerLevel, 72 | logger: logger, 73 | store: store 74 | }) 75 | } 76 | 77 | export function getConfigByCtx(ctx: Context): Config { 78 | return ctx.get(ctxKeyConfig) 79 | } -------------------------------------------------------------------------------- /packages/core/src/config/getConfig.ts: -------------------------------------------------------------------------------- 1 | import {AppSettings, newInternalAppSettings, newISVAppSettings} from "./settings"; 2 | import {Config, newTestConfig} from "./config"; 3 | import {Domain} from "../constants/constants"; 4 | 5 | const domainFeiShu = (env: string): string => { 6 | return process.env[env + "_FEISHU_DOMAIN"] as string 7 | } 8 | 9 | const getISVAppSettings = (env: string): AppSettings => { 10 | return newISVAppSettings(process.env[env + "_ISV_APP_ID"] as string, process.env[env + "_ISV_APP_SECRET"] as string, 11 | process.env[env + "_ISV_VERIFICATION_TOKEN"] as string, process.env[env + "_ISV_ENCRYPT_KEY"] as string) 12 | } 13 | 14 | const getInternalAppSettings = (env: string): AppSettings => { 15 | return newInternalAppSettings(process.env[env + "_INTERNAL_APP_ID"] as string, process.env[env + "_INTERNAL_APP_SECRET"] as string, 16 | process.env[env + "_INTERNAL_VERIFICATION_TOKEN"] as string, process.env[env + "_INTERNAL_ENCRYPT_KEY"] as string) 17 | } 18 | 19 | const getDomain = (env: string): Domain => { 20 | if (env != "STAGING" && env != "PRE" && env != "ONLINE") { 21 | throw new Error("env must in [staging, pre, online]") 22 | } 23 | if (env == "ONLINE") { 24 | return Domain.FeiShu 25 | } 26 | return domainFeiShu(env) as Domain 27 | } 28 | 29 | export const getTestISVConf = (env: string): Config => { 30 | env = env.toUpperCase(); 31 | return newTestConfig(getDomain(env), getISVAppSettings(env)) 32 | } 33 | 34 | export const getTestInternalConf = (env: string): Config => { 35 | env = env.toUpperCase(); 36 | return newTestConfig(getDomain(env), getInternalAppSettings(env)) 37 | } 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /packages/core/src/config/settings.ts: -------------------------------------------------------------------------------- 1 | import {AppType} from "../constants/constants"; 2 | 3 | export interface AppSettingsOpts { 4 | appID: string 5 | appSecret: string 6 | encryptKey?: string 7 | verificationToken?: string 8 | helpDeskID?: string 9 | helpDeskToken?: string 10 | } 11 | 12 | export class AppSettings { 13 | readonly appType: AppType 14 | readonly appID: string 15 | readonly appSecret: string 16 | readonly encryptKey: string 17 | readonly verificationToken: string 18 | private readonly helpDeskID: string 19 | private readonly helpDeskToken: string 20 | 21 | constructor(appType: AppType, opts: AppSettingsOpts) { 22 | if (!opts.appID || !opts.appSecret) { 23 | throw new Error("appID or appSecret is empty") 24 | } 25 | this.appType = appType 26 | this.appID = opts.appID 27 | this.appSecret = opts.appSecret 28 | this.encryptKey = opts.encryptKey 29 | this.verificationToken = opts.verificationToken 30 | this.helpDeskID = opts.helpDeskID 31 | this.helpDeskToken = opts.helpDeskToken 32 | } 33 | 34 | helpDeskAuthorization(): string { 35 | if (this.helpDeskID && this.helpDeskToken) { 36 | return Buffer.from(this.helpDeskID + ":" + this.helpDeskToken).toString('base64') 37 | } 38 | return "" 39 | } 40 | } 41 | 42 | export function newISVAppSettingsWithOpts(opts: AppSettingsOpts): AppSettings { 43 | return new AppSettings(AppType.ISV, opts) 44 | } 45 | 46 | export function newInternalAppSettingsWithOpts(opts: AppSettingsOpts): AppSettings { 47 | return new AppSettings(AppType.Internal, opts) 48 | } 49 | 50 | 51 | export function newISVAppSettings(appID: string, appSecret: string, verificationToken: string, encryptKey: string): AppSettings { 52 | return new AppSettings(AppType.ISV, { 53 | appID: appID, 54 | appSecret: appSecret, 55 | verificationToken: verificationToken, 56 | encryptKey: encryptKey 57 | }) 58 | } 59 | 60 | export function newInternalAppSettings(appID: string, appSecret: string, verificationToken: string, encryptKey: string): AppSettings { 61 | return new AppSettings(AppType.Internal, { 62 | appID: appID, 63 | appSecret: appSecret, 64 | verificationToken: verificationToken, 65 | encryptKey: encryptKey 66 | }) 67 | } 68 | 69 | export function getISVAppSettingsByEnv(): AppSettings { 70 | return new AppSettings(AppType.ISV, { 71 | appID: process.env["APP_ID"] as string, 72 | appSecret: process.env["APP_SECRET"] as string, 73 | verificationToken: process.env["VERIFICATION_TOKEN"] as string, 74 | encryptKey: process.env["ENCRYPT_KEY"] as string, 75 | helpDeskID: process.env["HELP_DESK_ID"] as string, 76 | helpDeskToken: process.env["HELP_DESK_TOKEN"] as string 77 | }) 78 | } 79 | 80 | export function getInternalAppSettingsByEnv(): AppSettings { 81 | return new AppSettings(AppType.Internal, { 82 | appID: process.env["APP_ID"] as string, 83 | appSecret: process.env["APP_SECRET"] as string, 84 | verificationToken: process.env["VERIFICATION_TOKEN"] as string, 85 | encryptKey: process.env["ENCRYPT_KEY"] as string, 86 | helpDeskID: process.env["HELP_DESK_ID"] as string, 87 | helpDeskToken: process.env["HELP_DESK_TOKEN"] as string 88 | }) 89 | } 90 | 91 | -------------------------------------------------------------------------------- /packages/core/src/constants/constants.ts: -------------------------------------------------------------------------------- 1 | export const ContentType = "Content-Type" 2 | export const ContentTypeJson = "application/json" 3 | export const DefaultContentType = ContentTypeJson + "; charset=utf-8" 4 | 5 | export const HTTPHeaderKey = "HTTP-Header" 6 | export const HTTPHeaderKeyRequestID = "X-Request-Id" 7 | export const HTTPHeaderKeyLogID = "X-Tt-Logid" 8 | export const HTTPKeyStatusCode = "http_status_code" 9 | 10 | 11 | export enum AppType { 12 | ISV = "isv", 13 | Internal = "internal" 14 | } 15 | 16 | export enum Domain { 17 | FeiShu = "https://open.feishu.cn", 18 | LarkSuite = "https://open.larksuite.com", 19 | } 20 | 21 | export enum CallbackType { 22 | Event = "event_callback", 23 | Challenge = "url_verification" 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/context.ts: -------------------------------------------------------------------------------- 1 | import {HTTPHeaderKey, HTTPHeaderKeyLogID, HTTPHeaderKeyRequestID, HTTPKeyStatusCode} from "./constants/constants"; 2 | 3 | export class Context { 4 | 5 | private m: Map 6 | 7 | constructor() { 8 | this.m = new Map() 9 | } 10 | 11 | get(key: string): any { 12 | return this.m.get(key) 13 | } 14 | 15 | set(key: string, value: any): void { 16 | this.m.set(key, value) 17 | } 18 | 19 | getHeader(): { [key: string]: any } { 20 | return this.m.get(HTTPHeaderKey) 21 | } 22 | 23 | getRequestID(): string { 24 | let header = this.getHeader() 25 | let logID = header[HTTPHeaderKeyLogID.toLowerCase()] 26 | if (logID) { 27 | return logID 28 | } 29 | return header[HTTPHeaderKeyRequestID.toLowerCase()] 30 | } 31 | 32 | getHTTPStatusCode(): number { 33 | return this.get(HTTPKeyStatusCode) 34 | } 35 | } -------------------------------------------------------------------------------- /packages/core/src/errors/errors.ts: -------------------------------------------------------------------------------- 1 | export class TokenInvalidErr extends Error { 2 | name: string = "TokenInvalidErr" 3 | message: string = "token invalid" 4 | } 5 | 6 | export const throwTokenInvalidErr = () => { 7 | throw new TokenInvalidErr() 8 | } 9 | 10 | -------------------------------------------------------------------------------- /packages/core/src/http/handle.ts: -------------------------------------------------------------------------------- 1 | import {Config} from "../config/config"; 2 | import {Request, Response} from "./model"; 3 | 4 | export type HTTPHandle = (conf: Config, request: Request, err: any) => Promise -------------------------------------------------------------------------------- /packages/core/src/http/model.ts: -------------------------------------------------------------------------------- 1 | import {ServerResponse} from "http"; 2 | 3 | export class Request { 4 | params: { [key: string]: any } 5 | headers: { [key: string]: any } 6 | body: string | object 7 | 8 | constructor() { 9 | this.headers = {} 10 | this.params = {} 11 | } 12 | } 13 | 14 | export class Response { 15 | headers: { [key: string]: any } 16 | statusCode: number 17 | body: string | object 18 | 19 | constructor() { 20 | this.headers = {} 21 | } 22 | 23 | writeResponse(res: ServerResponse) { 24 | res.statusCode = this.statusCode 25 | Object.entries(this.headers).forEach(([k, v]) => { 26 | res.setHeader(k, v) 27 | }) 28 | if (typeof this.body == "string") { 29 | res.write(this.body) 30 | } else { 31 | res.write(JSON.stringify(this.body)) 32 | } 33 | res.end(); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /packages/core/src/http/requestListener.ts: -------------------------------------------------------------------------------- 1 | import {IncomingMessage, RequestListener, ServerResponse} from "http"; 2 | import {Config} from "../config/config"; 3 | import {Request} from "./model"; 4 | import {HTTPHandle} from "./handle"; 5 | 6 | const url = require('url'); 7 | 8 | export const requestListener = (conf: Config, httpHandle: HTTPHandle): RequestListener => { 9 | return (req: IncomingMessage, res: ServerResponse): void => { 10 | let body = [] 11 | req.on('data', (chunk) => { 12 | body.push(chunk); 13 | }).on('end', () => { 14 | let request = new Request() 15 | request.params = url.parse(req.url, true).query 16 | Object.entries(req.headers).forEach(([k, v]) => { 17 | request.headers[k] = v 18 | }) 19 | request.body = Buffer.concat(body).toString() 20 | httpHandle(conf, request, undefined).then(response => { 21 | response.writeResponse(res) 22 | }) 23 | }).on('error', (e) => { 24 | httpHandle(conf, undefined, e).then(response => { 25 | response.writeResponse(res) 26 | }) 27 | }); 28 | } 29 | } -------------------------------------------------------------------------------- /packages/core/src/http/startServer.ts: -------------------------------------------------------------------------------- 1 | import {createServer, RequestListener} from "http"; 2 | import util from "util"; 3 | 4 | export const startServer = (requestListener: RequestListener, port: number): void => { 5 | let server = createServer(requestListener) 6 | server.listen(port, () => { 7 | console.log(util.format("Listening for server on %s", server.address())); 8 | }) 9 | } -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config/config'; 2 | export * from './config/getConfig'; 3 | export * from './config/settings'; 4 | export * from './constants/constants'; 5 | export * from './context'; 6 | export * from './errors/errors'; 7 | export * from './log/log'; 8 | export * from './store/keys'; 9 | export * from './store/store'; 10 | export * from './tools/decrypt'; 11 | export * from './http/model' 12 | export * from './http/handle' 13 | export * from './http/requestListener' 14 | export * from './http/startServer' 15 | export * from './version'; 16 | -------------------------------------------------------------------------------- /packages/core/src/log/log.ts: -------------------------------------------------------------------------------- 1 | export enum LoggerLevel { 2 | ERROR = 4, 3 | WARN = 3, 4 | INFO = 2, 5 | DEBUG = 1, 6 | } 7 | 8 | export interface Logger { 9 | debug(...msg: any[]): void; 10 | 11 | info(...msg: any[]): void; 12 | 13 | warn(...msg: any[]): void; 14 | 15 | error(...msg: any[]): void; 16 | } 17 | 18 | export class LoggerProxy implements Logger { 19 | level: LoggerLevel 20 | logger: Logger 21 | 22 | constructor(level: LoggerLevel, logger: Logger) { 23 | this.level = level; 24 | this.logger = logger; 25 | } 26 | 27 | debug(...msg: any[]): void { 28 | if (this.level <= LoggerLevel.DEBUG) { 29 | this.logger.debug("[debug]", ...msg) 30 | } 31 | } 32 | 33 | info(...msg: any[]): void { 34 | if (this.level <= LoggerLevel.INFO) { 35 | this.logger.info("[info]", ...msg) 36 | } 37 | } 38 | 39 | warn(...msg: any[]): void { 40 | if (this.level <= LoggerLevel.WARN) { 41 | this.logger.warn("[warn]", ...msg) 42 | } 43 | } 44 | 45 | error(...msg: any[]): void { 46 | if (this.level <= LoggerLevel.ERROR) { 47 | this.logger.error("[error]", ...msg) 48 | } 49 | } 50 | 51 | } 52 | 53 | export class ConsoleLogger implements Logger { 54 | debug(...msg: any[]): void { 55 | console.log(...msg); 56 | } 57 | 58 | info(...msg: any[]): void { 59 | console.log(...msg); 60 | } 61 | 62 | warn(...msg: any[]): void { 63 | console.log(...msg); 64 | } 65 | 66 | error(...msg: any[]): void { 67 | console.log(...msg); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/core/src/store/keys.ts: -------------------------------------------------------------------------------- 1 | const appTicketKeyPrefix = "app_ticket" 2 | const appAccessTokenKeyPrefix = "app_access_token" 3 | const tenantAccessTokenKeyPrefix = "tenant_access_token" 4 | 5 | export function getAppTicketKey(appID: string): string { 6 | return appTicketKeyPrefix + "-" + appID 7 | } 8 | 9 | export function getAppAccessTokenKey(appID: string): string { 10 | return appAccessTokenKeyPrefix + "-" + appID 11 | } 12 | 13 | export function getTenantAccessTokenKey(appID: string, tenantKey: string): string { 14 | return tenantAccessTokenKeyPrefix + "-" + appID + "-" + tenantKey 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/store/store.ts: -------------------------------------------------------------------------------- 1 | export interface Store { 2 | get(key: string): Promise | string 3 | 4 | put(key: string, value: string, expire: number): Promise | void 5 | 6 | } 7 | 8 | class V { 9 | value: string 10 | expireTime: Date 11 | 12 | constructor(value: string, expire: number) { 13 | this.value = value 14 | this.expireTime = new Date(new Date().getTime() + expire * 1000) 15 | } 16 | } 17 | 18 | 19 | export class DefaultStore implements Store { 20 | private data: Map 21 | 22 | constructor() { 23 | this.data = new Map() 24 | } 25 | 26 | get = (key: string) => { 27 | let v = this.data.get(key) 28 | if (!v) { 29 | return "" 30 | } 31 | let now = new Date().getTime() 32 | if (v.expireTime.getTime() < now) { 33 | return "" 34 | } 35 | return v.value 36 | } 37 | 38 | put = (key: string, value: string, expire: number) => { 39 | let v = new V(value, expire) 40 | this.data.set(key, v) 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /packages/core/src/tools/decrypt.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | 3 | export class AESCipher { 4 | private readonly key: Buffer 5 | 6 | public constructor(key: string) { 7 | const hash = crypto.createHash('sha256') 8 | hash.update(key) 9 | this.key = hash.digest() 10 | } 11 | 12 | public decrypt(encrypt: string): string { 13 | const encryptBuffer = Buffer.from(encrypt, 'base64') 14 | const decipher = crypto.createDecipheriv( 15 | 'aes-256-cbc', 16 | this.key, 17 | encryptBuffer.slice(0, 16), 18 | ) 19 | let decrypted = decipher.update(encryptBuffer.slice(16).toString('hex'), 'hex', 'utf8') 20 | decrypted += decipher.final('utf8') 21 | return decrypted 22 | } 23 | } -------------------------------------------------------------------------------- /packages/core/src/version.ts: -------------------------------------------------------------------------------- 1 | export const SdkVersion = "1.0.14" -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "strict": false, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "*": [ 18 | "./types/*" 19 | ] 20 | }, 21 | "esModuleInterop": true 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ], 26 | "exclude": [ 27 | "src/**/*.test.ts", 28 | "src/**/*.js" 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/event/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@larksuiteoapi/event", 3 | "version": "1.0.14", 4 | "description": "larksuite open api event sdk", 5 | "keywords": [ 6 | "feishu", 7 | "larksuite" 8 | ], 9 | "author": { 10 | "name": "Lark Technologies Pte. Ltd" 11 | }, 12 | "main": "dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "files": [ 15 | "dist/**/*" 16 | ], 17 | "engines": { 18 | "node": ">= 8.9.0", 19 | "npm": ">= 5.5.1" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "license": "MIT", 25 | "repository": "larksuite/oapi-sdk-nodejs", 26 | "homepage": "https://github.com/larksuite/oapi-sdk-nodejs", 27 | "bugs": { 28 | "url": "https://github.com/larksuite/oapi-sdk-nodejs/issues" 29 | }, 30 | "scripts": { 31 | "prepare": "npm run build", 32 | "build": "npm run build:clean && tsc", 33 | "build:clean": "rm -rf ./dist" 34 | }, 35 | "dependencies": { 36 | "@larksuiteoapi/core": "1.0.14" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^14.6.4", 40 | "ts-node": "^9.0.0", 41 | "typescript": "^3.9.7" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/event/src/app/v1/appTicket.ts: -------------------------------------------------------------------------------- 1 | import {setTypeHandler} from "../../event"; 2 | import {AppType, Config, Context, getAppTicketKey, getConfigByCtx} from "@larksuiteoapi/core"; 3 | import {BaseEvent, V1} from "../../core/model/event"; 4 | 5 | const EventType = "app_ticket" 6 | 7 | interface EventData extends BaseEvent { 8 | app_ticket: string 9 | } 10 | 11 | interface Event extends V1 { 12 | } 13 | 14 | export const setHandler = (conf: Config) => { 15 | setTypeHandler(conf, EventType, (ctx: Context, event: Event) => { 16 | let conf = getConfigByCtx(ctx) 17 | if (conf.getAppSettings().appType == AppType.Internal) { 18 | return 19 | } 20 | return conf.getStore().put(getAppTicketKey(event.event.app_id), event.event.app_ticket, 12 * 3600) 21 | }) 22 | } 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /packages/event/src/core/handlers/err.ts: -------------------------------------------------------------------------------- 1 | import * as util from "util" 2 | 3 | export class NotFoundHandlerErr { 4 | eventType: string 5 | 6 | constructor(eventType: string) { 7 | this.eventType = eventType 8 | } 9 | 10 | toString(): string { 11 | return util.format("event type:%s, not found handler", this.eventType) 12 | } 13 | } -------------------------------------------------------------------------------- /packages/event/src/core/handlers/handlers.ts: -------------------------------------------------------------------------------- 1 | import * as util from "util"; 2 | import {getEventType2Handler, Handler} from "../../event"; 3 | import {NotFoundHandlerErr} from "./err"; 4 | import { 5 | AESCipher, 6 | CallbackType, ContentType, 7 | Context, DefaultContentType, 8 | getConfigByCtx, 9 | HTTPHeaderKeyRequestID, HTTPHeaderKeyLogID, 10 | Response, throwTokenInvalidErr, HTTPHeaderKey 11 | } from "@larksuiteoapi/core"; 12 | import {HTTPEvent, V1, V2, Version1} from "../model/event" 13 | 14 | const responseFormat = `{"codemsg":"%s"}` 15 | const challengeResponseFormat = `{"challenge":"%s"}` 16 | 17 | 18 | type handler = (ctx: Context, httpEvent: HTTPEvent) => Promise 19 | 20 | const validateFunc = async (_: Context, httpEvent: HTTPEvent) => { 21 | if (httpEvent.err) { 22 | throw httpEvent.err 23 | } 24 | } 25 | 26 | const unmarshalFunc = async (ctx: Context, httpEvent: HTTPEvent) => { 27 | let conf = getConfigByCtx(ctx) 28 | let body = httpEvent.request.body 29 | let json: object 30 | if (typeof body == "string") { 31 | json = JSON.parse(body) 32 | } else { 33 | json = body 34 | } 35 | if (conf.getAppSettings().encryptKey) { 36 | let content = json["encrypt"] 37 | let cipher = new AESCipher(conf.getAppSettings().encryptKey) 38 | content = cipher.decrypt(content) 39 | json = JSON.parse(content) 40 | } 41 | conf.getLogger().debug(util.format("[unmarshal] event: %s", JSON.stringify(json))) 42 | httpEvent.input = json 43 | let schema = Version1 44 | let token = json["token"] 45 | if (json["schema"]) { 46 | schema = json["schema"] 47 | } 48 | let eventType: string 49 | if (json["event"]) { 50 | eventType = json["event"]["type"] 51 | } 52 | if (json["header"]) { 53 | token = json["header"]["token"] 54 | eventType = json["header"]["event_type"] 55 | } 56 | httpEvent.schema = schema 57 | httpEvent.eventType = eventType 58 | httpEvent.type = json["type"] 59 | httpEvent.challenge = json["challenge"] 60 | if (token != conf.getAppSettings().verificationToken) { 61 | throwTokenInvalidErr() 62 | } 63 | } 64 | 65 | const handlerFunc = async (ctx: Context, httpEvent: HTTPEvent) => { 66 | if (httpEvent.type == CallbackType.Challenge) { 67 | return 68 | } 69 | let conf = getConfigByCtx(ctx) 70 | let handler: Handler 71 | let eventType2Handler = getEventType2Handler(conf) 72 | if (eventType2Handler) { 73 | handler = eventType2Handler.m.get(httpEvent.eventType) 74 | } 75 | if (!handler) { 76 | throw new NotFoundHandlerErr(httpEvent.eventType) 77 | } 78 | let input: V1 | V2 79 | if (httpEvent.schema == Version1) { 80 | input = httpEvent.input as V1 81 | } else { 82 | input = httpEvent.input as V2 83 | } 84 | await handler(ctx, input) 85 | } 86 | 87 | const writeHTTPResponse = (httpEvent: HTTPEvent, statusCode: number, body: string) => { 88 | let response = new Response() 89 | response.statusCode = statusCode 90 | response.headers[ContentType.toLowerCase()] = DefaultContentType 91 | response.body = body 92 | httpEvent.response = response 93 | } 94 | 95 | const complementFunc = async (ctx: Context, httpEvent: HTTPEvent) => { 96 | let conf = getConfigByCtx(ctx) 97 | if (httpEvent.err) { 98 | if (httpEvent.err instanceof NotFoundHandlerErr) { 99 | writeHTTPResponse(httpEvent, 200, util.format(responseFormat, httpEvent.err)) 100 | return 101 | } 102 | writeHTTPResponse(httpEvent, 500, util.format(responseFormat, httpEvent.err)) 103 | conf.getLogger().error(httpEvent.err) 104 | return 105 | } 106 | if (httpEvent.type == CallbackType.Challenge) { 107 | writeHTTPResponse(httpEvent, 200, util.format(challengeResponseFormat, httpEvent.challenge)) 108 | return 109 | } 110 | writeHTTPResponse(httpEvent, 200, util.format(responseFormat, "success")) 111 | } 112 | 113 | class Handlers { 114 | validate: handler 115 | unmarshal: handler 116 | handler: handler 117 | complement: handler 118 | 119 | constructor(validate: handler, unmarshal: handler, handler: handler, complement: handler) { 120 | this.validate = validate 121 | this.unmarshal = unmarshal 122 | this.handler = handler 123 | this.complement = complement 124 | } 125 | } 126 | 127 | const defaultHandlers = new Handlers(validateFunc, unmarshalFunc, handlerFunc, complementFunc) 128 | 129 | export const handle = async (ctx: Context, httpEvent: HTTPEvent) => { 130 | try { 131 | ctx.set(HTTPHeaderKey, httpEvent.request.headers) 132 | await defaultHandlers.validate(ctx, httpEvent) 133 | await defaultHandlers.unmarshal(ctx, httpEvent) 134 | await defaultHandlers.handler(ctx, httpEvent) 135 | } catch (e) { 136 | httpEvent.err = e 137 | } finally { 138 | await defaultHandlers.complement(ctx, httpEvent) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /packages/event/src/core/model/event.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from "@larksuiteoapi/core"; 2 | 3 | export const Version1 = "1.0" 4 | export const Version2 = "2.0" 5 | 6 | 7 | export class HTTPEvent { 8 | request: Request 9 | response: Response 10 | input: object 11 | schema: string 12 | type: string 13 | eventType: string 14 | challenge: string 15 | err: any 16 | } 17 | 18 | export interface V1Header { 19 | ts: string 20 | uuid: string 21 | token: string 22 | type: string 23 | } 24 | 25 | export interface BaseEvent { 26 | app_id: string 27 | type: string 28 | tenant_key: string 29 | } 30 | 31 | export interface V1 extends V1Header { 32 | event: T 33 | 34 | [propName: string]: any 35 | } 36 | 37 | export interface V2Header { 38 | event_id: string 39 | event_type: string 40 | app_id: string 41 | tenant_key: string 42 | create_time: string 43 | token: string 44 | 45 | [propName: string]: any 46 | } 47 | 48 | export interface V2 { 49 | header: V2Header 50 | event: T 51 | 52 | [propName: string]: any 53 | } -------------------------------------------------------------------------------- /packages/event/src/event.ts: -------------------------------------------------------------------------------- 1 | import {Config, Context} from "@larksuiteoapi/core"; 2 | import {V1, V2} from "./core/model/event"; 3 | 4 | export type Handler = (ctx: Context, event: V1 | V2) => Promise | void 5 | 6 | 7 | export class EventType2Handler { 8 | m: Map 9 | 10 | constructor() { 11 | this.m = new Map() 12 | } 13 | } 14 | 15 | const appID2EventType2Handler = new Map() 16 | 17 | 18 | export const setTypeHandler = (conf: Config, eventType: string, handler: Handler) => { 19 | let appID = conf.getAppSettings().appID 20 | let eventType2Handler = appID2EventType2Handler.get(appID) 21 | if (!eventType2Handler) { 22 | eventType2Handler = new EventType2Handler() 23 | appID2EventType2Handler.set(appID, eventType2Handler) 24 | } 25 | eventType2Handler.m.set(eventType, handler) 26 | } 27 | 28 | export const getEventType2Handler = (conf: Config): EventType2Handler => { 29 | return appID2EventType2Handler.get(conf.getAppSettings().appID) 30 | } -------------------------------------------------------------------------------- /packages/event/src/http/http.ts: -------------------------------------------------------------------------------- 1 | import {HTTPEvent} from "../core/model/event"; 2 | import {handle} from "../core/handlers/handlers"; 3 | import * as AppTicketEvent from "../app/v1/appTicket"; 4 | import {Config, Context, Request} from "@larksuiteoapi/core"; 5 | 6 | 7 | export const httpHandle = async (conf: Config, request: Request, err: any) => { 8 | AppTicketEvent.setHandler(conf) 9 | let httpEvent = new HTTPEvent() 10 | if (err) { 11 | httpEvent.err = err 12 | } 13 | httpEvent.request = request 14 | let ctx = new Context() 15 | conf.withContext(ctx) 16 | await handle(ctx, httpEvent) 17 | return httpEvent.response 18 | } -------------------------------------------------------------------------------- /packages/event/src/http/native/native.ts: -------------------------------------------------------------------------------- 1 | import {RequestListener} from "http"; 2 | import {httpHandle} from "../http"; 3 | import {Config, requestListener as requestListener1} from "@larksuiteoapi/core"; 4 | 5 | export const requestListener = (conf: Config): RequestListener => { 6 | return requestListener1(conf, httpHandle) 7 | } -------------------------------------------------------------------------------- /packages/event/src/http/server/server.ts: -------------------------------------------------------------------------------- 1 | import {requestListener} from "../native/native"; 2 | import {Config, startServer as startServer1} from "@larksuiteoapi/core"; 3 | 4 | export const startServer = (conf: Config, port: number): void => { 5 | startServer1(requestListener(conf), port) 6 | } -------------------------------------------------------------------------------- /packages/event/src/index.ts: -------------------------------------------------------------------------------- 1 | export {httpHandle} from "./http/http"; 2 | export {V1, V2, BaseEvent} from "./core/model/event"; 3 | export {setTypeHandler, Handler} from "./event"; 4 | export * from "./http/server/server" 5 | export * from "./http/native/native" 6 | export * as AppTicketEvent from "./app/v1/appTicket" -------------------------------------------------------------------------------- /packages/event/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "strict": false, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "*": [ 18 | "./types/*" 19 | ] 20 | }, 21 | "esModuleInterop": true 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ], 26 | "exclude": [ 27 | "src/**/*.test.ts", 28 | "src/**/*.js" 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@larksuiteoapi/simple", 3 | "version": "1.0.14", 4 | "description": "larksuite open api simple sdk", 5 | "keywords": [ 6 | "feishu", 7 | "larksuite" 8 | ], 9 | "author": { 10 | "name": "Lark Technologies Pte. Ltd" 11 | }, 12 | "main": "dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "files": [ 15 | "dist/**/*" 16 | ], 17 | "engines": { 18 | "node": ">= 8.9.0", 19 | "npm": ">= 5.5.1" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "license": "MIT", 25 | "repository": "larksuite/oapi-sdk-nodejs", 26 | "homepage": "https://github.com/larksuite/oapi-sdk-nodejs", 27 | "bugs": { 28 | "url": "https://github.com/larksuite/oapi-sdk-nodejs/issues" 29 | }, 30 | "scripts": { 31 | "prepare": "npm run build", 32 | "build": "npm run build:clean && tsc", 33 | "build:clean": "rm -rf ./dist" 34 | }, 35 | "dependencies": { 36 | "@larksuiteoapi/allcore": "1.0.14", 37 | "async-redis": "^1.1.7", 38 | "body-parser": "^1.19.0", 39 | "express": "^4.17.1" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "^14.6.4", 43 | "ts-node": "^9.0.0", 44 | "typescript": "^3.9.7" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/sample/src/api/api.js: -------------------------------------------------------------------------------- 1 | const lark = require("@larksuiteoapi/allcore"); 2 | 3 | const appSettings = lark.getInternalAppSettingsByEnv() 4 | 5 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, {}) 6 | 7 | // send text message 8 | let req = lark.api.newRequest("/open-apis/message/v4/send", "POST", lark.api.AccessTokenType.Tenant, { 9 | user_id: "77bbc392", 10 | msg_type: "text", 11 | content: { 12 | text: "test" 13 | } 14 | }) 15 | 16 | req.setTimeoutOfMs(6000) 17 | 18 | lark.api.sendRequest(conf, req).then(resp => { 19 | console.log(resp.getHeader()) 20 | console.log(resp.getRequestID()) 21 | console.log(resp.getHTTPStatusCode()) 22 | console.log(resp) // r = response.body 23 | }).catch(e => { 24 | console.log(e) 25 | }) 26 | 27 | 28 | // send card message 29 | lark.api.sendRequest(conf, lark.api.newRequest("/open-apis/message/v4/send", "POST", lark.api.AccessTokenType.Tenant, { 30 | user_id: "77bbc392", 31 | msg_type: "interactive", 32 | card: { 33 | "config": { 34 | "wide_screen_mode": true 35 | }, 36 | "i18n_elements": { 37 | "zh_cn": [ 38 | { 39 | "tag": "div", 40 | "text": { 41 | "tag": "lark_md", 42 | "content": "[飞书](https://www.feishu.cn)整合即时沟通、日历、音视频会议、云文档、云盘、工作台等功能于一体,成就组织和个人,更高效、更愉悦。" 43 | } 44 | }, 45 | { 46 | "tag": "div", 47 | "text": { 48 | "tag": "lark_md", 49 | "content": "深度整合使用率极高的办公工具,企业成员在一处即可实现高效沟通与协作。" 50 | }, 51 | "extra": { 52 | "tag": "img", 53 | "img_key": "img_e344c476-1e58-4492-b40d-7dcffe9d6dfg", 54 | "alt": { 55 | "tag": "plain_text", 56 | "content": "hover" 57 | } 58 | } 59 | }, 60 | { 61 | "tag": "action", 62 | "actions": [ 63 | { 64 | "tag": "button", 65 | "text": { 66 | "tag": "plain_text", 67 | "content": "主按钮" 68 | }, 69 | "type": "primary", 70 | "value": { 71 | "key": "primarykey" 72 | } 73 | }, 74 | { 75 | "tag": "button", 76 | "text": { 77 | "tag": "plain_text", 78 | "content": "次按钮" 79 | }, 80 | "type": "default", 81 | "value": { 82 | "key": "defaultkey" 83 | } 84 | }, 85 | { 86 | "tag": "button", 87 | "text": { 88 | "tag": "plain_text", 89 | "content": "危险按钮" 90 | }, 91 | "type": "danger" 92 | } 93 | ] 94 | } 95 | ], 96 | "en_us": [ 97 | { 98 | "tag": "div", 99 | "text": { 100 | "tag": "lark_md", 101 | "content": "Empowering teams by messenger, video conference, calendar, docs, and emails. It's all in one place." 102 | } 103 | }, 104 | { 105 | "tag": "action", 106 | "actions": [ 107 | { 108 | "tag": "button", 109 | "text": { 110 | "tag": "plain_text", 111 | "content": "Primary Button" 112 | }, 113 | "type": "primary" 114 | }, 115 | { 116 | "tag": "button", 117 | "text": { 118 | "tag": "plain_text", 119 | "content": "Secondary Button" 120 | }, 121 | "type": "default" 122 | }, 123 | { 124 | "tag": "button", 125 | "text": { 126 | "tag": "plain_text", 127 | "content": "Danger Button" 128 | }, 129 | "type": "danger" 130 | } 131 | ] 132 | }, 133 | { 134 | "tag": "div", 135 | "text": { 136 | "tag": "lark_md", 137 | "content": "Feishu interconnects many essential collaboration tools in a single platform. Always in sync, and easy to navigate." 138 | }, 139 | "extra": { 140 | "tag": "img", 141 | "img_key": "img_e344c476-1e58-4492-b40d-7dcffe9d6dfg", 142 | "alt": { 143 | "tag": "plain_text", 144 | "content": "hover" 145 | } 146 | } 147 | }, 148 | { 149 | "tag": "div", 150 | "text": { 151 | "tag": "lark_md", 152 | "content": "Feishu automatically syncs data between your devices, so everything you need is always within reach." 153 | }, 154 | "extra": { 155 | "tag": "select_static", 156 | "placeholder": { 157 | "tag": "plain_text", 158 | "content": "Enter placeholder text" 159 | }, 160 | "value": { 161 | "key": "value" 162 | }, 163 | "options": [ 164 | { 165 | "text": { 166 | "tag": "plain_text", 167 | "content": "Option1" 168 | }, 169 | "value": "1" 170 | }, 171 | { 172 | "text": { 173 | "tag": "plain_text", 174 | "content": "Option 2" 175 | }, 176 | "value": "2" 177 | }, 178 | { 179 | "text": { 180 | "tag": "plain_text", 181 | "content": "Option 3" 182 | }, 183 | "value": "3" 184 | }, 185 | { 186 | "text": { 187 | "tag": "plain_text", 188 | "content": "Option 4" 189 | }, 190 | "value": "4" 191 | } 192 | ] 193 | } 194 | }, 195 | { 196 | "tag": "div", 197 | "text": { 198 | "tag": "lark_md", 199 | "content": "With open API, Feishu allows integrating your own apps, existing systems, third-party systems, and quick tools." 200 | }, 201 | "extra": { 202 | "tag": "overflow", 203 | "options": [ 204 | { 205 | "text": { 206 | "tag": "plain_text", 207 | "content": "Open Feishu App Directory" 208 | }, 209 | "value": "appStore", 210 | "url": "https://app.feishu.cn" 211 | }, 212 | { 213 | "text": { 214 | "tag": "plain_text", 215 | "content": "View Feishu Developer Docs" 216 | }, 217 | "value": "document", 218 | "url": "https://open.feishu.cn" 219 | }, 220 | { 221 | "text": { 222 | "tag": "plain_text", 223 | "content": "Open Feishu website" 224 | }, 225 | "value": "document", 226 | "url": "https://www.feishu.cn" 227 | } 228 | ] 229 | } 230 | }, 231 | { 232 | "tag": "div", 233 | "text": { 234 | "tag": "lark_md", 235 | "content": "With ISO 27001 & 27018 certification, the security of your data is always our top priority." 236 | }, 237 | "extra": { 238 | "tag": "date_picker", 239 | "placeholder": { 240 | "tag": "plain_text", 241 | "content": "Please select date" 242 | }, 243 | "initial_date": "2020-9-20" 244 | } 245 | }, 246 | { 247 | "tag": "note", 248 | "elements": [ 249 | { 250 | "tag": "img", 251 | "img_key": "img_e344c476-1e58-4492-b40d-7dcffe9d6dfg", 252 | "alt": { 253 | "tag": "plain_text", 254 | "content": "hover" 255 | } 256 | }, 257 | { 258 | "tag": "plain_text", 259 | "content": "Notes" 260 | } 261 | ] 262 | } 263 | ] 264 | } 265 | } 266 | })).then(resp => { 267 | console.log("--------------------------") 268 | console.log(resp.getRequestID()) 269 | console.log(resp.getHTTPStatusCode()) 270 | console.log(resp) // r = response.body 271 | }).catch(e => { 272 | console.log(e) 273 | }) 274 | 275 | -------------------------------------------------------------------------------- /packages/sample/src/api/batchAPIReqCall.js: -------------------------------------------------------------------------------- 1 | const lark = require("@larksuiteoapi/allcore"); 2 | 3 | const appSettings = lark.getInternalAppSettingsByEnv() 4 | 5 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 6 | loggerLevel: lark.LoggerLevel.ERROR, 7 | }) 8 | 9 | let reqCall1 = new lark.api.APIReqCall(conf, lark.api.newRequest("/open-apis/message/v4/send", "POST", lark.api.AccessTokenType.Tenant, { 10 | "user_id": "77bbc392", 11 | msg_type: "text", 12 | content: { 13 | text: "test" 14 | } 15 | })); 16 | 17 | let reqCall2 = new lark.api.APIReqCall(conf, lark.api.newRequest("/open-apis/message/v4/send", "POST", lark.api.AccessTokenType.Tenant, { 18 | "user_id": "77bbc392", 19 | msg_type: "text", 20 | content: { 21 | text: "test2" 22 | } 23 | })); 24 | 25 | let batchReqCall = new lark.api.BatchAPIReqCall(reqCall1, reqCall2); 26 | 27 | batchReqCall.do().then(function (batchReqCall) { 28 | for (let result of batchReqCall.reqCallResults) { 29 | console.log("--------------------------") 30 | console.log(result.response.getRequestID()) 31 | console.log(result.response.getHTTPStatusCode()) 32 | console.log(result.response) 33 | //console.log(done.err) 34 | } 35 | }) 36 | 37 | -------------------------------------------------------------------------------- /packages/sample/src/api/helpdesk.js: -------------------------------------------------------------------------------- 1 | const lark = require("@larksuiteoapi/allcore"); 2 | 3 | const appSettings = lark.newInternalAppSettings({ 4 | appID: "App ID", 5 | appSecret: "App Secret", 6 | helpDeskID: "HelpDesk ID", 7 | helpDeskToken: "HelpDesk Token", 8 | }) 9 | 10 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, {loggerLevel: lark.LoggerLevel.DEBUG}) 11 | 12 | console.log(conf.getHelpDeskAuthorization()) 13 | 14 | let req = lark.api.newRequest("/open-apis/helpdesk/v1/tickets/6971250929135779860", "GET", lark.api.AccessTokenType.Tenant, null) 15 | req.setTimeoutOfMs(6000) 16 | req.setNeedHelpDeskAuth() 17 | 18 | lark.api.sendRequest(conf, req).then(resp => { 19 | console.log(resp.getHeader()) 20 | console.log(resp.getRequestID()) 21 | console.log(resp.getHTTPStatusCode()) 22 | console.log(resp) // r = response.body 23 | }).catch(e => { 24 | console.log(e) 25 | }) -------------------------------------------------------------------------------- /packages/sample/src/api/imageDownload.js: -------------------------------------------------------------------------------- 1 | const lark = require("@larksuiteoapi/allcore"); 2 | const fs = require("fs") 3 | 4 | const appSettings = lark.getInternalAppSettingsByEnv() 5 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 6 | loggerLevel: lark.LoggerLevel.ERROR, 7 | }) 8 | 9 | let queryParams = { 10 | image_key: "img_v2_332fc963-94f6-4147-ba3a-c8f5f745d6fg" 11 | } 12 | 13 | let req = lark.api.newRequest("image/v4/get", "GET", lark.api.AccessTokenType.Tenant, undefined) 14 | req.setQueryParams(queryParams) 15 | req.setIsResponseStream() 16 | lark.api.sendRequest(conf, req).then(resp => { 17 | fs.writeFileSync("./test.0.png", resp.data) 18 | console.log(resp.getRequestID()) 19 | console.log(resp.getHTTPStatusCode()) 20 | console.log(resp.getHeader()) 21 | }).catch(e => { 22 | console.error(e) 23 | }) 24 | 25 | 26 | let req2 = lark.api.newRequest("image/v4/get", "GET", lark.api.AccessTokenType.Tenant, undefined) 27 | req2.setQueryParams(queryParams) 28 | req2.setResponseStream(fs.createWriteStream("./test.1.png")) 29 | lark.api.sendRequest(conf, req2).then(resp => { 30 | console.log(resp.getRequestID()) 31 | console.log(resp.getHTTPStatusCode()) 32 | console.log(resp.getHeader()) 33 | }).catch(e => { 34 | console.error(e) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/sample/src/api/imageUpload.js: -------------------------------------------------------------------------------- 1 | const lark = require("@larksuiteoapi/allcore"); 2 | const fs = require("fs") 3 | 4 | const appSettings = lark.getInternalAppSettingsByEnv() 5 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 6 | loggerLevel: lark.LoggerLevel.ERROR, 7 | }) 8 | 9 | // upload image 10 | // use stream 11 | // let data = fs.createReadStream('./test.png'); 12 | // use byte[] 13 | let data = fs.readFileSync('./test.png'); 14 | let formData = new lark.api.FormData() 15 | formData.addField("image_type", "message") 16 | formData.addFile("image", new lark.api.File().setContent(data).setType("image/jpeg")) 17 | let req = lark.api.newRequest("image/v4/put", "POST", lark.api.AccessTokenType.Tenant, formData) 18 | lark.api.sendRequest(conf, req).then(resp => { 19 | console.log(resp.getRequestID()) 20 | console.log(resp.getHTTPStatusCode()) 21 | console.log(resp.getHeader()) 22 | console.log(resp) 23 | }).catch(e => { 24 | console.error(e) 25 | }) -------------------------------------------------------------------------------- /packages/sample/src/api/onlineApi.ts: -------------------------------------------------------------------------------- 1 | import * as lark from "@larksuiteoapi/allcore" 2 | import {RedisStore} from "../config/config"; 3 | 4 | const appSettings = lark.getInternalAppSettingsByEnv() 5 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 6 | loggerLevel: lark.LoggerLevel.ERROR, 7 | store: new RedisStore() 8 | }) 9 | 10 | lark.api.sendRequest(conf, lark.api.newRequest("message/v4/send", "POST", lark.api.AccessTokenType.Tenant, { 11 | "user_id": "77bbc392", 12 | msg_type: "text", 13 | content: { 14 | text: "test" 15 | } 16 | })).then(resp => { 17 | console.log("--------------------------") 18 | console.log(resp.getRequestID()) 19 | console.log(resp.getHTTPStatusCode()) 20 | console.log(resp) // r = response.body 21 | }).catch(e => { 22 | console.log(e) 23 | }) -------------------------------------------------------------------------------- /packages/sample/src/api/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/oapi-sdk-nodejs/75fd98c46680664ba138e4f043125ae93438bdc8/packages/sample/src/api/test.png -------------------------------------------------------------------------------- /packages/sample/src/card/express.js: -------------------------------------------------------------------------------- 1 | const lark = require("@larksuiteoapi/allcore"); 2 | const express = require('express'); 3 | 4 | const appSettings = lark.getInternalAppSettingsByEnv() 5 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 6 | loggerLevel: lark.LoggerLevel.ERROR, 7 | }) 8 | 9 | // set handler 10 | lark.card.setHandler(conf, (ctx, card) => { 11 | let conf = lark.core.getConfigByCtx(ctx); 12 | console.log(conf); 13 | console.log("----------------"); 14 | console.log(ctx.getHeader()); 15 | console.log(ctx.getRequestID()); 16 | console.log(card) 17 | return "{\"config\":{\"wide_screen_mode\":true},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"div\",\"text\":{\"tag\":\"lark_md\",\"content\":\"[飞书](https://www.feishu.cn)整合即时沟通、日历、音视频会议、云文档、云盘、工作台等功能于一体,成就组织和个人,更高效、更愉悦。\"}}]}}"; 18 | }) 19 | 20 | const app = express(); 21 | app.use(express.json()) 22 | 23 | // Start the httpserver, "Developer Console" -> "Features" -> "Bot", setting Message Card Request URL: https://domain/webhook/card 24 | app.post('/webhook/card', (req, res) => { 25 | const request = new lark.core.Request() 26 | Object.entries(req.headers).forEach(([k, v]) => { 27 | request.headers[k] = v 28 | }) 29 | request.body = req.body 30 | lark.card.httpHandle(conf, request, undefined).then(response => { 31 | console.log("=================\n", response.body); 32 | res.status(response.statusCode).send(response.body) 33 | }) 34 | }); 35 | 36 | // startup event http server, port: 8089 37 | app.listen(8089, () => { 38 | console.log(`listening at :8089`) 39 | }) -------------------------------------------------------------------------------- /packages/sample/src/card/httpServer.js: -------------------------------------------------------------------------------- 1 | const lark = require("@larksuiteoapi/allcore"); 2 | 3 | const appSettings = lark.getInternalAppSettingsByEnv() 4 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 5 | loggerLevel: lark.LoggerLevel.ERROR, 6 | }) 7 | // set handler 8 | lark.card.setHandler(conf, (ctx, card) => { 9 | let conf = lark.core.getConfigByCtx(ctx); 10 | console.log(conf); 11 | console.log(card.action) 12 | return "{\"config\":{\"wide_screen_mode\":true},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"div\",\"text\":{\"tag\":\"lark_md\",\"content\":\"[飞书](https://www.feishu.cn)整合即时沟通、日历、音视频会议、云文档、云盘、工作台等功能于一体,成就组织和个人,更高效、更愉悦。\"}}]}}"; 13 | }) 14 | 15 | // startup card http server by express, port: 8089 16 | lark.card.startServer(conf, 8089) -------------------------------------------------------------------------------- /packages/sample/src/config/config.ts: -------------------------------------------------------------------------------- 1 | import * as lark from "@larksuiteoapi/allcore"; 2 | 3 | const asyncRedis = require("async-redis"); 4 | 5 | const client = asyncRedis.createClient(6379, "127.0.0.1"); 6 | 7 | client.on("error", function (err) { 8 | console.log("Error " + err); 9 | }); 10 | 11 | // use redis implement store 12 | export class RedisStore { 13 | 14 | get = (key: string) => { 15 | return client.get(key) 16 | } 17 | 18 | put = (key: string, value: string, expire: number) => { 19 | return client.setex(key, expire, value) 20 | } 21 | 22 | } 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /packages/sample/src/event/express.js: -------------------------------------------------------------------------------- 1 | const lark = require("@larksuiteoapi/allcore"); 2 | const express = require('express'); 3 | 4 | const appSettings = lark.getInternalAppSettingsByEnv() 5 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 6 | loggerLevel: lark.LoggerLevel.ERROR, 7 | }) 8 | 9 | lark.event.setTypeHandler(conf, "app_status_change", (ctx, event) => { 10 | let conf = lark.core.getConfigByCtx(ctx); 11 | console.log(conf); 12 | console.log("----------------"); 13 | console.log(ctx.getRequestID()); 14 | console.log(event); 15 | }) 16 | 17 | lark.event.setTypeHandler(conf, "user_update", (ctx, event) => { 18 | let conf = lark.core.getConfigByCtx(ctx); 19 | console.log(conf); 20 | console.log("----------------"); 21 | console.log(ctx.getHeader()); 22 | console.log(ctx.getRequestID()); 23 | console.log(event); 24 | }) 25 | 26 | const app = express(); 27 | 28 | app.use(express.json()) 29 | 30 | // Start the httpserver, "Developer Console" -> "Event Subscriptions", setting Request URL:https://{domain}/webhook/event 31 | app.post('/webhook/event', function (req, res, next) { 32 | console.log(req.body) 33 | const request = new lark.core.Request() 34 | Object.entries(req.headers).forEach(([k, v]) => { 35 | request.headers[k] = v 36 | }) 37 | request.body = req.body 38 | lark.event.httpHandle(conf, request, undefined).then(response => { 39 | res.status(response.statusCode).send(response.body) 40 | }) 41 | }) 42 | 43 | // startup event http server by express, port: 8089 44 | app.listen(8089, () => { 45 | console.log(`listening at :8089`) 46 | }) -------------------------------------------------------------------------------- /packages/sample/src/event/httpServer.js: -------------------------------------------------------------------------------- 1 | const lark = require("@larksuiteoapi/allcore"); 2 | 3 | const appSettings = lark.getInternalAppSettingsByEnv() 4 | const conf = lark.newConfig(lark.Domain.FeiShu, appSettings, { 5 | loggerLevel: lark.LoggerLevel.ERROR, 6 | }) 7 | 8 | lark.event.setTypeHandler(conf, "user_update", (ctx, event) => { 9 | let conf = lark.core.getConfigByCtx(ctx); 10 | console.log(conf); 11 | console.log("----------------"); 12 | console.log(ctx.getHeader()); 13 | console.log(ctx.getRequestID()); 14 | console.log(event); 15 | }) 16 | 17 | // startup event http server, port: 8089 18 | lark.event.startServer(conf, 8089) -------------------------------------------------------------------------------- /packages/sample/src/tools/downFile.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | 3 | const lark = require("@larksuiteoapi/allcore") 4 | 5 | lark.api.downloadFile("https://s0.pstatp.com/ee/lark-open/web/static/apply.226f11cb.png", 3000).then(buf => { 6 | fs.writeFileSync("./test.png", buf) 7 | }) 8 | 9 | -------------------------------------------------------------------------------- /packages/sample/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "strict": false, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "*": [ 18 | "./types/*" 19 | ] 20 | }, 21 | "esModuleInterop": true 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ], 26 | "exclude": [ 27 | "src/**/*.test.ts", 28 | "src/**/*.js" 29 | ] 30 | } --------------------------------------------------------------------------------