Skip to content

Commit

Permalink
feat(dingtalk): decode message, markdown content
Browse files Browse the repository at this point in the history
  • Loading branch information
XxLittleCxX committed Jul 17, 2023
1 parent 2d796a1 commit dcb269e
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 33 deletions.
12 changes: 12 additions & 0 deletions adapters/dingtalk/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import { DingtalkMessageEncoder } from './message'

const logger = new Logger('dingtalk')

// https://open.dingtalk.com/document/orgapp/enterprise-created-chatbot
export class DingtalkBot extends Bot<DingtalkBot.Config> {
static MessageEncoder = DingtalkMessageEncoder
public oldHttp: Quester
public http: Quester
constructor(ctx: Context, config: DingtalkBot.Config) {
super(ctx, config)
this.http = ctx.http.extend(config)
this.oldHttp = ctx.http.extend({
endpoint: 'https://oapi.dingtalk.com/'

Check failure on line 16 in adapters/dingtalk/src/bot.ts

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
})
ctx.plugin(HttpServer, this)
}

Expand All @@ -33,6 +37,14 @@ export class DingtalkBot extends Bot<DingtalkBot.Config> {
}

Check failure on line 37 in adapters/dingtalk/src/bot.ts

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
}).extend(this.config)
}

// https://open.dingtalk.com/document/orgapp/download-the-file-content-of-the-robot-receiving-message
async downloadFile(downloadCode: string): Promise<string> {
const { downloadUrl } = await this.http.post('/robot/messageFiles/download', {
downloadCode, robotCode: this.selfId

Check failure on line 44 in adapters/dingtalk/src/bot.ts

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
})
return downloadUrl
}
}

export namespace DingtalkBot {
Expand Down
27 changes: 6 additions & 21 deletions adapters/dingtalk/src/http.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Adapter, Context, Logger } from '@satorijs/satori'
import { DingtalkBot } from './bot'
import crypto from 'node:crypto'
import internal from 'stream'
import { TextMessage } from './types'
import { Message } from './types'
import { decodeMessage } from './utils'

