@satorijs/adapter-dingtalk 2.0.3 → 2.0.5

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.
Files changed (56) hide show
  1. package/lib/bot.d.ts +1 -1
  2. package/lib/http.d.ts +3 -2
  3. package/lib/index.js +35 -14
  4. package/lib/index.js.map +2 -3
  5. package/package.json +8 -4
  6. package/src/api/.eslintrc.yml +2 -0
  7. package/src/api/alitrip.ts +467 -0
  8. package/src/api/attendance.ts +81 -0
  9. package/src/api/badge.ts +285 -0
  10. package/src/api/blackboard.ts +28 -0
  11. package/src/api/calendar.ts +817 -0
  12. package/src/api/card.ts +215 -0
  13. package/src/api/conference.ts +561 -0
  14. package/src/api/connector.ts +97 -0
  15. package/src/api/contact.ts +56 -0
  16. package/src/api/convFile.ts +166 -0
  17. package/src/api/crm.ts +830 -0
  18. package/src/api/customerService.ts +156 -0
  19. package/src/api/datacenter.ts +672 -0
  20. package/src/api/devicemng.ts +202 -0
  21. package/src/api/diot.ts +19 -0
  22. package/src/api/doc.ts +232 -0
  23. package/src/api/drive.ts +109 -0
  24. package/src/api/edu.ts +30 -0
  25. package/src/api/esign.ts +44 -0
  26. package/src/api/exclusive.ts +372 -0
  27. package/src/api/h3yun.ts +537 -0
  28. package/src/api/hrm.ts +272 -0
  29. package/src/api/im.ts +978 -0
  30. package/src/api/industry.ts +153 -0
  31. package/src/api/jzcrm.ts +304 -0
  32. package/src/api/link.ts +94 -0
  33. package/src/api/live.ts +162 -0
  34. package/src/api/microApp.ts +309 -0
  35. package/src/api/oapi.ts +4083 -0
  36. package/src/api/oauth2.ts +146 -0
  37. package/src/api/pedia.ts +222 -0
  38. package/src/api/project.ts +1519 -0
  39. package/src/api/resident.ts +133 -0
  40. package/src/api/robot.ts +326 -0
  41. package/src/api/rooms.ts +334 -0
  42. package/src/api/serviceGroup.ts +216 -0
  43. package/src/api/storage.ts +1701 -0
  44. package/src/api/swform.ts +94 -0
  45. package/src/api/todo.ts +220 -0
  46. package/src/api/wiki.ts +231 -0
  47. package/src/api/workbench.ts +73 -0
  48. package/src/api/yida.ts +2165 -0
  49. package/src/bot.ts +129 -0
  50. package/src/http.ts +44 -0
  51. package/src/index.ts +9 -0
  52. package/src/internal.ts +47 -0
  53. package/src/message.ts +141 -0
  54. package/src/types/index.ts +140 -0
  55. package/src/utils.ts +53 -0
  56. package/src/ws.ts +55 -0
