@satorijs/adapter-lark 3.6.0 → 3.6.2

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/message.ts CHANGED
@@ -1,28 +1,27 @@
1
- import { Context, h, MessageEncoder } from '@satorijs/core'
1
+ import { Context, Dict, h, MessageEncoder } from '@satorijs/core'
2
2
  import { LarkBot } from './bot'
3
- import { BaseResponse, Lark, MessageContent, MessageType } from './types'
3
+ import { BaseResponse, Lark, MessageContent } from './types'
4
4
  import { extractIdType } from './utils'
5
5
 
6
- export interface Addition {
7
- file: MessageContent.MediaContents
8
- type: MessageType
9
- }
10
-
11
6
  export class LarkMessageEncoder<C extends Context = Context> extends MessageEncoder<C, LarkBot<C>> {
12
- private quote: string | undefined
13
- private content = ''
14
- private addition: Addition
15
- // TODO: currently not used, would be supported in the future
16
- private richText: MessageContent.RichText[string]
7
+ private quote: Dict | undefined
8
+ private textContent = ''
9
+ private richContent: MessageContent.RichText.Paragraph[] = []
10
+ private card: MessageContent.Card | undefined
11
+ private noteElements: MessageContent.Card.NoteElement.InnerElement[] | undefined
12
+ private actionElements: MessageContent.Card.Element[] = []
17
13
 
18
14
  async post(data?: any) {
19
15
  try {
20
16
  let resp: BaseResponse & { data?: Lark.Message }
21
- if (this.quote) {
22
- resp = await this.bot.internal.replyImMessage(this.quote, data)
17
+ if (this.quote?.id) {
18
+ resp = await this.bot.internal.replyImMessage(this.quote.id, {
19
+ ...data,
20
+ reply_in_thread: this.quote.replyInThread,
21
+ })
23
22
  } else {
24
23
  data.receive_id = this.channelId
25
- resp = await this.bot.internal?.createImMessage(data, {
24
+ resp = await this.bot.internal.createImMessage(data, {
26
25
  receive_id_type: extractIdType(this.channelId),
27
26
  })
28
27
  }
@@ -46,131 +45,279 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
46
45
  }
47
46
  }
48
47
 
49
- async flush() {
50
- if (this.content === '' && !this.addition && !this.richText) return
51
-
52
- let message: MessageContent.Contents
53
- if (this.addition) {
54
- message = {
55
- ...message,
56
- ...this.addition.file,
57
- }
48
+ private flushText(button = false) {
49
+ if ((this.textContent || !button) && this.actionElements.length) {
50
+ this.card!.elements.push({ tag: 'action', actions: this.actionElements, layout: 'flow' })
51
+ this.actionElements = []
58
52
  }
59
- if (this.richText) {
60
- message = { zh_cn: this.richText }
53
+ if (this.textContent) {
54
+ this.richContent.push([{ tag: 'md', text: this.textContent }])
55
+ if (this.noteElements) {
56
+ this.noteElements.push({ tag: 'plain_text', content: this.textContent })
57
+ } else if (this.card) {
58
+ this.card.elements.push({ tag: 'markdown', content: this.textContent })
59
+ }
60
+ this.textContent = ''
61
61
  }
62
- if (this.content) {
63
- message = { text: this.content }
62
+ }
63
+
64
+ async flush() {
65
+ this.flushText()
66
+ if (!this.card && !this.richContent.length) return
67
+
68
+ if (this.card) {
69
+ this.bot.logger.debug('card', JSON.stringify(this.card.elements))
70
+ await this.post({
71
+ msg_type: 'interactive',
72
+ content: JSON.stringify({
73
+ elements: this.card.elements,
74
+ }),
75
+ })
76
+ } else {
77
+ await this.post({
78
+ msg_type: 'post',
79
+ content: JSON.stringify({
80
+ zh_cn: {
81
+ content: this.richContent,
82
+ },
83
+ }),
84
+ })
64
85
  }
65
- await this.post({
66
- msg_type: this.richText ? 'post' : this.addition ? this.addition.type : 'text',
67
- content: JSON.stringify(message),
68
- })
69
86
 
70
87
  // reset cached content
71
88
  this.quote = undefined
72
- this.content = ''
73
- this.addition = undefined
74
- this.richText = undefined
89
+ this.textContent = ''
90
+ this.richContent = []
91
+ this.card = undefined
75
92
  }
76
93
 
77
- async sendFile(type: 'img' | 'image' | 'video' | 'audio' | 'file', url: string): Promise<Addition> {
94
+ async createImage(url: string) {
95
+ const { filename, type, data } = await this.bot.assetsQuester.file(url)
78
96
  const payload = new FormData()
97
+ payload.append('image', new Blob([data], { type }), filename)
98
+ payload.append('image_type', 'message')
99
+ const { data: { image_key } } = await this.bot.internal.createImImage(payload)
100
+ return image_key
101
+ }
79
102
 
80
- const assetKey = type === 'img' || type === 'image' ? 'image' : 'file'
81
- const { filename, mime, data } = await this.bot.assetsQuester.file(url)
82
- payload.append(assetKey, new Blob([data], { type: mime }), filename)
103
+ async sendFile(_type: 'video' | 'audio' | 'file', attrs: any) {
104
+ const url = attrs.src || attrs.url
105
+ const payload = new FormData()
106
+ const { filename, type, data } = await this.bot.assetsQuester.file(url)
107
+ payload.append('file', new Blob([data], { type }), filename)
108
+ payload.append('file_name', filename)
83
109
 
84
- if (type === 'img' || type === 'image') {
85
- payload.append('image_type', 'message')
86
- const { data } = await this.bot.internal.createImImage(payload)
87
- return {
88
- type: 'image',
89
- file: {
90
- image_key: data.image_key,
91
- },
92
- }
110
+ if (attrs.duration) {
111
+ payload.append('duration', attrs.duration)
112
+ }
113
+
114
+ if (_type === 'audio') {
115
+ // FIXME: only support opus
116
+ payload.append('file_type', 'opus')
117
+ } else if (_type === 'video') {
118
+ // FIXME: only support mp4
119
+ payload.append('file_type', 'mp4')
93
120
  } else {
94
- let msgType: MessageType = 'file'
95
- if (type === 'audio') {
96
- // FIXME: only support opus
97
- payload.append('file_type', 'opus')
98
- msgType = 'audio'
99
- } else if (type === 'video') {
100
- // FIXME: only support mp4
101
- payload.append('file_type', 'mp4')
102
- msgType = 'media'
121
+ const ext = filename.split('.').pop()
122
+ if (['doc', 'xls', 'ppt', 'pdf'].includes(ext)) {
123
+ payload.append('file_type', ext)
103
124
  } else {
104
- const ext = filename.split('.').pop()
105
- if (['xls', 'ppt', 'pdf'].includes(ext)) {
106
- payload.append('file_type', ext)
107
- } else {
108
- payload.append('file_type', 'stream')
109
- }
125
+ payload.append('file_type', 'stream')
110
126
  }
111
- payload.append('file_name', filename)
112
- const { data } = await this.bot.internal.createImFile(payload)
113
- return {
114
- type: msgType,
115
- file: {
116
- file_key: data.file_key,
127
+ }
128
+
129
+ const { data: { file_key } } = await this.bot.internal.createImFile(payload)
130
+ await this.post({
131
+ msg_type: _type === 'video' ? 'media' : _type,
132
+ content: JSON.stringify({ file_key }),
133
+ })
134
+ }
135
+
136
+ private createBehavior(attrs: Dict) {
137
+ const behaviors: MessageContent.Card.ActionBehavior[] = []
138
+ if (attrs.type === 'link') {
139
+ behaviors.push({
140
+ type: 'open_url',
141
+ default_url: attrs.href,
142
+ })
143
+ } else if (attrs.type === 'input') {
144
+ behaviors.push({
145
+ type: 'callback',
146
+ value: {
147
+ _satori_type: 'command',
148
+ content: attrs.text,
117
149
  },
118
- }
150
+ })
151
+ } else if (attrs.type === 'action') {
152
+ // TODO
119
153
  }
154
+ return behaviors.length ? behaviors : undefined
120
155
  }
121
156
 
122
157
  async visit(element: h) {
123
158
  const { type, attrs, children } = element
124
-
125
- switch (type) {
126
- case 'text':
127
- this.content += attrs.content
128
- break
129
- case 'at': {
130
- if (attrs.type === 'all') {
131
- this.content += `<at user_id="all">${attrs.name ?? '所有人'}</at>`
132
- } else {
133
- this.content += `<at user_id="${attrs.id}">${attrs.name}</at>`
134
- }
135
- break
159
+ if (type === 'text') {
160
+ this.textContent += attrs.content
161
+ } else if (type === 'at') {
162
+ if (attrs.type === 'all') {
163
+ this.textContent += `<at user_id="all">${attrs.name ?? '所有人'}</at>`
164
+ } else {
165
+ this.textContent += `<at user_id="${attrs.id}">${attrs.name}</at>`
136
166
  }
137
- case 'a':
138
- await this.render(children)
139
- if (attrs.href) this.content += ` (${attrs.href})`
140
- break
141
- case 'p':
142
- if (!this.content.endsWith('\n')) this.content += '\n'
167
+ } else if (type === 'a') {
168
+ await this.render(children)
169
+ if (attrs.href) this.textContent += ` (${attrs.href})`
170
+ } else if (type === 'p') {
171
+ if (!this.textContent.endsWith('\n')) this.textContent += '\n'
172
+ await this.render(children)
173
+ if (!this.textContent.endsWith('\n')) this.textContent += '\n'
174
+ } else if (type === 'br') {
175
+ this.textContent += '\n'
176
+ } else if (type === 'sharp') {
177
+ // platform does not support sharp
178
+ } else if (type === 'quote') {
179
+ await this.flush()
180
+ this.quote = attrs
181
+ } else if (type === 'img' || type === 'image') {
182
+ const image_key = await this.createImage(attrs.src || attrs.url)
183
+ this.textContent += `![${attrs.alt ?? '图片'}](${image_key})`
184
+ this.flushText()
185
+ this.richContent.push([{ tag: 'img', image_key }])
186
+ } else if (['video', 'audio', 'file'].includes(type)) {
187
+ await this.flush()
188
+ await this.sendFile(type as any, attrs)
189
+ } else if (type === 'figure' || type === 'message') {
190
+ await this.flush()
191
+ await this.render(children, true)
192
+ } else if (type === 'hr') {
193
+ this.flushText()
194
+ this.richContent.push([{ tag: 'hr' }])
195
+ this.card?.elements.push({ tag: 'hr' })
196
+ } else if (type === 'form') {
197
+ this.flushText()
198
+ const length = this.card?.elements.length
199
+ await this.render(children)
200
+ if (this.card?.elements.length > length) {
201
+ const elements = this.card?.elements.slice(length)
202
+ this.card.elements.push({
203
+ tag: 'form',
204
+ name: attrs.name || 'Form',
205
+ elements,
206
+ })
207
+ }
208
+ } else if (type === 'input') {
209
+ this.flushText()
210
+ this.card?.elements.push({
211
+ tag: 'action',
212
+ actions: [{
213
+ tag: 'input',
214
+ name: attrs.name,
215
+ width: attrs.width,
216
+ label: attrs.label && {
217
+ tag: 'plain_text',
218
+ content: attrs.label,
219
+ },
220
+ placeholder: attrs.placeholder && {
221
+ tag: 'plain_text',
222
+ content: attrs.placeholder,
223
+ },
224
+ behaviors: this.createBehavior(attrs),
225
+ }],
226
+ })
227
+ } else if (type === 'button') {
228
+ this.card ??= { elements: [] }
229
+ this.flushText(true)
230
+ await this.render(children)
231
+ this.actionElements.push({
232
+ tag: 'button',
233
+ text: {
234
+ tag: 'plain_text',
235
+ content: this.textContent,
236
+ },
237
+ disabled: attrs.disabled,
238
+ behaviors: this.createBehavior(attrs),
239
+ })
240
+ this.textContent = ''
241
+ } else if (type === 'button-group') {
242
+ this.flushText()
243
+ await this.render(children)
244
+ this.flushText()
245
+ } else if (type.startsWith('lark:') || type.startsWith('feishu:')) {
246
+ const tag = type.slice(type.split(':', 1)[0].length + 1)
247
+ if (tag === 'share-chat') {
248
+ await this.flush()
249
+ await this.post({
250
+ msg_type: 'share_chat',
251
+ content: JSON.stringify({ chat_id: attrs.chatId }),
252
+ })
253
+ } else if (tag === 'share-user') {
254
+ await this.flush()
255
+ await this.post({
256
+ msg_type: 'share_user',
257
+ content: JSON.stringify({ user_id: attrs.userId }),
258
+ })
259
+ } else if (tag === 'system') {
260
+ await this.flush()
143
261
  await this.render(children)
144
- if (!this.content.endsWith('\n')) this.content += '\n'
145
- break
146
- case 'br':
147
- this.content += '\n'
148
- break
149
- case 'sharp':
150
- // platform does not support sharp
151
- break
152
- case 'quote':
262
+ await this.post({
263
+ msg_type: 'system',
264
+ content: JSON.stringify({
265
+ type: 'divider',
266
+ params: { divider_text: { text: this.textContent } },
267
+ options: { need_rollup: attrs.needRollup },
268
+ }),
269
+ })
270
+ this.textContent = ''
271
+ } else if (tag === 'card') {
153
272
  await this.flush()
154
- this.quote = attrs.id
155
- break
156
- case 'img':
157
- case 'image':
158
- case 'video':
159
- case 'audio':
160
- case 'file':
161
- if (attrs.src || attrs.url) {
162
- await this.flush()
163
- this.addition = await this.sendFile(type, attrs.src || attrs.url)
164
- await this.flush()
273
+ this.card = {
274
+ elements: [],
275
+ header: attrs.title && {
276
+ template: attrs.color,
277
+ ud_icon: attrs.icon && {
278
+ tag: 'standard_icon',
279
+ token: attrs.icon,
280
+ },
281
+ title: {
282
+ tag: 'plain_text',
283
+ content: attrs.title,
284
+ },
285
+ subtitle: attrs.subtitle && {
286
+ tag: 'plain_text',
287
+ content: attrs.subtitle,
288
+ },
289
+ },
165
290
  }
166
- break
167
- case 'figure': // FIXME: treat as message element for now
168
- case 'message':
169
- await this.flush()
170
291
  await this.render(children, true)
171
- break
172
- default:
292
+ } else if (tag === 'div') {
293
+ this.flushText()
173
294
  await this.render(children)
295
+ this.card?.elements.push({
296
+ tag: 'markdown',
297
+ text_align: attrs.align,
298
+ text_size: attrs.size,
299
+ content: this.textContent,
300
+ })
301
+ this.textContent = ''
302
+ } else if (tag === 'note') {
303
+ this.flushText()
304
+ this.noteElements = []
305
+ await this.render(children)
306
+ this.flushText()
307
+ this.card?.elements.push({
308
+ tag: 'note',
309
+ elements: this.noteElements,
310
+ })
311
+ this.noteElements = undefined
312
+ } else if (tag === 'icon') {
313
+ this.flushText()
314
+ this.noteElements?.push({
315
+ tag: 'standard_icon',
316
+ token: attrs.token,
317
+ })
318
+ }
319
+ } else {
320
+ await this.render(children)
174
321
  }
175
322
  }
176
323
  }
package/src/types/api.ts CHANGED
@@ -16681,6 +16681,8 @@ export interface ReplyImMessageRequest {
16681
16681
  content: string
16682
16682
  /** 消息类型,包括:text、post、image、file、audio、media、sticker、interactive、share_card、share_user */
16683
16683
  msg_type: string
16684
+ /** 是否以话题形式回复。取值为 true 时将以话题形式回复。注意:如果要回复的消息已经是话题形式的消息,则默认以话题形式进行回复。 */
16685
+ reply_in_thread?: boolean
16684
16686
  /** 由开发者生成的唯一字符串序列,用于回复消息请求去重;持有相同uuid的请求1小时内至多成功执行一次 */
16685
16687
  uuid?: string
16686
16688
  }