RocketChat研究

什么是RocketChat

Rocket.Chat 是特性最丰富的 Slack 开源替代品之一。github仓库地址,主要功能:群组聊天,直接通信,私聊群,桌面通知,媒体嵌入,链接预览,文件上传,语音/视频 聊天,截图等等。Rocket.Chat 原生支持 Windows,Mac OS X ,Linux,iOS 和 Android 平台。Rocket.Chat 通过 hubot 集成了非常流行的服务,比如 GitHub,GitLab,Confluence,JIRA 等等

mendix的Buzz功能分析

主页聊天

  • 发起投票、发消息

  • 消息窗口可以分为组织层(全局)以及各个项目层(数据是相互隔离的,项目层窗口也看不到组织层窗口内容)

  • 回复消息在相应消息的下面(类似论坛帖子)

在具体页面聊天

  • 右侧显示聊天窗口,仅针对当前页面

  • 该窗口的聊天信息在项目层的聊天窗口中展示,并带上链接(关联具体页面)

RocketChat的功能分析

架构

采用MongoDB做数据存储,用Meteor构建的js服务端,可以提供GraphQL APIREST API实时API的请求方式

image

功能分析

频道

可以设置频道的自定义字段

  • 公共频道:使用渠道进行对整个团队开放的对话。您的团队中的任何人都可以加入频道

频道下面可以创建讨论(邀请部分成员),讨论的内容会显示在频道下

  • 私人团体:私人团体只能通过邀请加入

  • 私聊:1对1

用REST API请求接口

  • 注入maven依赖retrofit
1
2
3
4
5
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.5.0</version>
</dependency>
  • 请求接口:RequestInterface
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @author shinan.chen
* @since 2019/3/3
*/
public interface RequestInterface {
@POST("api/v1/login")
Call<String> userLogin(@Body UserLogin userLogin);

@GET("api/v1/channels.list")
Call<String> getChannelsList(@Query("query")String query);

@POST("api/v1/chat.sendMessage")
Call<String> sendMessage(@Body SendMessage message);

@POST("api/v1/chat.reportMessage")
Call<String> reportMessage(@Body ReportMessage message);

@GET("api/v1/channels.messages")
Call<String> getAllMessageByRid(@Query("roomId") String roomId);

@POST("api/v1/rooms.createDiscussion")
Call<String> createDiscussion(@Body Discussion discussion);

@POST("api/v1/users.create")
Call<String> createUser(@Body UserCreate create);

@POST("api/v1/channels.setCustomFields")
Call<String> channelSetCustomFields(@Body ChannelCustField channelCustField);
}
  • 请求拦截器增加token信息:AddCookiesInterceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author shinan.chen
* @since 2019/8/2
*/
public class AddCookiesInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request.Builder builder = chain.request().newBuilder();
String authToken = RestMain.headerMap.get("X-Auth-Token");
String userId = RestMain.headerMap.get("X-User-Id");
if (authToken != null) {
builder.addHeader("X-Auth-Token", authToken);
}
if (userId != null) {
builder.addHeader("X-User-Id", userId);
}
return chain.proceed(builder.build());
}
}
  • 发送REST请求:RestMain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* @author shinan.chen
