@satorijs/adapter-lark 3.6.0 → 3.6.1

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,19 +1,14 @@
1
1
  import { Context, 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
7
  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]
8
+ private textContent = ''
9
+ private richContent: MessageContent.RichText.Paragraph[] = []
10
+ private cardElements: MessageContent.Card.Element[] | undefined
11
+ private actionElements: MessageContent.Card.ButtonElement[] = []
17
12
 
18
13
  async post(data?: any) {
19
14
  try {
@@ -46,131 +41,204 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
46
41
  }
47
42
  }
48
43
 
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
- }
44
+ private flushText(flushAction = false) {
45
+ if ((this.textContent || flushAction) && this.actionElements.length) {
46
+ this.cardElements?.push({ tag: 'action', actions: this.actionElements })
47
+ this.actionElements = []
58
48
  }
59
- if (this.richText) {
60
- message = { zh_cn: this.richText }
49
+ if (this.textContent) {
50
+ this.richContent.push([{ tag: 'md', text: this.textContent }])
51
+ this.cardElements?.push({ tag: 'markdown', content: this.textContent })
52
+ this.textContent = ''
61
53
  }
62
- if (this.content) {
63
- message = { text: this.content }
54
+ }
55
+
56
+ async flush() {
57
+ this.flushText()
58
+ if (!this.cardElements && !this.richContent.length) return
59
+
60
+ if (this.cardElements) {
61
+ await this.post({
62
+ msg_type: 'interactive',
63
+ content: JSON.stringify({
64
+ elements: this.cardElements,
65
+ }),
66
+ })
67
+ } else {
68
+ await this.post({
69
+ msg_type: 'post',
70
+ content: JSON.stringify({
71
+ zh_cn: {
72
+ content: this.richContent,
73
+ },
74
+ }),
75
+ })
64
76
  }
65
- await this.post({
66
- msg_type: this.richText ? 'post' : this.addition ? this.addition.type : 'text',
67
- content: JSON.stringify(message),
68
- })
69
77
 
70
78
  // reset cached content
71
79
  this.quote = undefined
72
- this.content = ''
73
- this.addition = undefined
74
- this.richText = undefined
80
+ this.textContent = ''
81
+ this.richContent = []
82
+ this.cardElements = undefined
75
83
  }
76
84
 
77
- async sendFile(type: 'img' | 'image' | 'video' | 'audio' | 'file', url: string): Promise<Addition> {
85
+ async createImage(url: string) {
86
+ const { filename, type, data } = await this.bot.assetsQuester.file(url)
78
87
  const payload = new FormData()
88
+ payload.append('image', new Blob([data], { type }), filename)
89
+ payload.append('image_type', 'message')
90
+ const { data: { image_key } } = await this.bot.internal.createImImage(payload)
91
+ return image_key
92
+ }
79
93
 
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)
94
+ async sendFile(_type: 'video' | 'audio' | 'file', attrs: any) {
95
+ const url = attrs.src || attrs.url
96
+ const payload = new FormData()
97
+ const { filename, type, data } = await this.bot.assetsQuester.file(url)
98
+ payload.append('file', new Blob([data], { type }), filename)
99
+ payload.append('file_name', filename)
83
100
 
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
- }
101
+ if (attrs.duration) {
102
+ payload.append('duration', attrs.duration)
103
+ }
104
+
105
+ if (_type === 'audio') {
106
+ // FIXME: only support opus
107
+ payload.append('file_type', 'opus')
108
+ } else if (_type === 'video') {
109
+ // FIXME: only support mp4
110
+ payload.append('file_type', 'mp4')
93
111
  } 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'
112
+ const ext = filename.split('.').pop()
113
+ if (['doc', 'xls', 'ppt', 'pdf'].includes(ext)) {
114
+ payload.append('file_type', ext)
103
115
  } 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
- }
110
- }
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,
117
- },
116
+ payload.append('file_type', 'stream')
118
117
  }
119
118
  }
119
+
120
+ const { data: { file_key } } = await this.bot.internal.createImFile(payload)
121
+ await this.post({
122
+ msg_type: _type === 'video' ? 'media' : _type,
123
+ content: JSON.stringify({ file_key }),
124
+ })
120
125
  }
