From 7414f1034d32c249294a081f1e176a9266fc92ac Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Thu, 8 Feb 2018 22:06:08 +0300 Subject: reorganize project structure --- .../src/main/java/com/juick/api/ApiServer.java | 251 ---------------- .../main/java/com/juick/api/CrosspostManager.java | 165 ----------- .../src/main/java/com/juick/api/EmailManager.java | 110 ------- .../java/com/juick/api/FacebookPageManager.java | 32 --- .../main/java/com/juick/api/MessengerManager.java | 143 --------- .../java/com/juick/api/TelegramBotManager.java | 298 ------------------- .../src/main/java/com/juick/api/TopManager.java | 57 ---- .../main/java/com/juick/api/WebsocketManager.java | 163 ----------- .../api/configuration/ApiAppConfiguration.java | 135 --------- .../juick/api/configuration/ApiInitializer.java | 65 ----- .../juick/api/configuration/ApiSecurityConfig.java | 128 --------- .../api/configuration/ApiSecurityInitializer.java | 37 --- .../com/juick/api/controllers/ApiController.java | 54 ---- .../java/com/juick/api/controllers/Messages.java | 181 ------------ .../juick/api/controllers/MessengerWebhook.java | 62 ---- .../com/juick/api/controllers/Notifications.java | 190 ------------ .../java/com/juick/api/controllers/Others.java | 66 ----- .../main/java/com/juick/api/controllers/PM.java | 127 -------- .../main/java/com/juick/api/controllers/Post.java | 281 ------------------ .../java/com/juick/api/controllers/Service.java | 74 ----- .../com/juick/api/controllers/SkypeEndpoint.java | 47 --- .../main/java/com/juick/api/controllers/Tags.java | 54 ---- .../com/juick/api/controllers/TelegramWebhook.java | 48 ---- .../main/java/com/juick/api/controllers/Users.java | 125 -------- .../juick/api/controllers/util/JsonpAdvice.java | 31 -- .../java/com/juick/server/CrosspostManager.java | 165 +++++++++++ .../main/java/com/juick/server/EmailManager.java | 110 +++++++ .../java/com/juick/server/FacebookPageManager.java | 32 +++ .../java/com/juick/server/MessengerManager.java | 143 +++++++++ .../main/java/com/juick/server/ServerManager.java | 252 ++++++++++++++++ .../java/com/juick/server/TelegramBotManager.java | 298 +++++++++++++++++++ .../src/main/java/com/juick/server/TopManager.java | 57 ++++ .../java/com/juick/server/WebsocketManager.java | 163 +++++++++++ .../src/main/java/com/juick/server/api/Index.java | 54 ++++ .../main/java/com/juick/server/api/Messages.java | 180 ++++++++++++ .../java/com/juick/server/api/Notifications.java | 190 ++++++++++++ .../src/main/java/com/juick/server/api/PM.java | 148 ++++++++++ .../src/main/java/com/juick/server/api/Post.java | 281 ++++++++++++++++++ .../main/java/com/juick/server/api/Service.java | 74 +++++ .../src/main/java/com/juick/server/api/Tags.java | 54 ++++ .../src/main/java/com/juick/server/api/Users.java | 125 ++++++++ .../server/api/webhooks/MessengerWebhook.java | 62 ++++ .../juick/server/api/webhooks/SkypeWebhook.java | 47 +++ .../juick/server/api/webhooks/TelegramWebhook.java | 48 ++++ .../server/configuration/ApiAppConfiguration.java | 128 +++++++++ .../juick/server/configuration/ApiInitializer.java | 65 +++++ .../server/configuration/ApiSecurityConfig.java | 128 +++++++++ .../java/com/juick/server/util/JsonpAdvice.java | 31 ++ .../java/com/juick/api/tests/MessagesTests.java | 319 --------------------- .../java/com/juick/server/tests/MessagesTests.java | 319 +++++++++++++++++++++ 50 files changed, 3154 insertions(+), 3243 deletions(-) delete mode 100644 juick-server/src/main/java/com/juick/api/ApiServer.java delete mode 100644 juick-server/src/main/java/com/juick/api/CrosspostManager.java delete mode 100644 juick-server/src/main/java/com/juick/api/EmailManager.java delete mode 100644 juick-server/src/main/java/com/juick/api/FacebookPageManager.java delete mode 100644 juick-server/src/main/java/com/juick/api/MessengerManager.java delete mode 100644 juick-server/src/main/java/com/juick/api/TelegramBotManager.java delete mode 100644 juick-server/src/main/java/com/juick/api/TopManager.java delete mode 100644 juick-server/src/main/java/com/juick/api/WebsocketManager.java delete mode 100644 juick-server/src/main/java/com/juick/api/configuration/ApiAppConfiguration.java delete mode 100644 juick-server/src/main/java/com/juick/api/configuration/ApiInitializer.java delete mode 100644 juick-server/src/main/java/com/juick/api/configuration/ApiSecurityConfig.java delete mode 100644 juick-server/src/main/java/com/juick/api/configuration/ApiSecurityInitializer.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/ApiController.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/Messages.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/MessengerWebhook.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/Notifications.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/Others.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/PM.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/Post.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/Service.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/SkypeEndpoint.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/Tags.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/TelegramWebhook.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/Users.java delete mode 100644 juick-server/src/main/java/com/juick/api/controllers/util/JsonpAdvice.java create mode 100644 juick-server/src/main/java/com/juick/server/CrosspostManager.java create mode 100644 juick-server/src/main/java/com/juick/server/EmailManager.java create mode 100644 juick-server/src/main/java/com/juick/server/FacebookPageManager.java create mode 100644 juick-server/src/main/java/com/juick/server/MessengerManager.java create mode 100644 juick-server/src/main/java/com/juick/server/ServerManager.java create mode 100644 juick-server/src/main/java/com/juick/server/TelegramBotManager.java create mode 100644 juick-server/src/main/java/com/juick/server/TopManager.java create mode 100644 juick-server/src/main/java/com/juick/server/WebsocketManager.java create mode 100644 juick-server/src/main/java/com/juick/server/api/Index.java create mode 100644 juick-server/src/main/java/com/juick/server/api/Messages.java create mode 100644 juick-server/src/main/java/com/juick/server/api/Notifications.java create mode 100644 juick-server/src/main/java/com/juick/server/api/PM.java create mode 100644 juick-server/src/main/java/com/juick/server/api/Post.java create mode 100644 juick-server/src/main/java/com/juick/server/api/Service.java create mode 100644 juick-server/src/main/java/com/juick/server/api/Tags.java create mode 100644 juick-server/src/main/java/com/juick/server/api/Users.java create mode 100644 juick-server/src/main/java/com/juick/server/api/webhooks/MessengerWebhook.java create mode 100644 juick-server/src/main/java/com/juick/server/api/webhooks/SkypeWebhook.java create mode 100644 juick-server/src/main/java/com/juick/server/api/webhooks/TelegramWebhook.java create mode 100644 juick-server/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java create mode 100644 juick-server/src/main/java/com/juick/server/configuration/ApiInitializer.java create mode 100644 juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java create mode 100644 juick-server/src/main/java/com/juick/server/util/JsonpAdvice.java delete mode 100644 juick-server/src/test/java/com/juick/api/tests/MessagesTests.java create mode 100644 juick-server/src/test/java/com/juick/server/tests/MessagesTests.java (limited to 'juick-server') diff --git a/juick-server/src/main/java/com/juick/api/ApiServer.java b/juick-server/src/main/java/com/juick/api/ApiServer.java deleted file mode 100644 index 51097440..00000000 --- a/juick-server/src/main/java/com/juick/api/ApiServer.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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.api; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.juick.User; -import com.juick.service.MessagesService; -import com.juick.service.SubscriptionService; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.socket.TextMessage; -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.XmppException; -import rocks.xmpp.core.session.Extension; -import rocks.xmpp.core.session.XmppSessionConfiguration; -import rocks.xmpp.core.session.debug.LogbackDebugger; -import rocks.xmpp.core.stanza.model.Message; -import rocks.xmpp.extensions.component.accept.ExternalComponent; -import rocks.xmpp.extensions.oob.model.x.OobX; -import rocks.xmpp.util.XmppUtils; - -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import javax.xml.bind.JAXBException; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import java.io.IOException; -import java.io.StringWriter; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; -import java.util.stream.Collectors; - -/** - * @author Ugnich Anton - */ -public class ApiServer implements AutoCloseable { - private static Logger logger = LoggerFactory.getLogger(ApiServer.class); - - private ExternalComponent xmpp; - - @Value("${xmpp_host:localhost}") - private String xmppHost; - @Value("${xmpp_password:secret}") - private String xmppPassword; - @Value("${ws_jid:ws.localhost}") - private String xmppJid; - @Value("${xmpp_port:5347}") - private int xmppPort; - @Value("${xmpp_disabled:false}") - private boolean isXmppDisabled; - @Inject - private ObjectMapper jsonMapper; - @Inject - private MessagesService messagesService; - @Inject - private SubscriptionService subscriptionService; - @Inject - private WebsocketManager wsHandler; - @Value("${service_user:juick}") - private String serviceUser; - - @PostConstruct - public void init() { - if (!isXmppDisabled) { - setupXmppComponent(xmppHost, xmppPort, xmppJid, xmppPassword); - } else { - logger.warn("XMPP is not enabled"); - } - } - - @Override - public void close() { - try { - if (xmpp != null) - xmpp.close(); - - logger.info("ExternalComponent on juick-api destroyed"); - } catch (Exception e) { - logger.warn("Exception occurs on juick-api destroy", e); - } - } - - public void setupXmppComponent(final String host, final int port, final String jid, final String password) { - XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() - .extensions(Extension.of(com.juick.Message.class)) - .build(); - xmpp = ExternalComponent.create(jid, password, configuration, host, port); - xmpp.addInboundMessageListener(e -> { - try { - Message msg = e.getMessage(); - com.juick.Message jmsg = msg.getExtension(com.juick.Message.class); - if (jmsg != null) { - if (logger.isInfoEnabled()) { // prevent writeValueAsString execution if log is disabled - try { - StringWriter stanzaWriter = new StringWriter(); - XMLStreamWriter xmppStreamWriter = XmppUtils.createXmppStreamWriter( - xmpp.getConfiguration().getXmlOutputFactory().createXMLStreamWriter(stanzaWriter)); - xmpp.createMarshaller().marshal(msg, xmppStreamWriter); - xmppStreamWriter.flush(); - xmppStreamWriter.close(); - logger.info("got msg: {}", stanzaWriter.toString()); - } catch (XMLStreamException e1) { - logger.info("jaxb exception", e1); - } - - } - if (jmsg.getMid() == 0) { - int uid_to = NumberUtils.toInt(msg.getTo().getLocal(), 0); - if (uid_to > 0) { - onJuickPM(uid_to, jmsg); - } - } else if (jmsg.getRid() == 0) { - // to get full message with attachment, etc. - onJuickMessagePost(messagesService.getMessage(jmsg.getMid())); - } else { - // to get quote and attachment - com.juick.Message reply = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); - onJuickMessageReply(reply); - } - } - } catch (JsonProcessingException ex) { - logger.error("mapper exception", ex); - } catch (JAXBException exc) { - logger.error("jaxb exception", exc); - } - }); - try { - xmpp.connect(); - } catch (XmppException e) { - logger.warn("xmpp extension", e); - } - } - - public void sendMessage(Message message) { - if (!isXmppDisabled) { - xmpp.sendMessage(message); - } - } - - public void processMessage(User visitor, String body, String attachmentName) { - Message xmsg = new Message(); - xmsg.setFrom(Jid.of(String.valueOf(visitor.getUid()), "uid.juick.com", "perl")); - xmsg.setTo(Jid.of("juick@juick.com/Juick")); - xmsg.setBody(body); - try { - if (StringUtils.isNotEmpty(attachmentName)) { - String attachmentUrl = String.format("juick://%s", attachmentName); - xmsg.addExtension(new OobX(new URI(attachmentUrl), "!!!!Juick!!")); - } - sendMessage(xmsg); - } catch (URISyntaxException e1) { - logger.warn("attachment error", e1); - } - } - - private void onJuickPM(final int uid_to, final com.juick.Message jmsg) throws JsonProcessingException { - String json = jsonMapper.writeValueAsString(jmsg); - synchronized (wsHandler.getClients()) { - wsHandler.getClients().stream().filter(c -> - (!c.legacy && c.visitor.getUid() == uid_to) || c.visitor.getName().equals(serviceUser)) - .forEach(c -> { - try { - logger.info("sending pm to {}", c.visitor.getUid()); - c.session.sendMessage(new TextMessage(json)); - } catch (IOException e) { - logger.warn("ws error", e); - } - }); - } - } - - private void onJuickMessagePost(final com.juick.Message jmsg) throws JsonProcessingException { - String json = jsonMapper.writeValueAsString(jmsg); - List uids = subscriptionService.getSubscribedUsers(jmsg.getUser().getUid(), jmsg.getMid()) - .stream().map(User::getUid).collect(Collectors.toList()); - synchronized (wsHandler.getClients()) { - wsHandler.getClients().stream().filter(c -> - (!c.legacy && c.visitor.getUid() == 0) // anonymous users - || c.visitor.getName().equals(serviceUser) // services - || (!c.legacy && uids.contains(c.visitor.getUid()))) // subscriptions - .forEach(c -> { - try { - logger.info("sending message to {}", c.visitor.getUid()); - c.session.sendMessage(new TextMessage(json)); - } catch (IOException e) { - logger.warn("ws error", e); - } - }); - wsHandler.getClients().stream().filter(c -> - c.legacy && c.allMessages) // legacy all posts - .forEach(c -> { - try { - logger.info("sending message to legacy client {}", c.visitor.getUid()); - c.session.sendMessage(new TextMessage(json)); - } catch (IOException e) { - logger.warn("ws error", e); - } - }); - } - } - - private void onJuickMessageReply(final com.juick.Message jmsg) throws JsonProcessingException { - String json = jsonMapper.writeValueAsString(jmsg); - List threadUsers = - subscriptionService.getUsersSubscribedToComments(jmsg.getMid(), jmsg.getUser().getUid()) - .stream().map(User::getUid).collect(Collectors.toList()); - synchronized (wsHandler.getClients()) { - wsHandler.getClients().stream().filter(c -> - (!c.legacy && c.visitor.getUid() == 0) // anonymous users - || c.visitor.getName().equals(serviceUser) // services - || (!c.legacy && threadUsers.contains(c.visitor.getUid()))) // subscriptions - .forEach(c -> { - try { - logger.info("sending reply to {}", c.visitor.getUid()); - c.session.sendMessage(new TextMessage(json)); - } catch (IOException e) { - logger.warn("ws error", e); - } - }); - wsHandler.getClients().stream().filter(c -> - (c.legacy && c.allReplies) || (c.legacy && c.MID == jmsg.getMid())) // legacy replies - .forEach(c -> { - try { - logger.info("sending reply to legacy client {}", c.visitor.getUid()); - c.session.sendMessage(new TextMessage(json)); - } catch (IOException e) { - logger.warn("ws error", e); - } - }); - } - } -} diff --git a/juick-server/src/main/java/com/juick/api/CrosspostManager.java b/juick-server/src/main/java/com/juick/api/CrosspostManager.java deleted file mode 100644 index a601d7b7..00000000 --- a/juick-server/src/main/java/com/juick/api/CrosspostManager.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * 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.api; - -import com.juick.Message; -import com.juick.server.component.MessageEvent; -import com.juick.service.CrosspostService; -import com.juick.util.MessageUtils; -import org.apache.commons.codec.CharEncoding; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationListener; -import org.springframework.social.twitter.api.Twitter; -import org.springframework.social.twitter.api.impl.TwitterTemplate; -import org.springframework.stereotype.Component; - -import javax.annotation.Nonnull; -import javax.inject.Inject; -import javax.net.ssl.HttpsURLConnection; -import java.io.OutputStreamWriter; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; - -/** - * @author Ugnich Anton - */ -@Component -public class CrosspostManager implements ApplicationListener { - private final static String FBURL = "https://graph.facebook.com/me/feed"; - private final static String VKURL = "https://api.vk.com/method/wall.post"; - - private static Logger logger = LoggerFactory.getLogger(CrosspostManager.class); - - @Inject - private CrosspostService crosspostService; - - @Value("${twitter_consumer_key:}") - private String twitter_consumer_key; - @Value("${twitter_consumer_secret:}") - private String twitter_consumer_secret; - - @Override - public void onApplicationEvent(@Nonnull MessageEvent event) { - Message msg = event.getMessage(); - if (msg.getMid() > 0 && msg.getRid() == 0) { - if (StringUtils.isNotEmpty(crosspostService.getTwitterName(msg.getUser().getUid()))) { - if (msg.getTags().stream().noneMatch(t -> t.getName().equals("notwitter"))) { - twitterPost(msg); - } - } - // TODO: approve application for facebook crosspost - } - } - - private boolean facebookPost(final com.juick.Message jmsg) { - String token = crosspostService.getFacebookTokens(jmsg.getUser().getUid()) - .orElse(Pair.of(StringUtils.EMPTY, StringUtils.EMPTY)).getRight(); - if (token.isEmpty()) { - return false; - } - - logger.info("FB: #{}", jmsg.getMid()); - - String status = MessageUtils.getMessageHashTags(jmsg) + "\n" + jmsg.getText(); - - boolean ret = false; - try { - String body = "access_token=" - + URLEncoder.encode(token, CharEncoding.UTF_8) - + "&message=" - + URLEncoder.encode(status, CharEncoding.UTF_8) - + "&link=http%3A%2F%2Fjuick.com%2F" - + jmsg.getMid(); - - HttpsURLConnection conn = (HttpsURLConnection) new URL(FBURL).openConnection(); - conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - conn.setRequestProperty("User-Agent", "Juick"); - conn.setRequestProperty("Content-Length", Integer.toString(body.length())); - conn.setUseCaches(false); - conn.setDoInput(true); - conn.setDoOutput(true); - conn.setRequestMethod("POST"); - conn.connect(); - - OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); - wr.write(body); - wr.close(); - - ret = StringUtils.isNotEmpty(IOUtils.toString(conn.getInputStream(), StandardCharsets.UTF_8)); - - conn.disconnect(); - } catch (Exception e) { - logger.error("fbPost exception", e); - } - return ret; - } - - private boolean vkontaktePost(final com.juick.Message jmsg) { - Pair tokens = crosspostService.getVkTokens(jmsg.getUser().getUid()).orElse(Pair.of(StringUtils.EMPTY, StringUtils.EMPTY)); - if (tokens.getLeft().isEmpty() || tokens.getRight().isEmpty()) { - return false; - } - - logger.info("VK: #", jmsg.getMid()); - - String status = MessageUtils.getMessageHashTags(jmsg) + "\n" + jmsg.getText() + "\nhttp://juick.com/" + jmsg.getMid(); - - boolean ret = false; - try { - String body = "owner_id=" + tokens.getLeft() + "&access_token=" + URLEncoder.encode(tokens.getRight(), CharEncoding.UTF_8) + "&from_group=1&message=" + URLEncoder.encode(status, CharEncoding.UTF_8); - - HttpsURLConnection conn = (HttpsURLConnection) new URL(VKURL).openConnection(); - conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - conn.setRequestProperty("User-Agent", "Juick"); - conn.setRequestProperty("Content-Length", Integer.toString(body.length())); - conn.setUseCaches(false); - conn.setDoInput(true); - conn.setDoOutput(true); - conn.setRequestMethod("POST"); - conn.connect(); - - OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); - wr.write(body); - wr.close(); - - ret = StringUtils.isNotEmpty(IOUtils.toString(conn.getInputStream(), StandardCharsets.UTF_8)); - - conn.disconnect(); - } catch (Exception e) { - logger.error("vkPost exception", e); - } - return ret; - } - - private void twitterPost(final com.juick.Message jmsg) { - crosspostService.getTwitterToken(jmsg.getUser().getUid()).ifPresent(t -> { - String status = MessageUtils.getMessageHashTags(jmsg) + StringUtils.defaultString(jmsg.getText()); - if (status.length() > 255) { - status = status.substring(0, 254) + "…"; - } - status += " http://juick.com/" + jmsg.getMid(); - Twitter twitter = new TwitterTemplate(twitter_consumer_key, twitter_consumer_secret, t.getToken(), t.getSecret()); - twitter.timelineOperations().updateStatus(status); - }); - } -} diff --git a/juick-server/src/main/java/com/juick/api/EmailManager.java b/juick-server/src/main/java/com/juick/api/EmailManager.java deleted file mode 100644 index fa86b406..00000000 --- a/juick-server/src/main/java/com/juick/api/EmailManager.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.juick.api; - -import com.juick.Message; -import com.juick.server.component.MessageEvent; -import com.juick.service.EmailService; -import com.juick.service.MessagesService; -import com.juick.service.SubscriptionService; -import com.juick.service.UserService; -import com.juick.util.MessageUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.ApplicationListener; -import org.springframework.stereotype.Component; - -import javax.annotation.Nonnull; -import javax.inject.Inject; -import javax.mail.MessagingException; -import javax.mail.Multipart; -import javax.mail.Session; -import javax.mail.Transport; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeBodyPart; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMultipart; -import java.util.Properties; - -import static com.juick.formatters.PlainTextFormatter.formatPost; -import static com.juick.formatters.PlainTextFormatter.formatUrl; - -@Component -public class EmailManager implements ApplicationListener { - - public static final String MSGID_PATTERN = "\\.|@|<"; - - private static final Logger logger = LoggerFactory.getLogger(EmailManager.class); - @Inject - private EmailService emailService; - @Inject - private SubscriptionService subscriptionService; - @Inject - private MessagesService messagesService; - @Inject - private UserService userService; - @Override - public void onApplicationEvent(@Nonnull MessageEvent event) { - Message msg = event.getMessage(); - if (msg.getMid() > 0 && msg.getRid() == 0) { - String subject = String.format("New message from %s", msg.getUser().getName()); - subscriptionService.getSubscribedUsers(msg.getUser().getUid(), msg.getMid()) - .forEach(user -> emailService.getEmails(user.getUid(), true) - .forEach(email -> emailNotify(email, subject, msg))); - } else if (msg.getRid() > 0) { - Message originalMessage = messagesService.getMessage(msg.getMid()); - String subject = String.format("New reply to %s", originalMessage.getUser().getName()); - subscriptionService.getUsersSubscribedToComments(msg.getMid(), msg.getUser().getUid()) - .forEach(user -> emailService.getEmails(user.getUid(), true) - .forEach(email -> emailNotify(email, subject, msg))); - } - } - - private void emailNotify(String email, String subject, Message msg) { - Properties prop = System.getProperties(); - prop.put("mail.smtp.starttls.enable", "true"); - Session session = Session.getDefaultInstance(prop); - try { - Transport transport = session.getTransport("smtp"); - MimeMessage message = new MimeMessage(session) { - protected void updateMessageID() throws MessagingException { - setHeader("Message-ID", String.format("<%d.%d@juick.com>", msg.getMid(), msg.getRid())); - if (msg.getRid() > 0) { - if (msg.getReplyto() > 0) { - Message replyto = messagesService.getReply(msg.getMid(), msg.getReplyto()); - setHeader("In-Reply-To", String.format("<%d.%d@juick.com>", replyto.getMid(), replyto.getRid())); - } else { - Message original = messagesService.getMessage(msg.getMid()); - setHeader("In-Reply-To", String.format("<%d.%d@juick.com>", original.getMid(), original.getRid())); - } - } - } - }; - message.setFrom(new InternetAddress("juick@juick.com")); - message.addRecipient(javax.mail.Message.RecipientType.TO, new InternetAddress(email)); - message.setSubject(subject); - String plainText = String.format("%s\n\n--\nYou are receiving this because you are subscribed to this user " + - ", discussion or tag. Reply to this email directly or view it on Juick: %s.", - formatPost(msg), formatUrl(msg)); - MimeBodyPart textBodyPart = new MimeBodyPart(); - textBodyPart.setContent(plainText, "text/plain; charset=UTF-8"); - String htmlText = String.format("%s

--
You are receiving this because you are subscribed to this user" + - ", discussion or tag. Reply to this email directly or view it on Juick." + - "
Configure or disable notifications", - MessageUtils.formatHtml(msg), formatUrl(msg), - userService.getHashByUID(userService.getUserByEmail(email).getUid())); - MimeBodyPart htmlBodyPart = new MimeBodyPart(); - htmlBodyPart.setContent(htmlText, "text/html; charset=UTF-8"); - Multipart multipart = new MimeMultipart("alternative"); - multipart.addBodyPart(textBodyPart); - multipart.addBodyPart(htmlBodyPart); - message.setContent(multipart); - message.setHeader("List-Unsubscribe", String.format("https://juick.com/settings?hash=%s", - userService.getHashByUID(userService.getUserByEmail(email).getUid()))); - message.saveChanges(); - transport.connect(); - transport.sendMessage(message, message.getAllRecipients()); - } catch (MessagingException ex) { - logger.error("mail exception", ex); - } - } -} diff --git a/juick-server/src/main/java/com/juick/api/FacebookPageManager.java b/juick-server/src/main/java/com/juick/api/FacebookPageManager.java deleted file mode 100644 index e9ed0e59..00000000 --- a/juick-server/src/main/java/com/juick/api/FacebookPageManager.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.juick.api; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - -@Component -public class FacebookPageManager { - private static Logger logger = LoggerFactory.getLogger(FacebookPageManager.class); - @Value("${facebook_page_id:12345678}") - private String pageId; - @Value("${fb_page_access_token:12345678}") - private String accessToken; - - public void post(String status, String link) { - UriComponents uriComponents = UriComponentsBuilder.fromUriString("https://graph.facebook.com/{page_id}/feed") - .queryParam("message", status) - .queryParam("link", link) - .queryParam("access_token", accessToken) - .buildAndExpand(pageId); - RestTemplate api = new RestTemplate(); - ResponseEntity response = api.exchange(uriComponents.toUri(), - HttpMethod.POST, null, String.class); - logger.info("Facebook response: {}", response.getBody()); - } -} diff --git a/juick-server/src/main/java/com/juick/api/MessengerManager.java b/juick-server/src/main/java/com/juick/api/MessengerManager.java deleted file mode 100644 index 7341b946..00000000 --- a/juick-server/src/main/java/com/juick/api/MessengerManager.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.juick.api; - -import com.github.messenger4j.Messenger; -import com.github.messenger4j.exception.MessengerApiException; -import com.github.messenger4j.exception.MessengerIOException; -import com.github.messenger4j.exception.MessengerVerificationException; -import com.github.messenger4j.send.MessagePayload; -import com.github.messenger4j.send.message.TemplateMessage; -import com.github.messenger4j.send.message.TextMessage; -import com.github.messenger4j.send.message.template.ButtonTemplate; -import com.github.messenger4j.send.message.template.button.UrlButton; -import com.github.messenger4j.userprofile.UserProfile; -import com.github.messenger4j.webhook.event.TextMessageEvent; -import com.juick.Message; -import com.juick.User; -import com.juick.server.component.MessageEvent; -import com.juick.service.MessagesService; -import com.juick.service.MessengerService; -import com.juick.service.SubscriptionService; -import com.juick.service.UserService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationListener; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; - -import javax.annotation.Nonnull; -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import java.net.MalformedURLException; -import java.net.URL; -import java.time.Instant; -import java.util.Collections; -import java.util.Optional; - -import static com.juick.formatters.PlainTextFormatter.formatPost; -import static com.juick.formatters.PlainTextFormatter.formatUrl; - -@Component -public class MessengerManager implements ApplicationListener { - private static final Logger logger = LoggerFactory.getLogger(MessengerManager.class); - @Inject - private MessagesService messagesService; - @Inject - private SubscriptionService subscriptionService; - @Inject - private UserService userService; - @Inject - private MessengerService messengerService; - @Inject - private ApiServer apiServer; - - @Value("${fb_page_access_token:12345678}") - private String facebookPageAccessToken; - @Value("${fb_verify_token:12345678}") - private String facebookVerifyToken; - @Value("${fb_secret:12345678}") - private String facebookSecret; - - private Messenger messenger; - - @PostConstruct - public void init() { - messenger = Messenger.create(facebookPageAccessToken, facebookSecret, facebookVerifyToken); - } - - public String getFacebookVerifyToken() { - return facebookVerifyToken; - } - - public void processUpdate(String signature , String data) throws MessengerVerificationException { - messenger.onReceiveEvents(data, Optional.of(signature), event -> { - final String senderId = event.senderId(); - final Instant timestamp = event.timestamp(); - - User user_from = userService.getUserByUID(messengerService.getUserId(senderId)).orElse(new User()); - logger.info("Found juick user {}", user_from.getUid()); - if (user_from.getUid() == 0) { - try { - UserProfile profile = messenger.queryUserProfile(senderId); - signupNotify(senderId, messengerService.getSignUpHash(senderId, profile.firstName())); - } catch (MessengerApiException | MessengerIOException | MalformedURLException e) { - logger.warn("messenger profile error", e); - try { - signupNotify(senderId, messengerService.getSignUpHash(senderId, "anonymous")); - } catch (MalformedURLException | MessengerApiException | MessengerIOException e1) { - logger.warn("signup error", e1); - } - } - } else { - if (event.isTextMessageEvent()) { - final TextMessageEvent textMessageEvent = event.asTextMessageEvent(); - final String messageId = textMessageEvent.messageId(); - final String text = textMessageEvent.text(); - logger.info("Received text message from '{}' at '{}' with content: {} (mid: {})", - senderId, timestamp, text, messageId); - apiServer.processMessage(user_from, text, null); - messengerNotify(senderId, "Message sent", null); - } - } - }); - } - - @Override - public void onApplicationEvent(@Nonnull MessageEvent event) { - Message msg = event.getMessage(); - if (msg.getMid() > 0 && msg.getRid() == 0) { - String subject = formatPost(msg); - subscriptionService.getSubscribedUsers(msg.getUser().getUid(), msg.getMid()) - .forEach(user -> messengerService.getSenderId(user) - .ifPresent(t -> messengerNotify(t, subject, formatUrl(msg)))); - } else if (msg.getRid() > 0) { - // get quote - com.juick.Message jmsg = messagesService.getReply(msg.getMid(), msg.getRid()); - String subject = formatPost(jmsg); - subscriptionService.getUsersSubscribedToComments(msg.getMid(), msg.getUser().getUid()) - .forEach(user -> messengerService.getSenderId(user) - .ifPresent(t -> messengerNotify(t, subject, formatUrl(jmsg)))); - } - } - - private void messengerNotify(String messengerUser, String text, String url) { - try { - if (!StringUtils.isEmpty(url)) { - final UrlButton showMessage = UrlButton.create("VIEW MESSAGE", new URL(url)); - ButtonTemplate template = ButtonTemplate.create(text, Collections.singletonList(showMessage)); - messenger.send(MessagePayload.create(messengerUser, TemplateMessage.create(template))); - } else { - messenger.send(MessagePayload.create(messengerUser, TextMessage.create(text))); - } - } catch (MessengerApiException | MessengerIOException | MalformedURLException e) { - logger.warn("messenger error", e); - } - } - private void signupNotify(String messengerUser, String hash) throws MalformedURLException, MessengerApiException, MessengerIOException { - final UrlButton urlButton = UrlButton.create("LOGIN", - new URL("https://juick.com/signup?type=messenger&hash=" + hash)); - ButtonTemplate template = ButtonTemplate.create("Login to receive notifications", - Collections.singletonList(urlButton)); - messenger.send(MessagePayload.create(messengerUser, TemplateMessage.create(template))); - } -} diff --git a/juick-server/src/main/java/com/juick/api/TelegramBotManager.java b/juick-server/src/main/java/com/juick/api/TelegramBotManager.java deleted file mode 100644 index 6f2e50d1..00000000 --- a/juick-server/src/main/java/com/juick/api/TelegramBotManager.java +++ /dev/null @@ -1,298 +0,0 @@ -/* - * 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.api; - -import com.juick.User; -import com.juick.server.component.MessageEvent; -import com.juick.server.util.HttpUtils; -import com.juick.service.MessagesService; -import com.juick.service.SubscriptionService; -import com.juick.service.TelegramService; -import com.juick.service.UserService; -import com.pengrad.telegrambot.BotUtils; -import com.pengrad.telegrambot.Callback; -import com.pengrad.telegrambot.TelegramBot; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.MessageEntity; -import com.pengrad.telegrambot.model.PhotoSize; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.GetFile; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SetWebhook; -import com.pengrad.telegrambot.response.GetFileResponse; -import com.pengrad.telegrambot.response.SendResponse; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationListener; -import org.springframework.stereotype.Component; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - -import javax.annotation.Nonnull; -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import java.io.IOException; -import java.net.URL; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; - -import static com.juick.formatters.PlainTextFormatter.formatPost; -import static com.juick.formatters.PlainTextFormatter.formatUrl; - -/** - * Created by vt on 12/05/16. - */ -@Component -public class TelegramBotManager implements ApplicationListener { - private static final Logger logger = LoggerFactory.getLogger(TelegramBotManager.class); - - private TelegramBot bot; - - @Value("${telegram_token}") - private String telegramToken; - @Inject - private TelegramService telegramService; - @Inject - private MessagesService messagesService; - @Inject - private SubscriptionService subscriptionService; - @Inject - private UserService userService; - @Inject - private ApiServer apiServer; - @Value("${upload_tmp_dir:/var/www/juick.com/i/tmp/}") - private String tmpDir; - - private static final String MSG_LINK = "🔗"; - - @PostConstruct - public void init() { - if (StringUtils.isBlank(telegramToken)) { - logger.info("telegram token is not set, exiting"); - return; - } - bot = new TelegramBot(telegramToken); - try { - SetWebhook webhook = new SetWebhook().url("https://api.juick.com/tlgmbtwbhk"); - if (!bot.execute(webhook).isOk()) { - logger.error("error setting webhook"); - } - } catch (Exception e) { - logger.warn("couldn't initialize telegram bot", e); - } - } - - public void processUpdate(String data) throws Exception { - Update update = BotUtils.parseUpdate(data); - Message message = update.message(); - if (update.message() == null) { - message = update.editedMessage(); - if (message == null) { - logger.error("error parsing telegram update: {}", update); - return; - } - } - User user_from = userService.getUserByUID(telegramService.getUser(message.chat().id())).orElse(new User()); - logger.info("Found juick user {}", user_from.getUid()); - - List chats = telegramService.getChats(); - String username = message.from().username(); - if (username == null) { - username = message.from().firstName(); - } - if (!chats.contains(message.chat().id())) { - telegramService.addChat(message.chat().id()); - logger.info("added chat with {}", username); - telegramService.createTelegramUser(message.from().id(), username); - telegramSignupNotify(message.from().id().longValue(), userService.getSignUpHashByTelegramID(message.from().id().longValue(), username)); - } else { - if (user_from.getUid() == 0) { - telegramSignupNotify(message.from().id().longValue(), userService.getSignUpHashByTelegramID(message.from().id().longValue(), username)); - } else { - String attachment = StringUtils.EMPTY; - if (message.photo() != null) { - String fileId = Arrays.stream(message.photo()).max(Comparator.comparingInt(PhotoSize::fileSize)) - .orElse(new PhotoSize()).fileId(); - if (StringUtils.isNotEmpty(fileId)) { - GetFile request = new GetFile(fileId); - GetFileResponse response = bot.execute(request); - logger.info("got file {}", response.file()); - URL fileURL = new URL(bot.getFullFilePath(response.file())); - attachment = HttpUtils.downloadImage(fileURL, tmpDir); - logger.info("received {}", attachment); - } - } - String text = message.text(); - if (StringUtils.isBlank(text)) { - text = message.caption(); - } - if (StringUtils.isBlank(text)) { - text = StringUtils.EMPTY; - } - if (StringUtils.isNotEmpty(text) || StringUtils.isNotEmpty(attachment)) { - if (text.equalsIgnoreCase("LOGIN") - || text.equalsIgnoreCase("PING") - || text.equalsIgnoreCase("HELP") - || text.equalsIgnoreCase("/login") - || text.equalsIgnoreCase("/start") - || text.equalsIgnoreCase("/help")) { - String msgUrl = "http://juick.com/login?hash=" + userService.getHashByUID(user_from.getUid()); - String msg = String.format("Hi, %s!\nYou can post messages and images to Juick there.\n" + - "Tap to [log into website](%s) to get more info", user_from.getName(), msgUrl); - telegramNotify(message.from().id().longValue(), msg); - } else { - Message replyMessage = message.replyToMessage(); - if (replyMessage != null) { - if (replyMessage.entities() != null) { - Optional juickLink = Arrays.stream(replyMessage.entities()) - .filter(this::isJuickLink) - .findFirst(); - if (juickLink.isPresent()) { - if (StringUtils.isNotEmpty(juickLink.get().url())) { - UriComponents uriComponents = UriComponentsBuilder.fromUriString( - juickLink.get().url()).build(); - String path = uriComponents.getPath(); - if (StringUtils.isNotEmpty(path) && path.length() > 1) { - int mid = Integer.valueOf(path.substring(1)); - String prefix = String.format("#%d ", mid); - if (StringUtils.isNotEmpty(uriComponents.getFragment())) { - int rid = Integer.valueOf(uriComponents.getFragment()); - prefix = String.format("#%d/%d ", mid, rid); - } - apiServer.processMessage(user_from, prefix + text, attachment); - telegramNotify(message.from().id().longValue(), "Reply sent"); - } else { - logger.warn("invalid path: {}", path); - } - } else { - logger.warn("invalid entity: {}", juickLink); - } - } else { - telegramNotify(message.from().id().longValue(), - "Can not reply to this message", replyMessage.messageId()); - } - } else { - telegramNotify(message.from().id().longValue(), - "Can not reply to this message", replyMessage.messageId()); - } - } else { - apiServer.processMessage(user_from, text, attachment); - telegramNotify(message.from().id().longValue(), "Message sent"); - } - } - } - } - } - } - - private boolean isJuickLink(MessageEntity e) { - return e.offset() == 0 && e.type().equals(MessageEntity.Type.text_link) && e.length() == 2; - } - - @Override - public void onApplicationEvent(@Nonnull MessageEvent event) { - com.juick.Message jmsg = event.getMessage(); - String msgUrl = formatUrl(jmsg); - if (jmsg.getMid() > 0 && jmsg.getRid() == 0) { - String msg = String.format("[%s](%s) %s", MSG_LINK, msgUrl, formatPost(jmsg, true)); - - List users = telegramService.getTelegramIdentifiers(subscriptionService.getSubscribedUsers(jmsg.getUser().getUid(), jmsg.getMid())); - List chats = telegramService.getChats(); - // registered subscribed users - - users.forEach(c -> telegramNotify(c, msg)); - // anonymous - chats.stream().filter(u -> telegramService.getUser(u) == 0).forEach(c -> telegramNotify(c, msg)); - } else if (jmsg.getRid() > 0) { - // get quote - com.juick.Message msg = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); - String fmsg = String.format("[%s](%s) %s", MSG_LINK, msgUrl, formatPost(msg, true)); - telegramService.getTelegramIdentifiers( - subscriptionService.getUsersSubscribedToComments(jmsg.getMid(), jmsg.getUser().getUid()) - ).forEach(c -> telegramNotify(c, fmsg)); - } - } - - public void telegramNotify(Long chatId, String msg) { - telegramNotify(chatId, msg, 0); - } - - public void telegramNotify(Long chatId, String msg, Integer replyTo) { - SendMessage telegramMessage = new SendMessage(chatId, msg); - if (replyTo > 0) { - telegramMessage.replyToMessageId(replyTo); - } - telegramMessage.parseMode(ParseMode.Markdown).disableWebPagePreview(true); - bot.execute(telegramMessage, new Callback() { - @Override - public void onResponse(SendMessage request, SendResponse response) { - if (!response.isOk()) { - if (response.errorCode() == 403) { - // remove from anonymous chat - telegramService.getChats().stream().filter(c -> c.equals(chatId)).findFirst().ifPresent( - d -> { - telegramService.deleteChat(d); - logger.info("deleted {} chat", d); - } - ); - int userId = telegramService.getUser(chatId); - if (userId > 0) { - User userToDelete = userService.getUserByUID(userId) - .orElse(new User()); - boolean status = telegramService.deleteTelegramUser(userToDelete.getUid()); - logger.info("deleting telegram id of @{} : {}", userToDelete.getName(), status); - boolean chatStatus = telegramService.deleteChat(chatId); - logger.info("deleting telegram chat {} : {}", chatId, chatStatus); - } - } else { - logger.warn("error response, isOk: {}, errorCode: {}, description: {}", - response.isOk(), response.errorCode(), response.description()); - } - } - } - - @Override - public void onFailure(SendMessage request, IOException e) { - logger.warn("telegram failure", e); - } - }); - } - - public void telegramSignupNotify(Long telegramId, String hash) { - bot.execute(new SendMessage(telegramId, - String.format("You are subscribed to all Juick messages. " + - "[Create or link](http://juick.com/signup?type=durov&hash=%s) " + - "an existing Juick account to get your subscriptions and ability to post messages", hash)) - .parseMode(ParseMode.Markdown), new Callback() { - @Override - public void onResponse(SendMessage request, SendResponse response) { - logger.info("got response: {}", response.message()); - } - - @Override - public void onFailure(SendMessage request, IOException e) { - logger.warn("telegram failure", e); - } - }); - } -} diff --git a/juick-server/src/main/java/com/juick/api/TopManager.java b/juick-server/src/main/java/com/juick/api/TopManager.java deleted file mode 100644 index b8123639..00000000 --- a/juick-server/src/main/java/com/juick/api/TopManager.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.api; - -import com.juick.Message; -import com.juick.service.MessagesService; -import com.juick.util.MessageUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import org.springframework.web.client.HttpClientErrorException; - -import javax.inject.Inject; - -@Component -public class TopManager { - private static Logger logger = LoggerFactory.getLogger(TopManager.class); - @Inject - private MessagesService messagesService; - @Inject - private FacebookPageManager facebookPageManager; - - @Scheduled(fixedRate = 3600000) - public void updateTop() { - messagesService.getPopularCandidates().forEach(m -> { - logger.info("added {} to popular", m); - messagesService.setMessagePopular(m, 1); - Message jmsg = messagesService.getMessage(m); - String status = MessageUtils.getMessageHashTags(jmsg) + StringUtils.defaultString(jmsg.getText()); - try { - facebookPageManager.post(status, "https://juick.com/" + jmsg.getMid()); - } catch (HttpClientErrorException ex) { - HttpStatus statusCode = ex.getStatusCode(); - String responseString = ex.getResponseBodyAsString(); - logger.warn("facebook error {}: {}", statusCode.value(), responseString); - } - }); - } -} diff --git a/juick-server/src/main/java/com/juick/api/WebsocketManager.java b/juick-server/src/main/java/com/juick/api/WebsocketManager.java deleted file mode 100644 index 2ed70b3c..00000000 --- a/juick-server/src/main/java/com/juick/api/WebsocketManager.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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.api; - -import com.juick.User; -import com.juick.server.helpers.AnonymousUser; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpForbiddenException; -import com.juick.server.util.HttpNotFoundException; -import com.juick.service.MessagesService; -import com.juick.service.SubscriptionService; -import com.juick.service.UserService; -import org.apache.commons.lang3.math.NumberUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.PingMessage; -import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.handler.TextWebSocketHandler; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - -import javax.inject.Inject; -import java.io.IOException; -import java.net.URI; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -/** - * Created by vitalyster on 28.06.2016. - */ -public class WebsocketManager extends TextWebSocketHandler { - private static final Logger logger = LoggerFactory.getLogger(WebsocketManager.class); - - private final List clients = Collections.synchronizedList(new LinkedList<>()); - - @Inject - private UserService userService; - @Inject - private MessagesService messagesService; - @Inject - private SubscriptionService subscriptionService; - - - @Override - public void afterConnectionEstablished(WebSocketSession session) throws Exception { - URI hLocation; - String hXRealIP; - - hLocation = session.getUri(); - HttpHeaders headers = session.getHandshakeHeaders(); - hXRealIP = headers.getOrDefault("X-Real-IP", - Collections.singletonList(session.getRemoteAddress().toString())).get(0); - - // Auth - User visitor = AnonymousUser.INSTANCE; - UriComponents uriComponents = UriComponentsBuilder.fromUri(hLocation).build(); - List hash = uriComponents.getQueryParams().get("hash"); - if (hash != null && hash.get(0).length() == 16) { - visitor = userService.getUserByHash(hash.get(0)); - } else { - logger.debug("wrong hash for {} from {}", visitor.getUid(), hXRealIP); - } - - int MID = 0; - SocketSubscribed sockSubscr = null; - if (hLocation.getPath().equals("/ws/")) { - logger.debug("user {} connected", visitor.getUid()); - sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, false); - } else if (hLocation.getPath().equals("/ws/_all")) { - logger.debug("user {} connected to legacy _all ({})", visitor.getUid(), hLocation.getPath()); - sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, true); - sockSubscr.allMessages = true; - } else if (hLocation.getPath().equals("/ws/_replies")) { - logger.debug("user {} connected to legacy _replies ({})", visitor.getUid(), hLocation.getPath()); - sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, true); - sockSubscr.allReplies = true; - } else if (hLocation.getPath().matches("^/ws/(\\d)+$")) { - MID = NumberUtils.toInt(hLocation.getPath().substring(4), 0); - if (MID > 0) { - if (messagesService.canViewThread(MID, visitor.getUid())) { - logger.debug("user {} connected to legacy thread ({}) from {}", visitor.getUid(), MID, hXRealIP); - sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, true); - sockSubscr.MID = MID; - } else { - throw new HttpForbiddenException(); - } - } - } else { - throw new HttpNotFoundException(); - } - if (sockSubscr != null) { - synchronized (clients) { - clients.add(sockSubscr); - logger.debug("{} clients connected", clients.size()); - } - } else { - throw new HttpBadRequestException(); - } - } - - @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { - synchronized (clients) { - logger.debug("session closed with status {}: {}", status.getCode(), status.getReason()); - clients.removeIf(c -> c.session.getId().equals(session.getId())); - logger.debug("{} clients connected", clients.size()); - } - - } - - @Scheduled(fixedRate = 30000) - public void ping() { - clients.forEach(c -> { - try { - c.session.sendMessage(new PingMessage()); - } catch (IOException e) { - logger.error("WebSocket PING exception", e); - } - }); - } - public List getClients() { - return clients; - } - - class SocketSubscribed { - WebSocketSession session; - String clientName; - User visitor; - int MID; - boolean allMessages; - boolean allReplies; - long tsConnected; - long tsLastData; - boolean legacy; - - public SocketSubscribed(WebSocketSession session, String clientName, User visitor, boolean legacy) { - this.session = session; - this.clientName = clientName; - this.visitor = visitor; - tsConnected = tsLastData = System.currentTimeMillis(); - this.legacy = legacy; - } - } -} diff --git a/juick-server/src/main/java/com/juick/api/configuration/ApiAppConfiguration.java b/juick-server/src/main/java/com/juick/api/configuration/ApiAppConfiguration.java deleted file mode 100644 index ea59ac52..00000000 --- a/juick-server/src/main/java/com/juick/api/configuration/ApiAppConfiguration.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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.api.configuration; - -import com.juick.api.ApiServer; -import com.juick.api.WebsocketManager; -import com.juick.server.component.JuickServerComponent; -import com.juick.server.component.JuickServerReconnectManager; -import com.juick.server.configuration.BaseWebConfiguration; -import com.juick.service.UserService; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.*; -import org.springframework.core.Ordered; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.socket.client.WebSocketConnectionManager; -import org.springframework.web.socket.client.standard.StandardWebSocketClient; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.ServletWebSocketHandlerRegistry; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; -import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; -import org.springframework.web.util.UriComponentsBuilder; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; - -import javax.annotation.Nonnull; -import javax.inject.Inject; -import java.util.Collections; - -/** - * Created by aalexeev on 11/12/16. - */ -@Configuration -@EnableAsync -@EnableWebMvc -@EnableSwagger2 -@EnableScheduling -@EnableWebSocket -@PropertySource("classpath:juick.conf") -@ComponentScan(basePackages = "com.juick") -public class ApiAppConfiguration extends BaseWebConfiguration implements WebSocketConfigurer { - @Inject - UserService userService; - @Value("${api_user:juick}") - private String serviceUser; - @Value("${websocket_url:ws://localhost:8080/ws/}") - private String baseUri; - @Lazy - @Bean - public JuickServerComponent juickServerComponent() { - return new JuickServerComponent(); - } - @Lazy - @Bean - public JuickServerReconnectManager juickServerReconnectManager() { - return new JuickServerReconnectManager(); - } - @Bean - public WebSocketConnectionManager connectionManager() { - String websocketURI = UriComponentsBuilder.fromUriString(baseUri) - .queryParam("hash", userService.getHashByUID(userService.getUIDbyName(serviceUser))).build().toUriString(); - return new WebSocketConnectionManager(client(), juickServerComponent(), websocketURI); - } - @Bean - public StandardWebSocketClient client() { - return new StandardWebSocketClient(); - } - @Bean - public ApiServer apiServer() { - return new ApiServer(); - } - @Bean - public Docket api() { - return new Docket(DocumentationType.SWAGGER_2) - .select() - .apis(RequestHandlerSelectors.any()) - .paths(PathSelectors.any()).build().apiInfo(new ApiInfo("Juick API", "Juick REST API Documentation", - "2.0", "https://juick.com/help/tos", null, - "AGPLv3", "https://www.gnu.org/licenses/agpl-3.0.html", Collections.emptyList())); - } - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/swagger-ui.html") - .addResourceLocations("classpath:/META-INF/resources/"); - - registry.addResourceHandler("/webjars/**") - .addResourceLocations("classpath:/META-INF/resources/webjars/"); - } - @Bean - public WebsocketManager wsHandler() { - return new WebsocketManager(); - } - - @Override - public void registerWebSocketHandlers(@Nonnull WebSocketHandlerRegistry registry) { - //((ServletWebSocketHandlerRegistry) registry).setOrder(Ordered.LOWEST_PRECEDENCE); - registry.addHandler(wsHandler(), "/ws/**").setAllowedOrigins("*"); - } - - @Bean - public ServletServerContainerFactoryBean createWebSocketContainer() { - ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); - container.setMaxTextMessageBufferSize(8192); - container.setMaxBinaryMessageBufferSize(8192); - return container; - } - @Override - public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { - configurer.enable(); - } - -} diff --git a/juick-server/src/main/java/com/juick/api/configuration/ApiInitializer.java b/juick-server/src/main/java/com/juick/api/configuration/ApiInitializer.java deleted file mode 100644 index 8f35e3f7..00000000 --- a/juick-server/src/main/java/com/juick/api/configuration/ApiInitializer.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.api.configuration; - -import com.juick.configuration.DataConfiguration; -import com.juick.server.configuration.JuickServerWebsocketConfiguration; -import com.juick.server.configuration.StorageConfiguration; -import org.apache.commons.codec.CharEncoding; -import org.springframework.web.filter.CharacterEncodingFilter; -import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; - -import javax.annotation.Nonnull; -import javax.servlet.Filter; - -/** - * Created by vt on 09/02/16. - */ -public class ApiInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { - - @Override - protected Class[] getRootConfigClasses() { - return new Class[]{ - ApiSecurityConfig.class, - DataConfiguration.class, - StorageConfiguration.class - }; - } - - @Override - protected Class[] getServletConfigClasses() { - return null; - } - - @Override - @Nonnull - protected String[] getServletMappings() { - return new String[]{"/"}; - } - - @Override - protected Filter[] getServletFilters() { - return new Filter[]{new CharacterEncodingFilter(CharEncoding.UTF_8)}; - } - - @Override - @Nonnull - protected String getServletName() { - return "API dispatcher servlet"; - } -} diff --git a/juick-server/src/main/java/com/juick/api/configuration/ApiSecurityConfig.java b/juick-server/src/main/java/com/juick/api/configuration/ApiSecurityConfig.java deleted file mode 100644 index 2ae1d9e5..00000000 --- a/juick-server/src/main/java/com/juick/api/configuration/ApiSecurityConfig.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.api.configuration; - -import com.juick.service.UserService; -import com.juick.service.security.JuickUserDetailsService; -import com.juick.service.security.NotAuthorizedAuthenticationEntryPoint; -import com.juick.service.security.deprecated.RequestParamHashRememberMeServices; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.PropertySource; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.authentication.RememberMeServices; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import javax.inject.Inject; -import java.util.Arrays; -import java.util.Collections; -import java.util.concurrent.TimeUnit; - -/** - * Created by aalexeev on 11/21/16. - */ -@Configuration -@EnableWebSecurity -@PropertySource("classpath:juick.conf") -@Import(ApiAppConfiguration.class) -public class ApiSecurityConfig extends WebSecurityConfigurerAdapter { - @Value("${auth_remember_me_key}") - private String rememberMeKey; - @Inject - private UserService userService; - - ApiSecurityConfig() { - super(true); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS).permitAll() - .antMatchers("/messages", "/users", "/thread", "/tags", "/tlgmbtwbhk", "/fbwbhk", - "/skypebotendpoint").permitAll() - .anyRequest().hasRole("USER") - .and().httpBasic().authenticationEntryPoint(getJuickAuthenticationEntryPoint()) - .and().anonymous() - .and().cors().configurationSource(corsConfigurationSource()) - .and().servletApi() - .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and().exceptionHandling().authenticationEntryPoint(getJuickAuthenticationEntryPoint()) - .and() - .rememberMe() - .alwaysRemember(true) - .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(6 * 30)) - .rememberMeServices(rememberMeServices()) - .key(rememberMeKey) - .and().authenticationProvider(authenticationProvider()) - .headers().defaultsDisabled().cacheControl(); - } - - @Bean - public DaoAuthenticationProvider authenticationProvider() { - DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); - - authenticationProvider.setUserDetailsService(userDetailsService()); - - return authenticationProvider; - } - - @Bean - public JuickUserDetailsService userDetailsService() { - return new JuickUserDetailsService(userService); - } - - @Bean - public RememberMeServices rememberMeServices() throws Exception { - return new RequestParamHashRememberMeServices(rememberMeKey, userService); - } - - @Bean - public NotAuthorizedAuthenticationEntryPoint getJuickAuthenticationEntryPoint() { - return new NotAuthorizedAuthenticationEntryPoint(); - } - - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - - configuration.setAllowedOrigins(Collections.singletonList("*")); - configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE")); - configuration.setAllowedHeaders(Collections.singletonList("*")); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - - return source; - } - @Override - public void configure(WebSecurity web) throws Exception { - web.ignoring().antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources/**", - "/configuration/**", "/swagger-ui.html", "/webjars/**", "/ws/**"); - } -} diff --git a/juick-server/src/main/java/com/juick/api/configuration/ApiSecurityInitializer.java b/juick-server/src/main/java/com/juick/api/configuration/ApiSecurityInitializer.java deleted file mode 100644 index 259315fb..00000000 --- a/juick-server/src/main/java/com/juick/api/configuration/ApiSecurityInitializer.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.api.configuration; - -/** - * Created by vitalyster on 25.11.2016. - */ - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; - -import javax.servlet.ServletContext; - -public class ApiSecurityInitializer extends AbstractSecurityWebApplicationInitializer { - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Override - protected void afterSpringSecurityFilterChain(ServletContext servletContext) { - logger.info("SpringSecurityFilterChain initialized"); - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/ApiController.java b/juick-server/src/main/java/com/juick/api/controllers/ApiController.java deleted file mode 100644 index 0929d1ea..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/ApiController.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.api.controllers; - -import com.juick.Status; -import com.juick.api.WebsocketManager; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import springfox.documentation.annotations.ApiIgnore; - -import javax.inject.Inject; -import java.net.URI; - -/** - * Created by vitalyster on 25.07.2016. - */ -@ApiIgnore -@RestController -public class ApiController { - @Inject - private WebsocketManager wsHandler; - - @RequestMapping(value = "/", method = RequestMethod.GET, headers = "Connection!=Upgrade") - public ResponseEntity description() { - URI redirectUri = ServletUriComponentsBuilder.fromCurrentRequestUri().path("/swagger-ui.html").build().toUri(); - return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).location(redirectUri).build(); - } - - @RequestMapping(value = "/api/status", method = RequestMethod.GET, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE, headers = "Connection!=Upgrade") - public Status status() { - return Status.getStatus(String.valueOf(wsHandler.getClients().size())); - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/Messages.java b/juick-server/src/main/java/com/juick/api/controllers/Messages.java deleted file mode 100644 index e16e46d5..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/Messages.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * 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.api.controllers; - -import com.juick.Status; -import com.juick.Tag; -import com.juick.User; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpForbiddenException; -import com.juick.server.util.UserUtils; -import com.juick.service.MessagesService; -import com.juick.service.TagService; -import com.juick.service.UserService; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.*; -import springfox.documentation.annotations.ApiIgnore; - -import javax.inject.Inject; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Collections; -import java.util.List; - -/** - * @author ugnich - */ -@RestController -@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) -public class Messages { - - private static final ResponseEntity> NOT_FOUND = ResponseEntity - .status(HttpStatus.NOT_FOUND) - .body(Collections.emptyList()); - - private static final ResponseEntity> FORBIDDEN = ResponseEntity - .status(HttpStatus.FORBIDDEN) - .body(Collections.emptyList()); - - @Inject - private MessagesService messagesService; - @Inject - private UserService userService; - @Inject - private TagService tagService; - - // TODO: serialize image urls - - @RequestMapping("/home") - public ResponseEntity> getHome( - @RequestParam(defaultValue = "0") int before_mid) { - User visitor = UserUtils.getCurrentUser(); - if (!visitor.isAnonymous()) { - int vuid = visitor.getUid(); - List mids = messagesService.getMyFeed(vuid, before_mid, true); - - if (!mids.isEmpty()) - return ResponseEntity.ok(messagesService.getMessages(mids)); - - return NOT_FOUND; - } - return FORBIDDEN; - } - - @RequestMapping("/messages") - public ResponseEntity> getMessages( - @RequestParam(required = false) String uname, - @RequestParam(name = "before_mid", defaultValue = "0") Integer before, - @RequestParam(required = false, defaultValue = "0") Integer daysback, - @RequestParam(required = false) String withrecommended, - @RequestParam(required = false) String popular, - @RequestParam(required = false) String media, - @RequestParam(required = false) String tag) { - User visitor = UserUtils.getCurrentUser(); - int vuid = visitor.getUid(); - - List mids; - if (!StringUtils.isEmpty(uname)) { - User user = userService.getUserByName(uname); - if (user != null) { - if (!StringUtils.isEmpty(media)) { - mids = messagesService.getUserPhotos(user.getUid(), 0, before); - } else if (!StringUtils.isEmpty(tag)) { - Tag tagObject = tagService.getTag(tag, false); - if (tagObject != null) { - mids = messagesService.getUserTag(user.getUid(), tagObject.TID, 0, before); - } else { - return NOT_FOUND; - } - } else if (!StringUtils.isEmpty(withrecommended)) { - mids = messagesService.getUserBlogWithRecommendations(user.getUid(), 0, before); - } else if (daysback > 0) { - mids = messagesService.getUserBlogAtDay(user.getUid(), 0, daysback); - } else { - mids = messagesService.getUserBlog(user.getUid(), 0, before); - } - } else { - return NOT_FOUND; - } - } else { - if (!StringUtils.isEmpty(popular)) { - mids = messagesService.getPopular(vuid, before); - } else if (!StringUtils.isEmpty(media)) { - mids = messagesService.getPhotos(vuid, before); - } else if (!StringUtils.isEmpty(tag)) { - Tag tagObject = tagService.getTag(tag, false); - if (tagObject != null) { - mids = messagesService.getTag(tagObject.TID, vuid, before, 20); - } else { - return NOT_FOUND; - } - } else { - mids = messagesService.getAll(vuid, before); - } - } - return ResponseEntity.ok(messagesService.getMessages(mids)); - } - - @GetMapping("/messages/notifications") - public ResponseEntity> getNotifications( - @RequestParam(required = false) Long before - ) { - User visitor = UserUtils.getCurrentUser(); - LocalDateTime beforeTime = before != null ? - LocalDateTime.ofInstant(Instant.ofEpochMilli(before), ZoneId.systemDefault()) - : null; - return ResponseEntity.ok(messagesService.getNotifications(visitor, beforeTime)); - } - @RequestMapping("/thread") - public ResponseEntity> getThread( - @RequestParam(defaultValue = "0") int mid) { - User visitor = UserUtils.getCurrentUser(); - int vuid = visitor.getUid(); - com.juick.Message msg = messagesService.getMessage(mid); - if (msg != null) { - if (!messagesService.canViewThread(mid, vuid)) { - return FORBIDDEN; - } else { - List replies = messagesService.getReplies(mid); - replies.add(0, msg); - return ResponseEntity.ok(replies); - } - } - return NOT_FOUND; - } - - @ApiIgnore - @RequestMapping("/messages/set_privacy") - @ResponseBody - public ResponseEntity doSetPrivacy( - @RequestParam(defaultValue = "0") int mid) { - User visitor = UserUtils.getCurrentUser(); - int vuid = visitor.getUid(); - if (vuid == 0) { - throw new HttpForbiddenException(); - } - com.juick.User user = messagesService.getMessageAuthor(mid); - if (user != null && user.getUid() == vuid && messagesService.setMessagePrivacy(mid)) { - return ResponseEntity.ok(Status.OK); - } - throw new HttpForbiddenException(); - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/MessengerWebhook.java b/juick-server/src/main/java/com/juick/api/controllers/MessengerWebhook.java deleted file mode 100644 index 835165ba..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/MessengerWebhook.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.api.controllers; - -import com.github.messenger4j.exception.MessengerVerificationException; -import com.juick.api.MessengerManager; -import com.juick.server.util.HttpForbiddenException; -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import springfox.documentation.annotations.ApiIgnore; - -import javax.inject.Inject; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -/** - * Created by vitalyster on 27.03.2017. - */ -@ApiIgnore -@RestController -public class MessengerWebhook { - private static Logger logger = LoggerFactory.getLogger(MessengerWebhook.class); - - @Inject - private MessengerManager messengerManager; - - @RequestMapping(value = "/fbwbhk", method = RequestMethod.GET) - public ResponseEntity verifyHook(@RequestParam(name = "hub.mode") String hubMode, - @RequestParam(name = "hub.challenge") Integer hubChallenge, - @RequestParam(name = "hub.verify_token") String verifyToken) { - if (hubMode.equals("subscribe") && verifyToken.equals(messengerManager.getFacebookVerifyToken())) { - return new ResponseEntity<>(hubChallenge, HttpStatus.OK); - } - throw new HttpForbiddenException(); - } - @RequestMapping(value = "/fbwbhk", method = RequestMethod.POST) - @ResponseStatus(value = HttpStatus.OK) - public void processUpdate(@RequestHeader(name = "X-Hub-Signature", required = false) String signature, InputStream body) throws IOException, MessengerVerificationException { - String data = IOUtils.toString(body, StandardCharsets.UTF_8); - messengerManager.processUpdate(signature, data); - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/Notifications.java b/juick-server/src/main/java/com/juick/api/controllers/Notifications.java deleted file mode 100644 index e4474e6e..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/Notifications.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * 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.api.controllers; - -import com.juick.Message; -import com.juick.Status; -import com.juick.ExternalToken; -import com.juick.User; -import com.juick.server.helpers.AnonymousUser; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpForbiddenException; -import com.juick.service.MessagesService; -import com.juick.service.PushQueriesService; -import com.juick.service.SubscriptionService; -import com.juick.server.util.UserUtils; -import com.juick.service.UserService; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import springfox.documentation.annotations.ApiIgnore; - -import javax.inject.Inject; -import java.io.IOException; -import java.security.Principal; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Created by vitalyster on 24.10.2016. - */ -@RestController -public class Notifications { - - @Inject - private PushQueriesService pushQueriesService; - @Inject - private MessagesService messagesService; - @Inject - private SubscriptionService subscriptionService; - @Inject - private UserService userService; - - - private User collectTokens(Integer uid) { - User user = userService.getUserByUID(uid).orElse(AnonymousUser.INSTANCE); - pushQueriesService.getGCMRegID(uid).forEach(t -> user.getTokens().add(new ExternalToken(null, "gcm", t, null))); - pushQueriesService.getAPNSToken(uid).forEach(t -> user.getTokens().add(new ExternalToken(null, "apns", t, null))); - pushQueriesService.getMPNSURL(uid).forEach(t -> user.getTokens().add(new ExternalToken(null, "mpns", t, null))); - return user; - } - - @ApiIgnore - @RequestMapping(value = "/notifications", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public ResponseEntity> doGet( - @RequestParam(required = false, defaultValue = "0") int uid, - @RequestParam(required = false, defaultValue = "0") int mid, - @RequestParam(required = false, defaultValue = "0") int rid) { - User visitor = UserUtils.getCurrentUser(); - if ((visitor.getUid() == 0) || !(visitor.getName().equals("juick"))) { - throw new HttpForbiddenException(); - } - if (uid > 0 && mid == 0) { - // PM - return ResponseEntity.ok(Collections.singletonList(collectTokens(uid))); - } else { - if (mid > 0) { - Message msg = messagesService.getMessage(mid); - if (msg != null) { - List users; - if (rid > 0) { - Message reply = messagesService.getReply(mid, rid); - users = subscriptionService.getUsersSubscribedToComments(mid, reply.getUser().getUid()); - } else { - users = subscriptionService.getSubscribedUsers(msg.getUser().getUid(), mid); - } - - return ResponseEntity.ok(users.stream().map(User::getUid) - .map(this::collectTokens).collect(Collectors.toList())); - } - } - } - throw new HttpBadRequestException(); - } - - @ApiIgnore - @RequestMapping(value = "/notifications", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Status doDelete( - @RequestBody List list) throws IOException { - User visitor = UserUtils.getCurrentUser(); - // FIXME: it is possible to delete other user's tokens - if ((visitor.getUid() == 0) || !(visitor.getName().equals("juick"))) { - throw new HttpForbiddenException(); - } - list.forEach(t -> { - switch (t.getType()) { - case "gcm": - pushQueriesService.deleteGCMToken(t.getToken()); - break; - case "apns": - pushQueriesService.deleteAPNSToken(t.getToken()); - break; - case "mpns": - pushQueriesService.deleteMPNSToken(t.getToken()); - break; - default: - throw new HttpBadRequestException(); - } - }); - - return Status.OK; - } - - @ApiIgnore - @RequestMapping(value = "/notifications", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Status doPut( - @RequestBody List list) throws IOException { - User visitor = UserUtils.getCurrentUser(); - if (visitor.getUid() == 0) { - throw new HttpForbiddenException(); - } - list.forEach(t -> { - switch (t.getType()) { - case "gcm": - pushQueriesService.addGCMToken(visitor.getUid(), t.getToken()); - break; - case "apns": - pushQueriesService.addAPNSToken(visitor.getUid(), t.getToken()); - break; - case "mpns": - pushQueriesService.addMPNSToken(visitor.getUid(), t.getToken()); - break; - default: - throw new HttpBadRequestException(); - } - }); - return Status.OK; - } - - @Deprecated - @RequestMapping(value = "/android/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Status doAndroidRegister( - @RequestParam(name = "regid") String regId) { - User visitor = UserUtils.getCurrentUser(); - if (visitor.getUid() == 0) { - throw new HttpForbiddenException(); - } - pushQueriesService.addGCMToken(visitor.getUid(), regId); - return Status.OK; - } - - @Deprecated - @RequestMapping(value = "/android/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Status doAndroidUnRegister(@RequestParam(name = "regid") String regId) { - pushQueriesService.deleteGCMToken(regId); - return Status.OK; - } - - @Deprecated - @RequestMapping(value = "/winphone/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Status doWinphoneRegister( - Principal principal, - @RequestParam(name = "url") String regId) { - User visitor = UserUtils.getCurrentUser(); - pushQueriesService.addMPNSToken(visitor.getUid(), regId); - return Status.OK; - } - - @Deprecated - @RequestMapping(value = "/winphone/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Status doWinphoneUnRegister(@RequestParam(name = "url") String regId) { - pushQueriesService.deleteMPNSToken(regId); - return Status.OK; - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/Others.java b/juick-server/src/main/java/com/juick/api/controllers/Others.java deleted file mode 100644 index 4245de81..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/Others.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.api.controllers; - -import com.juick.User; -import com.juick.server.helpers.PrivateChats; -import com.juick.server.util.HttpForbiddenException; -import com.juick.server.util.HttpNotFoundException; -import com.juick.server.util.UserUtils; -import com.juick.service.PMQueriesService; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import javax.inject.Inject; -import java.util.List; - -/** - * @author ugnich - */ -@RestController -public class Others { - @Inject - private PMQueriesService pmQueriesService; - - @RequestMapping(value = "groups_pms", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public PrivateChats doGetGroupsPMs( - @RequestParam(defaultValue = "5") int cnt) { - User visitor = UserUtils.getCurrentUser(); - int vuid = visitor.getUid(); - if (vuid == 0) { - throw new HttpForbiddenException(); - } - if (cnt < 3) { - cnt = 3; - } - if (cnt > 10) { - cnt = 10; - } - - List lastconv = pmQueriesService.getPMLastConversationsUsers(vuid, cnt); - if (lastconv != null && !lastconv.isEmpty()) { - PrivateChats pms = new PrivateChats(); - pms.setUsers(lastconv); - return pms; - } - throw new HttpNotFoundException(); - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/PM.java b/juick-server/src/main/java/com/juick/api/controllers/PM.java deleted file mode 100644 index 87858e8c..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/PM.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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.api.controllers; - -import com.juick.User; -import com.juick.api.ApiServer; -import com.juick.server.helpers.AnonymousUser; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpForbiddenException; -import com.juick.service.PMQueriesService; -import com.juick.service.UserService; -import com.juick.server.util.UserUtils; -import com.juick.server.util.WebUtils; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.stanza.model.Message; - -import javax.inject.Inject; -import java.util.List; - -/** - * @author ugnich - */ -@RestController -public class PM { - @Inject - private UserService userService; - @Inject - private PMQueriesService pmQueriesService; - @Inject - private ApiServer apiServer; - - @RequestMapping(value = "/pm", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public List doGetPM( - @RequestParam(required = false) String uname) { - User visitor = UserUtils.getCurrentUser(); - int vuid = visitor.getUid(); - if (vuid == 0) { - throw new HttpForbiddenException(); - } - int uid = 0; - if (uname != null && uname.matches("^[a-zA-Z0-9\\-]{2,16}$")) { - uid = userService.getUIDbyName(uname); - } - - if (uid == 0) { - throw new HttpBadRequestException(); - } - - return pmQueriesService.getPMMessages(vuid, uid); - } - - @RequestMapping(value = "/pm", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public com.juick.Message doPostPM( - @RequestParam String uname, - @RequestParam String body) { - User visitor = UserUtils.getCurrentUser(); - int vuid = visitor.getUid(); - if (vuid == 0) { - throw new HttpForbiddenException(); - } - User userTo = AnonymousUser.INSTANCE; - if (WebUtils.isUserName(uname)) { - userTo = userService.getUserByName(uname); - } - - if (userTo.getUid() == 0 || body == null || body.length() < 1 || body.length() > 10240) { - throw new HttpBadRequestException(); - } - - if (userService.isInBLAny(userTo.getUid(), vuid)) { - throw new HttpForbiddenException(); - } - - if (pmQueriesService.createPM(vuid, userTo.getUid(), body)) { - Message msg = new Message(); - msg.setFrom(Jid.of("juick@juick.com")); - msg.setTo(Jid.of(String.format("%d@push.juick.com", userTo.getUid()))); - com.juick.Message jmsg = new com.juick.Message(); - jmsg.setUser(visitor); - jmsg.setText(body); - jmsg.setTo(userTo); - msg.addExtension(jmsg); - apiServer.sendMessage(msg); - - msg.setTo(Jid.of(String.format("%d@ws.juick.com", userTo.getUid()))); - apiServer.sendMessage(msg); - - List jids = userService.getJIDsbyUID(userTo.getUid()); - for (String jid : jids) { - Message mm = new Message(); - mm.setTo(Jid.of(jid)); - mm.setType(Message.Type.CHAT); - if (pmQueriesService.havePMinRoster(vuid, jid)) { - mm.setFrom(Jid.of(jmsg.getUser().getName(), "juick.com", "Juick")); - mm.setBody(body); - } else { - mm.setFrom(Jid.of("juick", "juick.com", "Juick")); - mm.setBody("Private message from @" + jmsg.getUser().getName() + ":\n" + body); - } - apiServer.sendMessage(mm); - } - return jmsg; - - } - throw new HttpBadRequestException(); - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/Post.java b/juick-server/src/main/java/com/juick/api/controllers/Post.java deleted file mode 100644 index a89786b1..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/Post.java +++ /dev/null @@ -1,281 +0,0 @@ -/* - * 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.api.controllers; - -import com.juick.User; -import com.juick.api.ApiServer; -import com.juick.api.EmailManager; -import com.juick.server.util.*; -import com.juick.service.MessagesService; -import com.juick.service.SubscriptionService; -import com.juick.service.UserService; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.mail.util.MimeMessageParser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.stanza.model.Message; -import rocks.xmpp.extensions.nick.model.Nickname; -import rocks.xmpp.extensions.oob.model.x.OobX; - -import javax.inject.Inject; -import javax.mail.Session; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeMessage; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; -import java.util.Properties; -import java.util.Scanner; -import java.util.UUID; - -/** - * Created by vt on 24/11/2016. - */ -@RestController -public class Post { - private static Logger logger = LoggerFactory.getLogger(ApiServer.class); - - @Inject - private UserService userService; - @Inject - private ApiServer apiServer; - @Inject - private MessagesService messagesService; - @Inject - private SubscriptionService subscriptionService; - @Value("${upload_tmp_dir:/var/www/juick.com/i/tmp/}") - private String tmpDir; - @Value("${img_path:/var/www/juick.com/i/}") - private String imgDir; - - @RequestMapping(value = "/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - @ResponseStatus(value = HttpStatus.OK) - public void doPostMessage( - @RequestParam String body, - @RequestParam(required = false) String img, - @RequestParam(required = false) MultipartFile attach) throws IOException { - User visitor = UserUtils.getCurrentUser(); - - if (visitor.isAnonymous()) - throw new HttpForbiddenException(); - - if (body == null || body.length() < 1 || body.length() > 4096) { - throw new HttpBadRequestException(); - } - body = body.replace("\r", StringUtils.EMPTY); - - String attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir); - - if (StringUtils.isBlank(attachmentFName) && img != null && img.length() > 10) { - try { - URL imgUrl = new URL(img); - attachmentFName = HttpUtils.downloadImage(imgUrl, tmpDir); - } catch (Exception e) { - logger.error("DOWNLOAD ERROR", e); - throw new HttpBadRequestException(); - } - } - apiServer.processMessage(visitor, body, attachmentFName); - } - - @RequestMapping(value = "/comment", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public com.juick.Message doPostComment( - @RequestParam(defaultValue = "0") int mid, - @RequestParam(defaultValue = "0") int rid, - @RequestParam String body, - @RequestParam(required = false) String img, - @RequestParam(required = false) MultipartFile attach) - throws IOException { - User visitor = UserUtils.getCurrentUser(); - int vuid = visitor.getUid(); - if (vuid == 0) { - throw new HttpForbiddenException(); - } - if (mid == 0) { - throw new HttpBadRequestException(); - } - com.juick.Message msg = messagesService.getMessage(mid); - if (msg == null) { - throw new HttpNotFoundException(); - } - - com.juick.Message reply = null; - if (rid > 0) { - reply = messagesService.getReply(mid, rid); - if (reply == null) { - throw new HttpNotFoundException(); - } - } - - if (body == null || body.length() < 1 || body.length() > 4096) { - throw new HttpBadRequestException(); - } - body = body.replace("\r", StringUtils.EMPTY); - - if ((msg.ReadOnly && msg.getUser().getUid() != vuid) || userService.isInBLAny(msg.getUser().getUid(), vuid) - || (reply != null && userService.isInBLAny(reply.getUser().getUid(), vuid))) { - throw new HttpForbiddenException(); - } - - String attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir); - - if (StringUtils.isBlank(attachmentFName) && img != null && img.length() > 10) { - try { - attachmentFName = HttpUtils.downloadImage(new URL(img), tmpDir); - } catch (Exception e) { - logger.error("DOWNLOAD ERROR", e); - throw new HttpBadRequestException(); - } - } - - String attachmentType = StringUtils.isNotEmpty(attachmentFName) ? attachmentFName.substring(attachmentFName.length() - 3) : null; - int ridnew = messagesService.createReply(mid, rid, vuid, body, attachmentType); - subscriptionService.subscribeMessage(mid, vuid); - - com.juick.Message jmsg = messagesService.getReply(mid, ridnew); - - Message xmsg = new Message(); - xmsg.setFrom(Jid.of("juick@juick.com")); - xmsg.setType(Message.Type.CHAT); - xmsg.setThread("juick-" + mid); - xmsg.addExtension(jmsg); - - String quote = reply != null ? StringUtils.defaultString(reply.getText()) : StringUtils.defaultString(msg.getText()); - if (quote.length() >= 50) { - quote = quote.substring(0, 47) + "..."; - } - - xmsg.addExtension(new Nickname("@" + jmsg.getUser().getName())); - - if (StringUtils.isNotEmpty(attachmentFName)) { - String fname = mid + "-" + ridnew + "." + attachmentType; - String attachmentURL = "http://i.juick.com/photos-1024/" + fname; - - ImageUtils.saveImageWithPreviews(attachmentFName, fname, tmpDir, imgDir); - - body = attachmentURL + "\n" + body; - try { - xmsg.addExtension(new OobX(new URI(attachmentURL))); - } catch (URISyntaxException e) { - logger.error("invalid uri: {}, exception {}", attachmentURL, e); - } - } - - xmsg.setBody("Reply by @" + jmsg.getUser().getName() + ":\n>" + quote + "\n" + body + "\n\n#" + - mid + "/" + ridnew + " http://juick.com/" + mid + "#" + ridnew); - - xmsg.setTo(Jid.of("juick@s2s.juick.com")); - apiServer.sendMessage(xmsg); - - xmsg.setTo(Jid.of("juick@ws.juick.com")); - apiServer.sendMessage(xmsg); - - xmsg.setTo(Jid.of("juick@push.juick.com")); - apiServer.sendMessage(xmsg); - return jmsg; - } - - Session session = Session.getDefaultInstance(new Properties()); - - @PostMapping("/mail") - @ResponseStatus(value = HttpStatus.OK) - public void processMail(InputStream data) throws Exception { - MimeMessage msg = new MimeMessage(session, data); - String from = msg.getFrom().length > 1 ? ((InternetAddress) msg.getSender()).getAddress() - : ((InternetAddress) msg.getFrom()[0]).getAddress(); - logger.info("got msg from {}", from); - - User visitor = userService.getUserByEmail(from); - if (!visitor.isAnonymous()) { - MimeMessageParser parser = new MimeMessageParser(msg); - parser.parse(); - final String[] body = {parser.getPlainContent()}; - if (body[0] == null) { - parser.getAttachmentList().stream() - .filter(a -> a.getContentType().equals("text/plain")).findFirst() - .ifPresent(a -> { - try { - body[0] = IOUtils.toString(a.getInputStream(), StandardCharsets.UTF_8); - logger.info("got text: {}", body[0]); - } catch (IOException e) { - logger.info("attachment error: {}", e); - } - }); - } - final String[] attachmentFName = new String[1]; - parser.getAttachmentList().stream().filter(a -> - a.getContentType().equals("image/jpeg") || a.getContentType().equals("image/png")) - .findFirst().ifPresent(a -> { - logger.info("got attachment: {}", a.getContentType()); - String attachmentType; - if (a.getContentType().equals("image/jpeg")) { - attachmentType = "jpg"; - } else { - attachmentType = "png"; - } - attachmentFName[0] = DigestUtils.md5Hex(UUID.randomUUID().toString()) + "." + attachmentType; - try { - logger.info("got inputstream: {}", a.getInputStream()); - FileOutputStream fos = new FileOutputStream(Paths.get(tmpDir, attachmentFName[0]).toString()); - IOUtils.copy(a.getInputStream(), fos); - fos.close(); - } catch (IOException e) { - logger.info("attachment error: {}", e); - } - }); - String[] inReplyToHeaders = msg.getHeader("In-Reply-To"); - if (inReplyToHeaders != null && inReplyToHeaders.length > 0) { - Scanner inReplyToScanner = new Scanner(inReplyToHeaders[0].trim()).useDelimiter(EmailManager.MSGID_PATTERN); - int mid = Integer.parseInt(inReplyToScanner.next()); - int rid = Integer.parseInt(inReplyToScanner.next()); - logger.info("Message is reply to #{}/{}", mid, rid); - body[0] = rid > 0 ? String.format("#%d/%d %s", mid, rid, body[0]) - : String.format("#%d %s", mid, body[0]); - } - rocks.xmpp.core.stanza.model.Message xmsg = new rocks.xmpp.core.stanza.model.Message(); - xmsg.setType(rocks.xmpp.core.stanza.model.Message.Type.CHAT); - xmsg.setFrom(Jid.of(String.valueOf(visitor.getUid()), "uid.juick.com", "mail")); - xmsg.setTo(Jid.of("juick@juick.com/Juick")); - xmsg.setBody(body[0]); - try { - if (StringUtils.isNotEmpty(attachmentFName[0])) { - String attachmentUrl = String.format("juick://%s", attachmentFName[0]); - xmsg.addExtension(new OobX(new URI(attachmentUrl), "!!!!Juick!!")); - } - apiServer.sendMessage(xmsg); - } catch (URISyntaxException e1) { - logger.warn("attachment error", e1); - } - } else { - logger.info("not registered: {}", from); - } - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/Service.java b/juick-server/src/main/java/com/juick/api/controllers/Service.java deleted file mode 100644 index 9e7f3333..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/Service.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.api.controllers; - -import com.juick.Message; -import com.juick.User; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpForbiddenException; -import com.juick.server.util.UserUtils; -import com.juick.service.CrosspostService; -import com.juick.service.MessagesService; -import com.juick.service.SubscriptionService; -import com.juick.service.UserService; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import springfox.documentation.annotations.ApiIgnore; - -import javax.inject.Inject; -import java.io.IOException; -import java.util.List; - -/** - * TODO: configure spring-security to allow only admin role - */ -@ApiIgnore -@RestController -public class Service { - @Inject - private SubscriptionService subscriptionService; - @Inject - private MessagesService messagesService; - @Inject - private CrosspostService crosspostService; - @Inject - private UserService userService; - - @RequestMapping(value = "/subscriptions", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public List doGet( - @RequestParam(defaultValue = "0") int mid, - @RequestParam(defaultValue = "0") int uid) throws IOException { - User visitor = UserUtils.getCurrentUser(); - if ((visitor.getUid() == 0) && !(visitor.getName().equals("juick"))) { - throw new HttpForbiddenException(); - } - if (uid > 0) { - return subscriptionService.getSubscribedUsers(uid, mid); - } else { - // thread - Message msg = messagesService.getMessage(mid); - if (msg != null) { - return subscriptionService.getUsersSubscribedToComments(mid, msg.getUser().getUid()); - } - } - throw new HttpBadRequestException(); - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/SkypeEndpoint.java b/juick-server/src/main/java/com/juick/api/controllers/SkypeEndpoint.java deleted file mode 100644 index 91a617b4..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/SkypeEndpoint.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.api.controllers; - -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; -import springfox.documentation.annotations.ApiIgnore; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -/** - * Created by vitalyster on 18.07.2016. - */ -@ApiIgnore -@RestController -public class SkypeEndpoint { - private static final Logger logger = LoggerFactory.getLogger(SkypeEndpoint.class); - @RequestMapping(value = "/skypebotendpoint", method = RequestMethod.POST) - @ResponseStatus(value = HttpStatus.OK) - public void doPost(InputStream body) throws IOException { - String data = IOUtils.toString(body, StandardCharsets.UTF_8); - logger.info("got data: {}", data); - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/Tags.java b/juick-server/src/main/java/com/juick/api/controllers/Tags.java deleted file mode 100644 index 85bb1ba4..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/Tags.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.api.controllers; - -import com.juick.User; -import com.juick.server.helpers.TagStats; -import com.juick.server.util.UserUtils; -import com.juick.service.TagService; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import javax.inject.Inject; -import java.util.List; - -/** - * Created by vitalyster on 29.11.2016. - */ -@RestController -public class Tags { - @Inject - private TagService tagService; - - @RequestMapping(value = "/tags", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public List tags( - @RequestParam(required = false, defaultValue = "0") int user_id - ) { - User visitor = UserUtils.getCurrentUser(); - if (user_id == 0) { - user_id = visitor.getUid(); - } - if (user_id > 0) { - return tagService.getUserTagStats(user_id); - } - return tagService.getTagStats(); - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/TelegramWebhook.java b/juick-server/src/main/java/com/juick/api/controllers/TelegramWebhook.java deleted file mode 100644 index 31e7b4ea..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/TelegramWebhook.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.api.controllers; - -import com.juick.api.TelegramBotManager; -import org.apache.commons.io.IOUtils; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; -import springfox.documentation.annotations.ApiIgnore; - -import javax.inject.Inject; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -/** - * Created by vt on 24/11/2016. - */ -@ApiIgnore -@RestController -public class TelegramWebhook { - @Inject - private TelegramBotManager telegramBotManager; - - @RequestMapping(value = "/tlgmbtwbhk", method = RequestMethod.POST) - @ResponseStatus(value = HttpStatus.OK) - public void processUpdate(InputStream body) throws Exception { - String data = IOUtils.toString(body, StandardCharsets.UTF_8); - telegramBotManager.processUpdate(data); - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/Users.java b/juick-server/src/main/java/com/juick/api/controllers/Users.java deleted file mode 100644 index b221d9fe..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/Users.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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.api.controllers; - -import com.juick.User; -import com.juick.server.util.HttpForbiddenException; -import com.juick.server.util.HttpNotFoundException; -import com.juick.service.UserService; -import com.juick.server.util.UserUtils; -import com.juick.server.util.WebUtils; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.*; - -import javax.inject.Inject; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * @author ugnich - */ -@RestController -public class Users { - @Inject - private UserService userService; - - @RequestMapping(value = "/auth", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public String getAuthToken() { - return userService.getHashByUID(UserUtils.getCurrentUser().getUid()); - } - - @RequestMapping(value = "/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public List doGetUsers( - @RequestParam(value = "uname", required = false) List unames) { - List users = new ArrayList<>(); - - if (unames != null) { - unames.removeIf(WebUtils::isNotUserName); - - if (!unames.isEmpty() && unames.size() < 20) - users.addAll(userService.getUsersByName(unames)); - } - - if (!users.isEmpty()) - return users; - if (!UserUtils.getCurrentUser().isAnonymous()) { - return Collections.singletonList(UserUtils.getCurrentUser()); - } - - throw new HttpNotFoundException(); - } - - @RequestMapping(value = "/users/read", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public List doGetUserRead( - @RequestParam String uname) { - User visitor = UserUtils.getCurrentUser(); - int vuid = visitor.getUid(); - if (vuid == 0) { - throw new HttpForbiddenException(); - } - int uid = 0; - if (uname == null) { - uid = vuid; - } else { - if (WebUtils.isUserName(uname)) { - com.juick.User u = userService.getUserByName(uname); - if (u != null && u.getUid() > 0) { - uid = u.getUid(); - } - } - } - - if (uid > 0) { - List uids = userService.getUserRead(uid); - if (uids.size() > 0) { - List users = userService.getUsersByID(uids); - if (users.size() > 0) { - return users; - } - } - } - throw new HttpNotFoundException(); - } - - @RequestMapping(value = "/users/readers", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public List doGetUserReaders( - @RequestParam String uname) { - User visitor = UserUtils.getCurrentUser(); - int vuid = visitor.getUid(); - if (vuid == 0) { - throw new HttpForbiddenException(); - } - int uid = 0; - if (uname == null) { - uid = vuid; - } else { - if (WebUtils.isUserName(uname)) { - com.juick.User u = userService.getUserByName(uname); - if (u != null && u.getUid() > 0) { - uid = u.getUid(); - } - } - } - - if (uid > 0) { - return userService.getUserReaders(uid); - } - throw new HttpNotFoundException(); - } -} diff --git a/juick-server/src/main/java/com/juick/api/controllers/util/JsonpAdvice.java b/juick-server/src/main/java/com/juick/api/controllers/util/JsonpAdvice.java deleted file mode 100644 index 90a01da0..00000000 --- a/juick-server/src/main/java/com/juick/api/controllers/util/JsonpAdvice.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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.api.controllers.util; - -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice; - -/** - * Created by vitalyster on 25.11.2016. - */ -@ControllerAdvice -public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice { - public JsonpAdvice() { - super("callback"); - } -} diff --git a/juick-server/src/main/java/com/juick/server/CrosspostManager.java b/juick-server/src/main/java/com/juick/server/CrosspostManager.java new file mode 100644 index 00000000..edffaf71 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/CrosspostManager.java @@ -0,0 +1,165 @@ +/* + * 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.server; + +import com.juick.Message; +import com.juick.server.component.MessageEvent; +import com.juick.service.CrosspostService; +import com.juick.util.MessageUtils; +import org.apache.commons.codec.CharEncoding; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationListener; +import org.springframework.social.twitter.api.Twitter; +import org.springframework.social.twitter.api.impl.TwitterTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.net.ssl.HttpsURLConnection; +import java.io.OutputStreamWriter; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * @author Ugnich Anton + */ +@Component +public class CrosspostManager implements ApplicationListener { + private final static String FBURL = "https://graph.facebook.com/me/feed"; + private final static String VKURL = "https://api.vk.com/method/wall.post"; + + private static Logger logger = LoggerFactory.getLogger(CrosspostManager.class); + + @Inject + private CrosspostService crosspostService; + + @Value("${twitter_consumer_key:}") + private String twitter_consumer_key; + @Value("${twitter_consumer_secret:}") + private String twitter_consumer_secret; + + @Override + public void onApplicationEvent(@Nonnull MessageEvent event) { + Message msg = event.getMessage(); + if (msg.getMid() > 0 && msg.getRid() == 0) { + if (StringUtils.isNotEmpty(crosspostService.getTwitterName(msg.getUser().getUid()))) { + if (msg.getTags().stream().noneMatch(t -> t.getName().equals("notwitter"))) { + twitterPost(msg); + } + } + // TODO: approve application for facebook crosspost + } + } + + private boolean facebookPost(final com.juick.Message jmsg) { + String token = crosspostService.getFacebookTokens(jmsg.getUser().getUid()) + .orElse(Pair.of(StringUtils.EMPTY, StringUtils.EMPTY)).getRight(); + if (token.isEmpty()) { + return false; + } + + logger.info("FB: #{}", jmsg.getMid()); + + String status = MessageUtils.getMessageHashTags(jmsg) + "\n" + jmsg.getText(); + + boolean ret = false; + try { + String body = "access_token=" + + URLEncoder.encode(token, CharEncoding.UTF_8) + + "&message=" + + URLEncoder.encode(status, CharEncoding.UTF_8) + + "&link=http%3A%2F%2Fjuick.com%2F" + + jmsg.getMid(); + + HttpsURLConnection conn = (HttpsURLConnection) new URL(FBURL).openConnection(); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("User-Agent", "Juick"); + conn.setRequestProperty("Content-Length", Integer.toString(body.length())); + conn.setUseCaches(false); + conn.setDoInput(true); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.connect(); + + OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); + wr.write(body); + wr.close(); + + ret = StringUtils.isNotEmpty(IOUtils.toString(conn.getInputStream(), StandardCharsets.UTF_8)); + + conn.disconnect(); + } catch (Exception e) { + logger.error("fbPost exception", e); + } + return ret; + } + + private boolean vkontaktePost(final com.juick.Message jmsg) { + Pair tokens = crosspostService.getVkTokens(jmsg.getUser().getUid()).orElse(Pair.of(StringUtils.EMPTY, StringUtils.EMPTY)); + if (tokens.getLeft().isEmpty() || tokens.getRight().isEmpty()) { + return false; + } + + logger.info("VK: #", jmsg.getMid()); + + String status = MessageUtils.getMessageHashTags(jmsg) + "\n" + jmsg.getText() + "\nhttp://juick.com/" + jmsg.getMid(); + + boolean ret = false; + try { + String body = "owner_id=" + tokens.getLeft() + "&access_token=" + URLEncoder.encode(tokens.getRight(), CharEncoding.UTF_8) + "&from_group=1&message=" + URLEncoder.encode(status, CharEncoding.UTF_8); + + HttpsURLConnection conn = (HttpsURLConnection) new URL(VKURL).openConnection(); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("User-Agent", "Juick"); + conn.setRequestProperty("Content-Length", Integer.toString(body.length())); + conn.setUseCaches(false); + conn.setDoInput(true); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.connect(); + + OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); + wr.write(body); + wr.close(); + + ret = StringUtils.isNotEmpty(IOUtils.toString(conn.getInputStream(), StandardCharsets.UTF_8)); + + conn.disconnect(); + } catch (Exception e) { + logger.error("vkPost exception", e); + } + return ret; + } + + private void twitterPost(final com.juick.Message jmsg) { + crosspostService.getTwitterToken(jmsg.getUser().getUid()).ifPresent(t -> { + String status = MessageUtils.getMessageHashTags(jmsg) + StringUtils.defaultString(jmsg.getText()); + if (status.length() > 255) { + status = status.substring(0, 254) + "…"; + } + status += " http://juick.com/" + jmsg.getMid(); + Twitter twitter = new TwitterTemplate(twitter_consumer_key, twitter_consumer_secret, t.getToken(), t.getSecret()); + twitter.timelineOperations().updateStatus(status); + }); + } +} diff --git a/juick-server/src/main/java/com/juick/server/EmailManager.java b/juick-server/src/main/java/com/juick/server/EmailManager.java new file mode 100644 index 00000000..4c9bf3da --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/EmailManager.java @@ -0,0 +1,110 @@ +package com.juick.server; + +import com.juick.Message; +import com.juick.server.component.MessageEvent; +import com.juick.service.EmailService; +import com.juick.service.MessagesService; +import com.juick.service.SubscriptionService; +import com.juick.service.UserService; +import com.juick.util.MessageUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import java.util.Properties; + +import static com.juick.formatters.PlainTextFormatter.formatPost; +import static com.juick.formatters.PlainTextFormatter.formatUrl; + +@Component +public class EmailManager implements ApplicationListener { + + public static final String MSGID_PATTERN = "\\.|@|<"; + + private static final Logger logger = LoggerFactory.getLogger(EmailManager.class); + @Inject + private EmailService emailService; + @Inject + private SubscriptionService subscriptionService; + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Override + public void onApplicationEvent(@Nonnull MessageEvent event) { + Message msg = event.getMessage(); + if (msg.getMid() > 0 && msg.getRid() == 0) { + String subject = String.format("New message from %s", msg.getUser().getName()); + subscriptionService.getSubscribedUsers(msg.getUser().getUid(), msg.getMid()) + .forEach(user -> emailService.getEmails(user.getUid(), true) + .forEach(email -> emailNotify(email, subject, msg))); + } else if (msg.getRid() > 0) { + Message originalMessage = messagesService.getMessage(msg.getMid()); + String subject = String.format("New reply to %s", originalMessage.getUser().getName()); + subscriptionService.getUsersSubscribedToComments(msg.getMid(), msg.getUser().getUid()) + .forEach(user -> emailService.getEmails(user.getUid(), true) + .forEach(email -> emailNotify(email, subject, msg))); + } + } + + private void emailNotify(String email, String subject, Message msg) { + Properties prop = System.getProperties(); + prop.put("mail.smtp.starttls.enable", "true"); + Session session = Session.getDefaultInstance(prop); + try { + Transport transport = session.getTransport("smtp"); + MimeMessage message = new MimeMessage(session) { + protected void updateMessageID() throws MessagingException { + setHeader("Message-ID", String.format("<%d.%d@juick.com>", msg.getMid(), msg.getRid())); + if (msg.getRid() > 0) { + if (msg.getReplyto() > 0) { + Message replyto = messagesService.getReply(msg.getMid(), msg.getReplyto()); + setHeader("In-Reply-To", String.format("<%d.%d@juick.com>", replyto.getMid(), replyto.getRid())); + } else { + Message original = messagesService.getMessage(msg.getMid()); + setHeader("In-Reply-To", String.format("<%d.%d@juick.com>", original.getMid(), original.getRid())); + } + } + } + }; + message.setFrom(new InternetAddress("juick@juick.com")); + message.addRecipient(javax.mail.Message.RecipientType.TO, new InternetAddress(email)); + message.setSubject(subject); + String plainText = String.format("%s\n\n--\nYou are receiving this because you are subscribed to this user " + + ", discussion or tag. Reply to this email directly or view it on Juick: %s.", + formatPost(msg), formatUrl(msg)); + MimeBodyPart textBodyPart = new MimeBodyPart(); + textBodyPart.setContent(plainText, "text/plain; charset=UTF-8"); + String htmlText = String.format("%s

--
You are receiving this because you are subscribed to this user" + + ", discussion or tag. Reply to this email directly or view it on Juick." + + "
Configure or disable notifications", + MessageUtils.formatHtml(msg), formatUrl(msg), + userService.getHashByUID(userService.getUserByEmail(email).getUid())); + MimeBodyPart htmlBodyPart = new MimeBodyPart(); + htmlBodyPart.setContent(htmlText, "text/html; charset=UTF-8"); + Multipart multipart = new MimeMultipart("alternative"); + multipart.addBodyPart(textBodyPart); + multipart.addBodyPart(htmlBodyPart); + message.setContent(multipart); + message.setHeader("List-Unsubscribe", String.format("https://juick.com/settings?hash=%s", + userService.getHashByUID(userService.getUserByEmail(email).getUid()))); + message.saveChanges(); + transport.connect(); + transport.sendMessage(message, message.getAllRecipients()); + } catch (MessagingException ex) { + logger.error("mail exception", ex); + } + } +} diff --git a/juick-server/src/main/java/com/juick/server/FacebookPageManager.java b/juick-server/src/main/java/com/juick/server/FacebookPageManager.java new file mode 100644 index 00000000..971b206c --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/FacebookPageManager.java @@ -0,0 +1,32 @@ +package com.juick.server; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +@Component +public class FacebookPageManager { + private static Logger logger = LoggerFactory.getLogger(FacebookPageManager.class); + @Value("${facebook_page_id:12345678}") + private String pageId; + @Value("${fb_page_access_token:12345678}") + private String accessToken; + + public void post(String status, String link) { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("https://graph.facebook.com/{page_id}/feed") + .queryParam("message", status) + .queryParam("link", link) + .queryParam("access_token", accessToken) + .buildAndExpand(pageId); + RestTemplate api = new RestTemplate(); + ResponseEntity response = api.exchange(uriComponents.toUri(), + HttpMethod.POST, null, String.class); + logger.info("Facebook response: {}", response.getBody()); + } +} diff --git a/juick-server/src/main/java/com/juick/server/MessengerManager.java b/juick-server/src/main/java/com/juick/server/MessengerManager.java new file mode 100644 index 00000000..2979fa28 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/MessengerManager.java @@ -0,0 +1,143 @@ +package com.juick.server; + +import com.github.messenger4j.Messenger; +import com.github.messenger4j.exception.MessengerApiException; +import com.github.messenger4j.exception.MessengerIOException; +import com.github.messenger4j.exception.MessengerVerificationException; +import com.github.messenger4j.send.MessagePayload; +import com.github.messenger4j.send.message.TemplateMessage; +import com.github.messenger4j.send.message.TextMessage; +import com.github.messenger4j.send.message.template.ButtonTemplate; +import com.github.messenger4j.send.message.template.button.UrlButton; +import com.github.messenger4j.userprofile.UserProfile; +import com.github.messenger4j.webhook.event.TextMessageEvent; +import com.juick.Message; +import com.juick.User; +import com.juick.server.component.MessageEvent; +import com.juick.service.MessagesService; +import com.juick.service.MessengerService; +import com.juick.service.SubscriptionService; +import com.juick.service.UserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Instant; +import java.util.Collections; +import java.util.Optional; + +import static com.juick.formatters.PlainTextFormatter.formatPost; +import static com.juick.formatters.PlainTextFormatter.formatUrl; + +@Component +public class MessengerManager implements ApplicationListener { + private static final Logger logger = LoggerFactory.getLogger(MessengerManager.class); + @Inject + private MessagesService messagesService; + @Inject + private SubscriptionService subscriptionService; + @Inject + private UserService userService; + @Inject + private MessengerService messengerService; + @Inject + private ServerManager serverManager; + + @Value("${fb_page_access_token:12345678}") + private String facebookPageAccessToken; + @Value("${fb_verify_token:12345678}") + private String facebookVerifyToken; + @Value("${fb_secret:12345678}") + private String facebookSecret; + + private Messenger messenger; + + @PostConstruct + public void init() { + messenger = Messenger.create(facebookPageAccessToken, facebookSecret, facebookVerifyToken); + } + + public String getFacebookVerifyToken() { + return facebookVerifyToken; + } + + public void processUpdate(String signature , String data) throws MessengerVerificationException { + messenger.onReceiveEvents(data, Optional.of(signature), event -> { + final String senderId = event.senderId(); + final Instant timestamp = event.timestamp(); + + User user_from = userService.getUserByUID(messengerService.getUserId(senderId)).orElse(new User()); + logger.info("Found juick user {}", user_from.getUid()); + if (user_from.getUid() == 0) { + try { + UserProfile profile = messenger.queryUserProfile(senderId); + signupNotify(senderId, messengerService.getSignUpHash(senderId, profile.firstName())); + } catch (MessengerApiException | MessengerIOException | MalformedURLException e) { + logger.warn("messenger profile error", e); + try { + signupNotify(senderId, messengerService.getSignUpHash(senderId, "anonymous")); + } catch (MalformedURLException | MessengerApiException | MessengerIOException e1) { + logger.warn("signup error", e1); + } + } + } else { + if (event.isTextMessageEvent()) { + final TextMessageEvent textMessageEvent = event.asTextMessageEvent(); + final String messageId = textMessageEvent.messageId(); + final String text = textMessageEvent.text(); + logger.info("Received text message from '{}' at '{}' with content: {} (mid: {})", + senderId, timestamp, text, messageId); + serverManager.processMessage(user_from, text, null); + messengerNotify(senderId, "Message sent", null); + } + } + }); + } + + @Override + public void onApplicationEvent(@Nonnull MessageEvent event) { + Message msg = event.getMessage(); + if (msg.getMid() > 0 && msg.getRid() == 0) { + String subject = formatPost(msg); + subscriptionService.getSubscribedUsers(msg.getUser().getUid(), msg.getMid()) + .forEach(user -> messengerService.getSenderId(user) + .ifPresent(t -> messengerNotify(t, subject, formatUrl(msg)))); + } else if (msg.getRid() > 0) { + // get quote + com.juick.Message jmsg = messagesService.getReply(msg.getMid(), msg.getRid()); + String subject = formatPost(jmsg); + subscriptionService.getUsersSubscribedToComments(msg.getMid(), msg.getUser().getUid()) + .forEach(user -> messengerService.getSenderId(user) + .ifPresent(t -> messengerNotify(t, subject, formatUrl(jmsg)))); + } + } + + private void messengerNotify(String messengerUser, String text, String url) { + try { + if (!StringUtils.isEmpty(url)) { + final UrlButton showMessage = UrlButton.create("VIEW MESSAGE", new URL(url)); + ButtonTemplate template = ButtonTemplate.create(text, Collections.singletonList(showMessage)); + messenger.send(MessagePayload.create(messengerUser, TemplateMessage.create(template))); + } else { + messenger.send(MessagePayload.create(messengerUser, TextMessage.create(text))); + } + } catch (MessengerApiException | MessengerIOException | MalformedURLException e) { + logger.warn("messenger error", e); + } + } + private void signupNotify(String messengerUser, String hash) throws MalformedURLException, MessengerApiException, MessengerIOException { + final UrlButton urlButton = UrlButton.create("LOGIN", + new URL("https://juick.com/signup?type=messenger&hash=" + hash)); + ButtonTemplate template = ButtonTemplate.create("Login to receive notifications", + Collections.singletonList(urlButton)); + messenger.send(MessagePayload.create(messengerUser, TemplateMessage.create(template))); + } +} diff --git a/juick-server/src/main/java/com/juick/server/ServerManager.java b/juick-server/src/main/java/com/juick/server/ServerManager.java new file mode 100644 index 00000000..79ccb1e4 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/ServerManager.java @@ -0,0 +1,252 @@ +/* + * 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.server; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.User; +import com.juick.service.MessagesService; +import com.juick.service.SubscriptionService; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.TextMessage; +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.XmppException; +import rocks.xmpp.core.session.Extension; +import rocks.xmpp.core.session.XmppSessionConfiguration; +import rocks.xmpp.core.stanza.model.Message; +import rocks.xmpp.extensions.component.accept.ExternalComponent; +import rocks.xmpp.extensions.oob.model.x.OobX; +import rocks.xmpp.util.XmppUtils; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.xml.bind.JAXBException; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Ugnich Anton + */ +@Component +public class ServerManager implements AutoCloseable { + private static Logger logger = LoggerFactory.getLogger(ServerManager.class); + + private ExternalComponent xmpp; + + @Value("${xmpp_host:localhost}") + private String xmppHost; + @Value("${xmpp_password:secret}") + private String xmppPassword; + @Value("${ws_jid:ws.localhost}") + private String xmppJid; + @Value("${xmpp_port:5347}") + private int xmppPort; + @Value("${xmpp_disabled:false}") + private boolean isXmppDisabled; + @Inject + private ObjectMapper jsonMapper; + @Inject + private MessagesService messagesService; + @Inject + private SubscriptionService subscriptionService; + @Inject + private WebsocketManager wsHandler; + @Value("${service_user:juick}") + private String serviceUser; + + @PostConstruct + public void init() { + if (!isXmppDisabled) { + setupXmppComponent(xmppHost, xmppPort, xmppJid, xmppPassword); + } else { + logger.warn("XMPP is not enabled"); + } + } + + @Override + public void close() { + try { + if (xmpp != null) + xmpp.close(); + + logger.info("ExternalComponent on juick-server destroyed"); + } catch (Exception e) { + logger.warn("Exception occurs on juick-server destroy", e); + } + } + + public void setupXmppComponent(final String host, final int port, final String jid, final String password) { + XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() + .extensions(Extension.of(com.juick.Message.class)) + .build(); + xmpp = ExternalComponent.create(jid, password, configuration, host, port); + xmpp.addInboundMessageListener(e -> { + try { + Message msg = e.getMessage(); + com.juick.Message jmsg = msg.getExtension(com.juick.Message.class); + if (jmsg != null) { + if (logger.isInfoEnabled()) { // prevent writeValueAsString execution if log is disabled + try { + StringWriter stanzaWriter = new StringWriter(); + XMLStreamWriter xmppStreamWriter = XmppUtils.createXmppStreamWriter( + xmpp.getConfiguration().getXmlOutputFactory().createXMLStreamWriter(stanzaWriter)); + xmpp.createMarshaller().marshal(msg, xmppStreamWriter); + xmppStreamWriter.flush(); + xmppStreamWriter.close(); + logger.info("got msg: {}", stanzaWriter.toString()); + } catch (XMLStreamException e1) { + logger.info("jaxb exception", e1); + } + + } + if (jmsg.getMid() == 0) { + int uid_to = NumberUtils.toInt(msg.getTo().getLocal(), 0); + if (uid_to > 0) { + onJuickPM(uid_to, jmsg); + } + } else if (jmsg.getRid() == 0) { + // to get full message with attachment, etc. + onJuickMessagePost(messagesService.getMessage(jmsg.getMid())); + } else { + // to get quote and attachment + com.juick.Message reply = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); + onJuickMessageReply(reply); + } + } + } catch (JsonProcessingException ex) { + logger.error("mapper exception", ex); + } catch (JAXBException exc) { + logger.error("jaxb exception", exc); + } + }); + try { + xmpp.connect(); + } catch (XmppException e) { + logger.warn("xmpp extension", e); + } + } + + public void sendMessage(Message message) { + if (!isXmppDisabled) { + xmpp.sendMessage(message); + } + } + + public void processMessage(User visitor, String body, String attachmentName) { + Message xmsg = new Message(); + xmsg.setFrom(Jid.of(String.valueOf(visitor.getUid()), "uid.juick.com", "perl")); + xmsg.setTo(Jid.of("juick@juick.com/Juick")); + xmsg.setBody(body); + try { + if (StringUtils.isNotEmpty(attachmentName)) { + String attachmentUrl = String.format("juick://%s", attachmentName); + xmsg.addExtension(new OobX(new URI(attachmentUrl), "!!!!Juick!!")); + } + sendMessage(xmsg); + } catch (URISyntaxException e1) { + logger.warn("attachment error", e1); + } + } + + private void onJuickPM(final int uid_to, final com.juick.Message jmsg) throws JsonProcessingException { + String json = jsonMapper.writeValueAsString(jmsg); + synchronized (wsHandler.getClients()) { + wsHandler.getClients().stream().filter(c -> + (!c.legacy && c.visitor.getUid() == uid_to) || c.visitor.getName().equals(serviceUser)) + .forEach(c -> { + try { + logger.info("sending pm to {}", c.visitor.getUid()); + c.session.sendMessage(new TextMessage(json)); + } catch (IOException e) { + logger.warn("ws error", e); + } + }); + } + } + + private void onJuickMessagePost(final com.juick.Message jmsg) throws JsonProcessingException { + String json = jsonMapper.writeValueAsString(jmsg); + List uids = subscriptionService.getSubscribedUsers(jmsg.getUser().getUid(), jmsg.getMid()) + .stream().map(User::getUid).collect(Collectors.toList()); + synchronized (wsHandler.getClients()) { + wsHandler.getClients().stream().filter(c -> + (!c.legacy && c.visitor.getUid() == 0) // anonymous users + || c.visitor.getName().equals(serviceUser) // services + || (!c.legacy && uids.contains(c.visitor.getUid()))) // subscriptions + .forEach(c -> { + try { + logger.info("sending message to {}", c.visitor.getUid()); + c.session.sendMessage(new TextMessage(json)); + } catch (IOException e) { + logger.warn("ws error", e); + } + }); + wsHandler.getClients().stream().filter(c -> + c.legacy && c.allMessages) // legacy all posts + .forEach(c -> { + try { + logger.info("sending message to legacy client {}", c.visitor.getUid()); + c.session.sendMessage(new TextMessage(json)); + } catch (IOException e) { + logger.warn("ws error", e); + } + }); + } + } + + private void onJuickMessageReply(final com.juick.Message jmsg) throws JsonProcessingException { + String json = jsonMapper.writeValueAsString(jmsg); + List threadUsers = + subscriptionService.getUsersSubscribedToComments(jmsg.getMid(), jmsg.getUser().getUid()) + .stream().map(User::getUid).collect(Collectors.toList()); + synchronized (wsHandler.getClients()) { + wsHandler.getClients().stream().filter(c -> + (!c.legacy && c.visitor.getUid() == 0) // anonymous users + || c.visitor.getName().equals(serviceUser) // services + || (!c.legacy && threadUsers.contains(c.visitor.getUid()))) // subscriptions + .forEach(c -> { + try { + logger.info("sending reply to {}", c.visitor.getUid()); + c.session.sendMessage(new TextMessage(json)); + } catch (IOException e) { + logger.warn("ws error", e); + } + }); + wsHandler.getClients().stream().filter(c -> + (c.legacy && c.allReplies) || (c.legacy && c.MID == jmsg.getMid())) // legacy replies + .forEach(c -> { + try { + logger.info("sending reply to legacy client {}", c.visitor.getUid()); + c.session.sendMessage(new TextMessage(json)); + } catch (IOException e) { + logger.warn("ws error", e); + } + }); + } + } +} diff --git a/juick-server/src/main/java/com/juick/server/TelegramBotManager.java b/juick-server/src/main/java/com/juick/server/TelegramBotManager.java new file mode 100644 index 00000000..564a2255 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/TelegramBotManager.java @@ -0,0 +1,298 @@ +/* + * 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.server; + +import com.juick.User; +import com.juick.server.component.MessageEvent; +import com.juick.server.util.HttpUtils; +import com.juick.service.MessagesService; +import com.juick.service.SubscriptionService; +import com.juick.service.TelegramService; +import com.juick.service.UserService; +import com.pengrad.telegrambot.BotUtils; +import com.pengrad.telegrambot.Callback; +import com.pengrad.telegrambot.TelegramBot; +import com.pengrad.telegrambot.model.Message; +import com.pengrad.telegrambot.model.MessageEntity; +import com.pengrad.telegrambot.model.PhotoSize; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.ParseMode; +import com.pengrad.telegrambot.request.GetFile; +import com.pengrad.telegrambot.request.SendMessage; +import com.pengrad.telegrambot.request.SetWebhook; +import com.pengrad.telegrambot.response.GetFileResponse; +import com.pengrad.telegrambot.response.SendResponse; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import static com.juick.formatters.PlainTextFormatter.formatPost; +import static com.juick.formatters.PlainTextFormatter.formatUrl; + +/** + * Created by vt on 12/05/16. + */ +@Component +public class TelegramBotManager implements ApplicationListener { + private static final Logger logger = LoggerFactory.getLogger(TelegramBotManager.class); + + private TelegramBot bot; + + @Value("${telegram_token}") + private String telegramToken; + @Inject + private TelegramService telegramService; + @Inject + private MessagesService messagesService; + @Inject + private SubscriptionService subscriptionService; + @Inject + private UserService userService; + @Inject + private ServerManager serverManager; + @Value("${upload_tmp_dir:/var/www/juick.com/i/tmp/}") + private String tmpDir; + + private static final String MSG_LINK = "🔗"; + + @PostConstruct + public void init() { + if (StringUtils.isBlank(telegramToken)) { + logger.info("telegram token is not set, exiting"); + return; + } + bot = new TelegramBot(telegramToken); + try { + SetWebhook webhook = new SetWebhook().url("https://api.juick.com/tlgmbtwbhk"); + if (!bot.execute(webhook).isOk()) { + logger.error("error setting webhook"); + } + } catch (Exception e) { + logger.warn("couldn't initialize telegram bot", e); + } + } + + public void processUpdate(String data) throws Exception { + Update update = BotUtils.parseUpdate(data); + Message message = update.message(); + if (update.message() == null) { + message = update.editedMessage(); + if (message == null) { + logger.error("error parsing telegram update: {}", update); + return; + } + } + User user_from = userService.getUserByUID(telegramService.getUser(message.chat().id())).orElse(new User()); + logger.info("Found juick user {}", user_from.getUid()); + + List chats = telegramService.getChats(); + String username = message.from().username(); + if (username == null) { + username = message.from().firstName(); + } + if (!chats.contains(message.chat().id())) { + telegramService.addChat(message.chat().id()); + logger.info("added chat with {}", username); + telegramService.createTelegramUser(message.from().id(), username); + telegramSignupNotify(message.from().id().longValue(), userService.getSignUpHashByTelegramID(message.from().id().longValue(), username)); + } else { + if (user_from.getUid() == 0) { + telegramSignupNotify(message.from().id().longValue(), userService.getSignUpHashByTelegramID(message.from().id().longValue(), username)); + } else { + String attachment = StringUtils.EMPTY; + if (message.photo() != null) { + String fileId = Arrays.stream(message.photo()).max(Comparator.comparingInt(PhotoSize::fileSize)) + .orElse(new PhotoSize()).fileId(); + if (StringUtils.isNotEmpty(fileId)) { + GetFile request = new GetFile(fileId); + GetFileResponse response = bot.execute(request); + logger.info("got file {}", response.file()); + URL fileURL = new URL(bot.getFullFilePath(response.file())); + attachment = HttpUtils.downloadImage(fileURL, tmpDir); + logger.info("received {}", attachment); + } + } + String text = message.text(); + if (StringUtils.isBlank(text)) { + text = message.caption(); + } + if (StringUtils.isBlank(text)) { + text = StringUtils.EMPTY; + } + if (StringUtils.isNotEmpty(text) || StringUtils.isNotEmpty(attachment)) { + if (text.equalsIgnoreCase("LOGIN") + || text.equalsIgnoreCase("PING") + || text.equalsIgnoreCase("HELP") + || text.equalsIgnoreCase("/login") + || text.equalsIgnoreCase("/start") + || text.equalsIgnoreCase("/help")) { + String msgUrl = "http://juick.com/login?hash=" + userService.getHashByUID(user_from.getUid()); + String msg = String.format("Hi, %s!\nYou can post messages and images to Juick there.\n" + + "Tap to [log into website](%s) to get more info", user_from.getName(), msgUrl); + telegramNotify(message.from().id().longValue(), msg); + } else { + Message replyMessage = message.replyToMessage(); + if (replyMessage != null) { + if (replyMessage.entities() != null) { + Optional juickLink = Arrays.stream(replyMessage.entities()) + .filter(this::isJuickLink) + .findFirst(); + if (juickLink.isPresent()) { + if (StringUtils.isNotEmpty(juickLink.get().url())) { + UriComponents uriComponents = UriComponentsBuilder.fromUriString( + juickLink.get().url()).build(); + String path = uriComponents.getPath(); + if (StringUtils.isNotEmpty(path) && path.length() > 1) { + int mid = Integer.valueOf(path.substring(1)); + String prefix = String.format("#%d ", mid); + if (StringUtils.isNotEmpty(uriComponents.getFragment())) { + int rid = Integer.valueOf(uriComponents.getFragment()); + prefix = String.format("#%d/%d ", mid, rid); + } + serverManager.processMessage(user_from, prefix + text, attachment); + telegramNotify(message.from().id().longValue(), "Reply sent"); + } else { + logger.warn("invalid path: {}", path); + } + } else { + logger.warn("invalid entity: {}", juickLink); + } + } else { + telegramNotify(message.from().id().longValue(), + "Can not reply to this message", replyMessage.messageId()); + } + } else { + telegramNotify(message.from().id().longValue(), + "Can not reply to this message", replyMessage.messageId()); + } + } else { + serverManager.processMessage(user_from, text, attachment); + telegramNotify(message.from().id().longValue(), "Message sent"); + } + } + } + } + } + } + + private boolean isJuickLink(MessageEntity e) { + return e.offset() == 0 && e.type().equals(MessageEntity.Type.text_link) && e.length() == 2; + } + + @Override + public void onApplicationEvent(@Nonnull MessageEvent event) { + com.juick.Message jmsg = event.getMessage(); + String msgUrl = formatUrl(jmsg); + if (jmsg.getMid() > 0 && jmsg.getRid() == 0) { + String msg = String.format("[%s](%s) %s", MSG_LINK, msgUrl, formatPost(jmsg, true)); + + List users = telegramService.getTelegramIdentifiers(subscriptionService.getSubscribedUsers(jmsg.getUser().getUid(), jmsg.getMid())); + List chats = telegramService.getChats(); + // registered subscribed users + + users.forEach(c -> telegramNotify(c, msg)); + // anonymous + chats.stream().filter(u -> telegramService.getUser(u) == 0).forEach(c -> telegramNotify(c, msg)); + } else if (jmsg.getRid() > 0) { + // get quote + com.juick.Message msg = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); + String fmsg = String.format("[%s](%s) %s", MSG_LINK, msgUrl, formatPost(msg, true)); + telegramService.getTelegramIdentifiers( + subscriptionService.getUsersSubscribedToComments(jmsg.getMid(), jmsg.getUser().getUid()) + ).forEach(c -> telegramNotify(c, fmsg)); + } + } + + public void telegramNotify(Long chatId, String msg) { + telegramNotify(chatId, msg, 0); + } + + public void telegramNotify(Long chatId, String msg, Integer replyTo) { + SendMessage telegramMessage = new SendMessage(chatId, msg); + if (replyTo > 0) { + telegramMessage.replyToMessageId(replyTo); + } + telegramMessage.parseMode(ParseMode.Markdown).disableWebPagePreview(true); + bot.execute(telegramMessage, new Callback() { + @Override + public void onResponse(SendMessage request, SendResponse response) { + if (!response.isOk()) { + if (response.errorCode() == 403) { + // remove from anonymous chat + telegramService.getChats().stream().filter(c -> c.equals(chatId)).findFirst().ifPresent( + d -> { + telegramService.deleteChat(d); + logger.info("deleted {} chat", d); + } + ); + int userId = telegramService.getUser(chatId); + if (userId > 0) { + User userToDelete = userService.getUserByUID(userId) + .orElse(new User()); + boolean status = telegramService.deleteTelegramUser(userToDelete.getUid()); + logger.info("deleting telegram id of @{} : {}", userToDelete.getName(), status); + boolean chatStatus = telegramService.deleteChat(chatId); + logger.info("deleting telegram chat {} : {}", chatId, chatStatus); + } + } else { + logger.warn("error response, isOk: {}, errorCode: {}, description: {}", + response.isOk(), response.errorCode(), response.description()); + } + } + } + + @Override + public void onFailure(SendMessage request, IOException e) { + logger.warn("telegram failure", e); + } + }); + } + + public void telegramSignupNotify(Long telegramId, String hash) { + bot.execute(new SendMessage(telegramId, + String.format("You are subscribed to all Juick messages. " + + "[Create or link](http://juick.com/signup?type=durov&hash=%s) " + + "an existing Juick account to get your subscriptions and ability to post messages", hash)) + .parseMode(ParseMode.Markdown), new Callback() { + @Override + public void onResponse(SendMessage request, SendResponse response) { + logger.info("got response: {}", response.message()); + } + + @Override + public void onFailure(SendMessage request, IOException e) { + logger.warn("telegram failure", e); + } + }); + } +} diff --git a/juick-server/src/main/java/com/juick/server/TopManager.java b/juick-server/src/main/java/com/juick/server/TopManager.java new file mode 100644 index 00000000..77518d37 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/TopManager.java @@ -0,0 +1,57 @@ +/* + * 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.server; + +import com.juick.Message; +import com.juick.service.MessagesService; +import com.juick.util.MessageUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; + +import javax.inject.Inject; + +@Component +public class TopManager { + private static Logger logger = LoggerFactory.getLogger(TopManager.class); + @Inject + private MessagesService messagesService; + @Inject + private FacebookPageManager facebookPageManager; + + @Scheduled(fixedRate = 3600000) + public void updateTop() { + messagesService.getPopularCandidates().forEach(m -> { + logger.info("added {} to popular", m); + messagesService.setMessagePopular(m, 1); + Message jmsg = messagesService.getMessage(m); + String status = MessageUtils.getMessageHashTags(jmsg) + StringUtils.defaultString(jmsg.getText()); + try { + facebookPageManager.post(status, "https://juick.com/" + jmsg.getMid()); + } catch (HttpClientErrorException ex) { + HttpStatus statusCode = ex.getStatusCode(); + String responseString = ex.getResponseBodyAsString(); + logger.warn("facebook error {}: {}", statusCode.value(), responseString); + } + }); + } +} diff --git a/juick-server/src/main/java/com/juick/server/WebsocketManager.java b/juick-server/src/main/java/com/juick/server/WebsocketManager.java new file mode 100644 index 00000000..6e3fbea2 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/WebsocketManager.java @@ -0,0 +1,163 @@ +/* + * 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.server; + +import com.juick.User; +import com.juick.server.helpers.AnonymousUser; +import com.juick.server.util.HttpBadRequestException; +import com.juick.server.util.HttpForbiddenException; +import com.juick.server.util.HttpNotFoundException; +import com.juick.service.MessagesService; +import com.juick.service.SubscriptionService; +import com.juick.service.UserService; +import org.apache.commons.lang3.math.NumberUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.PingMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.inject.Inject; +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by vitalyster on 28.06.2016. + */ +public class WebsocketManager extends TextWebSocketHandler { + private static final Logger logger = LoggerFactory.getLogger(WebsocketManager.class); + + private final List clients = Collections.synchronizedList(new LinkedList<>()); + + @Inject + private UserService userService; + @Inject + private MessagesService messagesService; + @Inject + private SubscriptionService subscriptionService; + + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + URI hLocation; + String hXRealIP; + + hLocation = session.getUri(); + HttpHeaders headers = session.getHandshakeHeaders(); + hXRealIP = headers.getOrDefault("X-Real-IP", + Collections.singletonList(session.getRemoteAddress().toString())).get(0); + + // Auth + User visitor = AnonymousUser.INSTANCE; + UriComponents uriComponents = UriComponentsBuilder.fromUri(hLocation).build(); + List hash = uriComponents.getQueryParams().get("hash"); + if (hash != null && hash.get(0).length() == 16) { + visitor = userService.getUserByHash(hash.get(0)); + } else { + logger.debug("wrong hash for {} from {}", visitor.getUid(), hXRealIP); + } + + int MID = 0; + SocketSubscribed sockSubscr = null; + if (hLocation.getPath().equals("/ws/")) { + logger.debug("user {} connected", visitor.getUid()); + sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, false); + } else if (hLocation.getPath().equals("/ws/_all")) { + logger.debug("user {} connected to legacy _all ({})", visitor.getUid(), hLocation.getPath()); + sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, true); + sockSubscr.allMessages = true; + } else if (hLocation.getPath().equals("/ws/_replies")) { + logger.debug("user {} connected to legacy _replies ({})", visitor.getUid(), hLocation.getPath()); + sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, true); + sockSubscr.allReplies = true; + } else if (hLocation.getPath().matches("^/ws/(\\d)+$")) { + MID = NumberUtils.toInt(hLocation.getPath().substring(4), 0); + if (MID > 0) { + if (messagesService.canViewThread(MID, visitor.getUid())) { + logger.debug("user {} connected to legacy thread ({}) from {}", visitor.getUid(), MID, hXRealIP); + sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, true); + sockSubscr.MID = MID; + } else { + throw new HttpForbiddenException(); + } + } + } else { + throw new HttpNotFoundException(); + } + if (sockSubscr != null) { + synchronized (clients) { + clients.add(sockSubscr); + logger.debug("{} clients connected", clients.size()); + } + } else { + throw new HttpBadRequestException(); + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + synchronized (clients) { + logger.debug("session closed with status {}: {}", status.getCode(), status.getReason()); + clients.removeIf(c -> c.session.getId().equals(session.getId())); + logger.debug("{} clients connected", clients.size()); + } + + } + + @Scheduled(fixedRate = 30000) + public void ping() { + clients.forEach(c -> { + try { + c.session.sendMessage(new PingMessage()); + } catch (IOException e) { + logger.error("WebSocket PING exception", e); + } + }); + } + public List getClients() { + return clients; + } + + class SocketSubscribed { + WebSocketSession session; + String clientName; + User visitor; + int MID; + boolean allMessages; + boolean allReplies; + long tsConnected; + long tsLastData; + boolean legacy; + + public SocketSubscribed(WebSocketSession session, String clientName, User visitor, boolean legacy) { + this.session = session; + this.clientName = clientName; + this.visitor = visitor; + tsConnected = tsLastData = System.currentTimeMillis(); + this.legacy = legacy; + } + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/Index.java b/juick-server/src/main/java/com/juick/server/api/Index.java new file mode 100644 index 00000000..dba8357d --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/Index.java @@ -0,0 +1,54 @@ +/* + * 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.server.api; + +import com.juick.Status; +import com.juick.server.WebsocketManager; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import springfox.documentation.annotations.ApiIgnore; + +import javax.inject.Inject; +import java.net.URI; + +/** + * Created by vitalyster on 25.07.2016. + */ +@ApiIgnore +@RestController +public class Index { + @Inject + private WebsocketManager wsHandler; + + @RequestMapping(value = "/", method = RequestMethod.GET, headers = "Connection!=Upgrade") + public ResponseEntity description() { + URI redirectUri = ServletUriComponentsBuilder.fromCurrentRequestUri().path("/swagger-ui.html").build().toUri(); + return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).location(redirectUri).build(); + } + + @RequestMapping(value = "/api/status", method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE, headers = "Connection!=Upgrade") + public Status status() { + return Status.getStatus(String.valueOf(wsHandler.getClients().size())); + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/Messages.java b/juick-server/src/main/java/com/juick/server/api/Messages.java new file mode 100644 index 00000000..21156d79 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/Messages.java @@ -0,0 +1,180 @@ +/* + * 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.server.api; + +import com.juick.Status; +import com.juick.Tag; +import com.juick.User; +import com.juick.server.util.HttpForbiddenException; +import com.juick.server.util.UserUtils; +import com.juick.service.MessagesService; +import com.juick.service.TagService; +import com.juick.service.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; +import springfox.documentation.annotations.ApiIgnore; + +import javax.inject.Inject; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Collections; +import java.util.List; + +/** + * @author ugnich + */ +@RestController +@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) +public class Messages { + + private static final ResponseEntity> NOT_FOUND = ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(Collections.emptyList()); + + private static final ResponseEntity> FORBIDDEN = ResponseEntity + .status(HttpStatus.FORBIDDEN) + .body(Collections.emptyList()); + + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private TagService tagService; + + // TODO: serialize image urls + + @RequestMapping("/home") + public ResponseEntity> getHome( + @RequestParam(defaultValue = "0") int before_mid) { + User visitor = UserUtils.getCurrentUser(); + if (!visitor.isAnonymous()) { + int vuid = visitor.getUid(); + List mids = messagesService.getMyFeed(vuid, before_mid, true); + + if (!mids.isEmpty()) + return ResponseEntity.ok(messagesService.getMessages(mids)); + + return NOT_FOUND; + } + return FORBIDDEN; + } + + @RequestMapping("/messages") + public ResponseEntity> getMessages( + @RequestParam(required = false) String uname, + @RequestParam(name = "before_mid", defaultValue = "0") Integer before, + @RequestParam(required = false, defaultValue = "0") Integer daysback, + @RequestParam(required = false) String withrecommended, + @RequestParam(required = false) String popular, + @RequestParam(required = false) String media, + @RequestParam(required = false) String tag) { + User visitor = UserUtils.getCurrentUser(); + int vuid = visitor.getUid(); + + List mids; + if (!StringUtils.isEmpty(uname)) { + User user = userService.getUserByName(uname); + if (user != null) { + if (!StringUtils.isEmpty(media)) { + mids = messagesService.getUserPhotos(user.getUid(), 0, before); + } else if (!StringUtils.isEmpty(tag)) { + Tag tagObject = tagService.getTag(tag, false); + if (tagObject != null) { + mids = messagesService.getUserTag(user.getUid(), tagObject.TID, 0, before); + } else { + return NOT_FOUND; + } + } else if (!StringUtils.isEmpty(withrecommended)) { + mids = messagesService.getUserBlogWithRecommendations(user.getUid(), 0, before); + } else if (daysback > 0) { + mids = messagesService.getUserBlogAtDay(user.getUid(), 0, daysback); + } else { + mids = messagesService.getUserBlog(user.getUid(), 0, before); + } + } else { + return NOT_FOUND; + } + } else { + if (!StringUtils.isEmpty(popular)) { + mids = messagesService.getPopular(vuid, before); + } else if (!StringUtils.isEmpty(media)) { + mids = messagesService.getPhotos(vuid, before); + } else if (!StringUtils.isEmpty(tag)) { + Tag tagObject = tagService.getTag(tag, false); + if (tagObject != null) { + mids = messagesService.getTag(tagObject.TID, vuid, before, 20); + } else { + return NOT_FOUND; + } + } else { + mids = messagesService.getAll(vuid, before); + } + } + return ResponseEntity.ok(messagesService.getMessages(mids)); + } + + @GetMapping("/messages/notifications") + public ResponseEntity> getNotifications( + @RequestParam(required = false) Long before + ) { + User visitor = UserUtils.getCurrentUser(); + LocalDateTime beforeTime = before != null ? + LocalDateTime.ofInstant(Instant.ofEpochMilli(before), ZoneId.systemDefault()) + : null; + return ResponseEntity.ok(messagesService.getNotifications(visitor, beforeTime)); + } + @RequestMapping("/thread") + public ResponseEntity> getThread( + @RequestParam(defaultValue = "0") int mid) { + User visitor = UserUtils.getCurrentUser(); + int vuid = visitor.getUid(); + com.juick.Message msg = messagesService.getMessage(mid); + if (msg != null) { + if (!messagesService.canViewThread(mid, vuid)) { + return FORBIDDEN; + } else { + List replies = messagesService.getReplies(mid); + replies.add(0, msg); + return ResponseEntity.ok(replies); + } + } + return NOT_FOUND; + } + + @ApiIgnore + @RequestMapping("/messages/set_privacy") + @ResponseBody + public ResponseEntity doSetPrivacy( + @RequestParam(defaultValue = "0") int mid) { + User visitor = UserUtils.getCurrentUser(); + int vuid = visitor.getUid(); + if (vuid == 0) { + throw new HttpForbiddenException(); + } + com.juick.User user = messagesService.getMessageAuthor(mid); + if (user != null && user.getUid() == vuid && messagesService.setMessagePrivacy(mid)) { + return ResponseEntity.ok(Status.OK); + } + throw new HttpForbiddenException(); + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/Notifications.java b/juick-server/src/main/java/com/juick/server/api/Notifications.java new file mode 100644 index 00000000..5f849080 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/Notifications.java @@ -0,0 +1,190 @@ +/* + * 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.server.api; + +import com.juick.Message; +import com.juick.Status; +import com.juick.ExternalToken; +import com.juick.User; +import com.juick.server.helpers.AnonymousUser; +import com.juick.server.util.HttpBadRequestException; +import com.juick.server.util.HttpForbiddenException; +import com.juick.service.MessagesService; +import com.juick.service.PushQueriesService; +import com.juick.service.SubscriptionService; +import com.juick.server.util.UserUtils; +import com.juick.service.UserService; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import springfox.documentation.annotations.ApiIgnore; + +import javax.inject.Inject; +import java.io.IOException; +import java.security.Principal; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Created by vitalyster on 24.10.2016. + */ +@RestController +public class Notifications { + + @Inject + private PushQueriesService pushQueriesService; + @Inject + private MessagesService messagesService; + @Inject + private SubscriptionService subscriptionService; + @Inject + private UserService userService; + + + private User collectTokens(Integer uid) { + User user = userService.getUserByUID(uid).orElse(AnonymousUser.INSTANCE); + pushQueriesService.getGCMRegID(uid).forEach(t -> user.getTokens().add(new ExternalToken(null, "gcm", t, null))); + pushQueriesService.getAPNSToken(uid).forEach(t -> user.getTokens().add(new ExternalToken(null, "apns", t, null))); + pushQueriesService.getMPNSURL(uid).forEach(t -> user.getTokens().add(new ExternalToken(null, "mpns", t, null))); + return user; + } + + @ApiIgnore + @RequestMapping(value = "/notifications", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity> doGet( + @RequestParam(required = false, defaultValue = "0") int uid, + @RequestParam(required = false, defaultValue = "0") int mid, + @RequestParam(required = false, defaultValue = "0") int rid) { + User visitor = UserUtils.getCurrentUser(); + if ((visitor.getUid() == 0) || !(visitor.getName().equals("juick"))) { + throw new HttpForbiddenException(); + } + if (uid > 0 && mid == 0) { + // PM + return ResponseEntity.ok(Collections.singletonList(collectTokens(uid))); + } else { + if (mid > 0) { + Message msg = messagesService.getMessage(mid); + if (msg != null) { + List users; + if (rid > 0) { + Message reply = messagesService.getReply(mid, rid); + users = subscriptionService.getUsersSubscribedToComments(mid, reply.getUser().getUid()); + } else { + users = subscriptionService.getSubscribedUsers(msg.getUser().getUid(), mid); + } + + return ResponseEntity.ok(users.stream().map(User::getUid) + .map(this::collectTokens).collect(Collectors.toList())); + } + } + } + throw new HttpBadRequestException(); + } + + @ApiIgnore + @RequestMapping(value = "/notifications", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Status doDelete( + @RequestBody List list) throws IOException { + User visitor = UserUtils.getCurrentUser(); + // FIXME: it is possible to delete other user's tokens + if ((visitor.getUid() == 0) || !(visitor.getName().equals("juick"))) { + throw new HttpForbiddenException(); + } + list.forEach(t -> { + switch (t.getType()) { + case "gcm": + pushQueriesService.deleteGCMToken(t.getToken()); + break; + case "apns": + pushQueriesService.deleteAPNSToken(t.getToken()); + break; + case "mpns": + pushQueriesService.deleteMPNSToken(t.getToken()); + break; + default: + throw new HttpBadRequestException(); + } + }); + + return Status.OK; + } + + @ApiIgnore + @RequestMapping(value = "/notifications", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Status doPut( + @RequestBody List list) throws IOException { + User visitor = UserUtils.getCurrentUser(); + if (visitor.getUid() == 0) { + throw new HttpForbiddenException(); + } + list.forEach(t -> { + switch (t.getType()) { + case "gcm": + pushQueriesService.addGCMToken(visitor.getUid(), t.getToken()); + break; + case "apns": + pushQueriesService.addAPNSToken(visitor.getUid(), t.getToken()); + break; + case "mpns": + pushQueriesService.addMPNSToken(visitor.getUid(), t.getToken()); + break; + default: + throw new HttpBadRequestException(); + } + }); + return Status.OK; + } + + @Deprecated + @RequestMapping(value = "/android/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Status doAndroidRegister( + @RequestParam(name = "regid") String regId) { + User visitor = UserUtils.getCurrentUser(); + if (visitor.getUid() == 0) { + throw new HttpForbiddenException(); + } + pushQueriesService.addGCMToken(visitor.getUid(), regId); + return Status.OK; + } + + @Deprecated + @RequestMapping(value = "/android/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Status doAndroidUnRegister(@RequestParam(name = "regid") String regId) { + pushQueriesService.deleteGCMToken(regId); + return Status.OK; + } + + @Deprecated + @RequestMapping(value = "/winphone/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Status doWinphoneRegister( + Principal principal, + @RequestParam(name = "url") String regId) { + User visitor = UserUtils.getCurrentUser(); + pushQueriesService.addMPNSToken(visitor.getUid(), regId); + return Status.OK; + } + + @Deprecated + @RequestMapping(value = "/winphone/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Status doWinphoneUnRegister(@RequestParam(name = "url") String regId) { + pushQueriesService.deleteMPNSToken(regId); + return Status.OK; + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/PM.java b/juick-server/src/main/java/com/juick/server/api/PM.java new file mode 100644 index 00000000..4c3128ed --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/PM.java @@ -0,0 +1,148 @@ +/* + * 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.server.api; + +import com.juick.User; +import com.juick.server.ServerManager; +import com.juick.server.helpers.AnonymousUser; +import com.juick.server.helpers.PrivateChats; +import com.juick.server.util.*; +import com.juick.service.PMQueriesService; +import com.juick.service.UserService; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.stanza.model.Message; + +import javax.inject.Inject; +import java.util.List; + +/** + * @author ugnich + */ +@RestController +public class PM { + @Inject + private UserService userService; + @Inject + private PMQueriesService pmQueriesService; + @Inject + private ServerManager serverManager; + + @RequestMapping(value = "/pm", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public List doGetPM( + @RequestParam(required = false) String uname) { + User visitor = UserUtils.getCurrentUser(); + int vuid = visitor.getUid(); + if (vuid == 0) { + throw new HttpForbiddenException(); + } + int uid = 0; + if (uname != null && uname.matches("^[a-zA-Z0-9\\-]{2,16}$")) { + uid = userService.getUIDbyName(uname); + } + + if (uid == 0) { + throw new HttpBadRequestException(); + } + + return pmQueriesService.getPMMessages(vuid, uid); + } + + @RequestMapping(value = "/pm", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public com.juick.Message doPostPM( + @RequestParam String uname, + @RequestParam String body) { + User visitor = UserUtils.getCurrentUser(); + int vuid = visitor.getUid(); + if (vuid == 0) { + throw new HttpForbiddenException(); + } + User userTo = AnonymousUser.INSTANCE; + if (WebUtils.isUserName(uname)) { + userTo = userService.getUserByName(uname); + } + + if (userTo.getUid() == 0 || body == null || body.length() < 1 || body.length() > 10240) { + throw new HttpBadRequestException(); + } + + if (userService.isInBLAny(userTo.getUid(), vuid)) { + throw new HttpForbiddenException(); + } + + if (pmQueriesService.createPM(vuid, userTo.getUid(), body)) { + Message msg = new Message(); + msg.setFrom(Jid.of("juick@juick.com")); + msg.setTo(Jid.of(String.format("%d@push.juick.com", userTo.getUid()))); + com.juick.Message jmsg = new com.juick.Message(); + jmsg.setUser(visitor); + jmsg.setText(body); + jmsg.setTo(userTo); + msg.addExtension(jmsg); + serverManager.sendMessage(msg); + + msg.setTo(Jid.of(String.format("%d@ws.juick.com", userTo.getUid()))); + serverManager.sendMessage(msg); + + List jids = userService.getJIDsbyUID(userTo.getUid()); + for (String jid : jids) { + Message mm = new Message(); + mm.setTo(Jid.of(jid)); + mm.setType(Message.Type.CHAT); + if (pmQueriesService.havePMinRoster(vuid, jid)) { + mm.setFrom(Jid.of(jmsg.getUser().getName(), "juick.com", "Juick")); + mm.setBody(body); + } else { + mm.setFrom(Jid.of("juick", "juick.com", "Juick")); + mm.setBody("Private message from @" + jmsg.getUser().getName() + ":\n" + body); + } + serverManager.sendMessage(mm); + } + return jmsg; + + } + throw new HttpBadRequestException(); + } + @RequestMapping(value = "groups_pms", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public PrivateChats doGetGroupsPMs( + @RequestParam(defaultValue = "5") int cnt) { + User visitor = UserUtils.getCurrentUser(); + int vuid = visitor.getUid(); + if (vuid == 0) { + throw new HttpForbiddenException(); + } + if (cnt < 3) { + cnt = 3; + } + if (cnt > 10) { + cnt = 10; + } + + List lastconv = pmQueriesService.getPMLastConversationsUsers(vuid, cnt); + if (lastconv != null && !lastconv.isEmpty()) { + PrivateChats pms = new PrivateChats(); + pms.setUsers(lastconv); + return pms; + } + throw new HttpNotFoundException(); + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/Post.java b/juick-server/src/main/java/com/juick/server/api/Post.java new file mode 100644 index 00000000..7f014d9e --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/Post.java @@ -0,0 +1,281 @@ +/* + * 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.server.api; + +import com.juick.User; +import com.juick.server.ServerManager; +import com.juick.server.EmailManager; +import com.juick.server.util.*; +import com.juick.service.MessagesService; +import com.juick.service.SubscriptionService; +import com.juick.service.UserService; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.mail.util.MimeMessageParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.stanza.model.Message; +import rocks.xmpp.extensions.nick.model.Nickname; +import rocks.xmpp.extensions.oob.model.x.OobX; + +import javax.inject.Inject; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Properties; +import java.util.Scanner; +import java.util.UUID; + +/** + * Created by vt on 24/11/2016. + */ +@RestController +public class Post { + private static Logger logger = LoggerFactory.getLogger(ServerManager.class); + + @Inject + private UserService userService; + @Inject + private ServerManager serverManager; + @Inject + private MessagesService messagesService; + @Inject + private SubscriptionService subscriptionService; + @Value("${upload_tmp_dir:/var/www/juick.com/i/tmp/}") + private String tmpDir; + @Value("${img_path:/var/www/juick.com/i/}") + private String imgDir; + + @RequestMapping(value = "/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @ResponseStatus(value = HttpStatus.OK) + public void doPostMessage( + @RequestParam String body, + @RequestParam(required = false) String img, + @RequestParam(required = false) MultipartFile attach) throws IOException { + User visitor = UserUtils.getCurrentUser(); + + if (visitor.isAnonymous()) + throw new HttpForbiddenException(); + + if (body == null || body.length() < 1 || body.length() > 4096) { + throw new HttpBadRequestException(); + } + body = body.replace("\r", StringUtils.EMPTY); + + String attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir); + + if (StringUtils.isBlank(attachmentFName) && img != null && img.length() > 10) { + try { + URL imgUrl = new URL(img); + attachmentFName = HttpUtils.downloadImage(imgUrl, tmpDir); + } catch (Exception e) { + logger.error("DOWNLOAD ERROR", e); + throw new HttpBadRequestException(); + } + } + serverManager.processMessage(visitor, body, attachmentFName); + } + + @RequestMapping(value = "/comment", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public com.juick.Message doPostComment( + @RequestParam(defaultValue = "0") int mid, + @RequestParam(defaultValue = "0") int rid, + @RequestParam String body, + @RequestParam(required = false) String img, + @RequestParam(required = false) MultipartFile attach) + throws IOException { + User visitor = UserUtils.getCurrentUser(); + int vuid = visitor.getUid(); + if (vuid == 0) { + throw new HttpForbiddenException(); + } + if (mid == 0) { + throw new HttpBadRequestException(); + } + com.juick.Message msg = messagesService.getMessage(mid); + if (msg == null) { + throw new HttpNotFoundException(); + } + + com.juick.Message reply = null; + if (rid > 0) { + reply = messagesService.getReply(mid, rid); + if (reply == null) { + throw new HttpNotFoundException(); + } + } + + if (body == null || body.length() < 1 || body.length() > 4096) { + throw new HttpBadRequestException(); + } + body = body.replace("\r", StringUtils.EMPTY); + + if ((msg.ReadOnly && msg.getUser().getUid() != vuid) || userService.isInBLAny(msg.getUser().getUid(), vuid) + || (reply != null && userService.isInBLAny(reply.getUser().getUid(), vuid))) { + throw new HttpForbiddenException(); + } + + String attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir); + + if (StringUtils.isBlank(attachmentFName) && img != null && img.length() > 10) { + try { + attachmentFName = HttpUtils.downloadImage(new URL(img), tmpDir); + } catch (Exception e) { + logger.error("DOWNLOAD ERROR", e); + throw new HttpBadRequestException(); + } + } + + String attachmentType = StringUtils.isNotEmpty(attachmentFName) ? attachmentFName.substring(attachmentFName.length() - 3) : null; + int ridnew = messagesService.createReply(mid, rid, vuid, body, attachmentType); + subscriptionService.subscribeMessage(mid, vuid); + + com.juick.Message jmsg = messagesService.getReply(mid, ridnew); + + Message xmsg = new Message(); + xmsg.setFrom(Jid.of("juick@juick.com")); + xmsg.setType(Message.Type.CHAT); + xmsg.setThread("juick-" + mid); + xmsg.addExtension(jmsg); + + String quote = reply != null ? StringUtils.defaultString(reply.getText()) : StringUtils.defaultString(msg.getText()); + if (quote.length() >= 50) { + quote = quote.substring(0, 47) + "..."; + } + + xmsg.addExtension(new Nickname("@" + jmsg.getUser().getName())); + + if (StringUtils.isNotEmpty(attachmentFName)) { + String fname = mid + "-" + ridnew + "." + attachmentType; + String attachmentURL = "http://i.juick.com/photos-1024/" + fname; + + ImageUtils.saveImageWithPreviews(attachmentFName, fname, tmpDir, imgDir); + + body = attachmentURL + "\n" + body; + try { + xmsg.addExtension(new OobX(new URI(attachmentURL))); + } catch (URISyntaxException e) { + logger.error("invalid uri: {}, exception {}", attachmentURL, e); + } + } + + xmsg.setBody("Reply by @" + jmsg.getUser().getName() + ":\n>" + quote + "\n" + body + "\n\n#" + + mid + "/" + ridnew + " http://juick.com/" + mid + "#" + ridnew); + + xmsg.setTo(Jid.of("juick@s2s.juick.com")); + serverManager.sendMessage(xmsg); + + xmsg.setTo(Jid.of("juick@ws.juick.com")); + serverManager.sendMessage(xmsg); + + xmsg.setTo(Jid.of("juick@push.juick.com")); + serverManager.sendMessage(xmsg); + return jmsg; + } + + Session session = Session.getDefaultInstance(new Properties()); + + @PostMapping("/mail") + @ResponseStatus(value = HttpStatus.OK) + public void processMail(InputStream data) throws Exception { + MimeMessage msg = new MimeMessage(session, data); + String from = msg.getFrom().length > 1 ? ((InternetAddress) msg.getSender()).getAddress() + : ((InternetAddress) msg.getFrom()[0]).getAddress(); + logger.info("got msg from {}", from); + + User visitor = userService.getUserByEmail(from); + if (!visitor.isAnonymous()) { + MimeMessageParser parser = new MimeMessageParser(msg); + parser.parse(); + final String[] body = {parser.getPlainContent()}; + if (body[0] == null) { + parser.getAttachmentList().stream() + .filter(a -> a.getContentType().equals("text/plain")).findFirst() + .ifPresent(a -> { + try { + body[0] = IOUtils.toString(a.getInputStream(), StandardCharsets.UTF_8); + logger.info("got text: {}", body[0]); + } catch (IOException e) { + logger.info("attachment error: {}", e); + } + }); + } + final String[] attachmentFName = new String[1]; + parser.getAttachmentList().stream().filter(a -> + a.getContentType().equals("image/jpeg") || a.getContentType().equals("image/png")) + .findFirst().ifPresent(a -> { + logger.info("got attachment: {}", a.getContentType()); + String attachmentType; + if (a.getContentType().equals("image/jpeg")) { + attachmentType = "jpg"; + } else { + attachmentType = "png"; + } + attachmentFName[0] = DigestUtils.md5Hex(UUID.randomUUID().toString()) + "." + attachmentType; + try { + logger.info("got inputstream: {}", a.getInputStream()); + FileOutputStream fos = new FileOutputStream(Paths.get(tmpDir, attachmentFName[0]).toString()); + IOUtils.copy(a.getInputStream(), fos); + fos.close(); + } catch (IOException e) { + logger.info("attachment error: {}", e); + } + }); + String[] inReplyToHeaders = msg.getHeader("In-Reply-To"); + if (inReplyToHeaders != null && inReplyToHeaders.length > 0) { + Scanner inReplyToScanner = new Scanner(inReplyToHeaders[0].trim()).useDelimiter(EmailManager.MSGID_PATTERN); + int mid = Integer.parseInt(inReplyToScanner.next()); + int rid = Integer.parseInt(inReplyToScanner.next()); + logger.info("Message is reply to #{}/{}", mid, rid); + body[0] = rid > 0 ? String.format("#%d/%d %s", mid, rid, body[0]) + : String.format("#%d %s", mid, body[0]); + } + rocks.xmpp.core.stanza.model.Message xmsg = new rocks.xmpp.core.stanza.model.Message(); + xmsg.setType(rocks.xmpp.core.stanza.model.Message.Type.CHAT); + xmsg.setFrom(Jid.of(String.valueOf(visitor.getUid()), "uid.juick.com", "mail")); + xmsg.setTo(Jid.of("juick@juick.com/Juick")); + xmsg.setBody(body[0]); + try { + if (StringUtils.isNotEmpty(attachmentFName[0])) { + String attachmentUrl = String.format("juick://%s", attachmentFName[0]); + xmsg.addExtension(new OobX(new URI(attachmentUrl), "!!!!Juick!!")); + } + serverManager.sendMessage(xmsg); + } catch (URISyntaxException e1) { + logger.warn("attachment error", e1); + } + } else { + logger.info("not registered: {}", from); + } + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/Service.java b/juick-server/src/main/java/com/juick/server/api/Service.java new file mode 100644 index 00000000..12ffee9c --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/Service.java @@ -0,0 +1,74 @@ +/* + * 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.server.api; + +import com.juick.Message; +import com.juick.User; +import com.juick.server.util.HttpBadRequestException; +import com.juick.server.util.HttpForbiddenException; +import com.juick.server.util.UserUtils; +import com.juick.service.CrosspostService; +import com.juick.service.MessagesService; +import com.juick.service.SubscriptionService; +import com.juick.service.UserService; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.annotations.ApiIgnore; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.List; + +/** + * TODO: configure spring-security to allow only admin role + */ +@ApiIgnore +@RestController +public class Service { + @Inject + private SubscriptionService subscriptionService; + @Inject + private MessagesService messagesService; + @Inject + private CrosspostService crosspostService; + @Inject + private UserService userService; + + @RequestMapping(value = "/subscriptions", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public List doGet( + @RequestParam(defaultValue = "0") int mid, + @RequestParam(defaultValue = "0") int uid) throws IOException { + User visitor = UserUtils.getCurrentUser(); + if ((visitor.getUid() == 0) && !(visitor.getName().equals("juick"))) { + throw new HttpForbiddenException(); + } + if (uid > 0) { + return subscriptionService.getSubscribedUsers(uid, mid); + } else { + // thread + Message msg = messagesService.getMessage(mid); + if (msg != null) { + return subscriptionService.getUsersSubscribedToComments(mid, msg.getUser().getUid()); + } + } + throw new HttpBadRequestException(); + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/Tags.java b/juick-server/src/main/java/com/juick/server/api/Tags.java new file mode 100644 index 00000000..8ced4ec9 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/Tags.java @@ -0,0 +1,54 @@ +/* + * 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.server.api; + +import com.juick.User; +import com.juick.server.helpers.TagStats; +import com.juick.server.util.UserUtils; +import com.juick.service.TagService; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.inject.Inject; +import java.util.List; + +/** + * Created by vitalyster on 29.11.2016. + */ +@RestController +public class Tags { + @Inject + private TagService tagService; + + @RequestMapping(value = "/tags", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public List tags( + @RequestParam(required = false, defaultValue = "0") int user_id + ) { + User visitor = UserUtils.getCurrentUser(); + if (user_id == 0) { + user_id = visitor.getUid(); + } + if (user_id > 0) { + return tagService.getUserTagStats(user_id); + } + return tagService.getTagStats(); + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/Users.java b/juick-server/src/main/java/com/juick/server/api/Users.java new file mode 100644 index 00000000..8b273354 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/Users.java @@ -0,0 +1,125 @@ +/* + * 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.server.api; + +import com.juick.User; +import com.juick.server.util.HttpForbiddenException; +import com.juick.server.util.HttpNotFoundException; +import com.juick.service.UserService; +import com.juick.server.util.UserUtils; +import com.juick.server.util.WebUtils; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author ugnich + */ +@RestController +public class Users { + @Inject + private UserService userService; + + @RequestMapping(value = "/auth", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public String getAuthToken() { + return userService.getHashByUID(UserUtils.getCurrentUser().getUid()); + } + + @RequestMapping(value = "/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public List doGetUsers( + @RequestParam(value = "uname", required = false) List unames) { + List users = new ArrayList<>(); + + if (unames != null) { + unames.removeIf(WebUtils::isNotUserName); + + if (!unames.isEmpty() && unames.size() < 20) + users.addAll(userService.getUsersByName(unames)); + } + + if (!users.isEmpty()) + return users; + if (!UserUtils.getCurrentUser().isAnonymous()) { + return Collections.singletonList(UserUtils.getCurrentUser()); + } + + throw new HttpNotFoundException(); + } + + @RequestMapping(value = "/users/read", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public List doGetUserRead( + @RequestParam String uname) { + User visitor = UserUtils.getCurrentUser(); + int vuid = visitor.getUid(); + if (vuid == 0) { + throw new HttpForbiddenException(); + } + int uid = 0; + if (uname == null) { + uid = vuid; + } else { + if (WebUtils.isUserName(uname)) { + com.juick.User u = userService.getUserByName(uname); + if (u != null && u.getUid() > 0) { + uid = u.getUid(); + } + } + } + + if (uid > 0) { + List uids = userService.getUserRead(uid); + if (uids.size() > 0) { + List users = userService.getUsersByID(uids); + if (users.size() > 0) { + return users; + } + } + } + throw new HttpNotFoundException(); + } + + @RequestMapping(value = "/users/readers", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public List doGetUserReaders( + @RequestParam String uname) { + User visitor = UserUtils.getCurrentUser(); + int vuid = visitor.getUid(); + if (vuid == 0) { + throw new HttpForbiddenException(); + } + int uid = 0; + if (uname == null) { + uid = vuid; + } else { + if (WebUtils.isUserName(uname)) { + com.juick.User u = userService.getUserByName(uname); + if (u != null && u.getUid() > 0) { + uid = u.getUid(); + } + } + } + + if (uid > 0) { + return userService.getUserReaders(uid); + } + throw new HttpNotFoundException(); + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/webhooks/MessengerWebhook.java b/juick-server/src/main/java/com/juick/server/api/webhooks/MessengerWebhook.java new file mode 100644 index 00000000..e746b67d --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/webhooks/MessengerWebhook.java @@ -0,0 +1,62 @@ +/* + * 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.server.api.webhooks; + +import com.github.messenger4j.exception.MessengerVerificationException; +import com.juick.server.MessengerManager; +import com.juick.server.util.HttpForbiddenException; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import springfox.documentation.annotations.ApiIgnore; + +import javax.inject.Inject; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** + * Created by vitalyster on 27.03.2017. + */ +@ApiIgnore +@RestController +public class MessengerWebhook { + private static Logger logger = LoggerFactory.getLogger(MessengerWebhook.class); + + @Inject + private MessengerManager messengerManager; + + @RequestMapping(value = "/fbwbhk", method = RequestMethod.GET) + public ResponseEntity verifyHook(@RequestParam(name = "hub.mode") String hubMode, + @RequestParam(name = "hub.challenge") Integer hubChallenge, + @RequestParam(name = "hub.verify_token") String verifyToken) { + if (hubMode.equals("subscribe") && verifyToken.equals(messengerManager.getFacebookVerifyToken())) { + return new ResponseEntity<>(hubChallenge, HttpStatus.OK); + } + throw new HttpForbiddenException(); + } + @RequestMapping(value = "/fbwbhk", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public void processUpdate(@RequestHeader(name = "X-Hub-Signature", required = false) String signature, InputStream body) throws IOException, MessengerVerificationException { + String data = IOUtils.toString(body, StandardCharsets.UTF_8); + messengerManager.processUpdate(signature, data); + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/webhooks/SkypeWebhook.java b/juick-server/src/main/java/com/juick/server/api/webhooks/SkypeWebhook.java new file mode 100644 index 00000000..425a9d10 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/webhooks/SkypeWebhook.java @@ -0,0 +1,47 @@ +/* + * 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.server.api.webhooks; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.annotations.ApiIgnore; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** + * Created by vitalyster on 18.07.2016. + */ +@ApiIgnore +@RestController +public class SkypeWebhook { + private static final Logger logger = LoggerFactory.getLogger(SkypeWebhook.class); + @RequestMapping(value = "/skypebotendpoint", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public void doPost(InputStream body) throws IOException { + String data = IOUtils.toString(body, StandardCharsets.UTF_8); + logger.info("got data: {}", data); + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/webhooks/TelegramWebhook.java b/juick-server/src/main/java/com/juick/server/api/webhooks/TelegramWebhook.java new file mode 100644 index 00000000..9c4f64ce --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/webhooks/TelegramWebhook.java @@ -0,0 +1,48 @@ +/* + * 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.server.api.webhooks; + +import com.juick.server.TelegramBotManager; +import org.apache.commons.io.IOUtils; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.annotations.ApiIgnore; + +import javax.inject.Inject; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** + * Created by vt on 24/11/2016. + */ +@ApiIgnore +@RestController +public class TelegramWebhook { + @Inject + private TelegramBotManager telegramBotManager; + + @RequestMapping(value = "/tlgmbtwbhk", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public void processUpdate(InputStream body) throws Exception { + String data = IOUtils.toString(body, StandardCharsets.UTF_8); + telegramBotManager.processUpdate(data); + } +} diff --git a/juick-server/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java b/juick-server/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java new file mode 100644 index 00000000..3839248d --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java @@ -0,0 +1,128 @@ +/* + * 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.server.configuration; + +import com.juick.server.ServerManager; +import com.juick.server.WebsocketManager; +import com.juick.server.component.JuickServerComponent; +import com.juick.server.component.JuickServerReconnectManager; +import com.juick.service.UserService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.*; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.socket.client.WebSocketConnectionManager; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; +import org.springframework.web.util.UriComponentsBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import java.util.Collections; + +/** + * Created by aalexeev on 11/12/16. + */ +@Configuration +@EnableAsync +@EnableWebMvc +@EnableSwagger2 +@EnableScheduling +@EnableWebSocket +@PropertySource("classpath:juick.conf") +@ComponentScan(basePackages = "com.juick.server") +public class ApiAppConfiguration extends BaseWebConfiguration implements WebSocketConfigurer { + @Inject + UserService userService; + @Value("${api_user:juick}") + private String serviceUser; + @Value("${websocket_url:ws://localhost:8080/ws/}") + private String baseUri; + @Lazy + @Bean + public JuickServerComponent juickServerComponent() { + return new JuickServerComponent(); + } + @Lazy + @Bean + public JuickServerReconnectManager juickServerReconnectManager() { + return new JuickServerReconnectManager(); + } + @Bean + public WebSocketConnectionManager connectionManager() { + String websocketURI = UriComponentsBuilder.fromUriString(baseUri) + .queryParam("hash", userService.getHashByUID(userService.getUIDbyName(serviceUser))).build().toUriString(); + return new WebSocketConnectionManager(client(), juickServerComponent(), websocketURI); + } + @Bean + public StandardWebSocketClient client() { + return new StandardWebSocketClient(); + } + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.any()).build().apiInfo(new ApiInfo("Juick API", "Juick REST API Documentation", + "2.0", "https://juick.com/help/tos", null, + "AGPLv3", "https://www.gnu.org/licenses/agpl-3.0.html", Collections.emptyList())); + } + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/swagger-ui.html") + .addResourceLocations("classpath:/META-INF/resources/"); + + registry.addResourceHandler("/webjars/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/"); + } + @Bean + public WebsocketManager wsHandler() { + return new WebsocketManager(); + } + + @Override + public void registerWebSocketHandlers(@Nonnull WebSocketHandlerRegistry registry) { + //((ServletWebSocketHandlerRegistry) registry).setOrder(Ordered.LOWEST_PRECEDENCE); + registry.addHandler(wsHandler(), "/ws/**").setAllowedOrigins("*"); + } + + @Bean + public ServletServerContainerFactoryBean createWebSocketContainer() { + ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); + container.setMaxTextMessageBufferSize(8192); + container.setMaxBinaryMessageBufferSize(8192); + return container; + } + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } + +} diff --git a/juick-server/src/main/java/com/juick/server/configuration/ApiInitializer.java b/juick-server/src/main/java/com/juick/server/configuration/ApiInitializer.java new file mode 100644 index 00000000..b25edd9a --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/configuration/ApiInitializer.java @@ -0,0 +1,65 @@ +/* + * 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.server.configuration; + +import com.juick.configuration.DataConfiguration; +import com.juick.server.configuration.JuickServerWebsocketConfiguration; +import com.juick.server.configuration.StorageConfiguration; +import org.apache.commons.codec.CharEncoding; +import org.springframework.web.filter.CharacterEncodingFilter; +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; + +import javax.annotation.Nonnull; +import javax.servlet.Filter; + +/** + * Created by vt on 09/02/16. + */ +public class ApiInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class[] getRootConfigClasses() { + return new Class[]{ + ApiSecurityConfig.class, + DataConfiguration.class, + StorageConfiguration.class + }; + } + + @Override + protected Class[] getServletConfigClasses() { + return null; + } + + @Override + @Nonnull + protected String[] getServletMappings() { + return new String[]{"/"}; + } + + @Override + protected Filter[] getServletFilters() { + return new Filter[]{new CharacterEncodingFilter(CharEncoding.UTF_8)}; + } + + @Override + @Nonnull + protected String getServletName() { + return "API dispatcher servlet"; + } +} diff --git a/juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java b/juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java new file mode 100644 index 00000000..4f7045a6 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java @@ -0,0 +1,128 @@ +/* + * 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.server.configuration; + +import com.juick.service.UserService; +import com.juick.service.security.JuickUserDetailsService; +import com.juick.service.security.NotAuthorizedAuthenticationEntryPoint; +import com.juick.service.security.deprecated.RequestParamHashRememberMeServices; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.PropertySource; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +/** + * Created by aalexeev on 11/21/16. + */ +@Configuration +@EnableWebSecurity +@PropertySource("classpath:juick.conf") +@Import(ApiAppConfiguration.class) +public class ApiSecurityConfig extends WebSecurityConfigurerAdapter { + @Value("${auth_remember_me_key}") + private String rememberMeKey; + @Inject + private UserService userService; + + ApiSecurityConfig() { + super(true); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .antMatchers(HttpMethod.OPTIONS).permitAll() + .antMatchers("/messages", "/users", "/thread", "/tags", "/tlgmbtwbhk", "/fbwbhk", + "/skypebotendpoint").permitAll() + .anyRequest().hasRole("USER") + .and().httpBasic().authenticationEntryPoint(getJuickAuthenticationEntryPoint()) + .and().anonymous() + .and().cors().configurationSource(corsConfigurationSource()) + .and().servletApi() + .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and().exceptionHandling().authenticationEntryPoint(getJuickAuthenticationEntryPoint()) + .and() + .rememberMe() + .alwaysRemember(true) + .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(6 * 30)) + .rememberMeServices(rememberMeServices()) + .key(rememberMeKey) + .and().authenticationProvider(authenticationProvider()) + .headers().defaultsDisabled().cacheControl(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); + + authenticationProvider.setUserDetailsService(userDetailsService()); + + return authenticationProvider; + } + + @Bean + public JuickUserDetailsService userDetailsService() { + return new JuickUserDetailsService(userService); + } + + @Bean + public RememberMeServices rememberMeServices() throws Exception { + return new RequestParamHashRememberMeServices(rememberMeKey, userService); + } + + @Bean + public NotAuthorizedAuthenticationEntryPoint getJuickAuthenticationEntryPoint() { + return new NotAuthorizedAuthenticationEntryPoint(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(Collections.singletonList("*")); + configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE")); + configuration.setAllowedHeaders(Collections.singletonList("*")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources/**", + "/configuration/**", "/swagger-ui.html", "/webjars/**", "/ws/**"); + } +} diff --git a/juick-server/src/main/java/com/juick/server/util/JsonpAdvice.java b/juick-server/src/main/java/com/juick/server/util/JsonpAdvice.java new file mode 100644 index 00000000..457a7df7 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/util/JsonpAdvice.java @@ -0,0 +1,31 @@ +/* + * 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.server.util; + +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice; + +/** + * Created by vitalyster on 25.11.2016. + */ +@ControllerAdvice +public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice { + public JsonpAdvice() { + super("callback"); + } +} diff --git a/juick-server/src/test/java/com/juick/api/tests/MessagesTests.java b/juick-server/src/test/java/com/juick/api/tests/MessagesTests.java deleted file mode 100644 index fff7e02c..00000000 --- a/juick-server/src/test/java/com/juick/api/tests/MessagesTests.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * 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.api.tests; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.juick.ExternalToken; -import com.juick.Message; -import com.juick.Tag; -import com.juick.User; -import com.juick.api.EmailManager; -import com.juick.api.configuration.ApiAppConfiguration; -import com.juick.api.configuration.ApiSecurityConfig; -import com.juick.configuration.RepositoryConfiguration; -import com.juick.server.helpers.TagStats; -import com.juick.service.ImagesService; -import com.juick.service.MessagesService; -import com.juick.service.TagService; -import com.juick.service.UserService; -import com.juick.util.DateFormattersHolder; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.http.MediaType; -import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - -import javax.inject.Inject; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Scanner; -import java.util.stream.IntStream; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -/** - * Created by vitalyster on 25.11.2016. - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {ApiAppConfiguration.class, ApiSecurityConfig.class, RepositoryConfiguration.class}) -@WebAppConfiguration -public class MessagesTests extends AbstractJUnit4SpringContextTests { - - private MockMvc mockMvc; - @Inject - private WebApplicationContext webApplicationContext; - - @Inject - private MessagesService messagesService; - @Inject - private UserService userService; - @Inject - private TagService tagService; - @Inject - private ObjectMapper jsonMapper; - @Inject - private ImagesService imagesService; - - private static User ugnich, freefd, juick; - static String ugnichName, ugnichPassword, freefdName, freefdPassword, juickName, juickPassword; - static Message msg; - static int juickTagId; - - private static boolean isSetUp = false; - - - @Before - public void setUp() { - mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) - .apply(SecurityMockMvcConfigurers.springSecurity()) - .dispatchOptions(true) - .build(); - if (!isSetUp) { - ugnichName = "ugnich"; - ugnichPassword = "MyPassw0rd!"; - freefdName = "freefd"; - freefdPassword = "MyPassw0rd!"; - juickName = "juick"; - juickPassword = "demo"; - int ugnichId = userService.createUser(ugnichName, ugnichPassword); - ugnich = userService.getUserByUID(ugnichId).orElseThrow(IllegalStateException::new); - int freefdId = userService.createUser(freefdName, freefdPassword); - freefd = userService.getUserByUID(freefdId).orElseThrow(IllegalStateException::new); - int juickId = userService.createUser(juickName, juickPassword); - juick = userService.getUserByUID(juickId).orElseThrow(IllegalStateException::new); - - String msgText = "Привет, я - Угнич"; - - int mid = messagesService.createMessage(ugnich.getUid(), msgText, "png", null); - msg = messagesService.getMessage(mid); - tagService.createTag("тест"); - juickTagId = tagService.createTag("juick"); - isSetUp = true; - } - } - - @Test - public void testAllUnAuthorized() throws Exception { - - mockMvc.perform(get("/")) - .andExpect(status().is4xxClientError()); - - mockMvc.perform(get("/auth")) - .andExpect(status().is4xxClientError()); - - mockMvc.perform(get("/home")) - .andExpect(status().is4xxClientError()); - - mockMvc.perform(get("/messages/recommended")) - .andExpect(status().is4xxClientError()); - - mockMvc.perform(get("/messages/set_privacy")) - .andExpect(status().is4xxClientError()); - } - - @Test - public void homeTestWithMessages() throws Exception { - mockMvc.perform( - get("/home") - .with(httpBasic(ugnichName, ugnichPassword))) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$[-1].mid", is(msg.getMid()))) - .andExpect(jsonPath("$[-1].timestamp", - is(DateFormattersHolder.getMessageFormatterInstance().format(msg.getTimestamp())))) - .andExpect(jsonPath("$[-1].body", is(msg.getText()))) - .andExpect(jsonPath("$[-1].attachment.url", is("https://i.juick.com/p/1.png"))) - .andExpect(jsonPath("$[-1].attachment.small.url", is("https://i.juick.com/photos-512/1.png"))); - } - - @Test - public void homeTestWithMessagesAndRememberMe() throws Exception { - String ugnichHash = userService.getHashByUID(ugnich.getUid()); - mockMvc.perform( - get("/home") - .with(httpBasic(ugnichName, ugnichPassword))) - .andExpect(status().isOk()) - .andReturn(); - - mockMvc.perform(get("/home") - .param("hash", ugnichHash)) - .andExpect(status().isOk()); - } - - @Test - public void homeTestWithMessagesAndSimpleCors() throws Exception { - mockMvc.perform( - get("/home") - .with(httpBasic(ugnichName, ugnichPassword)) - .header("Origin", "http://api.example.net")) - .andExpect(status().isOk()) - .andExpect(header().string("Access-Control-Allow-Origin", "*")); - } - - @Test - public void homeTestWithPreflightCors() throws Exception { - mockMvc.perform( - options("/home") - .with(httpBasic(ugnichName, ugnichPassword)) - .header("Origin", "http://api.example.net") - .header("Access-Control-Request-Method", "POST") - .header("Access-Control-Request-Headers", "X-PINGOTHER, Content-Type")) - .andExpect(status().isOk()) - .andExpect(header().string("Access-Control-Allow-Origin", "*")) - .andExpect(header().string("Access-Control-Allow-Methods", "POST,GET,PUT,OPTIONS,DELETE")) - .andExpect(header().string("Access-Control-Allow-Headers", "X-PINGOTHER, Content-Type")); - } - - @Test - public void anonymousApis() throws Exception { - - - mockMvc.perform(get("/messages")) - .andExpect(status().isOk()); - - mockMvc.perform(get("/users") - .param("uname", "ugnich") - .param("uname", "freefd")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$", hasSize(2))); - } - - @Test - public void tags() throws Exception { - Tag weather = tagService.getTag("weather", true); - Tag yo = tagService.getTag("yo", true); - messagesService.createMessage(ugnich.getUid(), "text", null, Arrays.asList(yo, weather)); - messagesService.createMessage(freefd.getUid(), "text2", null, Collections.singletonList(yo)); - MvcResult result = mockMvc.perform(get("/tags")) - .andExpect(status().isOk()) - .andReturn(); - List tagsFromApi = jsonMapper.readValue(result.getResponse().getContentAsString(), - new TypeReference>(){}); - TagStats yoStats = tagsFromApi.stream().filter(t -> t.getTag().getName().equals("yo")).findFirst().get(); - assertThat(yoStats.getUsageCount(), is(2)); - MvcResult result2 = mockMvc.perform(get("/tags") - .param("user_id", String.valueOf(ugnich.getUid()))) - .andExpect(status().isOk()) - .andReturn(); - List ugnichTagsFromApi = jsonMapper.readValue(result2.getResponse().getContentAsString(), - new TypeReference>(){}); - TagStats yoUgnichStats = ugnichTagsFromApi.stream().filter(t -> t.getTag().getName().equals("yo")).findFirst().get(); - assertThat(yoUgnichStats.getUsageCount(), is(1)); - } - - @Test - public void postWithReferer() throws Exception { - mockMvc.perform(post("/post") - .param("body", "yo") - .with(httpBasic(ugnichName, ugnichPassword))) - .andExpect(status().isOk()); - } - @Test - public void threadWithEphemeralNumberShouldReturn404() throws Exception { - mockMvc.perform(get("/thread").param("mid", "999999999") - .with(httpBasic(ugnichName, ugnichPassword))).andExpect(status().is4xxClientError()); - } - @Test - public void performRequestsWithIssuedToken() throws Exception { - String ugnichHash = userService.getHashByUID(ugnich.getUid()); - mockMvc.perform(get("/home")).andExpect(status().isUnauthorized()); - mockMvc.perform(get("/auth")) - .andExpect(status().isUnauthorized()); - mockMvc.perform(get("/auth").with(httpBasic(ugnichName, "wrongpassword"))) - .andExpect(status().isUnauthorized()); - MvcResult result = mockMvc.perform(get("/auth").with(httpBasic(ugnichName, ugnichPassword))) - .andExpect(status().isOk()) - .andReturn(); - String authHash = jsonMapper.readValue(result.getResponse().getContentAsString(), String.class); - assertThat(authHash, equalTo(ugnichHash)); - mockMvc.perform(get("/home").param("hash", ugnichHash)).andExpect(status().isOk()); - } - @Test - public void registerForNotificationsTests() throws Exception { - String token = "123456"; - ExternalToken registration = new ExternalToken(null, "apns", token, null); - mockMvc.perform(put("/notifications").with(httpBasic(ugnichName, ugnichPassword)) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(jsonMapper.writeValueAsBytes(Collections.singletonList(registration)))) - .andExpect(status().isOk()); - MvcResult result = mockMvc.perform(get("/notifications") - .param("uid", String.valueOf(ugnich.getUid())) - .with(httpBasic(juickName, juickPassword))) - .andExpect(status().isOk()) - .andReturn(); - List user = jsonMapper.readValue(result.getResponse().getContentAsString(), - new TypeReference>() { - }); - assertThat(user.get(0).getTokens().get(0).getToken(), equalTo(token)); - } - @Test - public void tg2juickLinks() { - UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://juick.com/123456#23").build(); - assertThat(uriComponents.getPath().substring(1), is("123456")); - assertThat(uriComponents.getFragment(), is("23")); - } - @Test - public void notificationsTokensTest() throws Exception { - List tokens = Collections.singletonList(new ExternalToken(null, "gcm", "123456", null)); - mockMvc.perform(delete("/notifications").with(httpBasic(ugnichName, ugnichPassword)) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(jsonMapper.writeValueAsBytes(tokens))).andExpect(status().isForbidden()); - mockMvc.perform(delete("/notifications").with(httpBasic(juickName, juickPassword)) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(jsonMapper.writeValueAsBytes(tokens))).andExpect(status().isOk()); - } - @Test - public void topTest() throws Exception { - int topmid = messagesService.createMessage(ugnich.getUid(), "top message", null, null); - IntStream.rangeClosed(6, 12).forEach(i -> { - messagesService.createReply(topmid, 0, i, "yo", null); - }); - - assertThat(messagesService.getPopularCandidates().get(0), is(topmid)); - Tag juickTag = tagService.getTag(juickTagId); - assertThat(juickTag.TID, is(2)); - tagService.updateTags(topmid, Collections.singletonList(juickTag)); - assertThat(messagesService.getPopularCandidates().isEmpty(), is(true)); - } - @Test - public void inReplyToScannerTest() { - String header = "<123456.56@juick.com>"; - Scanner headerScanner = new Scanner(header).useDelimiter(EmailManager.MSGID_PATTERN); - int mid = Integer.parseInt(headerScanner.next()); - int rid = Integer.parseInt(headerScanner.next()); - assertThat(mid, equalTo(123456)); - assertThat(rid, equalTo(56)); - } -} diff --git a/juick-server/src/test/java/com/juick/server/tests/MessagesTests.java b/juick-server/src/test/java/com/juick/server/tests/MessagesTests.java new file mode 100644 index 00000000..53267a0f --- /dev/null +++ b/juick-server/src/test/java/com/juick/server/tests/MessagesTests.java @@ -0,0 +1,319 @@ +/* + * 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.server.tests; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.ExternalToken; +import com.juick.Message; +import com.juick.Tag; +import com.juick.User; +import com.juick.server.EmailManager; +import com.juick.server.configuration.ApiAppConfiguration; +import com.juick.server.configuration.ApiSecurityConfig; +import com.juick.configuration.RepositoryConfiguration; +import com.juick.server.helpers.TagStats; +import com.juick.service.ImagesService; +import com.juick.service.MessagesService; +import com.juick.service.TagService; +import com.juick.service.UserService; +import com.juick.util.DateFormattersHolder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.MediaType; +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Scanner; +import java.util.stream.IntStream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Created by vitalyster on 25.11.2016. + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {ApiAppConfiguration.class, ApiSecurityConfig.class, RepositoryConfiguration.class}) +@WebAppConfiguration +public class MessagesTests extends AbstractJUnit4SpringContextTests { + + private MockMvc mockMvc; + @Inject + private WebApplicationContext webApplicationContext; + + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private TagService tagService; + @Inject + private ObjectMapper jsonMapper; + @Inject + private ImagesService imagesService; + + private static User ugnich, freefd, juick; + static String ugnichName, ugnichPassword, freefdName, freefdPassword, juickName, juickPassword; + static Message msg; + static int juickTagId; + + private static boolean isSetUp = false; + + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .dispatchOptions(true) + .build(); + if (!isSetUp) { + ugnichName = "ugnich"; + ugnichPassword = "MyPassw0rd!"; + freefdName = "freefd"; + freefdPassword = "MyPassw0rd!"; + juickName = "juick"; + juickPassword = "demo"; + int ugnichId = userService.createUser(ugnichName, ugnichPassword); + ugnich = userService.getUserByUID(ugnichId).orElseThrow(IllegalStateException::new); + int freefdId = userService.createUser(freefdName, freefdPassword); + freefd = userService.getUserByUID(freefdId).orElseThrow(IllegalStateException::new); + int juickId = userService.createUser(juickName, juickPassword); + juick = userService.getUserByUID(juickId).orElseThrow(IllegalStateException::new); + + String msgText = "Привет, я - Угнич"; + + int mid = messagesService.createMessage(ugnich.getUid(), msgText, "png", null); + msg = messagesService.getMessage(mid); + tagService.createTag("тест"); + juickTagId = tagService.createTag("juick"); + isSetUp = true; + } + } + + @Test + public void testAllUnAuthorized() throws Exception { + + mockMvc.perform(get("/")) + .andExpect(status().is4xxClientError()); + + mockMvc.perform(get("/auth")) + .andExpect(status().is4xxClientError()); + + mockMvc.perform(get("/home")) + .andExpect(status().is4xxClientError()); + + mockMvc.perform(get("/messages/recommended")) + .andExpect(status().is4xxClientError()); + + mockMvc.perform(get("/messages/set_privacy")) + .andExpect(status().is4xxClientError()); + } + + @Test + public void homeTestWithMessages() throws Exception { + mockMvc.perform( + get("/home") + .with(httpBasic(ugnichName, ugnichPassword))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$[-1].mid", is(msg.getMid()))) + .andExpect(jsonPath("$[-1].timestamp", + is(DateFormattersHolder.getMessageFormatterInstance().format(msg.getTimestamp())))) + .andExpect(jsonPath("$[-1].body", is(msg.getText()))) + .andExpect(jsonPath("$[-1].attachment.url", is("https://i.juick.com/p/1.png"))) + .andExpect(jsonPath("$[-1].attachment.small.url", is("https://i.juick.com/photos-512/1.png"))); + } + + @Test + public void homeTestWithMessagesAndRememberMe() throws Exception { + String ugnichHash = userService.getHashByUID(ugnich.getUid()); + mockMvc.perform( + get("/home") + .with(httpBasic(ugnichName, ugnichPassword))) + .andExpect(status().isOk()) + .andReturn(); + + mockMvc.perform(get("/home") + .param("hash", ugnichHash)) + .andExpect(status().isOk()); + } + + @Test + public void homeTestWithMessagesAndSimpleCors() throws Exception { + mockMvc.perform( + get("/home") + .with(httpBasic(ugnichName, ugnichPassword)) + .header("Origin", "http://api.example.net")) + .andExpect(status().isOk()) + .andExpect(header().string("Access-Control-Allow-Origin", "*")); + } + + @Test + public void homeTestWithPreflightCors() throws Exception { + mockMvc.perform( + options("/home") + .with(httpBasic(ugnichName, ugnichPassword)) + .header("Origin", "http://api.example.net") + .header("Access-Control-Request-Method", "POST") + .header("Access-Control-Request-Headers", "X-PINGOTHER, Content-Type")) + .andExpect(status().isOk()) + .andExpect(header().string("Access-Control-Allow-Origin", "*")) + .andExpect(header().string("Access-Control-Allow-Methods", "POST,GET,PUT,OPTIONS,DELETE")) + .andExpect(header().string("Access-Control-Allow-Headers", "X-PINGOTHER, Content-Type")); + } + + @Test + public void anonymousApis() throws Exception { + + + mockMvc.perform(get("/messages")) + .andExpect(status().isOk()); + + mockMvc.perform(get("/users") + .param("uname", "ugnich") + .param("uname", "freefd")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(2))); + } + + @Test + public void tags() throws Exception { + Tag weather = tagService.getTag("weather", true); + Tag yo = tagService.getTag("yo", true); + messagesService.createMessage(ugnich.getUid(), "text", null, Arrays.asList(yo, weather)); + messagesService.createMessage(freefd.getUid(), "text2", null, Collections.singletonList(yo)); + MvcResult result = mockMvc.perform(get("/tags")) + .andExpect(status().isOk()) + .andReturn(); + List tagsFromApi = jsonMapper.readValue(result.getResponse().getContentAsString(), + new TypeReference>(){}); + TagStats yoStats = tagsFromApi.stream().filter(t -> t.getTag().getName().equals("yo")).findFirst().get(); + assertThat(yoStats.getUsageCount(), is(2)); + MvcResult result2 = mockMvc.perform(get("/tags") + .param("user_id", String.valueOf(ugnich.getUid()))) + .andExpect(status().isOk()) + .andReturn(); + List ugnichTagsFromApi = jsonMapper.readValue(result2.getResponse().getContentAsString(), + new TypeReference>(){}); + TagStats yoUgnichStats = ugnichTagsFromApi.stream().filter(t -> t.getTag().getName().equals("yo")).findFirst().get(); + assertThat(yoUgnichStats.getUsageCount(), is(1)); + } + + @Test + public void postWithReferer() throws Exception { + mockMvc.perform(post("/post") + .param("body", "yo") + .with(httpBasic(ugnichName, ugnichPassword))) + .andExpect(status().isOk()); + } + @Test + public void threadWithEphemeralNumberShouldReturn404() throws Exception { + mockMvc.perform(get("/thread").param("mid", "999999999") + .with(httpBasic(ugnichName, ugnichPassword))).andExpect(status().is4xxClientError()); + } + @Test + public void performRequestsWithIssuedToken() throws Exception { + String ugnichHash = userService.getHashByUID(ugnich.getUid()); + mockMvc.perform(get("/home")).andExpect(status().isUnauthorized()); + mockMvc.perform(get("/auth")) + .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/auth").with(httpBasic(ugnichName, "wrongpassword"))) + .andExpect(status().isUnauthorized()); + MvcResult result = mockMvc.perform(get("/auth").with(httpBasic(ugnichName, ugnichPassword))) + .andExpect(status().isOk()) + .andReturn(); + String authHash = jsonMapper.readValue(result.getResponse().getContentAsString(), String.class); + assertThat(authHash, equalTo(ugnichHash)); + mockMvc.perform(get("/home").param("hash", ugnichHash)).andExpect(status().isOk()); + } + @Test + public void registerForNotificationsTests() throws Exception { + String token = "123456"; + ExternalToken registration = new ExternalToken(null, "apns", token, null); + mockMvc.perform(put("/notifications").with(httpBasic(ugnichName, ugnichPassword)) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(jsonMapper.writeValueAsBytes(Collections.singletonList(registration)))) + .andExpect(status().isOk()); + MvcResult result = mockMvc.perform(get("/notifications") + .param("uid", String.valueOf(ugnich.getUid())) + .with(httpBasic(juickName, juickPassword))) + .andExpect(status().isOk()) + .andReturn(); + List user = jsonMapper.readValue(result.getResponse().getContentAsString(), + new TypeReference>() { + }); + assertThat(user.get(0).getTokens().get(0).getToken(), equalTo(token)); + } + @Test + public void tg2juickLinks() { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://juick.com/123456#23").build(); + assertThat(uriComponents.getPath().substring(1), is("123456")); + assertThat(uriComponents.getFragment(), is("23")); + } + @Test + public void notificationsTokensTest() throws Exception { + List tokens = Collections.singletonList(new ExternalToken(null, "gcm", "123456", null)); + mockMvc.perform(delete("/notifications").with(httpBasic(ugnichName, ugnichPassword)) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(jsonMapper.writeValueAsBytes(tokens))).andExpect(status().isForbidden()); + mockMvc.perform(delete("/notifications").with(httpBasic(juickName, juickPassword)) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(jsonMapper.writeValueAsBytes(tokens))).andExpect(status().isOk()); + } + @Test + public void topTest() throws Exception { + int topmid = messagesService.createMessage(ugnich.getUid(), "top message", null, null); + IntStream.rangeClosed(6, 12).forEach(i -> { + messagesService.createReply(topmid, 0, i, "yo", null); + }); + + assertThat(messagesService.getPopularCandidates().get(0), is(topmid)); + Tag juickTag = tagService.getTag(juickTagId); + assertThat(juickTag.TID, is(2)); + tagService.updateTags(topmid, Collections.singletonList(juickTag)); + assertThat(messagesService.getPopularCandidates().isEmpty(), is(true)); + } + @Test + public void inReplyToScannerTest() { + String header = "<123456.56@juick.com>"; + Scanner headerScanner = new Scanner(header).useDelimiter(EmailManager.MSGID_PATTERN); + int mid = Integer.parseInt(headerScanner.next()); + int rid = Integer.parseInt(headerScanner.next()); + assertThat(mid, equalTo(123456)); + assertThat(rid, equalTo(56)); + } +} -- cgit v1.2.3