Skip to content

Commit

Permalink
feat(dingtalk): add adapter-dingtalk
Browse files Browse the repository at this point in the history
  • Loading branch information
XxLittleCxX committed Jul 17, 2023
1 parent 7bb4967 commit 2d796a1
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 0 deletions.
31 changes: 31 additions & 0 deletions adapters/dingtalk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@satorijs/adapter-dingtalk",
"description": "Dingtalk Adapter for Satorijs",
"version": "1.0.1",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"files": [
"lib"
],
"author": "LittleC <[email protected]>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/satorijs/satori.git",
"directory": "adapters/dingtalk"
},
"bugs": {
"url": "https://github.com/satorijs/satori/issues"
},
"homepage": "https://koishi.chat/plugins/adapter/dingtalk.html",
"keywords": [
"bot",
"dingtalk",
"adapter",
"chatbot",
"satori"
],
"peerDependencies": {
"@satorijs/satori": "^2.5.3"
}
}
55 changes: 55 additions & 0 deletions adapters/dingtalk/src/bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Bot, Context, Logger, Quester, Schema, Universal } from '@satorijs/satori'

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

View workflow job for this annotation

GitHub Actions / lint

'Universal' is defined but never used
import { HttpServer } from './http'
import { DingtalkMessageEncoder } from './message'

const logger = new Logger('dingtalk')

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)
ctx.plugin(HttpServer, this)
}

tokenExpiresAt: number;

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

View workflow job for this annotation

GitHub Actions / lint

Extra semicolon
public token: string

async refreshToken() {
if (this.tokenExpiresAt && this.tokenExpiresAt > Date.now()) return
const data = await this.http.post('/oauth2/accessToken', {
appKey: this.config.appkey,
appSecret: this.config.secret

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

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
})
logger.debug('gettoken result: %o', data)
this.tokenExpiresAt = Date.now() + data.expireIn * 1000
this.token = data.accessToken
// https://open.dingtalk.com/document/orgapp/authorization-overview
this.http = this.http.extend({
headers: {
'x-acs-dingtalk-access-token': data.accessToken

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

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
}

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

View workflow job for this annotation

GitHub Actions / lint

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

export namespace DingtalkBot {
export interface Config extends Bot.Config, Quester.Config {
secret: string
protocol: string
appkey: string
}

export const Config: Schema<Config> = Schema.intersect([
Schema.object({
protocol: Schema.string().required(),
secret: Schema.string().required().description('机器人密钥。'),
appkey: Schema.string().required()

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

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
}),
Quester.createConfig("https://api.dingtalk.com/v1.0/")

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

View workflow job for this annotation

GitHub Actions / lint

Strings must use singlequote

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

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
])
}

DingtalkBot.prototype.platform = 'dingtalk'
53 changes: 53 additions & 0 deletions adapters/dingtalk/src/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Adapter, Context, Logger } from '@satorijs/satori'
import { DingtalkBot } from './bot'
import crypto from 'node:crypto'
import internal from 'stream'

Check failure on line 4 in adapters/dingtalk/src/http.ts

View workflow job for this annotation

GitHub Actions / lint

'internal' is defined but never used
import { TextMessage } from './types'

export class HttpServer extends Adapter.Server<DingtalkBot> {
logger = new Logger('dingtalk')
constructor(ctx: Context, bot: DingtalkBot) {
super()
}

async start(bot: DingtalkBot) {
await bot.refreshToken()
bot.selfId = bot.config.appkey
bot.ctx.router.post('/dingtalk', async (ctx) => {
const timestamp = ctx.get('timestamp');

Check failure on line 17 in adapters/dingtalk/src/http.ts

View workflow job for this annotation

GitHub Actions / lint

Extra semicolon
const sign = ctx.get('sign');

if (!timestamp || !sign) return ctx.status = 403
const timeDiff = Math.abs(Date.now() - Number(timestamp));
if (timeDiff > 3600000) return ctx.status = 401
const signContent = timestamp + "\n" + bot.config.secret;
const computedSign = crypto
.createHmac('sha256', bot.config.secret)
.update(signContent)
.digest('base64');

if (computedSign !== sign) return ctx.status = 403
const body = ctx.request.body as TextMessage
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
}
this.logger.debug(require('util').inspect(session, false, null, true))
bot.dispatch(session)
})
}
}
9 changes: 9 additions & 0 deletions adapters/dingtalk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { DingtalkBot } from './bot'

export * from './bot'
export * from './utils'
export * from './types'
export * from './http'
export * from './message'

export default DingtalkBot
27 changes: 27 additions & 0 deletions adapters/dingtalk/src/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { h, MessageEncoder } from '@satorijs/satori'
import { DingtalkBot } from './bot'

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

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({
content: this.buffer
}),
robotCode: this.bot.config.appkey,
openConversationId: this.channelId
}))
}


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

if (type === 'text') {
this.buffer += attrs.content
}
}
}
35 changes: 35 additions & 0 deletions adapters/dingtalk/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type AtUser = {
dingtalkId: string;
staffId?: string; // 企业内部群有的发送者在企业内的userid
};

export type DingtalkRequestBase = {
msgtype: string; // 消息类型
content: string; // 消息文本
msgId: string; // 加密的消息ID
createAt: string; // 消息的时间戳,单位毫秒
conversationType: string; // 1:单聊 2:群聊
conversationId: string; // 会话ID
conversationTitle?: string; // 群聊时才有的会话标题
senderId: string; // 加密的发送者ID
senderNick: string; // 发送者昵称
senderCorpId?: string; // 企业内部群有的发送者当前群的企业corpId
sessionWebhook: string; // 当前会话的Webhook地址
sessionWebhookExpiredTime: number; // 当前会话的Webhook地址过期时间
isAdmin?: boolean; // 是否为管理员
chatbotCorpId?: string; // 加密的机器人所在的企业corpId
isInAtList?: boolean; // 是否在@列表中
senderStaffId?: string; // 企业内部群中@该机器人的成员userid
chatbotUserId: string; // 加密的机器人ID
atUsers?: AtUser[]; // 被@人的信息
robotCode: string
};

export type Message = TextMessage

export interface TextMessage extends DingtalkRequestBase {
msgtype: "text"
text: {
content: string
}
}
2 changes: 2 additions & 0 deletions adapters/dingtalk/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { h, Session } from '@satorijs/satori'

10 changes: 10 additions & 0 deletions adapters/dingtalk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src",
},
"include": [
"src",
],
}

0 comments on commit 2d796a1

Please sign in to comment.