Skip to content

Commit

Permalink
feat: 支持webhook并添加test
Browse files Browse the repository at this point in the history
  • Loading branch information
ineo6 committed May 15, 2020
1 parent 7d9085c commit 570ea5b
Show file tree
Hide file tree
Showing 13 changed files with 522 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
.idea
package-lock.json
yarn.lock
/test-results/
43 changes: 40 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,50 @@

该方案目前是简单实现,`conversationId`的获取还没想到比较好的方式,因为时间问题,临时获取方式是在控制台中输出。

## 配置Webhook主动发送

在机器人目录`conf`目录(没有请创建),添加`dingtalk-room.json`文件。

同一个机器人在不同群的`access_token`是不一样的,所以要主动发送消息到群是要指定`access_token`

这里通过维护别名的方式来实现发送到指定群,`room`是自定义的名称,可以设置成群聊名称一样,`env``access_token`的环境变量名称。

```json
[
{
"room": "room1",
"env": "HUBOT_DINGTALK_ACCESS_TOKEN"
},
{
"room": "room2",
"env": "HUBOT_DINGTALK_ACCESS_TOKEN2"
}
]
```

### 如何发送?

调用`robot.messageRoom`时,传入你想要发送的群聊别名即可。

```coffeescript
module.exports = (robot) ->
robot.on "dingtalk", (params) ->
robot.messageRoom 'room1', "response"
```

## Todo

- 接入主动发消息webhook
- 优化消息显示
- [x] 接入主动发消息webhook
- [ ] 优化消息显示

## 反馈

| Github Issue | 钉钉群 |
| --- | --- |
| [issues](https://github.com/ineo6/hubot-dingtalk/issues) | <img src="https://cdn.jsdelivr.net/gh/ineo6/hubot-dingtalk/dingtalk-group.JPG" width="260" /> |

## 如果喜欢的话

如果喜欢的话,欢迎请我喝一杯咖啡。`star`,`follow`也是对我工作的肯定和鼓励。

<img src="https://github.com/ineo6/hubot-dingtalk/blob/master/wechat-like.jpeg" alt="wechat-like" width=256 height=256 />
<img src="https://cdn.jsdelivr.net/gh/ineo6/hubot-dingtalk/wechat-like.jpeg" alt="wechat-like" width=256 height=256 />
58 changes: 58 additions & 0 deletions __tests__/adapter/receive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const got = require('got');

const context = require('../helpers/Context')
const { sign256 } = require("../helpers/util");

beforeAll(() => context.begin())
afterAll(() => context.end())

test('Adapter can receive messages', async () => {

const data = {
"msgtype": "text",
"text": {
"content": "我就是我, 是不一样的烟火"
},
"msgId": "XXXX",
"createAt": 1487561654123,
"conversationType": "2",
"conversationId": "XXXX",
"conversationTitle": "钉钉群标题",
"senderId": "XXXX",
"senderNick": "星星",
"senderCorpId": "XXXX",
"senderStaffId": "XXXX",
"chatbotUserId": "XXXX",
"atUsers": [
{
"dingtalkId": "XXXX",
"staffId": "XXXX"
}
]
};

const timeStamp = Date.now();
const sign = sign256(context.app.config.secret, timeStamp);

const res = await got.post(`http://localhost:8081/hubot/dingtalk/message/`, {
json: data,
headers: {
sign: sign,
timeStamp: timeStamp,
}
});

const textMessage = context.app.robot.receive.mock.calls[0][0]

expect(textMessage).toMatchObject({
id: data.createAt,
text: data.text.content,
user: {
id: data.senderId
}
})
expect(res.statusCode).toEqual(200)
expect(JSON.parse(res.body)).toMatchObject({
msgtype: "empty"
})
})
28 changes: 28 additions & 0 deletions __tests__/adapter/send.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const Robot = require("dingtalk-robot-sdk")

const context = require('../helpers/Context')
const util = require('../helpers/util')
const dingtalk = require('../helpers/mocks/dingtalk')

const Text = Robot.Text;

beforeAll(() => context.begin())
afterAll(async (done) => {
await context.end();
done();
})

test('Adapter.send sends message to Dingtalk', async () => {
const sendMessage = util.waitForRequest(dingtalk.sendMessage);

context.app.adapter.send({
room: "room"
}, 'test message')

const res = await sendMessage

const text = new Text();
text.setContent("test message");

expect(res.body).toEqual(text.get())
})
55 changes: 55 additions & 0 deletions __tests__/helpers/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const FakeRobot = require('./mocks/robot')

class App {
async start() {
this.config = {
secret: "test-client-secret",
type: "sign",
accessToken: "test-client-id",
}

process.env['HUBOT_DINGTALK_ACCESS_TOKEN'] = this.config.accessToken;
process.env['HUBOT_DINGTALK_SECRET'] = this.config.secret;
process.env['HUBOT_DINGTALK_AUTH_TYPE'] = this.config.type;

const dingtalkAdapter = require('../../dingtalk')

this.robot = new FakeRobot(__dirname, "dingtalk", true, "jarvis")

await this.robot.init();

this.adapter = dingtalkAdapter.use(this.robot)
this.client = null;

// load conf
const conf = this.adapter.read(__dirname)

conf.forEach((config) => {
if (config.room && config.env && process.env[config.env]) {
this.robot.brain.set(config.room, process.env[config.env])
}
})

await new Promise((resolve, reject) => {
this.adapter.once('connected', () => {
resolve(this.adapter)
})

this.adapter.once('error', (error) => {
reject(error)
})


this.adapter.run()
})

return this
}

async stop() {
this.robot.shutdown();
return this.adapter.close()
}
}

module.exports = App
29 changes: 29 additions & 0 deletions __tests__/helpers/Context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const App = require('./App')

class Context {
constructor(options = { app: true, worker: false }) {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000
this.client = null
this.app = null
}

async begin() {
const startApp = (new App()).start()
const app = await startApp

this.client = app.client

this.app = app

return this
}

async end() {
if (this.app) {
await this.app.stop()
}
}
}

module.exports = new Context()

10 changes: 10 additions & 0 deletions __tests__/helpers/conf/dingtalk-room.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"room": "room",
"env": "HUBOT_DINGTALK_ACCESS_TOKEN"
},
{
"room": "room2",
"env": "HUBOT_DINGTALK_ACCESS_TOKEN2"
}
]
15 changes: 15 additions & 0 deletions __tests__/helpers/mocks/dingtalk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const nock = require('nock')

