package com.chat.webservice; import ch.qos.logback.classic.Logger; import com.chat.db.Actions; import com.chat.tools.Tools; import com.chat.types.SessionScope; import com.chat.types.comment.Comment; import com.chat.types.comment.Comments; import com.chat.types.discussion.Discussion; import com.chat.types.user.User; import com.chat.types.user.Users; import com.chat.types.websocket.input.*; import com.fasterxml.jackson.databind.JsonNode; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.javalite.activejdbc.LazyList; import org.javalite.activejdbc.Model; import org.slf4j.LoggerFactory; import java.sql.Array; import java.util.*; import static com.chat.db.Tables.*; /** * Created by tyler on 6/5/16. */ @WebSocket public class ThreadedChatWebSocket { private static Long topLimit = 20L; private static Long maxDepth = 20L; public static Logger log = (Logger) LoggerFactory.getLogger(ThreadedChatWebSocket.class); static Set<SessionScope> sessionScopes = new HashSet<>(); private static final Integer PING_DELAY = 10000; enum MessageType { Comments, Users, Edit, Reply, TopReply, Vote, Delete, NextPage, Sticky, SaveFavoriteDiscussion, Ping, Pong; } public ThreadedChatWebSocket() { } @OnWebSocketConnect public void onConnect(Session session) { try { Tools.dbInit(); // Get or create the session scope SessionScope ss = setupSessionScope(session); sendRecurringPings(session); // Send them their user info // TODO // session.getRemote().sendString(ss.getUserObj().json("user")); LazyList<Model> comments = fetchComments(ss); // send the comments sendMessage(session, messageWrapper(MessageType.Comments, Comments .create(comments, fetchVotesMap(ss.getUserObj().getId()), topLimit, maxDepth, ss.getCommentComparator()) .json())); // send the updated users to everyone in the right scope(just discussion) Set<SessionScope> filteredScopes = SessionScope.constructFilteredUserScopesFromSessionRequest(sessionScopes, session); broadcastMessage(filteredScopes, messageWrapper(MessageType.Users, Users.create(SessionScope.getUserObjects(filteredScopes)).json())); log.debug("session scope " + ss + " joined"); } catch (Exception e) { e.printStackTrace(); } finally { Tools.dbClose(); } } @OnWebSocketClose public void onClose(Session session, int statusCode, String reason) { SessionScope ss = SessionScope.findBySession(sessionScopes, session); sessionScopes.remove(ss); log.debug("session scope " + ss + " left, " + statusCode + " " + reason); // Send the updated users to everyone in the right scope Set<SessionScope> filteredScopes = SessionScope.constructFilteredUserScopesFromSessionRequest(sessionScopes, session); broadcastMessage(filteredScopes, messageWrapper(MessageType.Users, Users.create(SessionScope.getUserObjects(filteredScopes)).json())); } @OnWebSocketMessage public void onMessage(Session session, String dataStr) { try { Tools.dbInit(); JsonNode node = null; node = Tools.JACKSON.readTree(dataStr); JsonNode data = node.get("data"); switch (getMessageType(node)) { case Reply: messageReply(session, data); break; case Edit: messageEdit(session, data); break; case Sticky: messageSticky(session, data); break; case TopReply: messageTopReply(session, data); break; case Delete: messageDelete(session, data); break; case Vote: saveCommentVote(session, data); break; case NextPage: messageNextPage(session, data); break; case Pong: pongReceived(session, data); break; } } catch (Exception e) { e.printStackTrace(); } finally { Tools.dbClose(); } } public MessageType getMessageType(JsonNode node) { return MessageType.values()[node.get("message_type").asInt()]; } public void messageNextPage(Session session, JsonNode data) { SessionScope ss = SessionScope.findBySession(sessionScopes, session); // Get the object NextPageData nextPageData = new NextPageData(data.get("topLimit").asLong(), data.get("maxDepth").asLong()); // Refetch the comments based on the new limit LazyList<Model> comments = fetchComments(ss); // send the comments from up to the new limit to them sendMessage(session, messageWrapper(MessageType.Comments, Comments.create(comments, fetchVotesMap(ss.getUserObj().getId()), nextPageData.getTopLimit(), nextPageData.getMaxDepth(), ss.getCommentComparator()).json())); } public void messageReply(Session session, JsonNode data) { SessionScope ss = SessionScope.findBySession(sessionScopes, session); // Get the object ReplyData replyData = new ReplyData(data.get("parentId").asLong(), data.get("reply").asText()); // Collect only works on refetch LazyList<Model> comments = fetchComments(ss); log.debug(ss.toString()); // Necessary for comment tree Array arr = (Array) comments.collect("breadcrumbs", "id", replyData.getParentId()).get(0); List<Long> parentBreadCrumbs = Tools.convertArrayToList(arr); com.chat.db.Tables.Comment newComment = Actions.createComment(ss.getUserObj().getId(), ss.getDiscussionId(), parentBreadCrumbs, replyData.getReply()); // Fetch the comment threaded view CommentThreadedView ctv = CommentThreadedView.findFirst("id = ?", newComment.getLongId()); // Convert to a proper commentObj Comment co = Comment.create(ctv, null); Set<SessionScope> filteredScopes = SessionScope.constructFilteredMessageScopesFromSessionRequest(sessionScopes, session, co.getBreadcrumbs()); broadcastMessage(filteredScopes, messageWrapper(MessageType.Reply, co.json())); // TODO find a way to do this without having to query every time? com.chat.types.discussion.Discussion do_ = Actions.saveFavoriteDiscussion(ss.getUserObj().getId(), ss.getDiscussionId()); if (do_ != null) sendMessage(session, messageWrapper(MessageType.SaveFavoriteDiscussion, do_.json())); } public void messageEdit(Session session, JsonNode data) { SessionScope ss = SessionScope.findBySession(sessionScopes, session); EditData editData = new EditData(data.get("id").asLong(), data.get("edit").asText()); com.chat.db.Tables.Comment c = Actions.editComment(ss.getUserObj().getId(), editData.getId(), editData.getEdit()); CommentThreadedView ctv = CommentThreadedView.findFirst("id = ?", c.getLongId()); // Convert to a proper commentObj, but with nothing embedded Comment co = Comment.create(ctv, null); Set<SessionScope> filteredScopes = SessionScope.constructFilteredMessageScopesFromSessionRequest(sessionScopes, session, co.getBreadcrumbs()); broadcastMessage(filteredScopes, messageWrapper(MessageType.Edit, co.json())); } public void messageSticky(Session session, JsonNode data) { StickyData stickyData = new StickyData(data.get("id").asLong(), data.get("sticky").asBoolean()); com.chat.db.Tables.Comment c = Actions.stickyComment(stickyData.getId(), stickyData.getSticky()); CommentThreadedView ctv = CommentThreadedView.findFirst("id = ?", c.getLongId()); // Convert to a proper commentObj, but with nothing embedded Comment co = Comment.create(ctv, null); Set<SessionScope> filteredScopes = SessionScope.constructFilteredMessageScopesFromSessionRequest(sessionScopes, session, co.getBreadcrumbs()); // Send an edit with the sticky info broadcastMessage(filteredScopes, messageWrapper(MessageType.Edit, co.json())); } public void messageDelete(Session session, JsonNode data) { SessionScope ss = SessionScope.findBySession(sessionScopes, session); DeleteData deleteData = new DeleteData(data.get("deleteId").asLong()); com.chat.db.Tables.Comment c = Actions.deleteComment(ss.getUserObj().getId(), deleteData.getDeleteId()); CommentThreadedView ctv = CommentThreadedView.findFirst("id = ?", c.getLongId()); // Convert to a proper commentObj, but with nothing embedded Comment co = Comment.create(ctv, null); Set<SessionScope> filteredScopes = SessionScope.constructFilteredMessageScopesFromSessionRequest(sessionScopes, session, co.getBreadcrumbs()); broadcastMessage(filteredScopes, messageWrapper(MessageType.Delete, co.json())); } public void messageTopReply(Session session, JsonNode data) { SessionScope ss = SessionScope.findBySession(sessionScopes, session); // Get the object TopReplyData topReplyData = new TopReplyData(data.get("topReply").asText()); com.chat.db.Tables.Comment newComment = Actions.createComment(ss.getUserObj().getId(), ss.getDiscussionId(), null, topReplyData.getTopReply()); // Fetch the comment threaded view CommentThreadedView ctv = CommentThreadedView.findFirst("id = ?", newComment.getLongId()); // Convert to a proper commentObj Comment co = Comment.create(ctv, null); Set<SessionScope> filteredScopes = SessionScope.constructFilteredMessageScopesFromSessionRequest(sessionScopes, session, co.getBreadcrumbs()); broadcastMessage(filteredScopes, messageWrapper(MessageType.TopReply, co.json())); // TODO find a way to do this without having to query every time? Discussion do_ = Actions.saveFavoriteDiscussion(ss.getUserObj().getId(), ss.getDiscussionId()); if (do_ != null) sendMessage(session, messageWrapper(MessageType.SaveFavoriteDiscussion, do_.json())); } public static void saveCommentVote(Session session, JsonNode data) { SessionScope ss = SessionScope.findBySession(sessionScopes, session); // Get the object CommentRankData commentRankData = new CommentRankData(data.get("rank").asInt(), data.get("commentId").asLong()); Long userId = ss.getUserObj().getId(); log.debug(userId.toString()); Long commentId = commentRankData.getCommentId(); Integer rank = commentRankData.getRank(); String message = Actions.saveCommentVote(userId, commentId, rank); // Getting the comment for the breadcrumbs for the scope CommentThreadedView ctv = CommentThreadedView.findFirst("id = ?", commentId); // Convert to a proper commentObj, but with nothing embedded Comment co = Comment.create(ctv, null); Set<SessionScope> filteredScopes = SessionScope.constructFilteredMessageScopesFromSessionRequest(sessionScopes, session, co.getBreadcrumbs()); // This sends an edit, which contains the average rank broadcastMessage(filteredScopes, messageWrapper(MessageType.Edit, co.json())); } // Sends a message from one user to all users // TODO need to get subsets of sessions based on discussion_id, and parent_id // Maybe Map<discussion_id, List<sessions> public static void broadcastMessage(Set<SessionScope> filteredScopes, String json) { SessionScope.getSessions(filteredScopes).stream().filter(Session::isOpen).forEach(session -> { try { session.getRemote().sendString(json); } catch (Exception e) { e.printStackTrace(); } }); } public static void sendMessage(Session session, String json) { try { session.getRemote().sendString(json); } catch (Exception e) { e.printStackTrace(); } } private SessionScope setupSessionScope(Session session) { User userObj = SessionScope.getUserFromSession(session); Long discussionId = SessionScope.getDiscussionIdFromSession(session); Long topParentId = SessionScope.getTopParentIdFromSession(session); String sortType = SessionScope.getSortTypeFromSession(session); log.debug(userObj.json()); SessionScope ss = new SessionScope(session, userObj, discussionId, topParentId, sortType); sessionScopes.add(ss); return ss; } private static LazyList<Model> fetchComments(SessionScope scope) { if (scope.getTopParentId() != null) { return CommentBreadcrumbsView.where("discussion_id = ? and parent_id = ?", scope.getDiscussionId(), scope.getTopParentId()); } else { return CommentThreadedView.where("discussion_id = ?", scope.getDiscussionId()); } } // These create maps from a user's comment id, to their rank/vote private static Map<Long, Integer> fetchVotesMap(Long userId) { List<CommentRank> ranks = CommentRank.where("user_id = ?", userId); return convertCommentRanksToVoteMap(ranks); } private static Map<Long, Integer> fetchVotesMap(Long userId, Long commentId) { List<CommentRank> ranks = CommentRank.where("comment_id = ? and user_id = ?", commentId, userId); return convertCommentRanksToVoteMap(ranks); } private static Map<Long, Integer> convertCommentRanksToVoteMap(List<CommentRank> ranks) { Map<Long, Integer> map = new HashMap<>(); for (CommentRank rank : ranks) { map.put(rank.getLong("comment_id"), rank.getInteger("rank")); } return map; } private void sendRecurringPings(Session session) { final Timer timer = new Timer(); final TimerTask tt = new TimerTask() { @Override public void run() { if (session.isOpen()) { sendMessage(session, messageWrapper(MessageType.Ping, "{\"ping\":\"ping\"}")); } else { timer.cancel(); timer.purge(); } } }; timer.scheduleAtFixedRate(tt, PING_DELAY, PING_DELAY); } private void pongReceived(Session session, JsonNode pongStr) { log.debug("Pong received from " + session.getRemoteAddress()); } private static String messageWrapper(MessageType type, String data) { return "{\"message_type\":" + type.ordinal() + ",\"data\":" + data + "}"; } }