Skip to content

Commit

Permalink
feat(lark): init card support
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Oct 30, 2024
1 parent 29b8d90 commit 7b30ac1
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 28 deletions.
44 changes: 28 additions & 16 deletions adapters/lark/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,7 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
private quote: string | undefined
private textContent = ''
private richContent: MessageComponent.RichText.Paragraph[] = []
private cardElements: MessageComponent.Card[] | undefined
private richElements: MessageComponent.RichText.InlineElement[] | undefined

private flushRich() {
if (!this.textContent) return
this.richContent.push([{ tag: 'md', text: this.textContent }])
this.textContent = ''
}
private cardElements: MessageComponent.Card.Element[] | undefined

async post(data?: any) {
try {
Expand Down Expand Up @@ -47,15 +40,23 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
}
}

private flushText() {
if (!this.textContent) return
this.richContent.push([{ tag: 'md', text: this.textContent }])
this.cardElements?.push({ tag: 'markdown', content: this.textContent })
this.textContent = ''
}

async flush() {
if (this.textContent === '' && !this.cardElements && !this.richContent.length) return
this.flushText()
if (!this.cardElements && !this.richContent.length) return

if (this.cardElements) {
await this.post({
msg_type: 'interactive',
elements: this.cardElements,
})
} else if (this.richContent.length) {
} else {
await this.post({
msg_type: 'post',
content: JSON.stringify({ zh_cn: this.richContent }),
Expand All @@ -65,7 +66,6 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
// reset cached content
this.quote = undefined
this.textContent = ''
this.richElements = undefined
this.richContent = []
this.cardElements = undefined
}
Expand All @@ -79,13 +79,17 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
return image_key
}

async sendFile(_type: 'video' | 'audio' | 'file', url: string) {
async sendFile(_type: 'video' | 'audio' | 'file', attrs: any) {
const url = attrs.src || attrs.url
const payload = new FormData()

const { filename, type, data } = await this.bot.assetsQuester.file(url)
payload.append('file', new Blob([data], { type }), filename)
payload.append('file_name', filename)

if (attrs.duration) {
payload.append('duration', attrs.duration)
}

if (_type === 'audio') {
// FIXME: only support opus
payload.append('file_type', 'opus')
Expand All @@ -94,7 +98,7 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
payload.append('file_type', 'mp4')
} else {
const ext = filename.split('.').pop()
if (['xls', 'ppt', 'pdf'].includes(ext)) {
if (['doc', 'xls', 'ppt', 'pdf'].includes(ext)) {
payload.append('file_type', ext)
} else {
payload.append('file_type', 'stream')
Expand Down Expand Up @@ -135,14 +139,18 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
} else if (type === 'img' || type === 'image') {
const image_key = await this.createImage(attrs.src || attrs.url)
this.textContent += `![${attrs.alt ?? '图片'}](${image_key})`
this.flushRich()
this.flushText()
this.richContent.push([{ tag: 'img', image_key }])
} else if (['video', 'audio', 'file'].includes(type)) {
await this.flush()
await this.sendFile(type as any, attrs.src || attrs.url)
await this.sendFile(type as any, attrs)
} else if (type === 'figure' || type === 'message') {
await this.flush()
await this.render(children, true)
} else if (type === 'hr' || type === 'lark:hr' || type === 'feishu:hr') {
this.flushText()
this.richContent.push([{ tag: 'hr' }])
this.cardElements?.push({ tag: 'hr' })
} else if (type.startsWith('lark:') || type.startsWith('feishu:')) {
const tag = type.slice(type.split(':', 1)[0].length + 1)
if (tag === 'share-chat') {
Expand All @@ -169,6 +177,10 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
}),
})
this.textContent = ''
} else if (tag === 'card') {
await this.flush()
this.cardElements = []
await this.render(children, true)
}
} else {
await this.render(children)
Expand Down
118 changes: 116 additions & 2 deletions adapters/lark/src/types/message/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export namespace MessageComponent {
text: string
}

export interface HRElement extends BaseElement<'hr'> {}
export interface HorizontalRuleElement extends BaseElement<'hr'> {}

export interface MarkdownElement extends BaseElement<'md'> {
text: string
Expand All @@ -132,10 +132,124 @@ export namespace MessageComponent {
| ImageElement
| MediaElement
| CodeBlockElement
| HRElement
| HorizontalRuleElement

export type Paragraph =
| InlineElement[]
| [BlockElement]
}

export interface Card {
config: Card.Config
card_link?: Card.URLs
elements?: Card.Element[]
}

export namespace Card {
/** @see https://open.larksuite.com/document/common-capabilities/message-card/getting-started/card-structure/card-configuration */
export interface Config {
enable_forward?: boolean
update_multi?: boolean
}

export interface URLs {
url: string
pc_url?: string
ios_url?: string
android_url?: string
}

/** @see https://open.larksuite.com/document/common-capabilities/message-card/message-cards-content/card-header */
export interface Header {
title: PlainTextElement
subtitle?: PlainTextElement
template?: Header.Template
icon?: CustomIconElement
ud_icon?: StandardIconElement
text_tag_list?: TextTagElement[]
i18n_text_tag_list?: Record<string, TextTagElement[]>
}

export namespace Header {
export type Template = 'blue' | 'wathet' | 'turquoise' | 'green' | 'yellow' | 'orange' | 'red' | 'carmine' | 'violet' | 'purple' | 'indigo' | 'grey' | 'default'
}

export interface BaseElement<T extends string = string> {
tag: T
}

export type TextSize =
| 'heading-0' | 'heading-1' | 'heading-2' | 'heading-3' | 'heading-4' | 'heading'
| 'normal' | 'notation' | 'xxxx-large' | 'xxx-large' | 'xx-large' | 'x-large' | 'large' | 'medium' | 'small' | 'x-small'

export type TextAlign = 'left' | 'center' | 'right'

export interface PlainTextElement extends BaseElement<'plain_text'> {
content: string
i18n?: Record<string, string>
text_size?: TextSize
text_color?: string
text_align?: TextAlign
lines?: number
icon?: IconElement
}

export type IconElement = StandardIconElement | CustomIconElement

export interface CustomIconElement extends BaseElement<'custom_icon'> {
img_key: string
}

export interface StandardIconElement extends BaseElement<'standard_icon'> {
token: string
color?: string
}

export interface TextTagElement extends BaseElement<'text_tag'> {
text: PlainTextElement
color: TextTagElement.Color
}

export namespace TextTagElement {
export type Color = 'neutral' | 'blue' | 'torqoise' | 'lime' | 'orange' | 'violet' | 'indigo' | 'wathet' | 'green' | 'yellow' | 'red' | 'purple' | 'carmine'
}

export interface ImageElement extends BaseElement<'image'> {
img_key: string
alt?: PlainTextElement
title?: PlainTextElement
custom_width?: number
compact_width?: boolean
mode?: 'crop_center' | 'fit_horizontal' | 'large' | 'medium' | 'small' | 'tiny'
preview?: boolean
}

export interface HorizontalRuleElement extends BaseElement<'hr'> {}

export interface ParagraphElement extends BaseElement<'div'> {
text?: PlainTextElement
}

export interface MarkdownElement extends BaseElement<'markdown'> {
content: string
text_size?: TextSize
text_align?: TextAlign
href?: Record<string, URLs>
}

export interface HorizontalRuleElement extends BaseElement<'hr'> {}

export type Element =
| ParagraphElement
| MarkdownElement
| HorizontalRuleElement
}

export interface Template {
type: 'template'
data: {
template_id: string
template_variable: object
}
}
}
20 changes: 10 additions & 10 deletions adapters/lark/src/types/message/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Lark } from '..'
import { MessageContent } from './content'
import { MessageComponent } from './content'

export * from './content'

export type MessageType = 'text' | 'post' | 'image' | 'file' | 'audio' | 'media' | 'sticker' | 'interactive' | 'share_chat' | 'share_user'

export interface MessageContentMap {
'text': MessageContent.Text
'post': MessageContent.RichText
'image': MessageContent.Image
'file': MessageContent.File
'audio': MessageContent.Audio
'media': MessageContent.Media
'sticker': MessageContent.Sticker
'share_chat': MessageContent.ShareChat
'share_user': MessageContent.ShareUser
'text': MessageComponent.Text
'post': MessageComponent.RichText
'image': MessageComponent.Image
'file': MessageComponent.File
'audio': MessageComponent.Audio
'media': MessageComponent.Media
'sticker': MessageComponent.Sticker
'share_chat': MessageComponent.ShareChat
'share_user': MessageComponent.ShareUser
}

export type MessageContentType<T extends MessageType> = T extends keyof MessageContentMap ? MessageContentMap[T] : any
Expand Down

0 comments on commit 7b30ac1

Please sign in to comment.