* @since 2019/7/31
*/
public class RestMain {
public static final Logger LOGGER = LoggerFactory.getLogger(RestMain.class);
public static Map<String, String> headerMap = new HashMap<>();

public static void main(String[] args) {
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new AddCookiesInterceptor())
.build();
//创建 Retrofit 实例
Retrofit retrofit = new Retrofit.Builder()
//设置网络请求url地址
.baseUrl("http://localhost:3000")
//设置数据解析器
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
//创建网络请求接口实例
RequestInterface requestInterface = retrofit.create(RequestInterface.class);
//获取封装的请求
UserLogin login = new UserLogin();
login.setUser("admin");
login.setPassword("csn000000");
Call<String> call = requestInterface.userLogin(login);
//发送网络请求(同步)
try {
Response<String> response = call.execute();
System.out.println(response.body());
headerMap.put("X-Auth-Token", ((Map<String, String>) (new Gson().fromJson(response.body(), Map.class).get("data"))).get("authToken"));
headerMap.put("X-User-Id", ((Map<String, String>) (new Gson().fromJson(response.body(), Map.class).get("data"))).get("userId"));
System.out.println(headerMap);
} catch (IOException e) {
e.printStackTrace();
}
//获取频道列表
Map<String, Object> channels = request("获取频道列表", requestInterface.getChannelsList(null));
String rid = (String) ((Map) ((List) channels.get("channels")).get(0)).get("_id");
String rname = (String) ((Map) ((List) channels.get("channels")).get(0)).get("name");
//发送聊天信息
Map<String, Object> sendMessage = request("发送聊天信息", requestInterface.sendMessage(new SendMessage(SendMessageParam.forSendMessage(null, "from java", rid))));
String mid = (String) ((Map) sendMessage.get("message")).get("_id");
//举报聊天信息
Map<String, Object> reportMessage = request("举报聊天信息", requestInterface.reportMessage(new ReportMessage(mid, "report java")));
//根据房间id获取所有聊天信息
Map<String, Object> allMessage = request("根据房间id获取所有聊天信息", requestInterface.getAllMessageByRid(rid));
//创建讨论
Map<String, Object> createDiscussion = request("创建讨论", requestInterface.createDiscussion(new Discussion(rid, "新讨论")));
//创建用户
Map<String, Object> createUser = request("创建用户", requestInterface.createUser(new UserCreate("cstt", "574466609@qq.com", "csn000000", "cstt")));
//给房间添加自定义字段
ChannelCustField channelCustField = new ChannelCustField();
channelCustField.setRoomId(rid);
channelCustField.setRoomName(rname);
Map<String, String> cusFieldMap = new HashMap<>(2);
cusFieldMap.put("organizationId", "1");
channelCustField.setCustomFields(cusFieldMap);
Map<String, Object> channelSetCustomFields = request("给房间添加自定义字段", requestInterface.channelSetCustomFields(channelCustField));
//获取频道列表,根据自定义搜索
Map<String, Object> channelsBySearch = request("获取频道列表,根据自定义搜索", requestInterface.getChannelsList("{ \"customFields.organizationId\": { \"$eq\": \"1\" } }"));
}

private static Map<String, Object> request(String log, Call<String> call) {
System.out.println(log);
try {
Response<String> response = call.execute();
System.out.println(response.body());
return Json2Map.json2Map(response.body());
} catch (Exception e) {
LOGGER.error(e.getMessage());
}
return null;
}
}
  • 运行结果

image

用实时API请求接口(websocket)

  • 注入maven依赖java-websocket客户端
1
2
3
4
5
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.3.8</version>
</dependency>
  • 自定义websocket客户端对象:MyWebSocketClient
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @author shinan.chen
* @since 2019/8/1
*/
public class MyWebSocketClient extends WebSocketClient {

private Logger logger = LoggerFactory.getLogger(MyWebSocketClient.class);

public MyWebSocketClient(URI serverUri) {
super(serverUri);
}

@Override
public void onOpen(ServerHandshake arg0) {
logger.info("------ MyWebSocket onOpen ------");
}

@Override
public void onClose(int arg0, String arg1, boolean arg2) {
// TODO Auto-generated method stub
logger.info("------ MyWebSocket onClose ------");
}

@Override
public void onError(Exception arg0) {
// TODO Auto-generated method stub
logger.info("------ MyWebSocket onError ------");
}

@Override
public void onMessage(String arg0) {
// TODO Auto-generated method stub
logger.info("-------- 接收到服务端数据: " + arg0 + "--------");
if(arg0.equals("{\"msg\":\"ping\"}")){
logger.info("-------- 客户端发送数据: {\"msg\":\"pong\"}--------");
this.send("{\"msg\":\"pong\"}");
}
}
}
  • 建立websocket连接:SocketMain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* @author shinan.chen
