/* * Copyright (C) 2008-2017, Juick * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package com.juick.www.controllers; import com.juick.Tag; import com.juick.formatters.PlainTextFormatter; import com.juick.server.helpers.TagStats; import com.juick.server.util.HttpForbiddenException; import com.juick.server.util.HttpNotFoundException; import com.juick.server.util.UserUtils; import com.juick.server.util.WebUtils; import com.juick.service.*; import com.juick.www.Utils; import com.mitchellbosecke.pebble.extension.i18n.UTF8Control; import org.apache.commons.codec.CharEncoding; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.text.StringEscapeUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponents; import ru.sape.Sape; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.ResourceBundle; import java.util.function.BooleanSupplier; import java.util.stream.Collectors; /** * * @author Ugnich Anton */ @Controller public class Messages { @Inject private UserService userService; @Inject private TagService tagService; @Inject private MessagesService messagesService; @Inject private SubscriptionService subscriptionService; @Inject private Sape sape; @Inject private PMQueriesService pmQueriesService; @Inject private CrosspostService crosspostService; void fillUserModel(ModelMap model, com.juick.User user, com.juick.User visitor) { model.addAttribute("user", user); model.addAttribute("isSubscribed", userService.isSubscribed(visitor.getUid(), user.getUid())); model.addAttribute("isInBL", userService.isInBL(visitor.getUid(), user.getUid())); model.addAttribute("isInBLAny", userService.isInBLAny(user.getUid(), visitor.getUid())); model.addAttribute("statsIRead", userService.getStatsIRead(user.getUid())); model.addAttribute("statsMyReaders", userService.getStatsMyReaders(user.getUid())); model.addAttribute("statsMyBL", userService.getUserBLUsers(user.getUid()).size()); model.addAttribute("statsMessages", userService.getStatsMessages(user.getUid())); model.addAttribute("statsReplies", userService.getStatsReplies(user.getUid())); model.addAttribute("statsPhotos", userService.getStatsPhotos(user.getUid())); model.addAttribute("statsFavorites", userService.getStatsFavorites(user.getUid())); model.addAttribute("iread", userService.getUserReadLeastPopular(user.getUid(), 8)); model.addAttribute("tagStats", tagService.getUserTagStats(user.getUid()) .stream() .sorted(Comparator.comparing(TagStats::getUsageCount).reversed()) .limit(20) .map(t -> t.getTag().getName()) .collect(Collectors.toList()) ); } @GetMapping("/{anything}/**") protected String parseAnyThing(@PathVariable String anything, @RequestParam(required = false, defaultValue = "0") int before) throws IOException { if (before == 0) { boolean isPostNumber = WebUtils.isPostNumber(anything); int messageId = isPostNumber ? NumberUtils.toInt(anything) : 0; if (isPostNumber && anything.equals(Integer.toString(messageId))) { if (messageId > 0) { com.juick.User author = messagesService.getMessageAuthor(messageId); if (author != null) { return "redirect:/" + author.getName() + "/" + anything; } } } com.juick.User user = userService.getUserByName(anything); if (user.getUid() > 0) { return "redirect:/" + user.getName() + "/"; } throw new HttpNotFoundException(); } com.juick.User user = userService.getUserByName(anything); if (user.getUid() > 0) { return "redirect:/" + user.getName() + "/?before=" + before; } else { throw new HttpNotFoundException(); } } @GetMapping("/") protected String doGet( @RequestParam(required = false) String tag, @RequestParam(name = "show", required = false) String paramShow, @RequestParam(name = "search", required = false) String paramSearch, @RequestParam(name = "before", required = false, defaultValue = "0") Integer paramBefore, @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie, ModelMap model, Locale locale) throws IOException { if (tag != null) { return "redirect:/tag/" + URLEncoder.encode(tag, CharEncoding.UTF_8); } if (paramSearch != null && paramSearch.length() > 64) { paramSearch = null; } ResourceBundle messagesBundle = ResourceBundle.getBundle("messages", locale, new UTF8Control()); com.juick.User visitor = UserUtils.getCurrentUser(); String title; List mids; if (paramSearch != null) { title = messagesBundle.getString("label.search") + ": " + StringEscapeUtils.escapeHtml4(paramSearch); mids = messagesService.getSearch(Utils.encodeSphinx(paramSearch), paramBefore); } else if (paramShow == null) { if (visitor.getUid() > 0) { title = messagesBundle.getString("title.index.user"); mids = messagesService.getPopular(visitor.getUid(), paramBefore); } else { title = messagesBundle.getString("title.index.anonym"); mids = messagesService.getPopular(0, paramBefore); } } else if (paramShow.equals("top")) { return "redirect:/"; } else if (paramShow.equals("my") && !visitor.isAnonymous()) { title = messagesBundle.getString("link.my"); mids = messagesService.getMyFeed(visitor.getUid(), paramBefore, true); } else if (paramShow.equals("private") && !visitor.isAnonymous()) { title = messagesBundle.getString("link.privateMessages"); mids = messagesService.getPrivate(visitor.getUid(), paramBefore); } else if (paramShow.equals("discuss") && !visitor.isAnonymous()) { title = messagesBundle.getString("link.discuss"); mids = messagesService.getDiscussions(visitor.getUid(), paramBefore); } else if (paramShow.equals("recommended") && !visitor.isAnonymous()) { title = messagesBundle.getString("link.recommended"); mids = messagesService.getRecommended(visitor.getUid(), paramBefore); } else if (paramShow.equals("photos")) { title = messagesBundle.getString("link.withPhotos"); mids = messagesService.getPhotos(visitor.getUid(), paramBefore); } else if (paramShow.equals("all")) { title = messagesBundle.getString("link.allMessages"); mids = messagesService.getAll(visitor.getUid(), paramBefore); } else { throw new HttpNotFoundException(); } List msgs = messagesService.getMessages(mids); if (visitor.getUid() != 0) { List blUIDs = userService.checkBL( visitor.getUid(), msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList()) ); msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); fillUserModel(model, visitor, visitor); } String head = StringUtils.EMPTY; if (paramBefore > 0 || paramShow != null) { head = ""; } model.addAttribute("paramShow", !StringUtils.isEmpty(paramShow) ? paramShow : ((paramSearch != null) ? "search" : "top")); model.addAttribute("title", title); model.addAttribute("headers", head); model.addAttribute("visitor", visitor); model.addAttribute("noindex", !(paramShow == null && paramBefore == 0)); model.addAttribute("msgs", msgs); model.addAttribute("tags", (visitor.getUid() != 0) ? subscriptionService.getSubscribedTags(visitor) : tagService.getPopularTags()); model.addAttribute("showAdv", paramShow == null && paramBefore == 0 && paramSearch == null && visitor.getUid() == 0); if (mids.size() >= 20) { String nextpage = "?before=" + mids.get(mids.size() - 1); if (paramShow != null) { nextpage += "&show=" + paramShow; } if (paramSearch != null) { nextpage += "&search=" + URLEncoder.encode(paramSearch, CharEncoding.UTF_8); } model.addAttribute("nextpage", nextpage); } UriComponents builder = ServletUriComponentsBuilder.fromCurrentRequestUri().build(); String queryString = builder.getQuery(); String requestURI = builder.toUri().getPath(); if (sape != null && visitor.getUid() == 0 && queryString == null) { String links = sape.getPageLinks(requestURI, sapeCookie).render(); model.addAttribute("links", links); } return "views/index"; } @GetMapping("/{uname}/") protected String doGetBlog( @RequestParam(required = false, name = "show") String paramShow, @RequestParam(required = false, name = "tag") String paramTagStr, @RequestParam(required = false, name = "search") String paramSearch, @PathVariable String uname, @RequestParam(required = false, defaultValue = "0") Integer before, @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie, ModelMap model) throws IOException { com.juick.User user = userService.getUserByName(uname); com.juick.User visitor = UserUtils.getCurrentUser(); if (user.isBanned()) { throw new HttpNotFoundException(); } List mids; com.juick.Tag paramTag = null; if (paramTagStr != null) { if (paramTagStr.length() < 64) { paramTag = tagService.getTag(paramTagStr, false); } if (paramTag == null) { throw new HttpNotFoundException(); } else if (!paramTag.getName().equals(paramTagStr)) { String url = user.getName() + "/?tag=" + URLEncoder.encode(paramTag.getName(), CharEncoding.UTF_8); return "redirect:/" + url; } } if (paramSearch != null && paramSearch.length() > 64) { paramSearch = null; } int privacy = 0; if (visitor.getUid() > 0) { if (user.getUid() == visitor.getUid() || visitor.getUid() == 1) { privacy = -3; } else if (userService.isInWL(user.getUid(), visitor.getUid())) { privacy = -2; } } String title; if (paramShow == null) { if (paramTag != null) { title = "Блог " + user.getName() + ": *" + StringEscapeUtils.escapeHtml4(paramTag.getName()); mids = messagesService.getUserTag(user.getUid(), paramTag.TID, privacy, before); } else if (paramSearch != null) { title = "Блог " + user.getName() + ": " + StringEscapeUtils.escapeHtml4(paramSearch); mids = messagesService.getUserSearch(user.getUid(), Utils.encodeSphinx(paramSearch), privacy, before); } else { title = "Блог " + user.getName(); mids = messagesService.getUserBlog(user.getUid(), privacy, before); } } else if (paramShow.equals("recomm")) { title = "Рекомендации " + user.getName(); mids = messagesService.getUserRecommendations(user.getUid(), before); } else if (paramShow.equals("photos")) { title = "Фотографии " + user.getName(); mids = messagesService.getUserPhotos(user.getUid(), privacy, before); } else { throw new HttpNotFoundException(); } String head = ""; if (paramTag != null && tagService.getTagNoIndex(paramTag.TID)) { head += ""; } else if (before > 0 || paramShow != null) { head += ""; } model.addAttribute("pageUrl", "http://juick.com/" + user.getName()); model.addAttribute("title", title); model.addAttribute("headers", head); model.addAttribute("visitor", visitor); model.addAttribute("noindex", paramShow == null && before == 0); fillUserModel(model, user, visitor); model.addAttribute("paramTag", paramTag); List msgs = messagesService.getMessages(mids); if (visitor.getUid() != 0) { List blUIDs = userService.checkBL(visitor.getUid(), msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList())); msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); } model.addAttribute("msgs", msgs); model.addAttribute("headers", head); model.addAttribute("showAdv", paramShow == null && before == 0 && paramSearch == null && visitor.getUid() == 0); if (mids.size() >= 20) { String nextpage = "?before=" + mids.get(mids.size() - 1); if (paramShow != null) { nextpage += "&show=" + paramShow; } if (paramSearch != null) { nextpage += "&search=" + URLEncoder.encode(paramSearch, CharEncoding.UTF_8); } if (paramTag != null) { nextpage += "&tag=" + URLEncoder.encode(paramTag.getName(), CharEncoding.UTF_8); } model.addAttribute("nextpage", nextpage); } UriComponents builder = ServletUriComponentsBuilder.fromCurrentRequestUri().build(); String queryString = builder.getQuery(); String requestURI = builder.toUri().getPath(); if (sape != null && visitor.getUid() == 0 && queryString == null) { String links = sape.getPageLinks(requestURI, sapeCookie).render(); model.addAttribute("links", links); } return "views/blog"; } @GetMapping("/{uname}/tags") protected String doGetTags(@PathVariable String uname, ModelMap model) throws IOException { com.juick.User user = userService.getUserByName(uname); com.juick.User visitor = UserUtils.getCurrentUser(); if (visitor.isBanned()) { throw new HttpNotFoundException(); } model.addAttribute("title", "Теги " + user.getName()); model.addAttribute("headers", ""); model.addAttribute("visitor", visitor); fillUserModel(model, user, visitor); model.addAttribute("tags", tagService.getUserTagStats(user.getUid()).stream() .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).map(t -> t.getTag().getName()).collect(Collectors.toList())); return "views/blog_tags"; } @GetMapping("/{uname}/friends") protected String doGetFriends(@PathVariable String uname, ModelMap model) throws IOException { com.juick.User user = userService.getUserByName(uname); com.juick.User visitor = UserUtils.getCurrentUser(); if (visitor.isBanned()) { throw new HttpNotFoundException(); } model.addAttribute("title", "Подписки " + user.getName()); model.addAttribute("headers", ""); model.addAttribute("visitor", visitor); fillUserModel(model, user, visitor); model.addAttribute("users", userService.getUserFriends(user.getUid())); return "views/users"; } @GetMapping("/{uname}/readers") protected String doGetReaders(@PathVariable String uname, ModelMap model) throws IOException { com.juick.User user = userService.getUserByName(uname); com.juick.User visitor = UserUtils.getCurrentUser(); if (visitor.isBanned()) { throw new HttpForbiddenException(); } model.addAttribute("title", "Читатели " + user.getName()); model.addAttribute("headers", ""); model.addAttribute("visitor", visitor); fillUserModel(model, user, visitor); model.addAttribute("users", userService.getUserReaders(user.getUid())); return "views/users"; } @GetMapping("/bl") protected String doGetBL(ModelMap model) throws IOException { com.juick.User visitor = UserUtils.getCurrentUser(); if (visitor.isBanned()) { throw new HttpForbiddenException(); } model.addAttribute("title", "Черный список " + visitor.getName()); model.addAttribute("headers", ""); model.addAttribute("visitor", visitor); fillUserModel(model, visitor, visitor); model.addAttribute("users", userService.getUserBLUsers(visitor.getUid())); return "views/users"; } @GetMapping("/tag/{tagName}") protected String tagAction(HttpServletRequest request, @PathVariable String tagName, @RequestParam(required = false, defaultValue = "0") int before, ModelMap model) throws IOException { com.juick.User visitor = UserUtils.getCurrentUser(); String paramTagStr = StringEscapeUtils.unescapeHtml4(tagName); com.juick.Tag paramTag = tagService.getTag(paramTagStr, false); if (paramTag == null) { throw new HttpNotFoundException(); } else if (paramTag.SynonymID > 0 && paramTag.TID != paramTag.SynonymID) { com.juick.Tag synTag = tagService.getTag(paramTag.SynonymID); String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(synTag.getName()), CharEncoding.UTF_8); if (request.getQueryString() != null) { url += "?" + request.getQueryString(); } return "redirect:" + url; } else if (!paramTag.getName().equals(paramTagStr)) { String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(paramTag.getName()), CharEncoding.UTF_8); if (request.getQueryString() != null) { url += "?" + request.getQueryString(); } return "redirect:" + url; } int visitor_uid = visitor.getUid(); List mids = messagesService.getTag(paramTag.TID, visitor_uid, before, (visitor_uid == 0) ? 40 : 20); List msgs = messagesService.getMessages(mids); if (visitor.getUid() != 0) { List blUIDs = userService.checkBL( visitor.getUid(), msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList()) ); msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); fillUserModel(model, visitor, visitor); } String head = StringUtils.EMPTY; if (tagService.getTagNoIndex(paramTag.TID)) { head = ""; } else if (before > 0 || mids.size() < 5) { head = ""; } model.addAttribute("headers", head); model.addAttribute("visitor", visitor); model.addAttribute("tag", paramTag); model.addAttribute("title", "*" + StringEscapeUtils.escapeHtml4(paramTag.getName())); model.addAttribute("msgs", msgs); model.addAttribute("tags", tagService.getPopularTags()); model.addAttribute("noindex", before > 0); model.addAttribute("showAdv", before == 0 && visitor.getUid() == 0); model.addAttribute("isSubscribed", tagService.isSubscribed(visitor, paramTag)); model.addAttribute("isInBL", tagService.isInBL(visitor, paramTag)); if (mids.size() >= 20) { String nextpage = "/tag/" + URLEncoder.encode(paramTag.getName(), CharEncoding.UTF_8) + "?before=" + mids.get(mids.size() - 1); model.addAttribute("nextpage", nextpage); } return "views/index"; } @GetMapping("/pm/inbox") protected String doGetInbox(ModelMap model) { com.juick.User visitor = UserUtils.getCurrentUser(); if (visitor.getUid() == 0) { return "redirect:/login"; } String title = "PM: Inbox"; List msgs = pmQueriesService.getLastPMInbox(visitor.getUid()); fillUserModel(model, visitor, visitor); model.addAttribute("title", title); model.addAttribute("visitor", visitor); model.addAttribute("msgs", msgs); model.addAttribute("tags", tagService.getPopularTags()); return "views/pm_inbox"; } @GetMapping("/pm/sent") protected String doGetSent(@RequestParam(required = false) String uname, ModelMap model) { com.juick.User visitor = UserUtils.getCurrentUser(); if (visitor.getUid() == 0) { return "redirect:/login"; } String title = "PM: Sent"; List msgs = pmQueriesService.getLastPMSent(visitor.getUid()); if (WebUtils.isNotUserName(uname)) { uname = StringUtils.EMPTY; } fillUserModel(model, visitor, visitor); model.addAttribute("title", title); model.addAttribute("visitor", visitor); model.addAttribute("msgs", msgs); model.addAttribute("tags", tagService.getPopularTags()); model.addAttribute("uname", uname); return "views/pm_sent"; } @GetMapping("/{uname}/{mid}") protected String threadAction(ModelMap model, @PathVariable String uname, @PathVariable int mid, @RequestParam(required = false, value = "view") String paramView) throws IOException { com.juick.User visitor = UserUtils.getCurrentUser(); if (!messagesService.canViewThread(mid, visitor.getUid())) { throw new HttpForbiddenException(); } com.juick.Message msg = messagesService.getMessage(mid); if (msg == null || msg.getUser().isBanned()) { throw new HttpNotFoundException(); } com.juick.User user = userService.getUserByName(uname); if (user.getUid() == 0 || !msg.getUser().equals(user)) { return String.format("redirect:/%s/%d", msg.getUser().getName(), mid); } msg.VisitorCanComment = visitor.getUid() > 0; fillUserModel(model, user, visitor); if (visitor.getUid() > 0) { boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid(); boolean isInBL = userService.isInBL(msg.getUser().getUid(), visitor.getUid()); msg.VisitorCanComment = isMsgAuthor || !(msg.ReadOnly || isInBL); } model.addAttribute("msg", msg); boolean listview = false; if (paramView != null) { if (paramView.equals("list")) { listview = true; if (visitor.getUid() > 0) { userService.setUserOptionInt(visitor.getUid(), "repliesview", 1); } } else if (paramView.equals("tree") && visitor.getUid() > 0) { userService.setUserOptionInt(visitor.getUid(), "repliesview", 0); } } else if (visitor.getUid() > 0 && userService.getUserOptionInt(visitor.getUid(), "repliesview", 0) == 1) { listview = true; } model.addAttribute("listview", listview); String title = msg.getUser().getName() + ": " + msg.getTagsString(); model.addAttribute("title", title); model.addAttribute("visitor", visitor); String headers = ""; String pageUrl = "https://juick.com/" + msg.getUser().getName() + "/" + msg.getMid(); if (paramView != null) { headers += ""; } if (msg.Hidden) { headers += ""; } String cardType = StringUtils.isNotEmpty(msg.getAttachmentType()) ? "summary_large_image" : "summary"; if (StringUtils.isNotEmpty(msg.getAttachmentType())) { // additional check in case of broken images if (msg.getAttachment() != null) { String msgImage = msg.getAttachment().getMedium().getUrl(); headers += ""; } } else { String msgImage ="https://i.juick.com/a/" + msg.getUser().getUid() + ".png"; headers += ""; } model.addAttribute("ogtype", "article"); String cardDescription = StringEscapeUtils.escapeHtml4(PlainTextFormatter.formatTwitterCard(msg)); headers += "\n" + "\n" + "\n" + "\n" + "\n" + "\n"; String twitterName = crosspostService.getTwitterName(msg.getUser().getUid()); if (StringUtils.isNotEmpty(twitterName)) { headers += "\n"; } if (msg.getTags().size() > 0) { headers += "\n"; } model.addAttribute("headers", headers); model.addAttribute("visitorSubscribed", messagesService.isSubscribed(visitor.getUid(), msg.getMid())); model.addAttribute("visitorInBL", userService.isInBL(msg.getUser().getUid(), visitor.getUid())); model.addAttribute("recomm", messagesService.getMessageRecommendations(msg.getMid())); List replies = messagesService.getReplies(msg.getMid()); List blUIDs = new ArrayList<>(); for (int i = 0; i < replies.size(); i++) { com.juick.Message reply = replies.get(i); if (reply.getUser().getUid() != msg.getUser().getUid() && !blUIDs.contains(reply.getUser().getUid())) { blUIDs.add(reply.getUser().getUid()); } if (reply.getReplyto() > 0) { boolean added = false; for (int n = 0; n < replies.size(); n++) { if (replies.get(n).getRid() == reply.getReplyto()) { replies.get(n).childs.add(reply); added = true; break; } } if (!added) { reply.setReplyto(0); } } reply.VisitorCanComment = visitor.getUid() > 0; if (visitor.getUid() > 0) { boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid(); boolean isReplyAuthor = visitor.getUid() == reply.getUser().getUid(); BooleanSupplier isInBL2 = () -> userService.checkBL(visitor.getUid(), blUIDs).contains(reply.getUser().getUid()); reply.VisitorCanComment = isMsgAuthor || (!msg.ReadOnly && (isReplyAuthor || !isInBL2.getAsBoolean())); } } boolean foldable = false; if (replies.size() > 10) { for (int i = 0; i < replies.size() - 1; i++) { if (replies.get(i).getChildsCount() > 1) { foldable = true; break; } } } model.addAttribute("replies", replies); model.addAttribute("foldable", foldable); return "views/thread"; } // when message id is not fit to int @ExceptionHandler(NumberFormatException.class) public ResponseEntity notFoundAction() { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } }