package org.rakam.plugin.user.mailbox;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Singleton;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.AttributeKey;
import org.rakam.plugin.user.mailbox.UserMailboxStorage;
import org.rakam.plugin.user.mailbox.UserMailboxStorage.MessageListener;
import org.rakam.plugin.user.mailbox.UserMailboxStorage.Operation;
import org.rakam.server.http.WebSocketService;
import org.rakam.server.http.annotations.Api;
import org.rakam.server.http.annotations.ApiOperation;
import org.rakam.server.http.annotations.Authorization;
import org.rakam.util.JsonHelper;
import javax.inject.Inject;
import javax.ws.rs.Path;
import java.io.IOException;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull;
@Path("/user/mailbox/subscribe")
@Api(value = "/user/mailbox/subscribe", description = "Websocket service for subscribing user mails in real-time",
tags = "user", consumes = "ws", produces = "ws", protocols = "ws")
@Singleton
public class MailBoxWebSocketService extends WebSocketService {
public static final AttributeKey<String> USER_ID = AttributeKey.valueOf("user_id");
public static final AttributeKey<String> PROJECT_ID = AttributeKey.valueOf("project_id");
public static final AttributeKey<MessageListener> LISTENER = AttributeKey.valueOf("listener");
private final UserMailboxStorage storage;
private final Map<String, Map<Object, List<Channel>>> connectedClients = new ConcurrentHashMap<>();
@Inject
public MailBoxWebSocketService(com.google.common.base.Optional<UserMailboxStorage> storage) {
this.storage = storage.orNull();
}
@Override
public void onOpen(WebSocketRequest request) {
if(storage == null) {
// TODO: inform user.
request.context().close();
}
List<String> userParam = request.params().get("user");
List<String> projectParam = request.params().get("project");
String project;
String user;
if(userParam != null && !userParam.isEmpty() && projectParam !=null && !projectParam.isEmpty()) {
user = userParam.get(0);
project = projectParam.get(0);
ChannelHandlerContext context = request.context();
context.attr(USER_ID).set(user);
context.attr(PROJECT_ID).set(project);
connectedClients
.computeIfAbsent(project, s -> Maps.newConcurrentMap())
.computeIfAbsent(user, s -> Lists.newArrayList()).add(context.channel());
} else {
request.context().close();
return;
}
MessageListener listen = storage.listen(project, user, data -> {
Map<Object, List<Channel>> users = connectedClients.get(project);
if (users != null) {
List<Channel> channels = users.get(user);
if(channels != null) {
channels.forEach(channel -> channel.writeAndFlush(new TextWebSocketFrame(data.op + "\n" + data.payload)));
}
}
});
request.context().attr(LISTENER).set(listen);
}
@Override
@ApiOperation(value = "Realtime mailbox notification service",
notes = "Websocket service for sending and receiving mail noti fication",
response = WSMessage.class,
request = String.class,
responseContainer = "List",
authorizations = {@Authorization(value = "write_key"), @Authorization(value = "read_key")}
)
public void onMessage(ChannelHandlerContext ctx, String data) {
Integer jsonStart = data.indexOf("\n");
Operation op = Operation.valueOf(data.substring(0, jsonStart));
String jsonStr = data.substring(jsonStart);
switch (op) {
case msg:
try {
UserMessage message = JsonHelper.readSafe(jsonStr, UserMessage.class);
storage.send(ctx.attr(PROJECT_ID).get(), ctx.attr(USER_ID).get(), message.toUser,
message.parent, message.content, Instant.now());
} catch (IOException e) {
ctx.close();
}
break;
case typing:
break;
}
}
@Override
public void onClose(ChannelHandlerContext ctx) {
connectedClients
.computeIfAbsent(ctx.attr(PROJECT_ID).get(), s -> Maps.newConcurrentMap())
.computeIfAbsent(ctx.attr(USER_ID).get(), s -> Lists.newArrayList()).remove(ctx.channel());
ctx.attr(LISTENER).get().shutdown();
}
public static class UserMessage {
public final Integer parent;
public final String content;
public final String toUser;
@JsonCreator
public UserMessage(@JsonProperty("parent") Integer parent,
@JsonProperty("message") String content,
@JsonProperty("to_user") String toUser) {
this.parent = parent;
this.content = content;
this.toUser = toUser;
}
}
public Collection<Object> getConnectedUsers(String project) {
Map<Object, List<Channel>> objectListMap = connectedClients.get(project);
if(objectListMap == null)
return ImmutableList.of();
return objectListMap.entrySet().stream()
.filter(e -> !e.getValue().isEmpty())
.map(e -> e.getKey()).collect(Collectors.toList());
}
public static class WSMessage {
public final int time;
public final Operation op;
public final String value;
public WSMessage(int time, Operation op, String value) {
this.time = checkNotNull(time, "time is required");
this.op = checkNotNull(op, "op is required");
this.value = value;
}
}
}