121
126
 
122
127
  async visit(element: h) {
123
128
  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
129
+ if (type === 'text') {
130
+ this.textContent += attrs.content
131
+ } else if (type === 'at') {
132
+ if (attrs.type === 'all') {
133
+ this.textContent += `<at user_id="all">${attrs.name ?? '所有人'}</at>`
134
+ } else {
135
+ this.textContent += `<at user_id="${attrs.id}">${attrs.name}</at>`
136
136
  }
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'
143
- 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':
137
+ } else if (type === 'a') {
138
+ await this.render(children)
139
+ if (attrs.href) this.textContent += ` (${attrs.href})`
140
+ } else if (type === 'p') {
141
+ if (!this.textContent.endsWith('\n')) this.textContent += '\n'
142
+ await this.render(children)
143
+ if (!this.textContent.endsWith('\n')) this.textContent += '\n'
144
+ } else if (type === 'br') {
145
+ this.textContent += '\n'
146
+ } else if (type === 'sharp') {
147
+ // platform does not support sharp
148
+ } else if (type === 'quote') {
149
+ await this.flush()
150
+ this.quote = attrs.id
151
+ } else if (type === 'img' || type === 'image') {
152
+ const image_key = await this.createImage(attrs.src || attrs.url)
153
+ this.textContent += `![${attrs.alt ?? '图片'}](${image_key})`
154
+ this.flushText()
155
+ this.richContent.push([{ tag: 'img', image_key }])
156
+ } else if (['video', 'audio', 'file'].includes(type)) {
157
+ await this.flush()
158
+ await this.sendFile(type as any, attrs)
159
+ } else if (type === 'figure' || type === 'message') {
160
+ await this.flush()
161
+ await this.render(children, true)
162
+ } else if (type === 'hr') {
163
+ this.flushText()
164
+ this.richContent.push([{ tag: 'hr' }])
165
+ this.cardElements?.push({ tag: 'hr' })
166
+ } else if (type === 'button') {
167
+ this.flushText()
168
+ const behaviors: MessageContent.Card.ActionBehavior[] = []
169
+ if (attrs.type === 'link') {
170
+ behaviors.push({
171
+ type: 'open_url',
172
+ default_url: attrs.href,
173
+ })
174
+ } else if (attrs.type === 'input') {
175
+ behaviors.push({
176
+ type: 'callback',
177
+ value: {
178
+ _satori_type: 'command',
179
+ content: attrs.text,
180
+ },
181
+ })
182
+ } else if (attrs.type === 'action') {
183
+ // TODO
184
+ }
185
+ await this.render(children)
186
+ this.actionElements.push({
187
+ tag: 'button',
188
+ text: {
189
+ tag: 'plain_text',
190
+ content: this.textContent,
191
+ },
192
+ behaviors,
193
+ })
194
+ this.textContent = ''
195
+ } else if (type === 'button-group') {
196
+ this.flushText(true)
197
+ await this.render(children)
198
+ this.flushText(true)
199
+ } else if (type.startsWith('lark:') || type.startsWith('feishu:')) {
200
+ const tag = type.slice(type.split(':', 1)[0].length + 1)
201
+ if (tag === 'share-chat') {
153
202
  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()
165
- }
166
- break
167
- case 'figure': // FIXME: treat as message element for now
168
- case 'message':
203
+ await this.post({
204
+ msg_type: 'share_chat',
205
+ content: JSON.stringify({ chat_id: attrs.chatId }),
206
+ })
207
+ } else if (tag === 'share-user') {
208
+ await this.flush()
209
+ await this.post({
210
+ msg_type: 'share_user',
211
+ content: JSON.stringify({ user_id: attrs.userId }),
212
+ })
213
+ } else if (tag === 'system') {
214
+ await this.flush()
215
+ await this.render(children)
216
+ await this.post({
217
+ msg_type: 'system',
218
+ content: JSON.stringify({
219
+ type: 'divider',
220
+ params: { divider_text: { text: this.textContent } },
221
+ options: { need_rollup: attrs.needRollup },
222
+ }),
223
+ })
224
+ this.textContent = ''
225
+ } else if (tag === 'card') {
169
226
  await this.flush()
227
+ this.cardElements = []
170
228
  await this.render(children, true)
171
- break
172
- default:
229
+ } else if (tag === 'div') {
230
+ this.flushText()
173
231
  await this.render(children)
232
+ this.cardElements?.push({
233
+ tag: 'markdown',
234
+ text_align: attrs.align,
235
+ text_size: attrs.size,
236
+ content: this.textContent,
237
+ })
238
+ this.textContent = ''
239
+ }
240
+ } else {
241
+ await this.render(children)
174
242
  }
