Skip to content

Commit

Permalink
feat(lark): support input form / behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Oct 31, 2024
1 parent f00c1b9 commit 6f91d20
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 32 deletions.
82 changes: 60 additions & 22 deletions adapters/lark/src/message.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context, h, MessageEncoder } from '@satorijs/core'
import { Context, Dict, h, MessageEncoder } from '@satorijs/core'
import { LarkBot } from './bot'
import { BaseResponse, Lark, MessageContent } from './types'
import { extractIdType } from './utils'
Expand All @@ -9,7 +9,7 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
private richContent: MessageContent.RichText.Paragraph[] = []
private card: MessageContent.Card | undefined
private noteElements: MessageContent.Card.NoteElement.InnerElement[] | undefined
private actionElements: MessageContent.Card.ActionElement.InnerElement[] = []
private actionElements: MessageContent.Card.Element[] = []

async post(data?: any) {
try {
Expand Down Expand Up @@ -44,7 +44,7 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco

private flushText(button = false) {
if ((this.textContent || !button) && this.actionElements.length) {
this.card?.elements.push({ tag: 'action', actions: this.actionElements })
this.card!.elements.push({ tag: 'action', actions: this.actionElements, layout: 'flow' })
this.actionElements = []
}
if (this.textContent) {
Expand All @@ -60,9 +60,10 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco

async flush() {
this.flushText()
if (!this.card.elements && !this.richContent.length) return
if (!this.card && !this.richContent.length) return

if (this.card) {
this.bot.logger.debug('card', JSON.stringify(this.card.elements))
await this.post({
msg_type: 'interactive',
content: JSON.stringify({
Expand Down Expand Up @@ -129,6 +130,27 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
})
}

private createBehavior(attrs: Dict) {
const behaviors: MessageContent.Card.ActionBehavior[] = []
if (attrs.type === 'link') {
behaviors.push({
type: 'open_url',
default_url: attrs.href,
})
} else if (attrs.type === 'input') {
behaviors.push({
type: 'callback',
value: {
_satori_type: 'command',
content: attrs.text,
},
})
} else if (attrs.type === 'action') {
// TODO
}
return behaviors.length ? behaviors : undefined
}

async visit(element: h) {
const { type, attrs, children } = element
if (type === 'text') {
Expand Down Expand Up @@ -168,33 +190,49 @@ export class LarkMessageEncoder<C extends Context = Context> extends MessageEnco
this.flushText()
this.richContent.push([{ tag: 'hr' }])
this.card?.elements.push({ tag: 'hr' })
} else if (type === 'button') {
this.flushText(true)
const behaviors: MessageContent.Card.ActionBehavior[] = []
if (attrs.type === 'link') {
behaviors.push({
type: 'open_url',
default_url: attrs.href,
})
} else if (attrs.type === 'input') {
behaviors.push({
type: 'callback',
value: {
_satori_type: 'command',
content: attrs.text,
},
} else if (type === 'form') {
this.flushText()
const length = this.card?.elements.length
await this.render(children)
if (this.card?.elements.length > length) {
const elements = this.card?.elements.slice(length)
this.card.elements.push({
tag: 'form',
name: attrs.name || 'Form',
elements,
})
} else if (attrs.type === 'action') {
// TODO
}
} else if (type === 'input') {
this.flushText()
this.card?.elements.push({
tag: 'action',
actions: [{
tag: 'input',
name: attrs.name,
width: attrs.width,
label: attrs.label && {
tag: 'plain_text',
content: attrs.label,
},
placeholder: attrs.placeholder && {
tag: 'plain_text',
content: attrs.placeholder,
},
behaviors: this.createBehavior(attrs),
}],
})
} else if (type === 'button') {
this.card ??= { elements: [] }
this.flushText(true)
await this.render(children)
this.actionElements.push({
tag: 'button',
text: {
tag: 'plain_text',
content: this.textContent,
},
behaviors,
disabled: attrs.disabled,
behaviors: this.createBehavior(attrs),
})
this.textContent = ''
} else if (type === 'button-group') {
Expand Down
156 changes: 146 additions & 10 deletions adapters/lark/src/types/message/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,13 +328,15 @@ export namespace MessageContent {
export type InnerElement = IconElement | PlainTextElement | BaseImageElement
}

export interface ActionElement extends BaseElement<'action'> {
actions: ActionElement.InnerElement[]
layout?: 'bisected' | 'trisection' | 'flow'
export interface FormElement extends BaseElement<'form'> {
name: string
elements: Element[]
confirm?: ConfirmElement
}

export namespace ActionElement {
export type InnerElement = ButtonElement
export interface ActionElement extends BaseElement<'action'> {
actions: Element[]
layout?: 'bisected' | 'trisection' | 'flow'
}

export type ActionBehavior =
Expand All @@ -354,26 +356,157 @@ export namespace MessageContent {
value: Record<string, string>
}

export interface ButtonElement extends BaseElement<'button'> {
export interface BaseButtonElement extends BaseElement<'button'> {
text: PlainTextElement
type?: ButtonElement.Type
size?: ButtonElement.Size
width?: ButtonElement.Width
icon?: IconElement
hover_tips?: PlainTextElement
disabled?: boolean
behaviors?: ActionBehavior[]
}

export interface ButtonElement extends BaseButtonElement {
type?: ButtonElement.Type
width?: ButtonElement.Width
hover_tips?: PlainTextElement
disabled_tips?: PlainTextElement
confirm?: {
title: PlainTextElement
text: PlainTextElement
}
behaviors?: ActionBehavior[]
// form-related fields
name?: string
required?: boolean
action_type?: 'link' | 'request' | 'multi' | 'form_submit' | 'form_reset'
}

export interface ConfirmElement {
title: PlainTextElement
text: PlainTextElement
}

export interface InputElement extends BaseElement<'input'> {
name: string
required?: boolean
placeholder?: PlainTextElement
default_value?: string
disabled?: boolean
width?: 'default' | 'fill' | string
max_length?: number
input_type?: 'text' | 'multiline_text' | 'password'
show_icon?: boolean
rows?: number
auto_resize?: boolean
max_rows?: number
label?: PlainTextElement
label_position?: 'top' | 'left'
value?: string | object
behaviors?: ActionBehavior[]
confirm?: ConfirmElement
fallback?: {
tag?: string
text?: PlainTextElement
}
}

export interface OverflowElement extends BaseElement<'overflow'> {
width?: 'default' | 'fill' | string
options: OverflowElement.Option[]
value?: object
confirm?: ConfirmElement
}

export namespace OverflowElement {
export interface Option {
text?: PlainTextElement
multi_url?: URLs
value?: string
}
}

export interface BaseSelectElement<T extends string = string> extends BaseElement<T> {
type?: 'default' | 'text'
name?: string
required?: boolean
disabled?: boolean
placeholder?: PlainTextElement
width?: 'default' | 'fill' | string
confirm?: ConfirmElement
}

export interface OptionElement {
text: PlainTextElement
icon?: IconElement
value: string
}

export interface SelectElement extends BaseSelectElement<'select_static'> {
options: OptionElement[]
initial_option?: string
}

export interface MultiSelectElement extends BaseSelectElement<'multi_select_static'> {
options: OptionElement[]
selected_values?: string[]
}

export interface SelectPersonElement extends BaseSelectElement<'select_person'> {
options?: { value: string }[]
}

export interface MultiSelectPersonElement extends BaseSelectElement<'multi_select_person'> {
options?: { value: string }[]
selected_values?: string[]
}

export interface DatePickerElement extends BaseSelectElement<'date_picker'> {
initial_date?: string
value?: object
}

export interface TimePickerElement extends BaseSelectElement<'picker_time'> {
initial_time?: string
value?: object
}

export interface DateTimePickerElement extends BaseSelectElement<'picker_datetime'> {
initial_datetime?: string
value?: object
}

export interface CheckerElement extends BaseElement<'checker'> {
name?: string
checked?: boolean
disabled?: boolean
text?: CheckerElement.TextElement
overall_checkable?: boolean
button_area?: {
pc_display_rule?: 'always' | 'on_hover'
buttons?: CheckerElement.ButtonElement[]
}
checked_style?: {
show_strikethrough?: boolean
opacity?: number
}
margin?: string
padding?: string
confirm?: ConfirmElement
behaviors?: ActionBehavior[]
hover_tips?: PlainTextElement
disable_tips?: PlainTextElement
}

export namespace CheckerElement {
export interface TextElement extends PlainTextElement {
text_size?: 'normal' | 'heading' | 'notation'
text_color?: string
text_align?: TextAlign
}

export interface ButtonElement extends BaseButtonElement {
type: 'text' | 'primary_text' | 'danger_text'
}
}

export namespace ButtonElement {
export type Size = 'tiny' | 'small' | 'medium' | 'large'
export type Width = 'default' | 'fill' | string
Expand All @@ -389,6 +522,9 @@ export namespace MessageContent {
| ChartElement
| TableElement
| ImageElement
| FormElement
| InputElement
| ButtonElement
}

export interface Template {
Expand Down
3 changes: 3 additions & 0 deletions adapters/lark/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ export async function adaptSession<C extends Context>(bot: LarkBot<C>, body: Eve
for (const [key, value] of Object.entries(options)) {
content += ` --${key} ${value}`
}
if (body.event.action.input_value) {
content += ` ${body.event.action.input_value}`
}
session.content = content
session.messageId = body.event.context.open_message_id
session.channelId = body.event.context.open_chat_id
Expand Down

0 comments on commit 6f91d20

Please sign in to comment.