* @since 2019/8/1
*/
public class SocketMain {
public static void main(String[] args) {
MyWebSocketClient myClient = null;
try {
myClient = new MyWebSocketClient(new URI("ws://localhost:3000/websocket"));
} catch (URISyntaxException e) {
e.printStackTrace();
}
myClient.connect();
while (!myClient.getReadyState().equals(WebSocket.READYSTATE.OPEN)) {
System.out.println("还没有打开");
}
System.out.println("打开了");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myClient.send("{\n" +
"\t\"msg\": \"connect\",\n" +
"\t\"version\": \"1\",\n" +
"\t\"support\": [\"1\", \"pre2\", \"pre1\"]\n" +
"}");
//登陆
myClient.send("{\n" +
" \"msg\": \"method\",\n" +
" \"method\": \"login\",\n" +
" \"id\":\""+ UUID.randomUUID().toString()+"\",\n" +
" \"params\":[\n" +
" {\n" +
" \"user\": { \"username\":\"cstt\" },\n" +
" \"password\": {\n" +
" \"digest\":\"6237c385db8f889e133c86844472f606051e1b3eebe6f08e1ec4b5a9ff73b3ee\",\n" +
" \"algorithm\":\"sha-256\"\n" +
" }\n" +
" }\n" +
" ]\n" +
"}");
//获取历史记录
myClient.send("{\n" +
" \"msg\": \"method\",\n" +
" \"method\": \"loadHistory\",\n" +
" \"id\": \""+ UUID.randomUUID().toString()+"\",\n" +
" \"params\": [ \"GENERAL\", null, 50, { \"$date\": 1480377601 } ]\n" +
"}");
//获取房间消息流
myClient.send("{\n" +
" \"msg\": \"sub\",\n" +
" \"id\": \""+ UUID.randomUUID().toString()+"\",\n" +
" \"name\": \"stream-room-messages\",\n" +
" \"params\":[\n" +
" \"GENERAL\",\n" +
" false\n" +
" ]\n" +
"}");
//发送消息
myClient.send("{\n" +
" \"msg\": \"method\",\n" +
" \"method\": \"sendMessage\",\n" +
" \"id\": \""+ UUID.randomUUID().toString()+"\",\n" +
" \"params\": [\n" +
" {\n" +
" \"_id\": \"100\",\n" +
" \"rid\": \"GENERAL\",\n" +
" \"msg\": \"Hello World!\"\n" +
" }\n" +
" ]\n" +
"}");
//回复消息
myClient.send("{\n" +
" \"msg\": \"method\",\n" +
" \"method\": \"sendMessage\",\n" +
" \"id\": \""+ UUID.randomUUID().toString()+"\",\n" +
" \"params\": [\n" +
" {\n" +
" \"_id\": \""+ UUID.randomUUID().toString()+"\",\n" +
" \"rid\": \"GENERAL\",\n" +
" \"tmid\": \"100\",\n" +
" \"msg\": \"reply message\"\n" +
" }\n" +
" ]\n" +
"}");
}
}
  • 运行结果

image

考虑是否用RocketChat实现协作连接

Choerodon整合RocketChat带来的问题

用户数据

操作RocketChat必须要有用户授权登陆,如果choerodon通过实时API/RestAPI去调用RocketChat的数据需要先调用登陆api获取token

  • 如果采用LDAP,那就需要用户同步,这样就担心会和xwiki一样,带来问题

  • 如果采用LDAP,登陆的过程如何整合在choerodon中【如果要在前端建立websocket连接,需要用户密码】

  • 如果不采用LDAP,发消息的用户需要在使用协作连接时后端自动进行rocketChat的用户创建

image

websocket交互

RocketChat自己的前端是通过建立websocket建立实时数据通道

  • 如果想在choerodon这边做一层封装的话,就得在后端连接RocketChat服务端,然后再做一层webSocket

  • 还可以获取数据的不做封装,发送数据的在后端做一层封装(做额外动作)

image

拦截接口做通知封装

整合在choerodon中,用户通知等则需要做一层接口拦截,即需要封装rocketchat的请求

服务器资源消耗

需要额外启动RocketChat服务端+MongoDB

Choerodon整合RocketChat带来的好处

存储

相当于只是用RocketChat做一层数据存储,以及增删改查的基础功能,其他都需要choerodon这边做封装

机器人

可以用RocketChat做机器人自动回复等功能

参考文献