aboutsummaryrefslogblamecommitdiff
path: root/src/main/java/com/juick/www/controllers/Site.java
blob: 3bf277727ca1c824c1a3d225d97b29b162d4531a (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  
                                 












                                                                           
                                  
 

                               


                                                    
                            
                                                
                                               
                                        
 
                                   
                                            
                                           
                                                 
                                                          
                                            
                                           
                                          
                                               
                                                                 
                                                 
                                                 
                                                         
                           
                           
                                         
                           
                             
                      
                        
                          
                                





                                   
                   





                                            
                                    
           
                          
                             


                                           
 
                                                                         
                                                      


                                                                                                      
                                                                                           
                                                                                               


                                                                                           

                                                                                                              
                                                                  
     
                         
                                                            

                                                                                                 
                                     
                                                         

                                                                
                                                














                                                                                        
                    
                                                                                                                   


                                                                                                     
                                                                                                              
                          
                                                                                     
         
                                                            
 








                                                               
                                                                                                        
                                                                         
                                       
                                                                                           
                                                                             
                                             
                                                                                           
                                                                                                        
                                                 
                                                                      
                                                                                      
                                                                                  
                                                                                                   
                                                                             
                                                 
                                
                                                                               
                                                                                               
                                                                                 
                                                                                              

                                                                            
                                                                                               




                                                                         
                                                                                  
 
                                                                                                     




                                                                                
                                                                        
                                                                                       



                                                                        
                                                                           
                                                                                   


                                                                
                                
                                                                                       
                                                                                                          


                                                     
                                                                                                    

                                                     

                             
                                                                    
                                                                                                                             
                                                                                
                                                                                                                        
                                                                               
                                                                                                                     
                             
                                                     

                                                    
                                                            

                           
                            
                                  







                                                                                                                       














                                                                             


                                                                                                                 
                                                                                                         
                                                                                         
                                       












                                                                                   
                                                                                                       
                                                                            

                                                                           






                                                                            
                                                 
                                                                        
                                                                                       


                                                                        
                                                                           
                                                                                   

                                            
                                
                                                                                       


                                                     
                                                                                                    
             

                                                                                                        
                                                     


                                
                                                                                                          
                                                     

                                              
                                                            



                                                                                              

                                                                                                    
                                                                  



                                   
                                                                                                             
                                                     

                                              
                                                            








                                                                                     
                                                                                                             
                                                     
                                                            








                                                                                     
                                                                                                        
                                                     
                                                
                                               
                                                            






                                                                                     
 
                                 
                                                                                                                      
                                                                                             
                                                            
                                                                      
                                                             

                                                                                  
                                                               
                                                                                                                 



                                                             
                                                                                                                   






                                                                               
                                                                                           
                                                                        
                                                                                       

                                                                      
                                                                        
                                                                           
                                                                                   














                                                                          

                                                                                       
                                                                                                                  
                                                     
                             
 
                            
                                                                               
                                                            
                                   
                                                                    
                                                                                       







                                                                
                                                                                                                            
                                                            
                                  
                                                                     
                                                                                       









                                                                
 

                                                                                                                                                 
                                                       
                                                    
     
                                                                                                        
                                                                                                           
                                                                                                                       

                                                                    
                                                            
                                                                    
 
                                

                                              
                                    
                                                     


                                                                                  
                                                                        
                                                                                  




                                                                             
                                                                                          






                                                                                        
                                                                                                                   



                                                                                                              
                                                              




                                                                                         
                                                                     


                                                                                                          



                                                                                                                   
                                                                                


                                                                                              
                                                                                                             

                                                                                                              
                                                                                                                        
                                                        
                                                 
                                                                                                                   

                                                             
                                                                                

                                                                                     
                                                                                                               

                                               

                              
                        
                                                                                                                            














                                                            

                                                                                                    
                                                                  

                            




                                                          
/*
 * Copyright (C) 2008-2020, 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 <http://www.gnu.org/licenses/>.
 */
package com.juick.www.controllers;

import com.juick.model.Message;
import com.juick.model.Tag;
import com.juick.model.User;
import com.juick.util.formatters.PlainTextFormatter;
import com.juick.util.HttpForbiddenException;
import com.juick.util.HttpNotFoundException;
import com.juick.util.WebUtils;
import com.juick.www.WebApp;
import com.juick.www.api.activity.model.Context;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;

import com.juick.service.*;
import com.juick.util.MessageUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.text.StringEscapeUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.WebAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;

import javax.inject.Inject;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.stream.Collectors;

/**
 *
 * @author Ugnich Anton
 */
@Controller
public class Site {
    @Inject
    private UserService userService;
    @Inject
    private TagService tagService;
    @Inject
    private MessagesService messagesService;
    @Inject
    private ChatService chatService;
    @Inject
    private WebApp webApp;
    @Inject
    private User serviceUser;
    @Value("${web_domain:localhost}")
    private String webDomain;
    @Value("${telegram_botname:Juick_bot}")
    private String tgBot;

    private void fillUserModel(ModelMap model, User user, User visitor) {
        user.setAvatar(webApp.getAvatarWebPath(user));
        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.getUserFriends(user.getUid()).size());
        model.addAttribute("statsMyReaders", userService.getUserReaders(user.getUid()).size());
        model.addAttribute("statsMyBL", userService.getUserBLUsers(user.getUid()).size());
        model.addAttribute("statsMessages", userService.getStatsMessages(user.getUid()));
        model.addAttribute("statsReplies", userService.getStatsReplies(user.getUid()));
        model.addAttribute("iread", userService.getUserReadLeastPopular(user.getUid(), 8));
        model.addAttribute("tagStats",
                tagService.getUserTagStats(user.getUid()).stream()
                        .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).limit(20)
                        .map(t -> t.getTag().getName()).toList());
    }

    @GetMapping("/login")
    public String getloginForm(@ModelAttribute User visitor,
            @RequestParam(name = "retpath", required = false, defaultValue = "/") String retPath,
            HttpSession session,
            ModelMap model) {
        if (!visitor.isAnonymous()) {
            return String.format("redirect:%s", retPath);
        }
        model.addAttribute("visitor", visitor);
        model.addAttribute("tags", tagService.getPopularTags());
        model.addAttribute("domain", webDomain);
        model.addAttribute("tgBot", tgBot);
        AuthenticationException authEx = (AuthenticationException) session
                .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);

        if (authEx != null) {
            model.addAttribute("authErrorMessage", authEx.getLocalizedMessage());
        }

        String socialLoginError = (String) session.getAttribute(SocialLogin.AUTH_ERROR);

        if (socialLoginError != null) {
            model.addAttribute("authErrorMessage", socialLoginError);
        }

        return "views/login";
    }

    @GetMapping("/")
    protected String doGet(@ModelAttribute User visitor, Locale locale, @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,
            @RequestParam(name = "to", required = false, defaultValue = "0") Long paramTo,
            @RequestParam(name = "page", required = false, defaultValue = "0") Integer page, ModelMap model) {
        if (tag != null) {
            return "redirect:/tag/" + URLEncoder.encode(tag, StandardCharsets.UTF_8);
        }
        visitor.setAvatar(webApp.getAvatarWebPath(visitor));

        if (paramSearch != null && paramSearch.length() > 64) {
            paramSearch = null;
        }

        model.addAttribute("discover", false);

        String title;
        List<Integer> mids;

        if (paramSearch != null) {
            String searchTitle = ResourceBundle.getBundle("messages", locale).getString("title.search");
            title = searchTitle + StringEscapeUtils.escapeHtml4(paramSearch);
            mids = messagesService.getSearch(visitor, paramSearch, page);
        } else if (paramShow == null) {
            title = ResourceBundle.getBundle("messages", locale).getString("link.discuss");
            mids = messagesService.getDiscussions(visitor.getUid(), paramTo);
        } else if (paramShow.equals("top")) {
            title = ResourceBundle.getBundle("messages", locale).getString("link.popular");
            mids = messagesService.getUserBlogWithRecommendations(serviceUser, visitor, 0, paramBefore);
            model.addAttribute("discover", true);
        } else if (paramShow.equals("my") && !visitor.isAnonymous()) {
            title = ResourceBundle.getBundle("messages", locale).getString("link.my");
            mids = messagesService.getMyFeed(visitor.getUid(), paramBefore, true);
        } else if (paramShow.equals("private") && !visitor.isAnonymous()) {
            title = ResourceBundle.getBundle("messages", locale).getString("link.privateMessages");
            mids = messagesService.getPrivate(visitor.getUid(), paramBefore);
        } else if (paramShow.equals("discuss")) {
            return "redirect:/";
        } else if (paramShow.equals("recommended") && !visitor.isAnonymous()) {
            title = ResourceBundle.getBundle("messages", locale).getString("link.recommended");
            mids = messagesService.getRecommended(visitor.getUid(), paramBefore);
        } else if (paramShow.equals("photos")) {
            title = ResourceBundle.getBundle("messages", locale).getString("link.withPhotos");
            mids = messagesService.getPhotos(visitor.getUid(), paramBefore);
            model.addAttribute("discover", true);
        } else if (paramShow.equals("all")) {
            title = ResourceBundle.getBundle("messages", locale).getString("link.allMessages");
            mids = messagesService.getAll(visitor.getUid(), paramBefore);
            model.addAttribute("discover", true);
        } else {
            throw new HttpNotFoundException();
        }

        String head = "<meta name=\"Description\" content=\"" + title + "\" />\n";

        if (paramBefore > 0 || paramShow != null || paramTo > 0 || page > 0 || paramSearch != null) {
            head = "<meta name=\"robots\" content=\"noindex\"/>";
        }
        model.addAttribute("title", title);
        model.addAttribute("headers", head);
        model.addAttribute("visitor", visitor);
        model.addAttribute("noindex", !(paramShow == null && paramBefore == 0));
        List<Message> msgs = messagesService.getMessages(visitor, mids);
        msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser())));
        if (!visitor.isAnonymous()) {
            fillUserModel(model, visitor, visitor);
            List<Integer> unread = messagesService.getUnread(visitor);
            visitor.setUnreadCount(unread.size());
            List<Integer> blUIDs = userService.checkBL(visitor.getUid(),
                    msgs.stream().map(m -> m.getUser().getUid()).toList());
            msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid()));
        }
        model.addAttribute("msgs", msgs);
        model.addAttribute("tags", tagService.getPopularTags());
        model.addAttribute("headers", head);
        if (mids.size() >= 20) {
            String nextpage = paramSearch != null ? String.format("?page=%d", page + 1)
                    : (paramShow == null) ? "?to=" + msgs.get(msgs.size() - 1).getUpdated().toEpochMilli()
                            : "?before=" + mids.get(mids.size() - 1);
            if (paramShow != null) {
                nextpage += "&amp;show=" + paramShow;
            }
            if (paramSearch != null) {
                nextpage += "&amp;search=" + URLEncoder.encode(paramSearch, StandardCharsets.UTF_8);
            }
            model.addAttribute("nextpage", nextpage);
        }
        return "views/index";
    }

    @GetMapping(path = "/{uname}/", headers = "Connection!=Upgrade")
    protected String doGetBlog(@ModelAttribute User visitor, @RequestParam(required = false, name = "show") String paramShow,
            @RequestParam(required = false, name = "tag") String paramTagStr,
            @RequestParam(required = false, name = "search") String paramSearch,
            @RequestParam(required = false, name = "page", defaultValue = "0") Integer page, @PathVariable String uname,
            @RequestParam(required = false, defaultValue = "0") Integer before,
            @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie,
            ModelMap model) {
        User user = userService.getUserByName(uname);
        if (user.isBanned() || user.isAnonymous()) {
            throw new HttpNotFoundException();
        }
        visitor.setAvatar(webApp.getAvatarWebPath(visitor));

        List<Integer> mids;

        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(), StandardCharsets.UTF_8);
                return "redirect:/" + url;
            }
        }
        if (paramSearch != null && paramSearch.length() > 64) {
            paramSearch = null;
        }

        int privacy = 0;
        if (!visitor.isAnonymous()) {
            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(visitor, user.getUid(), paramSearch,
                        privacy, page);
            } 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 = "<link rel=\"alternate\" type=\"application/rss+xml\" title=\"@" + user.getName()
                + "\" href=\"//rss.juick.com/" + user.getName() + "/blog\"/>";
        head += "<meta name=\"Description\" content=\"" + title + "\" />\n";
        if (paramTag != null && tagService.getTagNoIndex(paramTag.TID)) {
            head += "<meta name=\"robots\" content=\"noindex,nofollow\"/>";
        } else if (before > 0 || paramShow != null) {
            head += "<meta name=\"robots\" content=\"noindex\"/>";
        }
        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<Message> msgs = messagesService.getMessages(visitor, mids);
        msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser())));
        if (!visitor.isAnonymous()) {
            List<Integer> unread = messagesService.getUnread(visitor);
            visitor.setUnreadCount(unread.size());
            List<Integer> blUIDs = userService.checkBL(visitor.getUid(),
                    msgs.stream().map(m -> m.getUser().getUid()).toList());
            msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid()));
        }
        model.addAttribute("msgs", msgs);
        model.addAttribute("headers", head);
        if (mids.size() >= 20) {
            String nextpage = paramSearch != null ? String.format("?page=%d", page + 1)
                    : "?before=" + mids.get(mids.size() - 1);
            if (paramShow != null) {
                nextpage += "&amp;show=" + paramShow;
            }
            if (paramSearch != null) {
                nextpage += "&amp;search=" + URLEncoder.encode(paramSearch, StandardCharsets.UTF_8);
            }
            if (paramTag != null) {
                nextpage += "&amp;tag=" + URLEncoder.encode(paramTag.getName(), StandardCharsets.UTF_8);
            }
            model.addAttribute("nextpage", nextpage);
        }
        return "views/blog";
    }

    @GetMapping("/{uname}/tags")
    protected String doGetTags(@ModelAttribute User visitor, @PathVariable String uname, ModelMap model) {
        User user = userService.getUserByName(uname);
        if (visitor.isBanned()) {
            throw new HttpNotFoundException();
        }
        visitor.setAvatar(webApp.getAvatarWebPath(visitor));

        model.addAttribute("title", "Теги " + user.getName());
        model.addAttribute("headers", "<meta name=\"robots\" content=\"noindex,nofollow\"/>");
        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()).toList());

        return "views/blog_tags";
    }

    @GetMapping("/{uname}/friends")
    protected String doGetFriends(@ModelAttribute User visitor, @PathVariable String uname, ModelMap model) {
        User user = userService.getUserByName(uname);
        if (visitor.isBanned()) {
            throw new HttpNotFoundException();
        }
        visitor.setAvatar(webApp.getAvatarWebPath(visitor));
        model.addAttribute("title", "Подписки " + user.getName());
        model.addAttribute("headers", "<meta name=\"robots\" content=\"noindex\"/>");
        model.addAttribute("visitor", visitor);
        fillUserModel(model, user, visitor);
        model.addAttribute("users", userService.getUserFriends(user.getUid()));

        return "views/users";
    }

    @GetMapping("/{uname}/readers")
    protected String doGetReaders(@ModelAttribute User visitor, @PathVariable String uname, ModelMap model) {
        User user = userService.getUserByName(uname);
        visitor.setAvatar(webApp.getAvatarWebPath(visitor));
        model.addAttribute("title", "Читатели " + user.getName());
        model.addAttribute("headers", "<meta name=\"robots\" content=\"noindex\"/>");
        model.addAttribute("visitor", visitor);
        fillUserModel(model, user, visitor);
        model.addAttribute("users", userService.getUserReaders(user.getUid()));

        return "views/users";
    }

    @GetMapping("/{uname}/bl")
    protected String doGetBL(@ModelAttribute User visitor, @PathVariable String uname, ModelMap model) {
        User user = userService.getUserByName(uname);
        if (visitor.getUid() != user.getUid()) {
            throw new HttpForbiddenException();
        }
        visitor.setAvatar(webApp.getAvatarWebPath(visitor));
        model.addAttribute("title", "Черный список " + user.getName());
        model.addAttribute("headers", "<meta name=\"robots\" content=\"noindex\"/>");
        model.addAttribute("visitor", visitor);
        fillUserModel(model, user, visitor);
        model.addAttribute("users", userService.getUserBLUsers(user.getUid()));

        return "views/users";
    }

    @GetMapping("/tag/{tagName}")
    protected String tagAction(@ModelAttribute User visitor, HttpServletRequest request, @PathVariable String tagName,
            @RequestParam(required = false, defaultValue = "0") int before, ModelMap model) {
        visitor.setAvatar(webApp.getAvatarWebPath(visitor));
        String paramTagStr = StringEscapeUtils.unescapeHtml4(tagName);
        Tag paramTag = tagService.getTag(paramTagStr, false);
        if (paramTag == null) {
            throw new HttpNotFoundException();
        } else if (paramTag.SynonymID > 0 && paramTag.TID != paramTag.SynonymID) {
            Tag synTag = tagService.getTag(paramTag.SynonymID);
            String url = "/tag/"
                    + URLEncoder.encode(StringEscapeUtils.escapeHtml4(synTag.getName()), StandardCharsets.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()), StandardCharsets.UTF_8);
            if (request.getQueryString() != null) {
                url += "?" + request.getQueryString();
            }
            return "redirect:" + url;
        }

        String title = "*" + StringEscapeUtils.escapeHtml4(paramTag.getName());
        model.addAttribute("title", title);
        List<Integer> mids = messagesService.getTag(paramTag.TID, visitor.getUid(), before,
                (visitor.isAnonymous()) ? 40 : 20);
        List<Message> msgs = messagesService.getMessages(visitor, mids);
        msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser())));
        if (!visitor.isAnonymous()) {
            List<Integer> unread = messagesService.getUnread(visitor);
            visitor.setUnreadCount(unread.size());
            List<Integer> blUIDs = userService.checkBL(visitor.getUid(),
                    msgs.stream().map(m -> m.getUser().getUid()).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 = "<meta name=\"robots\" content=\"noindex,nofollow\"/>";
        } else if (before > 0 || mids.size() < 5) {
            head = "<meta name=\"robots\" content=\"noindex\"/>";
        }
        model.addAttribute("headers", head);
        model.addAttribute("visitor", visitor);
        model.addAttribute("tag", paramTag);
        model.addAttribute("title", title);
        model.addAttribute("msgs", msgs);
        model.addAttribute("tags", tagService.getPopularTags());
        model.addAttribute("noindex", before > 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(), StandardCharsets.UTF_8) + "?before="
                    + mids.get(mids.size() - 1);
            model.addAttribute("nextpage", nextpage);
        }
        return "views/index";
    }

    @GetMapping("/pm/inbox")
    protected String doGetInbox(@ModelAttribute User visitor, ModelMap model) {
        visitor.setAvatar(webApp.getAvatarWebPath(visitor));
        String title = "PM: Inbox";
        List<Message> msgs = chatService.getInbox(visitor.getUid());
        msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser())));
        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(@ModelAttribute User visitor, @RequestParam(required = false) String uname, ModelMap model) {
        visitor.setAvatar(webApp.getAvatarWebPath(visitor));
        String title = "PM: Sent";
        List<Message> msgs = chatService.getOutbox(visitor.getUid());
        msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser())));
        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(value = "/{uname}/{mid}", produces = { MediaType.APPLICATION_JSON_VALUE, Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITY_MEDIA_TYPE,
        Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE })
    public RedirectView threadRedirect(@PathVariable String uname, @PathVariable int mid) {
        String linkedDataLocation = "/n/" + mid + "-0";
        return new RedirectView(linkedDataLocation);
    }

    @GetMapping(value = "/{uname}/{mid}", produces = { MediaType.TEXT_HTML_VALUE, MediaType.ALL_VALUE })
    protected String threadAction(@ModelAttribute User visitor, ModelMap model, @PathVariable String uname,
            @PathVariable int mid,
            @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie) {
        if (!messagesService.canViewThread(mid, visitor.getUid())) {
            throw new HttpForbiddenException();
        }
        visitor.setAvatar(webApp.getAvatarWebPath(visitor));
        Optional<Message> message = messagesService.getMessage(mid);

        if (message.isEmpty()) {
            throw new HttpNotFoundException();
        }

        Message msg = message.get();

        User user = userService.getUserByName(uname);
        if (user.isAnonymous() || !msg.getUser().equals(user)) {
            return String.format("redirect:/%s/%d", msg.getUser().getName(), mid);
        }
        msg.VisitorCanComment = !visitor.isAnonymous();
        msg.getUser().setAvatar(webApp.getAvatarWebPath(msg.getUser()));
        List<Message> replies = messagesService.getReplies(visitor, msg.getMid());
        // this should be after getReplies to mark thread as read
        fillUserModel(model, user, visitor);
        if (!visitor.isAnonymous()) {
            List<Integer> unread = messagesService.getUnread(visitor);
            visitor.setUnreadCount(unread.size());
            boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid();
            boolean isInBL = userService.isInBL(visitor.getUid(), msg.getUser().getUid());
            msg.VisitorCanComment = isMsgAuthor || !(msg.ReadOnly || isInBL);
        }
        model.addAttribute("msg", msg);

        String title = msg.getUser().getName() + ": " + MessageUtils.getTagsString(msg);

        model.addAttribute("title", title);
        model.addAttribute("visitor", visitor);
        String headers = "<link rel=\"alternate\" type=\"application/rss+xml\" title=\"@" + msg.getUser().getName()
                + "\" href=\"//rss.juick.com/" + msg.getUser().getName() + "/blog\"/>";
        String pageUrl = "https://juick.com/" + msg.getUser().getName() + "/" + msg.getMid();
        if (msg.Hidden) {
            headers += "<meta name=\"robots\" content=\"noindex\"/>";
        }
        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 += "<meta property=\"og:image\" content=\"" + msgImage + "\" />";
            }
        } else {
            String msgImage = webApp.getAvatarWebPath(msg.getUser());
            headers += "<meta property=\"og:image\" content=\"" + msgImage + "\" />";
        }
        model.addAttribute("ogtype", "article");
        String cardDescription = StringEscapeUtils.escapeHtml4(PlainTextFormatter.formatTwitterCard(msg));
        headers += "<meta name=\"twitter:card\" content=\"" + cardType + "\" />\n"
                + "<meta name=\"twitter:site\" content=\"@juick\" />\n" + "<meta property=\"og:url\" content=\""
                + pageUrl + "\" />\n" + "<meta property=\"og:title\" content=\"" + msg.getUser().getName()
                + " at Juick\" />\n" + "<meta property=\"og:description\" content=\"" + cardDescription + "\" />\n"
                + "<meta name=\"Description\" content=\"" + cardDescription + "\" />\n";
        String twitterName = userService.getTwitterName(msg.getUser().getUid());
        if (StringUtils.isNotEmpty(twitterName)) {
            headers += "<meta name=\"twitter:creator\" content=\"@" + twitterName + "\" />\n";
        }
        if (msg.getTags().size() > 0) {
            headers += "<meta name=\"Keywords\" content=\""
                    + msg.getTags().stream().map(Tag::getName).collect(Collectors.joining(", ")) + "\" />\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.getMessagesRecommendations(Collections.singletonList(msg.getMid()))
                .stream().map(Pair::getRight).toList());
        List<Integer> blUIDs = new ArrayList<>();
        for (Message reply : replies) {
            if (reply.getUser().getUid() != msg.getUser().getUid() && !blUIDs.contains(reply.getUser().getUid())) {
                blUIDs.add(reply.getUser().getUid());
            }
            reply.VisitorCanComment = !visitor.isAnonymous();
            reply.getUser().setAvatar(webApp.getAvatarWebPath(reply.getUser()));
            if (!visitor.isAnonymous()) {
                boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid();
                boolean isReplyAuthor = visitor.getUid() == reply.getUser().getUid();
                reply.VisitorCanComment = isMsgAuthor || (!msg.ReadOnly && msg.VisitorCanComment
                        && (isReplyAuthor || !userService.isInBL(visitor.getUid(), reply.getUser().getUid())));
            }
        }
        model.addAttribute("replies", replies);
        return "views/thread";
    }

    @GetMapping("/post")
    protected String postAction(@ModelAttribute User visitor, @RequestParam(required = false) String body, ModelMap model) {
        fillUserModel(model, visitor, visitor);
        visitor.setAvatar(webApp.getAvatarWebPath(visitor));
        model.addAttribute("title", "Написать");
        model.addAttribute("headers", "");
        model.addAttribute("visitor", visitor);
        if (body == null) {
            body = StringUtils.EMPTY;
        } else {
            if (body.length() > 4096) {
                body = body.substring(0, 4096);
            }
            body = StringEscapeUtils.escapeHtml4(body);
        }
        model.addAttribute("body", body);
        model.addAttribute("visitor", visitor);
        model.addAttribute("user", visitor);
        model.addAttribute("tags",
                tagService.getUserTagStats(visitor.getUid()).stream()
                        .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount()))
                        .map(t -> t.getTag().getName()).toList());
        return "views/post";
    }

    // when message id is not fit to int
    @ExceptionHandler(NumberFormatException.class)
    public ResponseEntity<String> notFoundAction() {
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
}