@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.
@@ -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
+ }
20
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
+ }
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,475 @@ 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
+ header?: Card.Header
159
+ elements: Card.Element[]
160
+ }
161
+
162
+ export namespace Card {
163
+ /** @see https://open.larksuite.com/document/common-capabilities/message-card/getting-started/card-structure/card-configuration */
164
+ export interface Config {
165
+ enable_forward?: boolean
166
+ update_multi?: boolean
167
+ }
168
+
169
+ export interface URLs {
170
+ url: string
171
+ pc_url?: string
172
+ ios_url?: string
173
+ android_url?: string
174
+ }
175
+
176
+ /** @see https://open.larksuite.com/document/common-capabilities/message-card/message-cards-content/card-header */
177
+ export interface Header {
178
+ title: I18nPlainTextElement
179
+ subtitle?: I18nPlainTextElement
180
+ template?: Header.Template
181
+ icon?: CustomIconElement
182
+ ud_icon?: StandardIconElement
183
+ text_tag_list?: TextTagElement[]
184
+ i18n_text_tag_list?: Record<string, TextTagElement[]>
185
+ }
186
+
187
+ export namespace Header {
188
+ export type Template = 'blue' | 'wathet' | 'turquoise' | 'green' | 'yellow' | 'orange' | 'red' | 'carmine' | 'violet' | 'purple' | 'indigo' | 'grey' | 'default'
189
+ }
190
+
191
+ export interface BaseElement<T extends string = string> {
192
+ tag: T
193
+ }
194
+
195
+ export type TextSize =
196
+ | 'heading-0' | 'heading-1' | 'heading-2' | 'heading-3' | 'heading-4' | 'heading'
197
+ | 'normal' | 'notation' | 'xxxx-large' | 'xxx-large' | 'xx-large' | 'x-large' | 'large' | 'medium' | 'small' | 'x-small'
198
+
199
+ export type TextAlign = 'left' | 'center' | 'right'
200
+
201
+ export interface PlainTextElement extends BaseElement<'plain_text'> {
202
+ content: string
203
+ }
204
+
205
+ export interface I18nPlainTextElement extends PlainTextElement {
206
+ i18n?: Record<string, string>
207
+ }
208
+
209
+ export interface DivPlainTextElement extends PlainTextElement {
210
+ text_size?: TextSize
211
+ text_color?: string
212
+ text_align?: TextAlign
213
+ lines?: number
214
+ icon?: IconElement
215
+ }
216
+
217
+ export type IconElement = StandardIconElement | CustomIconElement
218
+
219
+ export interface CustomIconElement extends BaseElement<'custom_icon'> {
220
+ img_key: string
221
+ }
222
+
223
+ export interface StandardIconElement extends BaseElement<'standard_icon'> {
224
+ token: string
225
+ color?: string
226
+ }
227
+
228
+ export interface TextTagElement extends BaseElement<'text_tag'> {
229
+ text: PlainTextElement
230
+ color: TextTagElement.Color
231
+ }
232
+
233
+ export namespace TextTagElement {
234
+ export type Color = 'neutral' | 'blue' | 'torqoise' | 'lime' | 'orange' | 'violet' | 'indigo' | 'wathet' | 'green' | 'yellow' | 'red' | 'purple' | 'carmine'
235
+ }
236
+
237
+ export interface BaseImageElement extends BaseElement<'image'> {
238
+ img_key: string
239
+ alt?: PlainTextElement
240
+ }
241
+
242
+ export interface ImageElement extends BaseImageElement {
243
+ title?: PlainTextElement
244
+ transparent?: string
245
+ preview?: boolean
246
+ corner_radius?: string
247
+ scale_type?: 'crop_center' | 'fit_horizontal' | 'crop_top'
248
+ size?: 'large' | 'medium' | 'small' | 'tiny' | 'stretch_without_padding' | 'stretch' | string
249
+ /** @deprecated */
250
+ custom_width?: number
251
+ /** @deprecated */
252
+ compact_width?: boolean
253
+ /** @deprecated */
254
+ mode?: 'crop_center' | 'fit_horizontal' | 'large' | 'medium' | 'small' | 'tiny'
255
+ }
256
+
257
+ export interface HRElement extends BaseElement<'hr'> {}
258
+
259
+ export interface DivElement extends BaseElement<'div'> {
260
+ text?: DivPlainTextElement
261
+ }
262
+
263
+ export interface MarkdownElement extends BaseElement<'markdown'> {
264
+ content: string
265
+ text_size?: TextSize
266
+ text_align?: TextAlign
267
+ href?: Record<string, URLs>
268
+ }
269
+
270
+ export interface PersonElement extends BaseElement<'person'> {
271
+ user_id: string
272
+ size?: 'large' | 'medium' | 'small' | 'extra_small'
273
+ }
274
+
275
+ export interface PersonListElement extends BaseElement<'person_list'> {
276
+ persons: { id: string }[]
277
+ size?: 'large' | 'medium' | 'small' | 'extra_small'
278
+ show_name?: boolean
279
+ show_avatar?: boolean
280
+ }
281
+
282
+ export interface ChartElement extends BaseElement<'chart'> {
283
+ chart_spec: {} // TODO
284
+ aspect_ratio?: '1:1' | '2:1' | '4:3' | '16:9'
285
+ color_theme?: 'brand' | 'rainbow' | 'complementary' | 'converse' | 'primary'
286
+ preview?: boolean
287
+ height?: 'auto' | string
288
+ }
289
+
290
+ export interface TableElement extends BaseElement<'table'> {
291
+ page_size?: number
292
+ row_height?: 'low' | 'medium' | 'high' | string
293
+ header_style?: TableElement.HeaderStyle
294
+ columns: TableElement.Column[]
295
+ rows: object[]
296
+ }
297
+
298
+ export namespace TableElement {
299
+ export interface HeaderStyle {
300
+ text_align?: TextAlign
301
+ text_size?: TextSize
302
+ background_style?: 'grey' | 'none'
303
+ text_color?: 'default' | 'grey'
304
+ bold?: boolean
305
+ lines?: number
306
+ }
307
+
308
+ export interface Column {
309
+ name: string
310
+ display_name?: string
311
+ width?: 'auto' | string
312
+ horizontal_align?: 'left' | 'center' | 'right'
313
+ data_type?: 'text' | 'lark_md' | 'options' | 'number' | 'persons' | 'date' | 'markdown'
314
+ format?: {
315
+ percision?: number
316
+ symbol?: string
317
+ separator?: string
318
+ }
319
+ date_format?: string
320
+ }
321
+ }
322
+
323
+ export interface NoteElement extends BaseElement<'note'> {
324
+ elements: NoteElement.InnerElement[]
325
+ }
326
+
327
+ export namespace NoteElement {
328
+ export type InnerElement = IconElement | PlainTextElement | BaseImageElement
329
+ }
330
+
331
+ export interface FormElement extends BaseElement<'form'> {
332
+ name: string
333
+ elements: Element[]
334
+ confirm?: ConfirmElement
335
+ }
336
+
337
+ export interface ActionElement extends BaseElement<'action'> {
338
+ actions: Element[]
339
+ layout?: 'bisected' | 'trisection' | 'flow'
340
+ }
341
+
342
+ export type ActionBehavior =
343
+ | OpenURLBehavior
344
+ | CallbackBehavior
345
+
346
+ export interface OpenURLBehavior {
347
+ type: 'open_url'
348
+ default_url: string
349
+ pc_url?: string
350
+ ios_url?: string
351
+ android_url?: string
352
+ }
353
+
354
+ export interface CallbackBehavior {
355
+ type: 'callback'
356
+ value: Record<string, string>
357
+ }
358
+
359
+ export interface BaseButtonElement extends BaseElement<'button'> {
360
+ text: PlainTextElement
361
+ size?: ButtonElement.Size
362
+ icon?: IconElement
363
+ disabled?: boolean
364
+ behaviors?: ActionBehavior[]
365
+ }
366
+
367
+ export interface ButtonElement extends BaseButtonElement {
368
+ type?: ButtonElement.Type
369
+ width?: ButtonElement.Width
370
+ hover_tips?: PlainTextElement
371
+ disabled_tips?: PlainTextElement
372
+ confirm?: {
373
+ title: PlainTextElement
374
+ text: PlainTextElement
375
+ }
376
+ // form-related fields
377
+ name?: string
378
+ required?: boolean
379
+ action_type?: 'link' | 'request' | 'multi' | 'form_submit' | 'form_reset'
380
+ }
381
+
382
+ export interface ConfirmElement {
383
+ title: PlainTextElement
384
+ text: PlainTextElement
385
+ }
386
+
387
+ export interface InputElement extends BaseElement<'input'> {
388
+ name: string
389
+ required?: boolean
390
+ placeholder?: PlainTextElement
391
+ default_value?: string
392
+ disabled?: boolean
393
+ width?: 'default' | 'fill' | string
394
+ max_length?: number
395
+ input_type?: 'text' | 'multiline_text' | 'password'
396
+ show_icon?: boolean
397
+ rows?: number
398
+ auto_resize?: boolean
399
+ max_rows?: number
400
+ label?: PlainTextElement
401
+ label_position?: 'top' | 'left'
402
+ value?: string | object
403
+ behaviors?: ActionBehavior[]
404
+ confirm?: ConfirmElement
405
+ fallback?: {
406
+ tag?: string
407
+ text?: PlainTextElement
408
+ }
409
+ }
410
+
411
+ export interface OverflowElement extends BaseElement<'overflow'> {
412
+ width?: 'default' | 'fill' | string
413
+ options: OverflowElement.Option[]
414
+ value?: object
415
+ confirm?: ConfirmElement
416
+ }
417
+
418
+ export namespace OverflowElement {
419
+ export interface Option {
420
+ text?: PlainTextElement
421
+ multi_url?: URLs
422
+ value?: string
423
+ }
424
+ }
425
+
426
+ export interface BaseSelectElement<T extends string = string> extends BaseElement<T> {
427
+ type?: 'default' | 'text'
428
+ name?: string
429
+ required?: boolean
430
+ disabled?: boolean
431
+ placeholder?: PlainTextElement
432
+ width?: 'default' | 'fill' | string
433
+ confirm?: ConfirmElement
434
+ }
435
+
436
+ export interface OptionElement {
437
+ text: PlainTextElement
438
+ icon?: IconElement
439
+ value: string
440
+ }
441
+
442
+ export interface SelectElement extends BaseSelectElement<'select_static'> {
443
+ options: OptionElement[]
444
+ initial_option?: string
445
+ }
446
+
447
+ export interface MultiSelectElement extends BaseSelectElement<'multi_select_static'> {
448
+ options: OptionElement[]
449
+ selected_values?: string[]
450
+ }
451
+
452
+ export interface SelectPersonElement extends BaseSelectElement<'select_person'> {
453
+ options?: { value: string }[]
454
+ }
455
+
456
+ export interface MultiSelectPersonElement extends BaseSelectElement<'multi_select_person'> {
457
+ options?: { value: string }[]
458
+ selected_values?: string[]
459
+ }
460
+
461
+ export interface DatePickerElement extends BaseSelectElement<'date_picker'> {
462
+ initial_date?: string
463
+ value?: object
464
+ }
465
+
466
+ export interface TimePickerElement extends BaseSelectElement<'picker_time'> {
467
+ initial_time?: string
468
+ value?: object
469
+ }
470
+
471
+ export interface DateTimePickerElement extends BaseSelectElement<'picker_datetime'> {
472
+ initial_datetime?: string
473
+ value?: object
474
+ }
475
+
476
+ export interface CheckerElement extends BaseElement<'checker'> {
477
+ name?: string
478
+ checked?: boolean
479
+ disabled?: boolean
480
+ text?: CheckerElement.TextElement
481
+ overall_checkable?: boolean
482
+ button_area?: {
483
+ pc_display_rule?: 'always' | 'on_hover'
484
+ buttons?: CheckerElement.ButtonElement[]
485
+ }
486
+ checked_style?: {
487
+ show_strikethrough?: boolean
488
+ opacity?: number
489
+ }
490
+ margin?: string
491
+ padding?: string
492
+ confirm?: ConfirmElement
493
+ behaviors?: ActionBehavior[]
494
+ hover_tips?: PlainTextElement
495
+ disable_tips?: PlainTextElement
496
+ }
497
+
498
+ export namespace CheckerElement {
499
+ export interface TextElement extends PlainTextElement {
500
+ text_size?: 'normal' | 'heading' | 'notation'
501
+ text_color?: string
502
+ text_align?: TextAlign
503
+ }
504
+
505
+ export interface ButtonElement extends BaseButtonElement {
506
+ type: 'text' | 'primary_text' | 'danger_text'
507
+ }
508
+ }
509
+
510
+ export namespace ButtonElement {
511
+ export type Size = 'tiny' | 'small' | 'medium' | 'large'
512
+ export type Width = 'default' | 'fill' | string
513
+ export type Type = 'default' | 'primary' | 'danger' | 'text' | 'primary_text' | 'danger_text' | 'primary_filled' | 'danger_filled' | 'laser'
514
+ }
515
+
516
+ export type Element =
517
+ | DivElement
518
+ | MarkdownElement
519
+ | HRElement
520
+ | ActionElement
521
+ | NoteElement
522
+ | ChartElement
523
+ | TableElement
524
+ | ImageElement
525
+ | FormElement
526
+ | InputElement
527
+ | ButtonElement
528
+ }
529
+
530
+ export interface Template {
531
+ type: 'template'
532
+ data: {
533
+ template_id: string
534
+ template_variable: object
535
+ }
101
536
  }
