@satorijs/adapter-lark 3.1.4 → 3.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/http.ts ADDED
@@ -0,0 +1,157 @@
1
+ import internal from 'stream'
2
+ import { Adapter, Context, Logger, Schema } from '@satorijs/satori'
3
+ import {} from '@satorijs/router'
4
+
5
+ import { FeishuBot } from './bot'
6
+ import { AllEvents } from './types'
7
+ import { adaptSession, Cipher } from './utils'
8
+
9
+ export class HttpServer<C extends Context = Context> extends Adapter<C, FeishuBot<C>> {
10
+ static inject = ['router']
11
+
12
+ private logger: Logger
13
+ private ciphers: Record<string, Cipher> = {}
14
+
15
+ constructor(ctx: C, bot: FeishuBot<C>) {
16
+ super(ctx)
17
+ this.logger = ctx.logger('lark')
18
+ }
19
+
20
+ fork(ctx: C, bot: FeishuBot<C>) {
21
+ super.fork(ctx, bot)
22
+
23
+ this._refreshCipher()
24
+ return bot.initialize()
25
+ }
26
+
27
+ async connect(bot: FeishuBot) {
28
+ const { path } = bot.config
29
+ bot.ctx.router.post(path, (ctx) => {
30
+ this._refreshCipher()
31
+
32
+ // compare signature if encryptKey is set
33
+ // But not every message contains signature
34
+ // https://open.larksuite.com/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/encrypt-key-encryption-configuration-case#d41e8916
35
+ const signature = ctx.get('X-Lark-Signature')
36
+ const enabledSignatureVerify = this.bots.filter((bot) => bot.config.verifySignature)
37
+ if (signature && enabledSignatureVerify.length) {
38
+ const result = enabledSignatureVerify.some((bot) => {
39
+ const timestamp = ctx.get('X-Lark-Request-Timestamp')
40
+ const nonce = ctx.get('X-Lark-Request-Nonce')
41
+ const body = ctx.request.rawBody
42
+ const actualSignature = this.ciphers[bot.config.appId]?.calculateSignature(timestamp, nonce, body)
43
+ if (actualSignature === signature) return true
44
+ else return false
45
+ })
46
+ if (!result) return (ctx.status = 403)
47
+ }
48
+
49
+ // try to decrypt message first if encryptKey is set
50
+ const body = this._tryDecryptBody(ctx.request.body)
51
+ // respond challenge message
52
+ // https://open.larksuite.com/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/request-url-configuration-case
53
+ if (body?.type === 'url_verification' && body?.challenge && typeof body.challenge === 'string') {
54
+ ctx.response.body = { challenge: body.challenge }
55
+ return
56
+ }
57
+
58
+ // compare verification token
59
+ const enabledVerifyTokenVerify = this.bots.filter((bot) => bot.config.verifyToken && bot.config.verificationToken)
60
+ if (enabledVerifyTokenVerify.length) {
61
+ const token = ctx.request.body?.token
62
+ // only compare token if token exists
63
+ if (token) {
64
+ const result = enabledVerifyTokenVerify.some((bot) => {
65
+ if (token === bot.config.verificationToken) return true
66
+ else return false
67
+ })
68
+ if (!result) return (ctx.status = 403)
69
+ }
70
+ }
71
+
72
+ // dispatch message
73
+ bot.logger.debug('received decryped event: %o', body)
74
+ this.dispatchSession(body)
75
+
76
+ // Lark requires 200 OK response to make sure event is received
77
+ return ctx.status = 200
78
+ })
79
+
80
+ bot.ctx.router.get(path + '/assets/:type/:message_id/:key', async (ctx) => {
81
+ const type = ctx.params.type === 'image' ? 'image' : 'file'
82
+ const key = ctx.params.key
83
+ const messageId = ctx.params.message_id
84
+ const selfId = ctx.request.query.self_id
85
+ const bot = this.bots.find((bot) => bot.selfId === selfId)
86
+ if (!bot) return ctx.status = 404
87
+
88
+ const resp = await bot.http.axios<internal.Readable>(`/im/v1/messages/${messageId}/resources/${key}`, {
89
+ method: 'GET',
90
+ params: { type },
91
+ responseType: 'stream',
92
+ })
93
+
94
+ ctx.status = 200
95
+ ctx.response.headers['Content-Type'] = resp.headers['content-type']
96
+ ctx.response.body = resp.data
97
+ })
98
+ }
99
+
100
+ dispatchSession(body: AllEvents): void {
101
+ const { header } = body
102
+ if (!header) return
103
+ const { app_id, event_type } = header
104
+ body.type = event_type // add type to body to ease typescript type narrowing
105
+ const bot = this.bots.find((bot) => bot.selfId === app_id)
106
+ const session = adaptSession(bot, body)
107
+ bot.dispatch(session)
108
+ }
109
+
110
+ private _tryDecryptBody(body: any): any {
111
+ this._refreshCipher()
112
+ // try to decrypt message if encryptKey is set
113
+ // https://open.larksuite.com/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/encrypt-key-encryption-configuration-case
114
+ const ciphers = Object.values(this.ciphers)
115
+ if (ciphers.length && typeof body.encrypt === 'string') {
116
+ for (const cipher of ciphers) {
117
+ try {
118
+ return JSON.parse(cipher.decrypt(body.encrypt))
119
+ } catch {}
120
+ }
121
+ this.logger.warn('failed to decrypt message: %o', body)
122
+ }
123
+
124
+ if (typeof body.encrypt === 'string' && !ciphers.length) {
125
+ this.logger.warn('encryptKey is not set, but received encrypted message: %o', body)
126
+ }
127
+
128
+ return body
129
+ }
130
+
131
+ private _refreshCipher(): void {
132
+ const ciphers = Object.keys(this.ciphers)
133
+ const bots = this.bots.map((bot) => bot.config.appId)
134
+ if (bots.length === ciphers.length && bots.every((bot) => ciphers.includes(bot))) return
135
+
136
+ this.ciphers = {}
137
+ for (const bot of this.bots) {
138
+ this.ciphers[bot.config.appId] = new Cipher(bot.config.encryptKey)
139
+ }
140
+ }
141
+ }
142
+
143
+ export namespace HttpServer {
144
+ export interface Config {
145
+ selfUrl?: string
146
+ path?: string
147
+ verifyToken?: boolean
148
+ verifySignature?: boolean
149
+ }
150
+
151
+ export const createConfig = (path: string): Schema<Config> => Schema.object({
152
+ path: Schema.string().role('url').description('要连接的服务器地址。').default(path),
153
+ selfUrl: Schema.string().role('link').description('服务器暴露在公网的地址。缺省时将使用全局配置。'),
154
+ verifyToken: Schema.boolean().description('是否验证令牌。'),
155
+ verifySignature: Schema.boolean().description('是否验证签名。'),
156
+ }).description('服务端设置')
157
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { FeishuBot } from './bot'
2
+ import * as Lark from './types'
3
+
4
+ export * from './bot'
5
+
6
+ export { Lark, Lark as Feishu }
7
+
8
+ export default FeishuBot
9
+
10
+ declare module '@satorijs/core' {
11
+ interface Session {
12
+ feishu: Lark.Internal
13
+ lark: Lark.Internal
14
+ }
15
+ }
package/src/message.ts ADDED
@@ -0,0 +1,185 @@
1
+ import { createReadStream } from 'fs'
2
+ import internal from 'stream'
3
+
4
+ import { Context, h, MessageEncoder, Quester } from '@satorijs/satori'
5
+ import FormData from 'form-data'
6
+
7
+ import { LarkBot } from './bot'
8
+ import { BaseResponse, Message, MessageContent, MessageType } from './types'
9
+ import { extractIdType } from './utils'
10
+
11
+ export interface Addition {
12
+ file: MessageContent.MediaContents
13
+ type: MessageType
14
+ }
15
+
16
+ export class LarkMessageEncoder<C extends Context = Context> extends MessageEncoder<C, LarkBot<C>> {
17
+ private quote: string | undefined
18
+ private content = ''
19
+ private addition: Addition
20
+ // TODO: currently not used, would be supported in the future
21
+ private richText: MessageContent.RichText[string]
22
+
23
+ async post(data?: any) {
24
+ try {
25
+ let resp: BaseResponse & { data: Message }
26
+ if (this.quote) {
27
+ resp = await this.bot.internal?.replyMessage(this.quote, data)
28
+ } else {
29
+ data.receive_id = this.channelId
30
+ resp = await this.bot.internal?.sendMessage(extractIdType(this.channelId), data)
31
+ }
32
+ const session = this.bot.session()
33
+ session.messageId = resp.data.message_id
34
+ session.timestamp = Number(resp.data.create_time) * 1000
35
+ session.userId = resp.data.sender.id
36
+ session.app.emit(session, 'send', session)
37
+ this.results.push(session.event.message)
38
+ } catch (e) {
39
+ // try to extract error message from Lark API
40
+ if (Quester.isAxiosError(e)) {
41
+ if (e.response?.data?.code) {
42
+ const generalErrorMsg = `Check error code at https://open.larksuite.com/document/server-docs/getting-started/server-error-codes`
43
+ e.message += ` (Lark error code ${e.response.data.code}: ${e.response.data.msg ?? generalErrorMsg})`
44
+ }
45
+ }
46
+ this.errors.push(e)
47
+ }
48
+ }
49
+
50
+ async flush() {
51
+ if (this.content === '' && !this.addition && !this.richText) return
52
+
53
+ let message: MessageContent.Contents
54
+ if (this.addition) {
55
+ message = {
56
+ ...message,
57
+ ...this.addition.file,
58
+ }
59
+ }
60
+ if (this.richText) {
61
+ message = { zh_cn: this.richText }
62
+ }
63
+ if (this.content) {
64
+ message = { text: this.content }
65
+ }
66
+ await this.post({
67
+ msg_type: this.richText ? 'post' : this.addition ? this.addition.type : 'text',
68
+ content: JSON.stringify(message),
69
+ })
70
+
71
+ // reset cached content
72
+ this.quote = undefined
73
+ this.content = ''
74
+ this.addition = undefined
75
+ this.richText = undefined
76
+ }
77
+
78
+ async sendFile(type: 'image' | 'video' | 'audio' | 'file', url: string): Promise<Addition> {
79
+ const payload = new FormData()
80
+
81
+ const assetKey = type === 'image' ? 'image' : 'file'
82
+ const [schema, file] = url.split('://')
83
+ const filename = schema === 'base64' ? 'unknown' : new URL(url).pathname.split('/').pop()
84
+ if (schema === 'file') {
85
+ payload.append(assetKey, createReadStream(file))
86
+ } else if (schema === 'base64') {
87
+ payload.append(assetKey, Buffer.from(file, 'base64'))
88
+ } else {
89
+ const resp = await this.bot.assetsQuester.get<internal.Readable>(url, { responseType: 'stream' })
90
+ payload.append(assetKey, resp)
91
+ }
92
+
93
+ if (type === 'image') {
94
+ payload.append('image_type', 'message')
95
+ const { data } = await this.bot.internal.uploadImage(payload)
96
+ return {
97
+ type: 'image',
98
+ file: {
99
+ image_key: data.image_key,
100
+ },
101
+ }
102
+ } else {
103
+ let msgType: MessageType = 'file'
104
+ if (type === 'audio') {
105
+ // FIXME: only support opus
106
+ payload.append('file_type', 'opus')
107
+ msgType = 'audio'
108
+ } else if (type === 'video') {
109
+ // FIXME: only support mp4
110
+ payload.append('file_type', 'mp4')
111
+ msgType = 'media'
112
+ } else {
113
+ const ext = filename.split('.').pop()
114
+ if (['xls', 'ppt', 'pdf'].includes(ext)) {
115
+ payload.append('file_type', ext)
116
+ } else {
117
+ payload.append('file_type', 'stream')
118
+ }
119
+ }
120
+ payload.append('file_name', filename)
121
+ const { data } = await this.bot.internal.uploadFile(payload)
122
+ return {
123
+ type: msgType,
124
+ file: {
125
+ file_key: data.file_key,
126
+ },
127
+ }
128
+ }
129
+ }
130
+
131
+ async visit(element: h) {
132
+ const { type, attrs, children } = element
133
+
134
+ switch (type) {
135
+ case 'text':
136
+ this.content += attrs.content
137
+ break
138
+ case 'at': {
139
+ if (attrs.type === 'all') {
140
+ this.content += `<at user_id="all">${attrs.name ?? '所有人'}</at>`
141
+ } else {
142
+ this.content += `<at user_id="${attrs.id}">${attrs.name}</at>`
143
+ }
144
+ break
145
+ }
146
+ case 'a':
147
+ await this.render(children)
148
+ if (attrs.href) this.content += ` (${attrs.href})`
149
+ break
150
+ case 'p':
151
+ if (!this.content.endsWith('\n')) this.content += '\n'
152
+ await this.render(children)
153
+ if (!this.content.endsWith('\n')) this.content += '\n'
154
+ break
155
+ case 'br':
156
+ this.content += '\n'
157
+ break
158
+ case 'sharp':
159
+ // platform does not support sharp
160
+ break
161
+ case 'quote':
162
+ await this.flush()
163
+ this.quote = attrs.id
164
+ break
165
+ case 'image':
166
+ case 'video':
167
+ case 'audio':
168
+ case 'file':
169
+ if (attrs.url) {
170
+ await this.flush()
171
+ this.addition = await this.sendFile(type, attrs.url)
172
+ }
173
+ break
174
+ case 'figure': // FIXME: treat as message element for now
175
+ case 'message':
176
+ await this.flush()
177
+ await this.render(children, true)
178
+ break
179
+ default:
180
+ await this.render(children)
181
+ }
182
+ }
183
+ }
184
+
185
+ export { LarkMessageEncoder as FeishuMessageEncoder }
@@ -0,0 +1,2 @@
1
+ rules:
2
+ max-len: off
@@ -0,0 +1,54 @@
1
+ import { BaseResponse, Internal } from '.'
2
+
3
+ /**
4
+ * Lark defines three types of token:
5
+ * - app_access_token: to access the API in an app (published on App Store).
6
+ * - tenant_access_token: to access the API as an enterprise or a team (tenant).
7
+ * *We commonly use this one*
8
+ * - user_access_token: to access the API as the specific user.
9
+ *
10
+ * @see https://open.larksuite.com/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM
11
+ */
12
+
13
+ export interface AppCredentials {
14
+ app_id: string
15
+ app_secret: string
16
+ }
17
+
18
+ export interface AppAccessToken extends BaseResponse {
19
+ /** access token */
20
+ app_access_token: string
21
+ /** expire time in seconds. e.g: 7140 (119 minutes) */
22
+ expire: number
23
+ }
24
+
25
+ export interface TenantAccessToken extends BaseResponse {
26
+ /** access token */
27
+ tenant_access_token: string
28
+ /** expire time in seconds. e.g: 7140 (119 minutes) */
29
+ expire: number
30
+ }
31
+
32
+ declare module './internal' {
33
+ export interface Internal {
34
+ /**
35
+ * Returns the app_access_token for the bot.
36
+ * @see https://open.larksuite.com/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/auth-v3/auth/app_access_token_internal
37
+ */
38
+ getAppAccessToken(data: AppCredentials): Promise<AppAccessToken>
39
+ /**
40
+ * Returns the tenant_access_token for the bot.
41
+ * @see https://open.larksuite.com/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/auth-v3/auth/tenant_access_token_internal
42
+ */
43
+ getTenantAccessToken(data: AppCredentials): Promise<TenantAccessToken>
44
+ }
45
+ }
46
+
47
+ Internal.define({
48
+ '/auth/v3/app_access_token/internal': {
49
+ POST: 'getAppAccessToken',
50
+ },
51
+ '/auth/v3/tenant_access_token/internal': {
52
+ POST: 'getTenantAccessToken',
53
+ },
54
+ })
@@ -0,0 +1,22 @@
1
+ export interface EventHeader<T extends string> {
2
+ event_id: string
3
+ event_type: T
4
+ create_time: string
5
+ token: string
6
+ app_id: string
7
+ tenant_key: string
8
+ }
9
+
10
+ export interface Events {}
11
+ export type EventName = keyof Events
12
+
13
+ // In fact, this is the 2.0 version of the event sent by Lark.
14
+ // And only the 2.0 version has the `schema` field.
15
+ export type EventSkeleton<T extends EventName, Event, Header = EventHeader<T>> = {
16
+ schema: '2.0'
17
+ type: T
18
+ header: Header
19
+ event: Event
20
+ }
21
+
22
+ export type AllEvents = Events[EventName]
@@ -0,0 +1,64 @@
1
+ import { Dict } from '@satorijs/satori'
2
+
3
+ import { Lark } from '.'
4
+ import { Internal } from './internal'
5
+ import { Paginated, Pagination } from './utils'
6
+
7
+ declare module '.' {
8
+ export namespace Lark {
9
+ export interface Guild {
10
+ avatar: string
11
+ name: string
12
+ description: string
13
+ i18n_names: Dict<string>
14
+ add_member_permission: string
15
+ share_card_permission: string
16
+ at_all_permission: string
17
+ edit_permission: string
18
+ owner_id_type: string
19
+ owner_id: string
20
+ chat_id: string
21
+ chat_mode: string
22
+ chat_type: string
23
+ chat_tag: string
24
+ join_message_visibility: string
25
+ leave_message_visibility: string
26
+ membership_approval: string
27
+ moderation_permission: string
28
+ external: boolean
29
+ tenant_key: string
30
+ user_count: string
31
+ bot_count: string
32
+ }
33
+ }
34
+ }
35
+
36
+ export interface GuildMember {
37
+ member_id_type: Lark.UserIdType
38
+ member_id: string
39
+ name: string
40
+ tenant_key: string
41
+ }
42
+
43
+ declare module './internal' {
44
+ export interface Internal {
45
+ /** @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/chat/list */
46
+ getCurrentUserGuilds(params?: Pagination<{ user_id_type?: Lark.UserIdType }>): Promise<{ data: Paginated<Lark.Guild> }>
47
+ /** @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/chat/get */
48
+ getGuildInfo(chat_id: string, params?: { user_id_type?: string }): Promise<BaseResponse & { data: Lark.Guild }>
49
+ /** @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/chat-members/get */
50
+ getGuildMembers(chat_id: string, params?: Pagination<{ member_id_type?: Lark.UserIdType }>): Promise<{ data: Paginated<GuildMember> }>
51
+ }
52
+ }
53
+
54
+ Internal.define({
55
+ '/im/v1/chats': {
56
+ GET: 'getCurrentUserGuilds',
57
+ },
58
+ '/im/v1/chats/{chat_id}': {
59
+ GET: 'getGuildInfo',
60
+ },
61
+ '/im/v1/chats/{chat_id}/members': {
62
+ GET: 'getGuildMembers',
63
+ },
64
+ })
@@ -0,0 +1,41 @@
1
+ export * from './internal'
2
+ export * from './auth'
3
+ export * from './event'
4
+ export * from './guild'
5
+ export * from './message'
6
+
7
+ export namespace Lark {
8
+ /**
9
+ * A user in Lark has several different IDs.
10
+ * @see https://open.larksuite.com/document/home/user-identity-introduction/introduction
11
+ */
12
+ export interface UserIds {
13
+ union_id: string
14
+ /** *user_id* only available when the app has permissions granted by the administrator */
15
+ user_id?: string
16
+ open_id: string
17
+ }
18
+
19
+ /**
20
+ * Identify a user in Lark.
21
+ * This behaves like {@link Lark.UserIds}, but it only contains *open_id*.
22
+ * (i.e. the id_type is always `open_id`)
23
+ */
24
+ export interface UserIdentifiers {
25
+ id: string
26
+ id_type: string
27
+ }
28
+
29
+ export type UserIdType = 'union_id' | 'user_id' | 'open_id'
30
+ /**
31
+ * The id type when specify a receiver, would be used in the request query.
32
+ *
33
+ * NOTE: we always use **open_id** to identify a user, use **chat_id** to identify a channel.
34
+ * @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create
35
+ */
36
+ export type ReceiveIdType = UserIdType | 'email' | 'chat_id'
37
+
38
+ export type DepartmentIdType = 'department_id' | 'open_department_id'
39
+ }
40
+
41
+ export { Lark as Feishu }
@@ -0,0 +1,62 @@
1
+ import FormData from 'form-data'
2
+ import { Dict, makeArray, Quester } from '@satorijs/satori'
3
+ import { LarkBot } from '../bot'
4
+
5
+ export interface Internal {}
6
+
7
+ export interface BaseResponse {
8
+ /** error code. would be 0 if success, and non-0 if failed. */
9
+ code: number
10
+ /** error message. would be 'success' if success. */
11
+ msg: string
12
+ }
13
+
14
+ type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
15
+
16
+ export class Internal {
17
+ constructor(private bot: LarkBot) {}
18
+
19
+ private processReponse(response: any): BaseResponse {
20
+ const { code, msg } = response
21
+ if (code === 0) {
22
+ return response
23
+ } else {
24
+ this.bot.logger.debug('response: %o', response)
25
+ throw new Error(`HTTP response with non-zero status (${code}) with message "${msg}"`)
26
+ }
27
+ }
28
+
29
+ static define(routes: Dict<Partial<Record<Method, string | string[]>>>) {
30
+ for (const path in routes) {
31
+ for (const key in routes[path]) {
32
+ const method = key as Method
33
+ for (const name of makeArray(routes[path][method])) {
34
+ Internal.prototype[name] = async function (this: Internal, ...args: any[]) {
35
+ const raw = args.join(', ')
36
+ const url = path.replace(/\{([^}]+)\}/g, () => {
37
+ if (!args.length) throw new Error(`too few arguments for ${path}, received ${raw}`)
38
+ return args.shift()
39
+ })
40
+ const config: Quester.AxiosRequestConfig = {}
41
+ if (args.length === 1) {
42
+ if (method === 'GET' || method === 'DELETE') {
43
+ config.params = args[0]
44
+ } else {
45
+ if (method === 'POST' && args[0] instanceof FormData) {
46
+ config.headers = args[0].getHeaders()
47
+ }
48
+ config.data = args[0]
49
+ }
50
+ } else if (args.length === 2 && method !== 'GET' && method !== 'DELETE') {
51
+ config.data = args[0]
52
+ config.params = args[1]
53
+ } else if (args.length > 1) {
54
+ throw new Error(`too many arguments for ${path}, received ${raw}`)
55
+ }
56
+ return this.processReponse(await this.bot.http(method, url, config))
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,50 @@
1
+ import FormData from 'form-data'
2
+
3
+ import { BaseResponse, Internal } from '..'
4
+
5
+ export interface Asset<T> extends BaseResponse {
6
+ data: T
7
+ }
8
+
9
+ export type Image = Asset<{ image_key: string }>
10
+ export type File = Asset<{ file_key: string }>
11
+
12
+ declare module '../internal' {
13
+ interface Internal {
14
+ /**
15
+ * Upload an image to obtain an `image_key` for use in sending messages or changing the avatar.
16
+ *
17
+ * The data should contain:
18
+ * - `image_type`: 'message' | 'avatar'
19
+ * - `image': Buffer
20
+ * @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/image/create
21
+ */
22
+ uploadImage(data: FormData): Promise<Image>
23
+ /**
24
+ * Upload a file to obtain a `file_key` for use in sending messages.
25
+ *
26
+ * The data should contain:
27
+ * - `file_type`: 'opus' | 'mp4' | 'pdf' | 'xls' | 'ppt' | 'stream'
28
+ * - `opus`: Opus audio file
29
+ * - `mp4`: MP4 video file
30
+ * - `pdf`: PDF file
31
+ * - `xls`: Excel file
32
+ * - `ppt`: PowerPoint file
33
+ * - `stream`: Stream file, or any other file not listed above
34
+ * - `file_name`: string, include extension
35
+ * - `duration`?: number, the duration of audio/video file in milliseconds
36
+ * - `file`: Buffer
37
+ * @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/file/create
38
+ */
39
+ uploadFile(data: FormData): Promise<File>
40
+ }
41
+ }
42
+
43
+ Internal.define({
44
+ '/im/v1/images': {
45
+ POST: 'uploadImage',
46
+ },
47
+ '/im/v1/files': {
48
+ POST: 'uploadFile',
49
+ },
50
+ })