@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.
- package/lib/bot.d.ts +1 -1
- package/lib/http.d.ts +3 -2
- package/lib/index.js +35 -14
- package/lib/index.js.map +2 -3
- package/package.json +8 -4
- package/src/api/.eslintrc.yml +2 -0
- package/src/api/alitrip.ts +467 -0
- package/src/api/attendance.ts +81 -0
- package/src/api/badge.ts +285 -0
- package/src/api/blackboard.ts +28 -0
- package/src/api/calendar.ts +817 -0
- package/src/api/card.ts +215 -0
- package/src/api/conference.ts +561 -0
- package/src/api/connector.ts +97 -0
- package/src/api/contact.ts +56 -0
- package/src/api/convFile.ts +166 -0
- package/src/api/crm.ts +830 -0
- package/src/api/customerService.ts +156 -0
- package/src/api/datacenter.ts +672 -0
- package/src/api/devicemng.ts +202 -0
- package/src/api/diot.ts +19 -0
- package/src/api/doc.ts +232 -0
- package/src/api/drive.ts +109 -0
- package/src/api/edu.ts +30 -0
- package/src/api/esign.ts +44 -0
- package/src/api/exclusive.ts +372 -0
- package/src/api/h3yun.ts +537 -0
- package/src/api/hrm.ts +272 -0
- package/src/api/im.ts +978 -0
- package/src/api/industry.ts +153 -0
- package/src/api/jzcrm.ts +304 -0
- package/src/api/link.ts +94 -0
- package/src/api/live.ts +162 -0
- package/src/api/microApp.ts +309 -0
- package/src/api/oapi.ts +4083 -0
- package/src/api/oauth2.ts +146 -0
- package/src/api/pedia.ts +222 -0
- package/src/api/project.ts +1519 -0
- package/src/api/resident.ts +133 -0
- package/src/api/robot.ts +326 -0
- package/src/api/rooms.ts +334 -0
- package/src/api/serviceGroup.ts +216 -0
- package/src/api/storage.ts +1701 -0
- package/src/api/swform.ts +94 -0
- package/src/api/todo.ts +220 -0
- package/src/api/wiki.ts +231 -0
- package/src/api/workbench.ts +73 -0
- package/src/api/yida.ts +2165 -0
- package/src/bot.ts +129 -0
- package/src/http.ts +44 -0
- package/src/index.ts +9 -0
- package/src/internal.ts +47 -0
- package/src/message.ts +141 -0
- package/src/types/index.ts +140 -0
- package/src/utils.ts +53 -0
- 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
package/src/internal.ts
ADDED
|
@@ -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 ')
|
|
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 += ``
|
|
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
|
+
}
|