const apiUrl = 'https://oapi.dingtalk.com';

function sendMessage(onReply, {} = {}) {
return nock(apiUrl)
.post(uri => uri.includes('/robot/send'))
.reply(200, onReply({
errmsg: 'ok'
}))
}

module.exports = {
sendMessage,
}
139 changes: 139 additions & 0 deletions __tests__/helpers/mocks/robot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
const { EventEmitter } = require('events')
const HttpClient = require('scoped-http-client')
const express = require('express')
const net = require('net')

const http = function (url, options) {
return HttpClient.create(url, options).header('User-Agent', `Hubot`)
}

class FakeHttp {
constructor(url) {
this.url = url;
}

header() {
return this;
}

post() {
return function (cb) {
console.log(cb.toString())
cb && cb()

return (function (_this) {
return function (callback) {
if (callback) {
return callback(null, {}, {});
}
return _this;
};
})(this);
}
}
}

function portIsOccupied(port) {
const server = net.createServer().listen(port)
return new Promise((resolve, reject) => {
server.on('listening', () => {
// console.log(`the server is runnint on port ${port}`)
server.close()
resolve(port)
})

server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
resolve(portIsOccupied(port + (Math.floor((Math.random() * 10) + 1))))
// console.log(`this port ${port} is occupied.try another.`)
} else {
reject(err)
}
})
})
}

class FakeRobot extends EventEmitter {
constructor() {
super()
this.logger = {
log: function () {
},
info: function () {
},
error: function () {
},
debug: function () {
},
}
this.receive = jest.fn()

this.http = http;
this.sockets = [];

this.brain = {
get: function (name) {
if (name === "room") {
return "token"
}

return "";
},
set: function () {
},
}

this.respondPattern = jest.fn()
}

async init() {
const app = express()

app.use((req, res, next) => {
res.setHeader('X-Powered-By', `hubot`)
return next()
})

app.use(express.query())

app.use(express.json())

const defaultPort = process.env.EXPRESS_PORT || process.env.PORT || 8081
const address = process.env.EXPRESS_BIND_ADDRESS || process.env.BIND_ADDRESS || '0.0.0.0'

try {
let port = await portIsOccupied(defaultPort)

this.server = app.listen(port, address)

this.server.on('error', async (err) => {
port = await portIsOccupied(defaultPort + 1)

this.server = app.listen(port, address)
})

this.router = app

return port;
} catch (error) {
const err = error
this.logger.error(`Error trying to start HTTP server: ${err}\n${err.stack}`)
}
}

closeServer() {
this.sockets.forEach(function (socket) {
socket.end();
socket.destroy();
});
this.server.close()
}

shutdown() {
if (this.server) {
this.closeServer()
}
}
}

module.exports = FakeRobot

0 comments on commit 570ea5b

Please sign in to comment.