export class HttpServer extends Adapter.Server<DingtalkBot> {
logger = new Logger('dingtalk')
Expand All @@ -13,6 +13,7 @@ export class HttpServer extends Adapter.Server<DingtalkBot> {
async start(bot: DingtalkBot) {
await bot.refreshToken()
bot.selfId = bot.config.appkey
// https://open.dingtalk.com/document/orgapp/receive-message
bot.ctx.router.post('/dingtalk', async (ctx) => {
const timestamp = ctx.get('timestamp');
const sign = ctx.get('sign');
Expand All @@ -27,27 +28,11 @@ export class HttpServer extends Adapter.Server<DingtalkBot> {
.digest('base64');

if (computedSign !== sign) return ctx.status = 403
const body = ctx.request.body as TextMessage
const body = ctx.request.body as Message
this.logger.debug(require('util').inspect(body, false, null, true))
const session = bot.session()
session.type = "message"
session.messageId = body.msgId
session.isDirect = body.conversationType === "1"
session.guildId = body.chatbotCorpId
session.channelId = body.conversationId
session.channelName = body.conversationTitle
session.userId = body.senderStaffId
session.author = {
userId: body.senderStaffId,
username: body.senderNick,
roles: body.isAdmin ? ['admin'] : [],
}
session.timestamp = Number(body.createAt)
if(body.msgtype === "text") {
session.content = body.text.content
}
const session = await decodeMessage(bot, body)
this.logger.debug(require('util').inspect(session, false, null, true))
bot.dispatch(session)
if(session) bot.dispatch(session)
})
}
}
130 changes: 120 additions & 10 deletions adapters/dingtalk/src/message.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,137 @@
import { h, MessageEncoder } from '@satorijs/satori'
import { Dict, h, MessageEncoder } from '@satorijs/satori'
import { DingtalkBot } from './bot'
import FormData from 'form-data'
import { SendMessageData } from './types'

export const escape = (val: string) =>
val
.replace(/(?<!\u200b)[\*_~`]/g, '\u200B$&')
.replace(/([\\`*_{}[\]\-(#!>])/g, '\\$&')
.replace(/([\-\*]|\d\.) /g, '\u200B$&')
.replace(/^(\s{4})/gm, '\u200B&nbsp;&nbsp;&nbsp;&nbsp;')

export const unescape = (val: string) =>
val
.replace(/\u200b([\*_~`])/g, '$1')

export class DingtalkMessageEncoder extends MessageEncoder<DingtalkBot> {
buffer = ''

/**
* Markdown: https://open.dingtalk.com/document/isvapp/robot-message-types-and-data-format
*/

hasRichContent = true
async flush(): Promise<void> {
console.log(await this.bot.http.post('/robot/groupMessages/send', {
// https://open.dingtalk.com/document/orgapp/types-of-messages-sent-by-robots
msgKey: 'sampleText',
msgParam: JSON.stringify({
if (this.buffer.length && !this.hasRichContent) {
await this.sendMessage("sampleText", {
content: this.buffer
}),
})
} else if (this.buffer.length && this.hasRichContent) {
await this.sendMessage("sampleMarkdown", {
text: this.buffer.replace(/\n/g, '\n\n')
})
}
}

// https://open.dingtalk.com/document/orgapp/the-robot-sends-a-group-message
async sendMessage<T extends keyof SendMessageData>(msgType: T, msgParam: SendMessageData[T]) {
console.log(this.session)
const { processQueryKey } = await this.bot.http.post(
this.session.isDirect ? '/robot/oToMessages/batchSend' : '/robot/groupMessages/send', {
// https://open.dingtalk.com/document/orgapp/types-of-messages-sent-by-robots
msgKey: msgType,

Check warning on line 43 in adapters/dingtalk/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 8 spaces but found 6
msgParam: JSON.stringify(msgParam),

Check warning on line 44 in adapters/dingtalk/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 8 spaces but found 6
robotCode: this.bot.config.appkey,

Check warning on line 45 in adapters/dingtalk/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 8 spaces but found 6
openConversationId: this.channelId
}))
...this.session.isDirect ? {

Check warning on line 46 in adapters/dingtalk/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 8 spaces but found 6
userIds: [this.session.channelId]

Check warning on line 47 in adapters/dingtalk/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 10 spaces but found 8
} : {

Check warning on line 48 in adapters/dingtalk/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 8 spaces but found 6
openConversationId: this.channelId

Check warning on line 49 in adapters/dingtalk/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 10 spaces but found 8
}

Check warning on line 50 in adapters/dingtalk/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 8 spaces but found 6
})

Check warning on line 51 in adapters/dingtalk/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 6 spaces but found 4
const session = this.bot.session()
session.messageId = processQueryKey
this.results.push(session)
}

// https://open.dingtalk.com/document/orgapp/upload-media-files?spm=ding_open_doc.document.0.0.3b166172ERBuHw
async uploadMedia(attrs: Dict) {
const { data, mime } = await this.bot.ctx.http.file(attrs.url, attrs)
const form = new FormData()
// https://github.com/form-data/form-data/issues/468
const value = process.env.KOISHI_ENV === 'browser'
? new Blob([data], { type: mime })
: Buffer.from(data)
let type;
if (mime.startsWith("image/") || mime.startsWith("video/")) {
type = mime.split("/")[0]
} else if (mime.startsWith("audio/")) {
type = "voice"
} else {
type = "file"
}
form.append("type", type)
form.append('media', value)
const { media_id } = await this.bot.oldHttp.post('/media/upload', form, {
headers: form.getHeaders()
})
return media_id
}

private listType: 'ol' | 'ul' = null

async visit(element: h) {
const { type, attrs, children } = element

if (type === 'text') {
this.buffer += attrs.content
console.log(attrs.content)
this.buffer += escape(attrs.content)
} else if (type === 'image' && attrs.url) {
// await this.flush()
// await this.sendMessage('sampleImageMsg', {
// photoURL: attrs.url
// })
this.buffer += `![${attrs.alt}](${attrs.url})`
} else if (type === 'message') {
await this.flush()
await this.render(children)
} else if (type === 'at') {
this.buffer += `@${attrs.id}`
} else if (type === 'p') {
this.buffer += '\n'
await this.render(children)
this.buffer += '\n'
} else if (type === 'b' || type === 'strong') {
this.buffer += ` **`
await this.render(children)
this.buffer += `** `
} else if (type === 'i' || type === 'em') {
this.buffer += ` *`
await this.render(children)
this.buffer += `* `
} else if (type === 'a' && attrs.href) {
this.buffer += `[`
console.log(children)
await this.render(children)
this.buffer += `](${encodeURI(attrs.href)})`
} else if (type === 'ul' || type === 'ol') {
this.listType = type
await this.render(children)
this.listType = null
} else if (type === 'li') {
if (!this.buffer.endsWith('\n')) this.buffer += '\n'
if (this.listType === 'ol') {
this.buffer += `1. `
} else if (this.listType === 'ul') {
this.buffer += '- '
}
this.render(children)
this.buffer += '\n'
} else if (type === 'blockquote') {
console.log(children)
if (!this.buffer.endsWith('\n')) this.buffer += '\n'
this.buffer += '> '
await this.render(children)
this.buffer += '\n\n'
}
}
}
56 changes: 54 additions & 2 deletions adapters/dingtalk/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export type AtUser = {

export type DingtalkRequestBase = {
msgtype: string; // 消息类型
content: string; // 消息文本
msgId: string; // 加密的消息ID
createAt: string; // 消息的时间戳,单位毫秒
conversationType: string; // 1:单聊 2:群聊
Expand All @@ -25,11 +24,64 @@ export type DingtalkRequestBase = {
robotCode: string
};

export type Message = TextMessage
export type Message = TextMessage | RichTextMessage

export interface TextMessage extends DingtalkRequestBase {
msgtype: "text"
text: {
content: string
}
}

export interface PictureMessage extends DingtalkRequestBase {
msgtype: "picture"
content: {
downloadCode: string
}
}

export interface RichTextMessage extends DingtalkRequestBase {
msgtype: "richText"
content: {
richText: ({
text: string
} & {
pictureDownloadCode: string
downloadCode: string
type: "picture"
})[]
}
}

// https://open.dingtalk.com/document/orgapp/types-of-messages-sent-by-robots
export interface SendMessageData {
sampleText: { content: string }
sampleMarkdown: {
title?: string
text: string
}
sampleImageMsg: {
photoURL: string
}
sampleLink: {
text: string
title: string
picUrl: string
messageUrl: string
}
sampleAudio: {
mediaId: string
duration: string
}
sampleFile: {
mediaId: string
fileName: string
fileType: string
}
sampleVideo: {
duration: string
videoMediaId: string
videoType: string
picMediaId: string
}
}
41 changes: 41 additions & 0 deletions adapters/dingtalk/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,43 @@
import { h, Session } from '@satorijs/satori'
import { Message } from './types';
import { DingtalkBot } from './bot';

export async function decodeMessage(bot: DingtalkBot, body: Message): Promise<Session> {
const session = bot.session()
session.type = "message"
session.messageId = body.msgId
session.isDirect = body.conversationType === "1"
session.guildId = body.chatbotCorpId

if (body.conversationTitle) session.channelName = body.conversationTitle
session.userId = body.senderStaffId
session.author = {
userId: body.senderStaffId,
username: body.senderNick,
roles: body.isAdmin ? ['admin'] : [],
}
session.timestamp = Number(body.createAt)
if (body.msgtype === "text") {
session.elements = [h.text(body.text.content)]
} else if (body.msgtype === 'richText') {
let elements: h[] = []
for (const item of body.content.richText) {
if (item.text) elements.push(h.text(item.text))
if (item.downloadCode) {
const url = await bot.downloadFile(item.downloadCode)
elements.push(h.image(url))
}
}
session.elements = elements
} else {
return
}
if (!session.isDirect) {
// group message
session.elements = [h.at(body.robotCode), ...session.elements]
session.channelId = body.conversationId
} else {
session.channelId = session.userId
}
return session
}

0 comments on commit dcb269e

Please sign in to comment.