package/src/bot.ts ADDED
@@ -0,0 +1,129 @@
1
+ import { Bot, Context, Quester, Schema } from '@satorijs/satori'
2
+ import { HttpServer } from './http'
3
+ import { DingtalkMessageEncoder } from './message'
4
+ import { WsClient } from './ws'
5
+ import { Internal } from './internal'
6
+
7
+ // https://open.dingtalk.com/document/orgapp/enterprise-created-chatbot
8
+ export class DingtalkBot<C extends Context = Context> extends Bot<C, DingtalkBot.Config> {
9
+ static MessageEncoder = DingtalkMessageEncoder
10
+
11
+ public oldHttp: Quester
12
+ public http: Quester
13
+ public internal: Internal
14
+ private refreshTokenTimer: NodeJS.Timeout
15
+
16
+ constructor(ctx: C, config: DingtalkBot.Config) {
17
+ super(ctx, config, 'dingtalk')
18
+ this.selfId = config.appkey
19
+ this.http = ctx.http.extend(config.api)
20
+ this.oldHttp = ctx.http.extend(config.oldApi)
21
+ this.internal = new Internal(this)
22
+
23
+ if (config.protocol === 'http') {
24
+ ctx.plugin(HttpServer, this)
25
+ } else if (config.protocol === 'ws') {
26
+ ctx.plugin(WsClient, this)
27
+ }
28
+ }
29
+
30
+ async getLogin() {
31
+ try {
32
+ const { appList } = await this.internal.listAllInnerApps()
33
+ const self = appList.find(v => v.agentId === this.config.agentId)
34
+ if (self) {
35
+ this.user.name = self.name
36
+ this.user.avatar = self.icon
37
+ return this.toJSON()
38
+ }
39
+ } catch (e) {
40
+ this.logger.warn(e)
41
+ }
42
+
43
+ const data = await this.internal.oapiMicroappList()
44
+ if (!data.appList) {
45
+ this.logger.error('getLogin failed: %o', data)
46
+ return this.toJSON()
47
+ }
48
+ const self = data.appList.find(v => v.agentId === this.config.agentId)
49
+ if (self) {
50
+ this.user.name = self.name
51
+ this.user.avatar = self.appIcon
52
+ }
53
+ return this.toJSON()
54
+ }
55
+
56
+ stop() {
57
+ clearTimeout(this.refreshTokenTimer)
58
+ return super.stop()
59
+ }
60
+
61
+ public token: string
62
+
63
+ async refreshToken() {
64
+ const data = await this.internal.getAccessToken({
65
+ appKey: this.config.appkey,
66
+ appSecret: this.config.secret,
67
+ })
68
+ this.logger.debug('gettoken result: %o', data)
69
+ this.token = data.accessToken
70
+ // https://open.dingtalk.com/document/orgapp/authorization-overview
71
+ this.http = this.http.extend({
72
+ headers: {
73
+ 'x-acs-dingtalk-access-token': data.accessToken,
74
+ },
75
+ }).extend(this.config.api)
76
+ this.refreshTokenTimer = setTimeout(this.refreshToken.bind(this), (data.expireIn - 10) * 1000)
77
+ }
78
+
79
+ // https://open.dingtalk.com/document/orgapp/download-the-file-content-of-the-robot-receiving-message
80
+ async downloadFile(downloadCode: string): Promise<string> {
81
+ const { downloadUrl } = await this.internal.robotMessageFileDownload({
82
+ downloadCode,
83
+ robotCode: this.selfId,
84
+ })
85
+ return downloadUrl
86
+ }
87
+
88
+ async deleteMessage(channelId: string, messageId: string): Promise<void> {
89
+ if (channelId.startsWith('cid')) {
90
+ await this.internal.orgGroupRecall({
91
+ robotCode: this.selfId,
92
+ processQueryKeys: [messageId],
93
+ openConversationId: channelId,
94
+ })
95
+ } else {
96
+ await this.internal.batchRecallOTO({
97
+ robotCode: this.selfId,
98
+ processQueryKeys: [messageId],
99
+ })
100
+ }
101
+ }
102
+ }
103
+
104
+ export namespace DingtalkBot {
105
+ export interface Config extends WsClient.Config {
106
+ secret: string
107
+ protocol: string
108
+ appkey: string
109
+ agentId?: number
110
+ api: Quester.Config
111
+ oldApi: Quester.Config
112
+ }
113
+
114
+ export const Config: Schema<Config> = Schema.intersect([
115
+ Schema.object({
116
+ protocol: process.env.KOISHI_ENV === 'browser'
117
+ ? Schema.const('ws').default('ws')
118
+ : Schema.union(['http', 'ws']).description('选择要使用的协议。').required(),
119
+ }),
120
+ Schema.object({
121
+ secret: Schema.string().required().description('机器人密钥。'),
122
+ agentId: Schema.number().description('AgentId'),
123
+ appkey: Schema.string().required(),
124
+ api: Quester.createConfig('https://api.dingtalk.com/v1.0/'),
125
+ oldApi: Quester.createConfig('https://oapi.dingtalk.com/'),
126
+ }),
127
+ WsClient.Config,
128
+ ])
129
+ }
package/src/http.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { Adapter, Context, Logger } from '@satorijs/satori'
2
+ import {} from '@satorijs/router'
3
+ import { DingtalkBot } from './bot'
4
+ import crypto from 'node:crypto'
5
+ import { Message } from './types'
6
+ import { decodeMessage } from './utils'
7
+
8
+ export class HttpServer<C extends Context = Context> extends Adapter<C, DingtalkBot<C>> {
9
+ static inject = ['router']
10
+
11
+ private logger: Logger
12
+
13
+ constructor(ctx: C, bot: DingtalkBot<C>) {
14
+ super(ctx)
15
+ this.logger = ctx.logger('dingtalk')
16
+ }
17
+
18
+ async connect(bot: DingtalkBot<C>) {
19
+ await bot.refreshToken()
20
+ await bot.getLogin()
21
+
22
+ // https://open.dingtalk.com/document/orgapp/receive-message
23
+ bot.ctx.router.post('/dingtalk', async (ctx) => {
24
+ const timestamp = ctx.get('timestamp')
25
+ const sign = ctx.get('sign')
26
+
27
+ if (!timestamp || !sign) return ctx.status = 403
28
+ const timeDiff = Math.abs(Date.now() - Number(timestamp))
29
+ if (timeDiff > 3600000) return ctx.status = 401
30
+ const signContent = timestamp + '\n' + bot.config.secret
31
+ const computedSign = crypto
32
+ .createHmac('sha256', bot.config.secret)
33
+ .update(signContent)
34
+ .digest('base64')
35
+
36
+ if (computedSign !== sign) return ctx.status = 403
37
+ const body = ctx.request.body as Message
38
+ this.logger.debug(body)
39
+ const session = await decodeMessage(bot, body)
40
+ this.logger.debug(session)
41
+ if (session) bot.dispatch(session)
42
+ })
43
+ }
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { DingtalkBot } from './bot'
2
+
3
+ export * from './bot'
4
+ export * from './utils'
5
+ export * from './types'
6
+ export * from './http'
7
+ export * from './message'
8
+
9
+ export default DingtalkBot
@@ -0,0 +1,47 @@
1
+ import { Dict, Quester } from '@satorijs/satori'
2
+ import { DingtalkBot } from './bot'
3
+
4
+ export class Internal {
5
+ constructor(private bot: DingtalkBot) { }
6
+
7
+ static define(routes: Dict<Partial<Record<Quester.Method, Record<string, boolean>>>>) {
8
+ for (const path in routes) {
9
+ for (const key in routes[path]) {
10
+ const method = key as Quester.Method
11
+ for (const name of Object.keys(routes[path][method])) {
12
+ const isOldApi = routes[path][method][name]
13
+ Internal.prototype[name] = async function (this: Internal, ...args: any[]) {
14
+ const raw = args.join(', ')
15
+ const url = path.replace(/\{([^}]+)\}/g, () => {
16
+ if (!args.length) throw new Error(`too few arguments for ${path}, received ${raw}`)
17
+ return args.shift()
18
+ })
19
+ const config: Quester.AxiosRequestConfig = {}
20
+ if (args.length === 1) {
21
+ if (method === 'GET' || method === 'DELETE') {
22
+ config.params = args[0]
23
+ } else {
24
+ config.data = args[0]
25
+ }
26
+ } else if (args.length === 2 && method !== 'GET' && method !== 'DELETE') {
27
+ config.data = args[0]
28
+ config.params = args[1]
29
+ } else if (args.length > 1) {
30
+ throw new Error(`too many arguments for ${path}, received ${raw}`)
31
+ }
32
+ const quester = isOldApi ? this.bot.oldHttp : this.bot.http
33
+ if (isOldApi) {
34
+ config.params = { ...config.params, access_token: this.bot.token }
35
+ }
36
+ try {
37
+ return await quester(method, url, config)
38
+ } catch (error) {
39
+ if (!Quester.isAxiosError(error) || !error.response) throw error
40
+ throw new Error(`[${error.response.status}] ${JSON.stringify(error.response.data)}`)
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
package/src/message.ts ADDED
@@ -0,0 +1,141 @@
1
+ import { Context, Dict, h, MessageEncoder } from '@satorijs/satori'
2
+ import { DingtalkBot } from './bot'
3
+ import FormData from 'form-data'
4
+ import { SendMessageData } from './types'
5
+
6
+ export const escape = (val: string) =>
7
+ val
8
+ .replace(/(?<!\u200b)[\*_~`]/g, '\u200B$&')
9
+ .replace(/([\\`*_{}[\]\-(#!>])/g, '\\$&')
10
+ .replace(/([\-\*]|\d\.) /g, '\u200B$&')
11
+ .replace(/^(\s{4})/gm, '\u200B&nbsp;&nbsp;&nbsp;&nbsp;')
12
+
13
+ export const unescape = (val: string) =>
14
+ val
15
+ .replace(/\u200b([\*_~`])/g, '$1')
16
+
17
+ export class DingtalkMessageEncoder<C extends Context = Context> extends MessageEncoder<C, DingtalkBot<C>> {
18
+ buffer = ''
19
+
20
+ /**
21
+ * Markdown: https://open.dingtalk.com/document/isvapp/robot-message-types-and-data-format
22
+ */
23
+ hasRichContent = true
24
+
25
+ async flush(): Promise<void> {
26
+ if (this.buffer.length && !this.hasRichContent) {
27
+ await this.sendMessage('sampleText', {
28
+ content: this.buffer,
29
+ })
30
+ } else if (this.buffer.length && this.hasRichContent) {
31
+ await this.sendMessage('sampleMarkdown', {
32
+ text: this.buffer.replace(/\n/g, '\n\n'),
33
+ })
34
+ }
35
+ }
36
+
37
+ // https://open.dingtalk.com/document/orgapp/the-robot-sends-a-group-message
38
+ async sendMessage<T extends keyof SendMessageData>(msgType: T, msgParam: SendMessageData[T]) {
39
+ const { processQueryKey } = this.session.isDirect ? await this.bot.internal.batchSendOTO({
40
+ msgKey: msgType,
41
+ msgParam: JSON.stringify(msgParam),
42
+ robotCode: this.bot.config.appkey,
43
+ userIds: [this.session.channelId],
44
+ }) : await this.bot.internal.orgGroupSend({
45
+ // https://open.dingtalk.com/document/orgapp/types-of-messages-sent-by-robots
46
+ msgKey: msgType,
47
+ msgParam: JSON.stringify(msgParam),
48
+ robotCode: this.bot.config.appkey,
49
+ openConversationId: this.channelId,
50
+ })
51
+ const session = this.bot.session()
52
+ session.messageId = processQueryKey
53
+ session.channelId = this.session.channelId
54
+ session.guildId = this.session.guildId
55
+ session.app.emit(session, 'send', session)
56
+ this.results.push({
57
+ id: processQueryKey,
58
+ })
59
+ }
60
+
61
+ // https://open.dingtalk.com/document/orgapp/upload-media-files?spm=ding_open_doc.document.0.0.3b166172ERBuHw
62
+ async uploadMedia(attrs: Dict) {
63
+ const { data, mime } = await this.bot.ctx.http.file(attrs.url, attrs)
64
+ const form = new FormData()
65
+ // https://github.com/form-data/form-data/issues/468
66
+ const value = process.env.KOISHI_ENV === 'browser'
67
+ ? new Blob([data], { type: mime })
68
+ : Buffer.from(data)
69
+ let type: string
70
+ if (mime.startsWith('image/') || mime.startsWith('video/')) {
71
+ type = mime.split('/')[0]
72
+ } else if (mime.startsWith('audio/')) {
73
+ type = 'voice'
74
+ } else {
75
+ type = 'file'
76
+ }
77
+ form.append('type', type)
78
+ form.append('media', value)
79
+ const { media_id } = await this.bot.oldHttp.post('/media/upload', form, {
80
+ headers: form.getHeaders(),
81
+ })
82
+ return media_id
83
+ }
84
+
85
+ private listType: 'ol' | 'ul' = null
86
+
87
+ async visit(element: h) {
88
+ const { type, attrs, children } = element
89
+ if (type === 'text') {
90
+ this.buffer += escape(attrs.content)
91
+ } else if (type === 'image' && attrs.url) {
92
+ // await this.flush()
93
+ // await this.sendMessage('sampleImageMsg', {
94
+ // photoURL: attrs.url
95
+ // })
96
+ this.buffer += `![${attrs.alt}](${attrs.url})`
97
+ } else if (type === 'message') {
98
+ await this.flush()
99
+ await this.render(children)
100
+ } else if (type === 'at') {
101
+ this.buffer += `@${attrs.id}`
102
+ } else if (type === 'br') {
103
+ this.buffer += '\n'
104
+ } else if (type === 'p') {
105
+ if (!this.buffer.endsWith('\n')) this.buffer += '\n'
106
+ await this.render(children)
107
+ if (!this.buffer.endsWith('\n')) this.buffer += '\n'
108
+ this.buffer += '\n'
109
+ } else if (type === 'b' || type === 'strong') {
110
+ this.buffer += ` **`
111
+ await this.render(children)
112
+ this.buffer += `** `
113
+ } else if (type === 'i' || type === 'em') {
114
+ this.buffer += ` *`
115
+ await this.render(children)
116
+ this.buffer += `* `
117
+ } else if (type === 'a' && attrs.href) {
118
+ this.buffer += `[`
119
+ await this.render(children)
120
+ this.buffer += `](${encodeURI(attrs.href)})`
121
+ } else if (type === 'ul' || type === 'ol') {
122
+ this.listType = type
123
+ await this.render(children)
124
+ this.listType = null
125
+ } else if (type === 'li') {
126
+ if (!this.buffer.endsWith('\n')) this.buffer += '\n'
127
+ if (this.listType === 'ol') {
128
+ this.buffer += `1. `
129
+ } else if (this.listType === 'ul') {
130
+ this.buffer += '- '
131
+ }
132
+ this.render(children)
133
+ this.buffer += '\n'
134
+ } else if (type === 'blockquote') {
135
+ if (!this.buffer.endsWith('\n')) this.buffer += '\n'
136
+ this.buffer += '> '
137
+ await this.render(children)
138
+ this.buffer += '\n\n'
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,140 @@
1
+ export type AtUser = {
2
+ dingtalkId: string
3
+ staffId?: string // 企业内部群有的发送者在企业内的userid
4
+ }
5
+
6
+ export type DingtalkRequestBase = {
7
+ msgtype: string // 消息类型
8
+ msgId: string // 加密的消息ID
9
+ createAt: string // 消息的时间戳,单位毫秒
10
+ conversationType: string // 1:单聊 2:群聊
11
+ conversationId: string // 会话ID
12
+ conversationTitle?: string // 群聊时才有的会话标题
13
+ senderId: string // 加密的发送者ID
14
+ senderNick: string // 发送者昵称
15
+ senderCorpId?: string // 企业内部群有的发送者当前群的企业corpId
16
+ sessionWebhook: string // 当前会话的Webhook地址
17
+ sessionWebhookExpiredTime: number // 当前会话的Webhook地址过期时间
18
+ isAdmin?: boolean // 是否为管理员
19
+ chatbotCorpId?: string // 加密的机器人所在的企业corpId
20
+ isInAtList?: boolean // 是否在@列表中
21
+ senderStaffId?: string // 企业内部群中@该机器人的成员userid
22
+ chatbotUserId: string // 加密的机器人ID
23
+ atUsers?: AtUser[] // 被@人的信息
24
+ robotCode: string
25
+ }
26
+
27
+ export type Message = TextMessage | RichTextMessage | PictureMessage | FileMessage
28
+
29
+ export interface TextMessage extends DingtalkRequestBase {
30
+ msgtype: 'text'
31
+ text: {
32
+ content: string
33
+ }
34
+ }
35
+
36
+ export interface FileMessage extends DingtalkRequestBase {
37
+ msgtype: 'file'
38
+ content: {
39
+ spaceId: string
40
+ fileName: string
41
+ downloadCode: string
42
+ fileId: string
43
+ }
44
+ }
45
+
46
+ export interface PictureMessage extends DingtalkRequestBase {
47
+ msgtype: 'picture'
48
+ content: {
49
+ downloadCode: string
50
+ }
51
+ }
52
+
53
+ export interface RichTextMessage extends DingtalkRequestBase {
54
+ msgtype: 'richText'
55
+ content: {
56
+ richText: ({
57
+ text: string
58
+ } & {
59
+ pictureDownloadCode: string
60
+ downloadCode: string
61
+ type: 'picture'
62
+ })[]
63
+ }
64
+ }
65
+
66
+ // https://open.dingtalk.com/document/orgapp/types-of-messages-sent-by-robots
67
+ export interface SendMessageData {
68
+ sampleText: { content: string }
69
+ sampleMarkdown: {
70
+ title?: string
71
+ text: string
72
+ }
73
+ sampleImageMsg: {
74
+ photoURL: string
75
+ }
76
+ sampleLink: {
77
+ text: string
78
+ title: string
79
+ picUrl: string
80
+ messageUrl: string
81
+ }
82
+ sampleAudio: {
83
+ mediaId: string
84
+ duration: string
85
+ }
86
+ sampleFile: {
87
+ mediaId: string
88
+ fileName: string
89
+ fileType: string
90
+ }
91
+ sampleVideo: {
92
+ duration: string
93
+ videoMediaId: string
94
+ videoType: string
95
+ picMediaId: string
96
+ }
97
+ }
98
+
99
+ export * from '../api/oauth2'
100
+ export * from '../api/oapi'
101
+ export * from '../api/contact'
102
+ export * from '../api/swform'
103
+ export * from '../api/hrm'
104
+ export * from '../api/todo'
105
+ export * from '../api/attendance'
106
+ export * from '../api/calendar'
107
+ export * from '../api/blackboard'
108
+ export * from '../api/microApp'
109
+ export * from '../api/im'
110
+ export * from '../api/connector'
111
+ export * from '../api/exclusive'
112
+ export * from '../api/alitrip'
113
+ export * from '../api/project'
114
+ export * from '../api/edu'
115
+ export * from '../api/crm'
116
+ export * from '../api/yida'
117
+ export * from '../api/drive'
118
+ export * from '../api/workbench'
119
+ export * from '../api/robot'
120
+ export * from '../api/conference'
121
+ export * from '../api/serviceGroup'
122
+ export * from '../api/customerService'
123
+ export * from '../api/esign'
124
+ export * from '../api/jzcrm'
125
+ export * from '../api/badge'
126
+ export * from '../api/datacenter'
127
+ export * from '../api/resident'
128
+ export * from '../api/wiki'
129
+ export * from '../api/storage'
130
+ export * from '../api/doc'
131
+ export * from '../api/diot'
132
+ export * from '../api/h3yun'
133
+ export * from '../api/link'
134
+ export * from '../api/pedia'
135
+ export * from '../api/devicemng'
136
+ export * from '../api/convFile'
137
+ export * from '../api/industry'
138
+ export * from '../api/live'
139
+ export * from '../api/card'
140
+ export * from '../api/rooms'
package/src/utils.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { Context, h } from '@satorijs/satori'
2
+ import { Message } from './types'
3
+ import { DingtalkBot } from './bot'
4
+
5
+ export async function decodeMessage<C extends Context>(bot: DingtalkBot<C>, body: Message) {
6
+ const session = bot.session()
7
+ session.type = 'message'
8
+ session.messageId = body.msgId
9
+ session.guildId = body.chatbotCorpId
10
+
11
+ if (body.conversationType === '1') {
12
+ session.channelId = session.userId
13
+ session.isDirect = true
14
+ } else {
15
+ const atUsers = body.atUsers.filter(v => v.dingtalkId !== body.chatbotUserId).map(v => h.at(v.staffId))
16
+ session.elements = [h.at(body.robotCode), ...atUsers]
17
+ session.channelId = body.conversationId
18
+ session.isDirect = false
19
+ }
20
+ if (body.conversationTitle) {
21
+ session.event.channel.name = body.conversationTitle
22
+ }
23
+
24
+ session.event.user = {
25
+ id: body.senderStaffId,
26
+ name: body.senderNick,
27
+ }
28
+ session.event.member = {
29
+ roles: body.isAdmin ? ['admin'] : [],
30
+ }
31
+ session.timestamp = +body.createAt
32
+ if (body.msgtype === 'text') {
33
+ session.elements = [h.text(body.text.content)]
34
+ } else if (body.msgtype === 'richText') {
35
+ const elements: h[] = []
36
+ for (const item of body.content.richText) {
37
+ if (item.text) elements.push(h.text(item.text))
38
+ if (item.downloadCode) {
39
+ const url = await bot.downloadFile(item.downloadCode)
40
+ elements.push(h.image(url))
41
+ }
42
+ }
43
+ session.elements = elements
44
+ } else if (body.msgtype === 'picture') {
45
+ session.elements = [h.image(await bot.downloadFile(body.content.downloadCode))]
46
+ } else if (body.msgtype === 'file') {
47
+ session.elements = [h.file(await bot.downloadFile(body.content.downloadCode))]
48
+ } else {
49
+ return
50
+ }
51
+ session.content = session.elements.join('')
52
+ return session
53
+ }
package/src/ws.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { Adapter, Context, Schema } from '@satorijs/satori'
2
+ import { DingtalkBot } from './bot'
3
+ import { decodeMessage } from './utils'
4
+
5
+ export class WsClient<C extends Context = Context> extends Adapter.WsClient<C, DingtalkBot<C>> {
6
+ async prepare() {
7
+ await this.bot.refreshToken()
8
+ await this.bot.getLogin()
9
+ const { endpoint, ticket } = await this.bot.http.post<{
10
+ endpoint: string
11
+ ticket: string
12
+ }>('/gateway/connections/open', {
13
+ clientId: this.bot.config.appkey,
14
+ clientSecret: this.bot.config.secret,
15
+ subscriptions: [
16
+ {
17
+ type: 'CALLBACK',
18
+ topic: '/v1.0/im/bot/messages/get',
19
+ },
20
+ ],
21
+ })
22
+ return this.bot.http.ws(`${endpoint}?ticket=${ticket}`)
23
+ }
24
+
25
+ accept() {
26
+ this.bot.online()
27
+ this.socket.addEventListener('message', async ({ data }) => {
28
+ const parsed = JSON.parse(data.toString())
29
+ this.bot.logger.debug(parsed)
30
+ if (parsed.type === 'SYSTEM') {
31
+ if (parsed.headers.topic === 'ping') {
32
+ this.socket.send(JSON.stringify({
33
+ code: 200,
34
+ headers: parsed.headers,
35
+ message: 'OK',
36
+ data: parsed.data,
37
+ }))
38
+ }
39
+ } else if (parsed.type === 'CALLBACK') {
40
+ this.bot.logger.debug(JSON.parse(parsed.data))
41
+ const session = await decodeMessage(this.bot, JSON.parse(parsed.data))
42
+ if (session) this.bot.dispatch(session)
43
+ this.bot.logger.debug(session)
44
+ }
45
+ })
46
+ }
47
+ }
48
+
49
+ export namespace WsClient {
50
+ export interface Config extends Adapter.WsClientConfig {}
51
+
52
+ export const Config: Schema<Config> = Schema.intersect([
53
+ Adapter.WsClientConfig,
54
+ ] as const)
55
+ }