102
537
  }
@@ -1,24 +1,7 @@
1
- import { Lark } from '..'
2
- import { MessageContent } from './content'
1
+ import { Lark, MessageContent } from '..'
3
2
 
4
3
  export * from './content'
5
4
 
6
- export type MessageType = 'text' | 'post' | 'image' | 'file' | 'audio' | 'media' | 'sticker' | 'interactive' | 'share_chat' | 'share_user'
7
-
8
- export interface MessageContentMap {
9
- 'text': MessageContent.Text
10
- 'post': MessageContent.RichText
11
- 'image': MessageContent.Image
12
- 'file': MessageContent.File
13
- 'audio': MessageContent.Audio
14
- 'media': MessageContent.Media
15
- 'sticker': MessageContent.Sticker
16
- 'share_chat': MessageContent.ShareChat
17
- 'share_user': MessageContent.ShareUser
18
- }
19
-
20
- export type MessageContentType<T extends MessageType> = T extends keyof MessageContentMap ? MessageContentMap[T] : any
21
-
22
5
  declare module '../event' {
23
6
  export interface Events {
24
7
  /**
@@ -38,7 +21,7 @@ declare module '../event' {
38
21
  create_time: string
39
22
  chat_id: string
40
23
  chat_type: string
41
- message_type: MessageType
24
+ message_type: keyof MessageContent
42
25
  content: string
43
26
  mentions: {
44
27
  key: string
@@ -93,5 +76,21 @@ declare module '../event' {
93
76
  open_chat_id: string
94
77
  }
95
78
  }
79
+ /**
80
+ * 机器人自定义菜单事件
81
+ * @see https://open.feishu.cn/document/client-docs/bot-v3/events/menu
82
+ */
83
+ 'application.bot.menu_v6': {
84
+ operator: {
85
+ operator_name: string
86
+ operator_id: {
87
+ union_id: string
88
+ user_id: string
89
+ open_id: string
90
+ }
91
+ }
92
+ event_key: string
93
+ timestamp: number
94
+ }
96
95
  }
97
96
  }
