diff options
6 files changed, 112 insertions, 31 deletions
diff --git a/juick-core/build.gradle b/juick-core/build.gradle index ae44f28c..c145a504 100644 --- a/juick-core/build.gradle +++ b/juick-core/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'java' dependencies { compile "com.fasterxml.jackson.core:jackson-annotations:${rootProject.jacksonVersion}" compile 'org.apache.commons:commons-lang3:3.6' + compile "commons-codec:commons-codec:1.10" compile 'org.apache.commons:commons-collections4:4.1' compile "org.apache.commons:commons-text:1.1" compile 'commons-io:commons-io:2.5' diff --git a/juick-core/src/main/java/com/juick/formatters/PlainTextFormatter.java b/juick-core/src/main/java/com/juick/formatters/PlainTextFormatter.java index b86ebb73..8f9378a8 100644 --- a/juick-core/src/main/java/com/juick/formatters/PlainTextFormatter.java +++ b/juick-core/src/main/java/com/juick/formatters/PlainTextFormatter.java @@ -17,6 +17,8 @@ package com.juick.formatters; +import com.juick.Message; +import com.juick.util.MessageUtils; import org.apache.commons.lang3.StringUtils; import org.ocpsoft.prettytime.PrettyTime; @@ -28,7 +30,7 @@ import java.util.Locale; public class PlainTextFormatter { static PrettyTime pt = new PrettyTime(new Locale("ru")); - public static String formatPost(com.juick.Message jmsg) { + public static String formatPost(Message jmsg) { StringBuilder sb = new StringBuilder(); boolean isReply = jmsg.getRid() > 0; String title = isReply ? "Reply by @" : "@"; @@ -41,7 +43,7 @@ public class PlainTextFormatter { return sb.toString(); } - public static String formatPostSummary(com.juick.Message m) { + public static String formatPostSummary(Message m) { int cropLength = 384; String timeAgo = pt.format(m.getDate()); String repliesCount = m.getReplies() == 1 ? "; 1 reply" : m.getReplies() == 0 ? "" @@ -58,4 +60,8 @@ public class PlainTextFormatter { } return "https://juick.com/" + jmsg.getMid(); } + + public static String formatTwitterCard(Message jmsg) { + return MessageUtils.getMessageHashTags(jmsg) + jmsg.getText(); + } } diff --git a/juick-core/src/main/java/com/juick/util/MessageUtils.java b/juick-core/src/main/java/com/juick/util/MessageUtils.java index a901fac1..03b677d4 100644 --- a/juick-core/src/main/java/com/juick/util/MessageUtils.java +++ b/juick-core/src/main/java/com/juick/util/MessageUtils.java @@ -17,8 +17,13 @@ package com.juick.util; +import com.juick.Message; +import com.juick.Tag; +import org.apache.commons.codec.CharEncoding; import org.apache.commons.lang3.StringUtils; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -137,4 +142,22 @@ public class MessageUtils { msg = msg.replaceAll("\n", "<br/>\n"); return msg; } + + public static String getMessageHashTags(final Message jmsg) { + StringBuilder hashtags = new StringBuilder(); + for (Tag tag : jmsg.getTags()) { + hashtags.append("#").append(tag).append(" "); + } + return hashtags.toString(); + } + + // TODO: check if it is really needed + public static String percentEncode(final String s) { + String ret = StringUtils.EMPTY; + try { + ret = URLEncoder.encode(s, CharEncoding.UTF_8).replace("+", "%20").replace("*", "%2A").replace("%7E", "~"); + } catch (UnsupportedEncodingException e) { + } + return ret; + } } diff --git a/juick-crosspost/src/main/java/com/juick/components/Crosspost.java b/juick-crosspost/src/main/java/com/juick/components/Crosspost.java index 6ad7f5d3..f481c348 100644 --- a/juick-crosspost/src/main/java/com/juick/components/Crosspost.java +++ b/juick-crosspost/src/main/java/com/juick/components/Crosspost.java @@ -19,9 +19,9 @@ package com.juick.components; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.juick.Message; -import com.juick.Tag; import com.juick.service.CrosspostService; import com.juick.service.MessagesService; +import com.juick.util.MessageUtils; import org.apache.commons.codec.CharEncoding; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; @@ -40,7 +40,6 @@ import javax.crypto.spec.SecretKeySpec; import javax.inject.Inject; import javax.net.ssl.HttpsURLConnection; import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -80,15 +79,6 @@ public class Crosspost extends TextWebSocketHandler { ms.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); } - public static String percentEncode(final String s) { - String ret = StringUtils.EMPTY; - try { - ret = URLEncoder.encode(s, CharEncoding.UTF_8).replace("+", "%20").replace("*", "%2A").replace("%7E", "~"); - } catch (UnsupportedEncodingException e) { - } - return ret; - } - @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { Message msg = ms.readValue(message.asBytes(), Message.class); @@ -111,7 +101,7 @@ public class Crosspost extends TextWebSocketHandler { logger.info("FB: #{}", jmsg.getMid()); - String status = getMessageHashTags(jmsg) + "\n" + jmsg.getText(); + String status = MessageUtils.getMessageHashTags(jmsg) + "\n" + jmsg.getText(); boolean ret = false; try { @@ -153,7 +143,7 @@ public class Crosspost extends TextWebSocketHandler { logger.info("VK: #", jmsg.getMid()); - String status = getMessageHashTags(jmsg) + "\n" + jmsg.getText() + "\nhttp://juick.com/" + jmsg.getMid(); + String status = MessageUtils.getMessageHashTags(jmsg) + "\n" + jmsg.getText() + "\nhttp://juick.com/" + jmsg.getMid(); boolean ret = false; try { @@ -187,23 +177,23 @@ public class Crosspost extends TextWebSocketHandler { if (tokens.getLeft().isEmpty() || tokens.getRight().isEmpty()) { return false; } - String token = percentEncode(tokens.getLeft()); - String token_secret = percentEncode(tokens.getRight()); + String token = MessageUtils.percentEncode(tokens.getLeft()); + String token_secret = MessageUtils.percentEncode(tokens.getRight()); logger.info("TWITTER: #{}", jmsg.getMid()); - String status = getMessageHashTags(jmsg) + jmsg.getText(); + String status = MessageUtils.getMessageHashTags(jmsg) + jmsg.getText(); if (status.length() > 115) { status = status.substring(0, 114) + "…"; } status += " http://juick.com/" + jmsg.getMid(); - status = percentEncode(status); + status = MessageUtils.percentEncode(status); boolean ret = false; try { String nonce = UUID.randomUUID().toString(); String timestamp = Long.toString(System.currentTimeMillis() / 1000L); - String signature = percentEncode(twitterSignature(status, nonce, timestamp, token, token_secret)); + String signature = MessageUtils.percentEncode(twitterSignature(status, nonce, timestamp, token, token_secret)); String auth = "OAuth " + "oauth_consumer_key=\"" + twitter_consumer_key + "\", " + "oauth_nonce=\"" + nonce + "\", " @@ -248,7 +238,7 @@ public class Crosspost extends TextWebSocketHandler { + "&oauth_version=1.0" + "&status=" + status; - String base = "POST&" + percentEncode(TWITTERURL) + "&" + percentEncode(params); + String base = "POST&" + MessageUtils.percentEncode(TWITTERURL) + "&" + MessageUtils.percentEncode(params); String key = twitter_consumer_secret + "&" + token_secret; Key signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1"); @@ -262,12 +252,4 @@ public class Crosspost extends TextWebSocketHandler { } return null; } - - public String getMessageHashTags(final com.juick.Message jmsg) { - String hashtags = StringUtils.EMPTY; - for (Tag tag : jmsg.getTags()) { - hashtags += "#" + tag + " "; - } - return hashtags; - } } diff --git a/juick-www/src/main/java/com/juick/www/controllers/UserThread.java b/juick-www/src/main/java/com/juick/www/controllers/UserThread.java index f15861f4..400f231a 100644 --- a/juick-www/src/main/java/com/juick/www/controllers/UserThread.java +++ b/juick-www/src/main/java/com/juick/www/controllers/UserThread.java @@ -16,11 +16,15 @@ */ package com.juick.www.controllers; +import com.juick.formatters.PlainTextFormatter; import com.juick.server.util.HttpForbiddenException; import com.juick.server.util.HttpNotFoundException; import com.juick.server.util.UserUtils; +import com.juick.service.CrosspostService; import com.juick.service.MessagesService; import com.juick.service.UserService; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringEscapeUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -46,6 +50,8 @@ public class UserThread { private MessagesService messagesService; @Inject private UserService userService; + @Inject + private CrosspostService crosspostService; @GetMapping("/{uname}/{mid}") protected String threadAction(ModelMap model, @@ -94,12 +100,25 @@ public class UserThread { 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 = "//juick.com/" + msg.getUser().getName() + "/" + msg.getMid(); if (paramView != null) { - headers += "<link rel=\"canonical\" href=\"http://juick.com/" + msg.getUser().getName() + "/" + msg.getMid() + "\"/>"; + headers += "<link rel=\"canonical\" href=\"" + pageUrl + "\"/>"; } if (msg.Hidden) { headers += "<meta name=\"robots\" content=\"noindex\"/>"; } + String cardType = StringUtils.isNotEmpty(msg.getAttachmentType()) ? "summary_large_image" : "summary"; + String msgImage = StringUtils.isNotEmpty(msg.getAttachmentType()) ? msg.getAttachmentURL() : "//i.juick.com/a/" + msg.getUser().getUid() + ".png"; + 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=\"" + StringEscapeUtils.escapeHtml4(PlainTextFormatter.formatTwitterCard(msg)) + "\" />\n" + + "<meta property=\"og:image\" content=\"" + msgImage + "\" />"; + String twitterName = crosspostService.getTwitterName(msg.getUser().getUid()); + if (StringUtils.isNotEmpty(twitterName)) { + headers += "<meta name=\"twitter:creator\" content=\"@" + twitterName + "\" />\n"; + } model.addAttribute("headers", headers); model.addAttribute("contentStyle", "margin-left: 0; width: 100%"); model.addAttribute("isModerator", visitor.getUid() == 3694); diff --git a/juick-www/src/test/java/com/juick/www/WebAppTests.java b/juick-www/src/test/java/com/juick/www/WebAppTests.java index d09bded2..464e9416 100644 --- a/juick-www/src/test/java/com/juick/www/WebAppTests.java +++ b/juick-www/src/test/java/com/juick/www/WebAppTests.java @@ -20,6 +20,7 @@ package com.juick.www; import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.css.StyleElement; +import com.gargoylesoftware.htmlunit.html.DomElement; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.juick.Message; import com.juick.Tag; @@ -29,6 +30,7 @@ import com.juick.server.configuration.BaseWebConfiguration; import com.juick.service.MessagesService; import com.juick.service.UserService; import com.juick.test.util.MockUtils; +import com.juick.util.MessageUtils; import com.juick.www.configuration.SapeConfiguration; import com.juick.www.configuration.WwwAppConfiguration; import com.juick.www.configuration.WwwServletConfiguration; @@ -51,13 +53,16 @@ import javax.inject.Inject; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; -import java.util.*; +import java.util.Collections; +import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.StreamSupport; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; import static org.mockito.Mockito.when; /** @@ -195,4 +200,49 @@ public class WebAppTests { String output = writer.toString().trim(); assertThat(output, equalTo("<a href=\"/ugnich/?tag=%26gt%3B_%26lt%3B\">>_<</a>")); } + + public DomElement fetchMeta(String url, String name) throws IOException { + HtmlPage threadPage = webClient.getPage(url); + DomElement emptyMeta = new DomElement("", "meta", null, null); + return threadPage.getElementsByTagName("meta").stream() + .filter(t -> t.getAttribute("name").equals(name)).findFirst().orElse(emptyMeta); + } + @Test + public void testTwitterCards() throws Exception { + String ugnichName = "ugnich"; + String ugnichPassword = "MyPassw0rd!"; + String msgText = "Привет, я - Угнич"; + String hash = "12345678"; + + User user = MockUtils.mockUser(1, ugnichName, ugnichPassword); + Message msg = MockUtils.mockMessage(1, user, msgText); + + when(userService.getUIDbyName(ugnichName)) + .thenReturn(1); + when(userService.getUserByName(ugnichName)) + .thenReturn(user); + when(userService.getUserByUID(1)) + .thenReturn(Optional.of(user)); + when(userService.getFullyUserByName(ugnichName)) + .thenReturn(user); + when(messagesService.getMyFeed(1, 0)) + .thenReturn(Collections.singletonList(1)); + when(messagesService.getMessages(Collections.singletonList(1))) + .thenReturn(Collections.singletonList(msg)); + when(userService.getUIDbyHash(hash)) + .thenReturn(1); + when(messagesService.getMessageAuthor(1)).thenReturn(user); + when(messagesService.canViewThread(1, 0)).thenReturn(true); + when(messagesService.getMessage(1)).thenReturn(msg); + + assertThat(fetchMeta("http://localhost:8080/ugnich/1", "twitter:card") + .getAttribute("content"), equalTo("summary")); + msg.setAttachmentType("png"); + assertThat(fetchMeta("http://localhost:8080/ugnich/1", "twitter:card") + .getAttribute("content"), equalTo("summary_large_image")); + assertThat(fetchMeta("http://localhost:8080/ugnich/1", "og:description") + .getAttribute("content"), + startsWith(StringEscapeUtils.escapeHtml4(MessageUtils.getMessageHashTags(msg)))); + + } } |