Hook机器人
最初看到这些机器人是在bearychat上,后来钉钉也做了类似的机器人,自己也写了点代码,聊当无聊至极之随便写写。GitHub代码地址
本文简单的实现了机器人简单的功能,通过webhook接受平台的事件,从而触发机器人执行SAP(storage、analysis、push)等功能。 文末讲了一种典型的应用场景:基于WebSocket的浏览器实时推送。
从GitHub Hook说起
Webhooks允许您构建或设置GitHub应用程序,该应用程序订阅GitHub.com上的某些事件。 当其中一个事件被触发时,我们会发送一个HTTP POST负载到webhook配置的URL。 Webhook可用于更新外部问题跟踪器,触发CI构建,更新备份镜像,甚至部署到生产服务器。
关于每个平台的WebHook的设置,hook的事件类型,事件的数据结构,都可以在文档中找到。GitHub WebHook的文档地址https://developer.github.com/webhooks/
Hook机器人实现的就是这样的一个应用,聚合不同平台的事件,存储,分析,消息通知等。在GitHub项目的Setting中可以配置自己的hook url,如下图。

下文,重点介绍下SAP功能。
Storage
框架使用SpringMVC,因为每个平台的事件结构不同,所以存储采用了文档型数据库Mongo db的方案。一个典型的GitHub push 事件接受服务如下:
package com.deepoove.hooks.web.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.deepoove.hooks.ThreadPoolFactory;
import com.deepoove.hooks.core.GitHubHook;
import com.deepoove.hooks.web.body.github.GitHubPushEvent;
@RestController
@RequestMapping("/github")
public class GitHubHookController {
@Autowired
private GitHubHook gitHubHook;
@RequestMapping("/={hookerId}")
public void hook(@RequestHeader("X-GitHub-Event") String event, @PathVariable String hookerId,
@RequestBody(required = false) GitHubPushEvent pushEvent) {
ThreadPoolFactory.getThreadPool().execute(() -> {
switch (event) {
case "push":
gitHubHook.hook(hookerId, pushEvent);
break;
}
});
return;
}
}
在抽象模板类Hook.java中,实现了模板方法:
public void hook(String hookerId, Object event) {
logger.info("pushEvent:" + JSON.toJSONString(event));
if (null == event) throw new IllegalArgumentException("event cannot be null");
//存储原始事件数据
saveEvent(event);
Stream stream = convert2Stream(event);
stream.setHookerId(hookerId);
//处理事件
if (null != handler) handler.handHook(stream);
//存储Stream
saveStream(stream);
}
所有平台,默认要做的就是实现convert2Stream方法,转化统一的数据结构。GitHubHook.java继承于模板Hook,实现了convert2Stream方法。
protected Stream convert2Stream(Object event) {
GitHubPushEvent pushEvent = (GitHubPushEvent) event;
Stream stream = new Stream();
StringBuffer sb = new StringBuffer();
sb.append("pushed ").append(pushEvent.getCommits().size())
.append(" commit(s) to ").append(pushEvent.getRef())
.append(" at ").append(pushEvent.getRepository().getFullName());
stream.setText(sb.toString());
MDBuilder md = new MDBuilder();
md.append("pushed ").append(pushEvent.getCommits().size())
.append(" commit(s) to ").append(pushEvent.getRef())
.append(" at ")
.appendLink(pushEvent.getRepository().getFullName(),
pushEvent.getRepository().getHtmlUrl());
stream.setMdText(md.toString());
stream.setUserAvatar(pushEvent.getSender().getAvatarUrl());
stream.setUserName(pushEvent.getPusher().getName());
List<String> attachments = new ArrayList<>();
List<String> mdAttachments = new ArrayList<>();
for (Commits commit : pushEvent.getCommits()) {
attachments.add(commit.getId() + " : " + commit.getMessage());
mdAttachments.add(new MDBuilder()
.appendLink(commit.getId(), commit.getUrl()).append(":")
.append(commit.getMessage()).toString());
}
stream.setAttachments(attachments);
stream.setMdAttachments(mdAttachments);
stream.setCreated(LocalDateTime.now());
return stream;
}
至此,我们针对不同平台,实现不同的Hook,可以完整的将事件数据记录在mongo中。
Analysis
待续。
Push
我们注意到,在Hook.java的模板方法中,有如下一段代码:
if (null != handler) handler.handHook(stream);
这段代码就是对事件的处理。默认实现了两个Handler:控制台日志打印和浏览器实时推送。

在每个平台的Hook实现类中,可以自定义Handler的顺序(采用了责任链的设计),以及定义哪些Handler来处理此平台的事件。
基于WebSocket的浏览器实时推送
浏览器实时推送采用了WebSocket方案。 前端使用了可重连的reconnecting-websocket。
var webSocket = new ReconnectingWebSocket("ws://115.29.10.121:9080/hooks/socket", null, {
bug: true,
reconnectInterval: 1000
});
webSocket.onerror = function(event) {
console.log(event);
};
webSocket.onopen = function(event) {
console.log("onpen" + event);
};
webSocket.onmessage = function(event) {
console.log("onMessage:" + event.data);
addScreean($.parseJSON(event.data), true);
};
webSocket.onclose = function () {
console.log("reconnect");
};
服务端需要引入jar包:
compile group: 'javax.websocket', name: 'javax.websocket-api', version: '1.1'
服务的端定义Server Endpoint。
package com.deepoove.hooks.socket;
import javax.websocket.CloseReason;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint("/socket")
public class WebSocketServer {
public static SessionHandler sessionHandler = SessionHandler.getInstance();
public WebSocketServer() {
System.out.println("====init WebSocketServer===" + this);
}
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
sessionHandler.addSession(session);
System.out.println("WebSocket opened: " + session.getId());
}
@OnMessage
public void handleMessage(String message, Session session) {
System.out.println("receiver message:" + message);
}
@OnClose
public void onClose(Session session, CloseReason closeReason) {
sessionHandler.removeSession(session);
}
@OnError
public void onError(Throwable error) {}
}
通过SessionHandler我们可以向每个session发送消息。
package com.deepoove.hooks.socket;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.websocket.Session;
public class SessionHandler {
private final Set<Session> sessions = new HashSet<>();
private static SessionHandler instance = new SessionHandler();
public static SessionHandler getInstance() {
return instance;
}
public void addSession(Session session) {
sessions.add(session);
}
public void removeSession(Session session) {
sessions.remove(session);
}
public void sendToAllConnectedSessions(String message) {
for (Session session : sessions) {
sendToSession(session, message);
}
}
private void sendToSession(Session session, String message) {
try {
System.out.println("send to:" + session.getId() + "===" + message);
session.getBasicRemote().sendText(message);
} catch (IOException ex) {
sessions.remove(session);
}
}
}
浏览器实时效果如下:

More 更多
如果接入的是外网的平台,那么Hook服务的请求合法性是必须要考虑的。 同样,除了浏览器实时显示,我们可以类似钉钉,推送到自己的移动APP上。