sayi.github.com icon indicating copy to clipboard operation
sayi.github.com copied to clipboard

Hook机器人

Open Sayi opened this issue 8 years ago • 0 comments

最初看到这些机器人是在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,如下图。 image

下文,重点介绍下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:控制台日志打印和浏览器实时推送。 image

在每个平台的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);
		}
	}

}

浏览器实时效果如下: browser img

More 更多

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

Sayi avatar Aug 15 '17 02:08 Sayi