package/src/utils.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import crypto from 'crypto'
2
2
  import { Context, h, Session, trimSlash, Universal } from '@satorijs/core'
3
- import { FeishuBot, LarkBot } from './bot'
4
- import { EventPayload, Events, GetImChatResponse, Lark, MessageContentType, MessageType } from './types'
3
+ import { LarkBot } from './bot'
4
+ import { EventPayload, Events, GetImChatResponse, Lark } from './types'
5
5
 
6
6
  export type Sender =
7
7
  | {
@@ -22,8 +22,8 @@ export function adaptSender(sender: Sender, session: Session): Session {
22
22
  return session
23
23
  }
24
24
 
25
- export async function adaptMessage(bot: FeishuBot, data: Events['im.message.receive_v1'], session: Session, details = true): Promise<Session> {
26
- const json = JSON.parse(data.message.content) as MessageContentType<MessageType>
25
+ export async function adaptMessage(bot: LarkBot, data: Events['im.message.receive_v1'], session: Session, details = true): Promise<Session> {
26
+ const json = JSON.parse(data.message.content)
27
27
  const assetEndpoint = trimSlash(bot.config.selfUrl ?? bot.ctx.server.config.selfUrl) + bot.config.path + '/assets'
28
28
  const content: (string | h)[] = []
29
29
  switch (data.message.message_type) {
@@ -71,7 +71,7 @@ export async function adaptMessage(bot: FeishuBot, data: Events['im.message.rece
71
71
  return session
72
72
  }
73
73
 
74
- export async function adaptSession<C extends Context>(bot: FeishuBot<C>, body: EventPayload) {
74
+ export async function adaptSession<C extends Context>(bot: LarkBot<C>, body: EventPayload) {
75
75
  const session = bot.session()
76
76
  session.setInternal('lark', body)
77
77
 
@@ -84,6 +84,15 @@ export async function adaptSession<C extends Context>(bot: FeishuBot<C>, body: E
84
84
  adaptSender(body.event.sender, session)
85
85
  await adaptMessage(bot, body.event, session)
86
86
  break
87
+ case 'application.bot.menu_v6':
88
+ if (body.event.event_key.startsWith('command:')) {
89
+ session.type = 'interaction/command'
90
+ session.content = body.event.event_key.slice(8)
91
+ session.channelId = body.event.operator.operator_id.open_id
92
+ session.userId = body.event.operator.operator_id.open_id
93
+ session.isDirect = true
94
+ }
95
+ break
87
96
  case 'card.action.trigger':
88
97
  if (body.event.action.value?._satori_type === 'command') {
89
98
  session.type = 'interaction/command'
@@ -106,11 +115,17 @@ export async function adaptSession<C extends Context>(bot: FeishuBot<C>, body: E
106
115
  for (const [key, value] of Object.entries(options)) {
107
116
  content += ` --${key} ${value}`
108
117
  }
118
+ if (body.event.action.input_value) {
119
+ content += ` ${body.event.action.input_value}`
120
+ }
109
121
  session.content = content
110
122
  session.messageId = body.event.context.open_message_id
111
123
  session.channelId = body.event.context.open_chat_id
112
124
  session.guildId = body.event.context.open_chat_id
113
125
  session.userId = body.event.operator.open_id
126
+ const { data } = await bot.internal.getImChat(session.channelId)
127
+ // TODO: add channel data
128
+ session.isDirect = data.chat_mode === 'p2p'
114
129
  }
115
130
  break
116
131
  }
@@ -119,7 +134,7 @@ export async function adaptSession<C extends Context>(bot: FeishuBot<C>, body: E
119
134
 
120
135
  // TODO: This function has many duplicated code with `adaptMessage`, should refactor them
121
136
  export async function decodeMessage(bot: LarkBot, body: Lark.Message, details = true): Promise<Universal.Message> {
122
- const json = JSON.parse(body.body.content) as MessageContentType<MessageType>
137
+ const json = JSON.parse(body.body.content)
123
138
  const assetEndpoint = trimSlash(bot.config.selfUrl ?? bot.ctx.server.config.selfUrl) + bot.config.path + '/assets'
124
139
  const content: h[] = []
125
140
  switch (body.msg_type) {