175
243
  }
176
244
  }
@@ -1,23 +1,35 @@
1
- // https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/im-v1/message/create_json
1
+ // https://open.larksuite.com/document/server-docs/im-v1/message-content-description/create_json
2
2
 
3
- export namespace MessageContent {
4
- export type Contents =
5
- | Text
6
- | Image
7
- | ShareChat
8
- | ShareUser
9
- | Audio
10
- | Media
11
- | File
12
- | Sticker
13
- | RichText
14
-
15
- export type MediaContents =
16
- | Image
17
- | Audio
18
- | Media
19
- | File
3
+ declare global {
4
+ namespace JSX {
5
+ interface IntrinsicElements {
6
+ 'lark:share-chat': {
7
+ 'chat-id': string
8
+ }
9
+ 'lark:share-user': {
10
+ 'user-id': string
11
+ }
12
+ 'lark:system': {
13
+ 'need-rollup'?: boolean
14
+ }
15
+ }
16
+ }
17
+ }
18
+
19
+ export interface MessageContent {
20
+ text: MessageContent.Text
21
+ post: MessageContent.RichText
22
+ image: MessageContent.Image
23
+ file: MessageContent.File
24
+ audio: MessageContent.Audio
25
+ media: MessageContent.Media
26
+ sticker: MessageContent.Sticker
27
+ share_chat: MessageContent.ShareChat
28
+ share_user: MessageContent.ShareUser
29
+ system: MessageContent.System
30
+ }
20
31
 
32
+ export namespace MessageContent {
21
33
  export interface Text {
22
34
  text: string
23
35
  }
@@ -40,7 +52,7 @@ export namespace MessageContent {
40
52
 
41
53
  export interface Media {
42
54
  file_key: string
43
- image_key: string
55
+ image_key?: string
44
56
  }
45
57
 
46
58
  export interface File {
@@ -51,52 +63,264 @@ export namespace MessageContent {
51
63
  file_key: string
52
64
  }
53
65
 
66
+ export interface System {
67
+ type: 'divider'
68
+ params: {
69
+ divider_text: {
70
+ text: string
71
+ i18n_text?: Record<string, string>
72
+ }
73
+ }
74
+ options?: {
75
+ need_rollup?: boolean
76
+ }
77
+ }
78
+
54
79
  export interface RichText {
55
80
  [locale: string]: {
56
- title: string
81
+ title?: string
57
82
  content: RichText.Paragraph[]
58
83
  }
59
84
  }
60
85
 
61
86
  export namespace RichText {
62
- export interface Paragraph extends Array<RichText.Content> {}
87
+ export type Style = 'bold' | 'italic' | 'underline' | 'lineThrough'
63
88
 
64
- export interface BaseContent {
65
- tag: string
89
+ export interface BaseElement<T extends string = string> {
90
+ tag: T
66
91
  }
67
92
 
68
- export interface TextContent extends BaseContent {
69
- tag: 'text'
93
+ export interface TextElement extends BaseElement<'text'> {
70
94
  text: string
71
95
  un_escape?: boolean
96
+ style?: Style[]
72
97
  }
73
- export interface LinkContent extends BaseContent {
74
- tag: 'a'
98
+
99
+ export interface LinkElement extends BaseElement<'a'> {
75
100
  text: string
76
101
  href: string
102
+ style?: Style[]
77
103
  }
78
- export interface AtContent extends BaseContent {
79
- tag: 'at'
104
+
105
+ export interface AtElement extends BaseElement<'at'> {
80
106
  user_id: string
81
- user_name?: string
107
+ style?: Style[]
108
+ // user_name?: string
82
109
  }
83
- export interface ImageContent extends BaseContent {
84
- tag: 'img'
110
+
111
+ export interface ImageElement extends BaseElement<'img'> {
85
112
  image_key: string
86
- height?: number
87
- width?: number
113
+ // height?: number
114
+ // width?: number
88
115
  }
89
- export interface MediaContent extends BaseContent {
90
- tag: 'media'
116
+
117
+ export interface MediaElement extends BaseElement<'media'> {
91
118
  file_key: string
92
119
  image_key?: string
93
120
  }
94
121
 
95
- export type Content =
96
- | RichText.TextContent
97
- | RichText.LinkContent
98
- | RichText.AtContent
99
- | RichText.ImageContent
100
- | RichText.MediaContent
122
+ export interface EmotionElement extends BaseElement<'emoji'> {
123
+ emoji_type: string
124
+ }
125
+
126
+ export interface CodeBlockElement extends BaseElement<'code_block'> {
127
+ language?: string
128
+ text: string
129
+ }
130
+
131
+ export interface HRElement extends BaseElement<'hr'> {}
132
+
133
+ export interface MarkdownElement extends BaseElement<'md'> {
134
+ text: string
135
+ }
136
+
137
+ export type InlineElement =
138
+ | TextElement
139
+ | LinkElement
140
+ | AtElement
141
+ | EmotionElement
142
+ | MarkdownElement
143
+
144
+ export type BlockElement =
145
+ | ImageElement
146
+ | MediaElement
147
+ | CodeBlockElement
148
+ | HRElement
149
+
150
+ export type Paragraph =
151
+ | InlineElement[]
152
+ | [BlockElement]
153
+ }
154
+
155
+ export interface Card {
156
+ config: Card.Config
157
+ card_link?: Card.URLs
158
+ elements?: Card.Element[]
159
+ }
160
+
161
+ export namespace Card {
162
+ /** @see https://open.larksuite.com/document/common-capabilities/message-card/getting-started/card-structure/card-configuration */
163
+ export interface Config {
164
+ enable_forward?: boolean
165
+ update_multi?: boolean
166
+ }
167
+
168
+ export interface URLs {
169
+ url: string
170
+ pc_url?: string
171
+ ios_url?: string
172
+ android_url?: string
173
+ }
174
+
175
+ /** @see https://open.larksuite.com/document/common-capabilities/message-card/message-cards-content/card-header */
176
+ export interface Header {
177
+ title: I18nPlainTextElement
178
+ subtitle?: I18nPlainTextElement
179
+ template?: Header.Template
180
+ icon?: CustomIconElement
181
+ ud_icon?: StandardIconElement
182
+ text_tag_list?: TextTagElement[]
183
+ i18n_text_tag_list?: Record<string, TextTagElement[]>
184
+ }
185
+
186
+ export namespace Header {
187
+ export type Template = 'blue' | 'wathet' | 'turquoise' | 'green' | 'yellow' | 'orange' | 'red' | 'carmine' | 'violet' | 'purple' | 'indigo' | 'grey' | 'default'
188
+ }
189
+
190
+ export interface BaseElement<T extends string = string> {
191
+ tag: T
192
+ }
193
+
194
+ export type TextSize =
195
+ | 'heading-0' | 'heading-1' | 'heading-2' | 'heading-3' | 'heading-4' | 'heading'
196
+ | 'normal' | 'notation' | 'xxxx-large' | 'xxx-large' | 'xx-large' | 'x-large' | 'large' | 'medium' | 'small' | 'x-small'
197
+
198
+ export type TextAlign = 'left' | 'center' | 'right'
199
+
200
+ export interface PlainTextElement extends BaseElement<'plain_text'> {
201
+ content: string
202
+ }
203
+
204
+ export interface I18nPlainTextElement extends PlainTextElement {
205
+ i18n?: Record<string, string>
206
+ }
207
+
208
+ export interface DivPlainTextElement extends PlainTextElement {
209
+ text_size?: TextSize
210
+ text_color?: string
211
+ text_align?: TextAlign
212
+ lines?: number
213
+ icon?: IconElement
214
+ }
215
+
216
+ export type IconElement = StandardIconElement | CustomIconElement
217
+
218
+ export interface CustomIconElement extends BaseElement<'custom_icon'> {
219
+ img_key: string
220
+ }
221
+
222
+ export interface StandardIconElement extends BaseElement<'standard_icon'> {
223
+ token: string
224
+ color?: string
225
+ }
226
+
227
+ export interface TextTagElement extends BaseElement<'text_tag'> {
228
+ text: PlainTextElement
229
+ color: TextTagElement.Color
230
+ }
231
+
232
+ export namespace TextTagElement {
233
+ export type Color = 'neutral' | 'blue' | 'torqoise' | 'lime' | 'orange' | 'violet' | 'indigo' | 'wathet' | 'green' | 'yellow' | 'red' | 'purple' | 'carmine'
234
+ }
235
+
236
+ export interface ImageElement extends BaseElement<'image'> {
237
+ img_key: string
238
+ alt?: PlainTextElement
239
+ title?: PlainTextElement
240
+ custom_width?: number
241
+ compact_width?: boolean
242
+ mode?: 'crop_center' | 'fit_horizontal' | 'large' | 'medium' | 'small' | 'tiny'
243
+ preview?: boolean
244
+ }
245
+
246
+ export interface HorizontalRuleElement extends BaseElement<'hr'> {}
247
+
248
+ export interface DivElement extends BaseElement<'div'> {
249
+ text?: DivPlainTextElement
250
+ }
251
+
252
+ export interface MarkdownElement extends BaseElement<'markdown'> {
253
+ content: string
254
+ text_size?: TextSize
255
+ text_align?: TextAlign
256
+ href?: Record<string, URLs>
257
+ }
258
+
259
+ export interface HorizontalRuleElement extends BaseElement<'hr'> {}
260
+
261
+ export interface ActionModule extends BaseElement<'action'> {
262
+ actions: ActionElement[]
263
+ layout?: 'bisected' | 'trisection' | 'flow'
264
+ }
265
+
266
+ export type ActionElement =
267
+ | ButtonElement
268
+
269
+ export type ActionBehavior =
270
+ | OpenURLBehavior
271
+ | CallbackBehavior
272
+
273
+ export interface OpenURLBehavior {
274
+ type: 'open_url'
275
+ default_url: string
276
+ pc_url?: string
277
+ ios_url?: string
278
+ android_url?: string
279
+ }
280
+
281
+ export interface CallbackBehavior {
282
+ type: 'callback'
283
+ value: Record<string, string>
284
+ }
285
+
286
+ export interface ButtonElement extends BaseElement<'button'> {
287
+ text: PlainTextElement
288
+ type?: ButtonElement.Type
289
+ size?: ButtonElement.Size
290
+ width?: ButtonElement.Width
291
+ icon?: IconElement
292
+ hover_tips?: PlainTextElement
293
+ disabled?: boolean
294
+ disabled_tips?: PlainTextElement
295
+ confirm?: {
296
+ title: PlainTextElement
297
+ text: PlainTextElement
298
+ }
299
+ behaviors?: ActionBehavior[]
300
+ // form-related fields
301
+ name?: string
302
+ required?: boolean
303
+ action_type?: 'link' | 'request' | 'multi' | 'form_submit' | 'form_reset'
304
+ }
305
+
306
+ export namespace ButtonElement {
307
+ export type Size = 'tiny' | 'small' | 'medium' | 'large'
308
+ export type Width = 'default' | 'fill' | string
309
+ export type Type = 'default' | 'primary' | 'danger' | 'text' | 'primary_text' | 'danger_text' | 'primary_filled' | 'danger_filled' | 'laser'
310
+ }
311
+
312
+ export type Element =
313
+ | DivElement
314
+ | MarkdownElement
315
+ | HorizontalRuleElement
316
+ | ActionModule
317
+ }
318
+
319
+ export interface Template {
320
+ type: 'template'
321
+ data: {
322
+ template_id: string
323
+ template_variable: object
324
+ }
101
325
  }
102
326
  }