@satorijs/adapter-lark 3.5.3 → 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,6 +1,6 @@
1
- export interface EventHeader<T extends string> {
1
+ export interface EventHeader<K extends keyof Events> {
2
2
  event_id: string
3
- event_type: T
3
+ event_type: K
4
4
  create_time: string
5
5
  token: string
6
6
  app_id: string
@@ -12,11 +12,12 @@ export type EventName = keyof Events
12
12
 
13
13
  // In fact, this is the 2.0 version of the event sent by Lark.
14
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]
15
+ export type EventPayload = {
16
+ [K in keyof Events]: {
17
+ schema: '2.0'
18
+ // special added field for TypeScript
19
+ type: K
20
+ header: EventHeader<K>
21
+ event: Events[K]
22
+ }
23
+ }[keyof Events]