diff options
Diffstat (limited to 'juick-server/src/main/java/com')
44 files changed, 1832 insertions, 61 deletions
diff --git a/juick-server/src/main/java/com/juick/server/CommandsManager.java b/juick-server/src/main/java/com/juick/server/CommandsManager.java new file mode 100644 index 00000000..018cef15 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/CommandsManager.java @@ -0,0 +1,531 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.server; + +import com.juick.Message; +import com.juick.Tag; +import com.juick.User; +import com.juick.formatters.PlainTextFormatter; +import com.juick.service.component.*; +import com.juick.model.CommandResult; +import com.juick.model.TagStats; +import com.juick.server.helpers.annotation.UserCommand; +import com.juick.server.util.HttpUtils; +import com.juick.service.*; +import com.juick.util.MessageUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.commons.lang3.reflect.MethodUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import java.lang.reflect.Method; +import java.net.URI; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * + * @author ugnich + */ +@Component +public class CommandsManager { + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private TagService tagService; + @Inject + private PMQueriesService pmQueriesService; + @Inject + private ShowQueriesService showQueriesService; + @Inject + private PrivacyQueriesService privacyQueriesService; + @Inject + private SubscriptionService subscriptionService; + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String imgDir; + @Inject + private ApplicationEventPublisher applicationEventPublisher; + @Inject + private ImagesService imagesService; + + public CommandResult processCommand(User user, String data, @Nonnull URI attachment) throws Exception { + String strippedData = StringUtils.stripStart(data, null); + if (strippedData.startsWith("?OTR")) { + return CommandResult.fromString("?OTR Error: we are not using OTR"); + } + String input = MessageUtils.stripNonSafeUrls(strippedData); + Optional<Method> cmd = MethodUtils.getMethodsListWithAnnotation(getClass(), UserCommand.class).stream() + .filter(m -> Pattern.compile(m.getAnnotation(UserCommand.class).pattern(), + m.getAnnotation(UserCommand.class).patternFlags()).matcher(input).matches()) + .findFirst(); + if (cmd.isPresent()) { + Matcher matcher = Pattern.compile(cmd.get().getAnnotation(UserCommand.class).pattern(), + cmd.get().getAnnotation(UserCommand.class).patternFlags()).matcher(input); + List<String> groups = new ArrayList<>(); + while (matcher.find()) { + for (int i = 1; i <= matcher.groupCount(); i++) { + groups.add(matcher.group(i)); + } + } + CommandResult commandResult = (CommandResult) getClass().getMethod(cmd.get().getName(), User.class, URI.class, String[].class) + .invoke(this, user, attachment, groups.toArray(new String[groups.size()])); + if (StringUtils.isNotEmpty(commandResult.getText())) { + return commandResult; + } + } + Pair<String, List<Tag>> tags = tagService.fromString(input); + if (tags.getRight().size() > 5) { + return CommandResult.fromString("Sorry, 5 tags maximum."); + } + // new message + String body = tags.getLeft().trim(); + boolean haveAttachment = StringUtils.isNotEmpty(attachment.toString()); + String attachmentFName = null; + String attachmentType = null; + if (haveAttachment) { + attachmentFName = attachment.getScheme().equals("juick") ? attachment.getHost() + : HttpUtils.downloadImage(attachment.toURL(), tmpDir).getHost(); + attachmentType = attachmentFName.substring(attachmentFName.length() - 3); + } + int mid = messagesService.createMessage(user.getUid(), body, attachmentType, tags.getRight()); + if (haveAttachment) { + String fname = String.format("%d.%s", mid, attachmentType); + imagesService.saveImageWithPreviews(attachmentFName, fname); + } + Message msg = messagesService.getMessage(mid); + subscriptionService.subscribeMessage(msg, user); + + applicationEventPublisher.publishEvent(new MessageReadEvent(this, user, msg)); + applicationEventPublisher.publishEvent(new MessageEvent(this, msg, subscriptionService.getSubscribedUsers(msg.getUser().getUid(), msg.getMid()))); + return CommandResult.build(msg, "New message posted.\n#" + msg.getMid() + " https://juick.com/m/" + msg.getMid(), String.format("[New message](%s) posted", PlainTextFormatter.formatUrl(msg))); + } + + @UserCommand(pattern = "^ping$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "PING - returns you a PONG") + public CommandResult commandPing(User user, URI attachment, String[] input) { + applicationEventPublisher.publishEvent(new PingEvent(this, user)); + return CommandResult.fromString("PONG"); + } + + @UserCommand(pattern = "^help$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "HELP - returns this help message") + public CommandResult commandHelp(User user, URI attachment, String[] input) { + return CommandResult.fromString(Arrays.stream(getClass().getDeclaredMethods()) + .filter(m -> m.isAnnotationPresent(UserCommand.class)) + .map(m -> m.getAnnotation(UserCommand.class).help()) + .collect(Collectors.joining("\n"))); + } + + @UserCommand(pattern = "^login$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "LOGIN - log in to Juick website") + public CommandResult commandLogin(User user_from, URI attachment, String[] input) { + return CommandResult.fromString("http://juick.com/login?hash=" + userService.getHashByUID(user_from.getUid())); + } + @UserCommand(pattern = "^\\@(\\S+)\\s+([\\s\\S]+)$", help = "@username message - send PM to username") + public CommandResult commandPM(User user_from, URI attachment, String... arguments) { + String body = arguments[1]; + + User user_to = userService.getUserByName(arguments[0]); + + if (!user_to.isAnonymous()) { + if (!userService.isInBLAny(user_to.getUid(), user_from.getUid())) { + if (pmQueriesService.createPM(user_from.getUid(), user_to.getUid(), body)) { + com.juick.Message jmsg = new com.juick.Message(); + jmsg.setUser(user_from); + jmsg.setTo(user_to); + jmsg.setText(body); + applicationEventPublisher.publishEvent(new MessageEvent(this, jmsg, Collections.singletonList(user_to))); + return CommandResult.fromString("Private message sent"); + } + } + } + return CommandResult.fromString("Error"); + } + @UserCommand(pattern = "^bl$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "BL - Show your blacklist") + public CommandResult commandBLShow(User user_from, URI attachment, String... arguments) { + List<User> blusers = userService.getUserBLUsers(user_from.getUid()); + List<String> bltags = tagService.getUserBLTags(user_from.getUid()); + + String txt = StringUtils.EMPTY; + if (bltags.size() > 0) { + for (String bltag : bltags) { + txt += "*" + bltag + "\n"; + } + + if (blusers.size() > 0) { + txt += "\n"; + } + } + if (blusers.size() > 0) { + for (User bluser : blusers) { + txt += "@" + bluser.getName() + "\n"; + } + } + if (txt.isEmpty()) { + txt = "You don't have any users or tags in your blacklist."; + } + + return CommandResult.fromString(txt); + } + + @UserCommand(pattern = "^#\\+$", help = "#+ - Show last Juick messages") + public CommandResult commandLast(User user_from, URI attachment, String... arguments) { + return CommandResult.fromString("Last messages:\n" + + printMessages(user_from, messagesService.getAll(user_from.getUid(), 0), true)); + } + + @UserCommand(pattern = "@", help = "@ - Show recommendations and popular personal blogs") + public CommandResult commandUsers(User user_from, URI attachment, String... arguments) { + StringBuilder msg = new StringBuilder(); + msg.append("Recommended blogs"); + List<String> recommendedUsers = showQueriesService.getRecommendedUsers(user_from); + if (recommendedUsers.size() > 0) { + for (String user : recommendedUsers) { + msg.append("\n@").append(user); + } + } else { + msg.append("\nNo recommendations now. Subscribe to more blogs. ;)"); + } + msg.append("\n\nTop 10 personal blogs:"); + List<String> topUsers = showQueriesService.getTopUsers(); + if (topUsers.size() > 0) { + for (String user : topUsers) { + msg.append("\n@").append(user); + } + } else { + msg.append("\nNo top users. Empty DB? ;)"); + } + return CommandResult.fromString(msg.toString()); + } + @UserCommand(pattern = "^bl\\s+@([^\\s\\n\\+]+)", patternFlags = Pattern.CASE_INSENSITIVE, + help = "BL @username - add @username to your blacklist") + public CommandResult blacklistUser(User user_from, URI attachment, String... arguments) { + User blUser = userService.getUserByName(arguments[0]); + if (!blUser.isAnonymous()) { + PrivacyQueriesService.PrivacyResult result = privacyQueriesService.blacklistUser(user_from, blUser); + if (result == PrivacyQueriesService.PrivacyResult.Added) { + return CommandResult.fromString("User added to your blacklist"); + } else { + return CommandResult.fromString("User removed from your blacklist"); + } + } + return CommandResult.fromString("User not found"); + } + @UserCommand(pattern = "^bl\\s\\*(\\S+)$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "BL *tag - add *tag to your blacklist") + public CommandResult blacklistTag(User user_from, URI attachment, String... arguments) { + if (!user_from.isAnonymous()) { + Tag tag = tagService.getTag(arguments[0], false); + if (tag != null) { + PrivacyQueriesService.PrivacyResult result = privacyQueriesService.blacklistTag(user_from, tag); + if (result == PrivacyQueriesService.PrivacyResult.Added) { + return CommandResult.fromString("Tag added to your blacklist"); + } else { + return CommandResult.fromString("Tag removed from your blacklist"); + } + } + } + return CommandResult.fromString("Tag not found"); + } + @UserCommand(pattern = "\\*", help = "* - Show your tags") + public CommandResult commandTags(User currentUser, URI attachment, String... args) { + List<TagStats> tags = tagService.getUserTagStats(currentUser.getUid()); + String msg = "Your tags: (tag - messages)\n" + + tags.stream() + .map(t -> String.format("\n*%s - %d", t.getTag().getName(), t.getUsageCount())).collect(Collectors.joining()); + return CommandResult.fromString(msg); + } + @UserCommand(pattern = "S", help = "S - Show your subscriptions", patternFlags = Pattern.CASE_INSENSITIVE) + public CommandResult commandSubscriptions(User currentUser, URI attachment, String... args) { + List<User> friends = userService.getUserFriends(currentUser.getUid()); + List<String> tags = subscriptionService.getSubscribedTags(currentUser); + String msg = friends.size() > 0 ? "You are subscribed to users:" + friends.stream().map(u -> "\n@" + u.getName()) + .collect(Collectors.joining()) + : "You are not subscribed to any user."; + msg += tags.size() > 0 ? "\nYou are subscribed to tags:" + tags.stream().map(t -> "\n*" + t) + .collect(Collectors.joining()) + : "\nYou are not subscribed to any tag."; + return CommandResult.fromString(msg); + } + @UserCommand(pattern = "!", help = "! - Show your favorite messages") + public CommandResult commandFavorites(User currentUser, URI attachment, String... args) { + List<Integer> mids = messagesService.getUserRecommendations(currentUser.getUid(), 0); + if (mids.size() > 0) { + return CommandResult.fromString("Favorite messages: \n" + printMessages(currentUser, mids, false)); + } + return CommandResult.fromString("No favorite messages, try to \"like\" something ;)"); + } + @UserCommand(pattern = "^\\!\\s+#(\\d+)", help = "! #12345 - recommend message") + public CommandResult commandRecommend(User user, URI attachment, String... arguments) { + int mid = NumberUtils.toInt(arguments[0], 0); + if (mid > 0) { + com.juick.Message msg = messagesService.getMessage(mid); + if (msg != null) { + if (msg.getUser() == user) { + return CommandResult.fromString("You can't recommend your own messages."); + } + MessagesService.RecommendStatus status = messagesService.recommendMessage(mid, user.getUid()); + switch (status) { + case Added: + applicationEventPublisher.publishEvent(new LikeEvent(this, user, msg, + subscriptionService.getUsersSubscribedToUserRecommendations( + user.getUid(), msg))); + return CommandResult.fromString("Message is added to your recommendations"); + case Deleted: + return CommandResult.fromString("Message deleted from your recommendations."); + } + } + return CommandResult.fromString("Message not found"); + } + return CommandResult.fromString("Message not found"); + } + // TODO: target notification + @UserCommand(pattern = "^(s|u)\\s+\\@(\\S+)$", help = "S @username - subscribe to user" + + "\nU @username - unsubscribe from user", patternFlags = Pattern.CASE_INSENSITIVE) + public CommandResult commandSubscribeUser(User user, URI attachment, String... args) { + boolean subscribe = args[0].equalsIgnoreCase("s"); + User toUser = userService.getUserByName(args[1]); + if (toUser.isAnonymous()) { + return CommandResult.fromString("User not found"); + } + if (subscribe) { + if (subscriptionService.subscribeUser(user, toUser)) { + // TODO: already subscribed case + applicationEventPublisher.publishEvent(new SubscribeEvent(this, user, toUser)); + return CommandResult.fromString("Subscribed to @" + toUser.getName()); + } + } else { + if (subscriptionService.unSubscribeUser(user, toUser)) { + return CommandResult.fromString("Unsubscribed from @" + toUser.getName()); + } + return CommandResult.fromString("You was not subscribed to @" + toUser.getName()); + } + return CommandResult.fromString("Error"); + } + @UserCommand(pattern = "^(s|u)\\s+\\*(\\S+)$", help = "S *tag - subscribe to tag" + + "\nU *tag - unsubscribe from tag", patternFlags = Pattern.CASE_INSENSITIVE) + public CommandResult commandSubscribeTag(User user, URI attachment, String... args) { + boolean subscribe = args[0].equalsIgnoreCase("s"); + Tag tag = tagService.getTag(args[1], true); + if (subscribe) { + if (subscriptionService.subscribeTag(user, tag)) { + return CommandResult.fromString("Subscribed"); + } + } else { + if (subscriptionService.unSubscribeTag(user, tag)) { + return CommandResult.fromString("Unsubscribed from " + tag.getName()); + } + return CommandResult.fromString("You was not subscribed to " + tag.getName()); + } + return CommandResult.fromString("Error"); + } + @UserCommand(pattern = "^(s|u)\\s+#(\\d+)$", help = "S #1234 - subscribe to comments" + + "\nU #1234 - unsubscribe from comments", patternFlags = Pattern.CASE_INSENSITIVE) + public CommandResult commandSubscribeMessage(User user, URI attachment, String... args) { + boolean subscribe = args[0].equalsIgnoreCase("s"); + int mid = NumberUtils.toInt(args[1], 0); + Message msg = messagesService.getMessage(mid); + if (msg != null) { + if (subscribe) { + if (subscriptionService.subscribeMessage(msg, user)) { + applicationEventPublisher.publishEvent(new MessageReadEvent(this, user, msg)); + return CommandResult.fromString("Subscribed"); + } + } else { + if (subscriptionService.unSubscribeMessage(mid, user.getUid())) { + return CommandResult.fromString("Unsubscribed from #" + mid); + } + return CommandResult.fromString("You was not subscribed to #" + mid); + } + } + return CommandResult.fromString("Error"); + } + @UserCommand(pattern = "^(on|off)$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "ON/OFF - Enable/disable subscriptions delivery") + public CommandResult commandOnOff(User user, URI attachment, String[] input) { + UserService.ActiveStatus newStatus; + String retValUpdated; + if (input[0].toLowerCase().equals("on")) { + newStatus = UserService.ActiveStatus.Active; + retValUpdated = "XMPP notifications are activated"; + } else { + newStatus = UserService.ActiveStatus.Inactive; + retValUpdated = "XMPP notifications are disabled"; + } + if (userService.getAllJIDs(user).stream().allMatch(jid -> userService.setActiveStatusForJID(jid, newStatus))) { + return CommandResult.fromString(retValUpdated); + } + return CommandResult.fromString("Error"); + } + @UserCommand(pattern = "^\\@([^\\s\\n\\+]+)(\\+?)$", + help = "@username+ - Show user's info and last 20 messages") + public CommandResult commandUser(User user, URI attachment, String... arguments) { + User blogUser = userService.getUserByName(arguments[0]); + int page = arguments[1].length(); + if (!blogUser.isAnonymous()) { + List<Integer> mids = messagesService.getUserBlog(blogUser.getUid(), 0, 0); + return CommandResult.fromString(String.format("Last messages from @%s:\n%s", arguments[0], + printMessages(user, mids, false))); + } + return CommandResult.fromString("User not found"); + } + @UserCommand(pattern = "^#(\\d+)(\\+?)$", help = "#1234 - Show message (#1234+ - message with replies)") + public CommandResult commandShow(User user, URI attachment, String... arguments) { + boolean showReplies = arguments[1].length() > 0; + int mid = NumberUtils.toInt(arguments[0], 0); + if (mid == 0) { + return CommandResult.fromString("Error"); + } + com.juick.Message msg = messagesService.getMessage(mid); + if (msg != null) { + if (showReplies) { + List<com.juick.Message> replies = messagesService.getReplies(user, mid); + applicationEventPublisher.publishEvent(new MessageReadEvent(this, user, msg)); + replies.add(0, msg); + return CommandResult.fromString(String.join("\n", + replies.stream().map(PlainTextFormatter::formatPostSummary).collect(Collectors.toList()))); + } + return CommandResult.fromString(PlainTextFormatter.formatPost(msg)); + } + return CommandResult.fromString("Message not found"); + } + @UserCommand(pattern = "^#(\\d+)\\/(\\d+)$", help = "#1234/5 - Show reply") + public CommandResult commandShowReply(User user, URI attachment, String... arguments) { + int mid = NumberUtils.toInt(arguments[0], 0); + int rid = NumberUtils.toInt(arguments[1], 0); + com.juick.Message reply = messagesService.getReply(mid, rid); + if (reply != null) { + return CommandResult.fromString(PlainTextFormatter.formatPost(reply)); + } + return CommandResult.fromString("Reply not found"); + } + @UserCommand(pattern = "^\\*(\\S+)(\\+?)$", help = "*tag - Show last messages with tag") + public CommandResult commandShowTag(User user, URI attachment, String... arguments) { + if (StringUtils.isNotEmpty(attachment.toString())) { + // new message with tag + return CommandResult.fromString(StringUtils.EMPTY); + } + Tag tag = tagService.getTag(arguments[0], false); + if (tag != null) { + // TODO: synonyms + List<Integer> mids = messagesService.getTag(tag.TID, user.getUid(), 0, 10); + return CommandResult.fromString("Last messages with *" + tag.getName() + ":\n" + printMessages(user, mids, true)); + } + return CommandResult.fromString("Tag not found"); + } + @UserCommand(pattern = "^D #(\\d+)$", help = "D #1234 - Delete post", patternFlags = Pattern.CASE_INSENSITIVE) + public CommandResult commandDeletePost(User user, URI attachment, String... args) { + int mid = Integer.valueOf(args[0]); + if (messagesService.deleteMessage(user.getUid(), mid)) { + return CommandResult.fromString("Message deleted"); + } + return CommandResult.fromString("This is not your message"); + } + @UserCommand(pattern = "^D #(\\d+)(\\.|\\-|\\/)(\\d+)$", help = "D #1234/5 - Delete comment", patternFlags = Pattern.CASE_INSENSITIVE) + public CommandResult commandDeleteReply(User user, URI attachment, String... args) { + int mid = Integer.valueOf(args[0]); + int rid = Integer.valueOf(args[2]); + if (messagesService.deleteReply(user.getUid(), mid, rid)) { + return CommandResult.fromString("Reply deleted"); + } else { + return CommandResult.fromString("This is not your reply"); + } + } + @UserCommand(pattern = "^(D L|DL|D LAST)$", help = "D L - Delete last message", patternFlags = Pattern.CASE_INSENSITIVE) + public CommandResult commandDeleteLast(User user, URI attachment, String... args) { + return CommandResult.fromString("Temporarily unavailable"); + } + @UserCommand(pattern = "^\\?\\s+\\@([a-zA-Z0-9\\-\\.\\@]+)\\s+([\\s\\S]+)$", help = "? @user string - search in user messages") + public CommandResult commandSearch(User user, URI attachment, String... args) { + return CommandResult.fromString("Temporarily unavailable"); + } + @UserCommand(pattern = "^\\?\\s+([\\s\\S]+)$", help = "? string - search in all messages") + public CommandResult commandSearchAll(User user, URI attachment, String... args) { + return CommandResult.fromString("Temporarily unavailable"); + } + @UserCommand(pattern = "^(#+)$", help = "# - Show last messages from your feed (## - second page, ...)") + public CommandResult commandMyFeed(User user, URI attachment, String... arguments) { + // number of # is the page count + int page = arguments[0].length() - 1; + List<Integer> mids = messagesService.getMyFeed(user.getUid(), page, false); + if (mids.size() > 0) { + return CommandResult.fromString("Your feed: \n" + printMessages(user, mids, true)); + } + return CommandResult.fromString("Your feed is empty"); + } + @UserCommand(pattern = "^(#|\\.)(\\d+)((\\.|\\-|\\/)(\\d+))?\\s([\\s\\S]+)?", + help = "#1234 *tag *tag2 - edit tags\n#1234 text - reply to message") + public CommandResult EditOrReply(User user, @Nonnull URI attachment, String... args) throws Exception { + int mid = NumberUtils.toInt(args[1]); + int rid = NumberUtils.toInt(args[4], 0); + String txt = StringUtils.defaultString(args[5]); + Message msg = messagesService.getMessage(mid); + Pair<String, List<Tag>> messageTags = tagService.fromString(txt); + if (messageTags.getRight().size() > 0) { + if (user.getUid() != msg.getUser().getUid()) { + return CommandResult.fromString("It is not your message"); + } + if (!CollectionUtils.isEqualCollection(tagService.updateTags(mid, messageTags.getRight()), msg.getTags())) { + return CommandResult.fromString("Tags are updated"); + } else { + return CommandResult.fromString("Tags are NOT updated (5 tags maximum?)"); + } + } else { + boolean haveAttachment = StringUtils.isNotEmpty(attachment.toString()); + String attachmentFName = null; + String attachmentType = null; + if (haveAttachment) { + attachmentFName = attachment.getScheme().equals("juick") ? attachment.getHost() + : HttpUtils.downloadImage(attachment.toURL(), tmpDir).getHost(); + attachmentType = attachmentFName.substring(attachmentFName.length() - 3); + } + int newrid = messagesService.createReply(mid, rid, user, txt, attachmentType); + applicationEventPublisher.publishEvent(new MessageReadEvent(this, user, msg)); + if (haveAttachment) { + String fname = String.format("%d-%d.%s", mid, newrid, attachmentType); + imagesService.saveImageWithPreviews(attachmentFName, fname); + } + Message original = messagesService.getMessage(mid); + subscriptionService.subscribeMessage(original, user); + Message reply = messagesService.getReply(mid, newrid); + applicationEventPublisher.publishEvent(new MessageEvent(this, reply, subscriptionService.getUsersSubscribedToComments(original, reply))); + return CommandResult.build(reply,"Reply posted.\n#" + mid + "/" + newrid + " " + + "https://juick.com/m/" + mid + "#" + newrid, + String.format("[Reply](%s) posted", PlainTextFormatter.formatUrl(reply))); + } + } + + String printMessages(User visitor, List<Integer> mids, boolean crop) { + return messagesService.getMessages(visitor, mids).stream() + .sorted(Collections.reverseOrder()) + .map(PlainTextFormatter::formatPostSummary).collect(Collectors.joining("\n\n")); + } +} diff --git a/juick-server/src/main/java/com/juick/server/EmailManager.java b/juick-server/src/main/java/com/juick/server/EmailManager.java index 6e8d0d76..d6c2bb3f 100644 --- a/juick-server/src/main/java/com/juick/server/EmailManager.java +++ b/juick-server/src/main/java/com/juick/server/EmailManager.java @@ -2,10 +2,9 @@ package com.juick.server; import com.juick.Message; import com.juick.User; -import com.juick.server.component.MessageEvent; +import com.juick.service.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; diff --git a/juick-server/src/main/java/com/juick/server/MessengerManager.java b/juick-server/src/main/java/com/juick/server/MessengerManager.java index c58380c5..1283d8b2 100644 --- a/juick-server/src/main/java/com/juick/server/MessengerManager.java +++ b/juick-server/src/main/java/com/juick/server/MessengerManager.java @@ -17,11 +17,10 @@ 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.server.helpers.AnonymousUser; +import com.juick.service.component.MessageEvent; +import com.juick.model.AnonymousUser; import com.juick.service.MessagesService; import com.juick.service.MessengerService; -import com.juick.service.SubscriptionService; import com.juick.service.UserService; import com.juick.util.MessageUtils; import org.apache.commons.lang3.StringUtils; diff --git a/juick-server/src/main/java/com/juick/server/NotificationListener.java b/juick-server/src/main/java/com/juick/server/NotificationListener.java index 47b350c1..750c8b18 100644 --- a/juick-server/src/main/java/com/juick/server/NotificationListener.java +++ b/juick-server/src/main/java/com/juick/server/NotificationListener.java @@ -1,6 +1,6 @@ package com.juick.server; -import com.juick.server.component.*; +import com.juick.service.component.*; import org.springframework.context.event.EventListener; public interface NotificationListener { diff --git a/juick-server/src/main/java/com/juick/server/ServerManager.java b/juick-server/src/main/java/com/juick/server/ServerManager.java index f762b0e7..a50571e6 100644 --- a/juick-server/src/main/java/com/juick/server/ServerManager.java +++ b/juick-server/src/main/java/com/juick/server/ServerManager.java @@ -20,7 +20,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.juick.Message; import com.juick.User; -import com.juick.server.component.*; +import com.juick.service.component.*; import com.juick.service.MessagesService; import com.juick.service.SubscriptionService; import com.juick.service.UserService; diff --git a/juick-server/src/main/java/com/juick/server/TelegramBotManager.java b/juick-server/src/main/java/com/juick/server/TelegramBotManager.java index ac2febf7..3c9bb46a 100644 --- a/juick-server/src/main/java/com/juick/server/TelegramBotManager.java +++ b/juick-server/src/main/java/com/juick/server/TelegramBotManager.java @@ -18,15 +18,14 @@ package com.juick.server; import com.juick.User; -import com.juick.server.component.*; -import com.juick.server.helpers.AnonymousUser; -import com.juick.server.helpers.CommandResult; +import com.juick.service.component.*; +import com.juick.model.AnonymousUser; +import com.juick.model.CommandResult; import com.juick.server.util.HttpUtils; import com.juick.service.MessagesService; import com.juick.service.TelegramService; import com.juick.service.UserService; import com.juick.util.MessageUtils; -import com.pengrad.telegrambot.BotUtils; import com.pengrad.telegrambot.Callback; import com.pengrad.telegrambot.TelegramBot; import com.pengrad.telegrambot.UpdatesListener; @@ -46,7 +45,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; diff --git a/juick-server/src/main/java/com/juick/server/TwitterManager.java b/juick-server/src/main/java/com/juick/server/TwitterManager.java index 9b83b197..eeef2b91 100644 --- a/juick-server/src/main/java/com/juick/server/TwitterManager.java +++ b/juick-server/src/main/java/com/juick/server/TwitterManager.java @@ -17,20 +17,17 @@ package com.juick.server; import com.juick.Message; -import com.juick.server.component.*; +import com.juick.service.component.*; import com.juick.service.CrosspostService; import com.juick.util.MessageUtils; 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.event.EventListener; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import twitter4j.TwitterFactory; import twitter4j.conf.ConfigurationBuilder; -import javax.annotation.Nonnull; import javax.inject.Inject; /** diff --git a/juick-server/src/main/java/com/juick/server/WebsocketManager.java b/juick-server/src/main/java/com/juick/server/WebsocketManager.java index 8974a49f..1b62b984 100644 --- a/juick-server/src/main/java/com/juick/server/WebsocketManager.java +++ b/juick-server/src/main/java/com/juick/server/WebsocketManager.java @@ -18,8 +18,8 @@ package com.juick.server; import com.juick.User; -import com.juick.server.helpers.AnonymousUser; -import com.juick.server.helpers.CommandResult; +import com.juick.model.AnonymousUser; +import com.juick.model.CommandResult; import com.juick.server.util.HttpForbiddenException; import com.juick.server.util.HttpNotFoundException; import com.juick.service.MessagesService; @@ -28,7 +28,6 @@ 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.http.HttpHeaders; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; diff --git a/juick-server/src/main/java/com/juick/server/XMPPConnection.java b/juick-server/src/main/java/com/juick/server/XMPPConnection.java index 692ed3fe..ba155fe7 100644 --- a/juick-server/src/main/java/com/juick/server/XMPPConnection.java +++ b/juick-server/src/main/java/com/juick/server/XMPPConnection.java @@ -19,9 +19,9 @@ package com.juick.server; import com.juick.User; import com.juick.formatters.PlainTextFormatter; -import com.juick.server.component.*; -import com.juick.server.helpers.CommandResult; -import com.juick.server.helpers.UserInfo; +import com.juick.service.component.*; +import com.juick.model.CommandResult; +import com.juick.model.UserInfo; import com.juick.server.xmpp.iq.MessageQuery; import com.juick.server.xmpp.s2s.BasicXmppSession; import com.juick.server.xmpp.s2s.StanzaListener; @@ -35,8 +35,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.annotation.DependsOn; -import org.springframework.stereotype.Component; import rocks.xmpp.addr.Jid; import rocks.xmpp.core.XmppException; import rocks.xmpp.core.session.XmppSession; diff --git a/juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java b/juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java index 2e484e3d..8d9f9402 100644 --- a/juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java +++ b/juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java @@ -24,13 +24,13 @@ import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.model.OAuthRequest; import com.github.scribejava.core.model.Verb; import com.github.scribejava.core.oauth.OAuth20Service; -import com.juick.facebook.User; +import com.juick.model.facebook.User; import com.juick.server.util.HttpBadRequestException; import com.juick.service.CrosspostService; import com.juick.service.EmailService; import com.juick.service.TelegramService; import com.juick.service.UserService; -import com.juick.vk.UsersResponse; +import com.juick.model.vk.UsersResponse; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; @@ -244,7 +244,7 @@ public class ApiSocialLogin { vkService.signRequest(token, meRequest); String graph = vkService.execute(meRequest).getBody(); - com.juick.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0); + com.juick.model.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0); String vkName = jsonUser.getFirstName() + " " + jsonUser.getLastName(); String vkLink = jsonUser.getScreenName(); 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 index e900810f..86f9a20d 100644 --- a/juick-server/src/main/java/com/juick/server/api/Messages.java +++ b/juick-server/src/main/java/com/juick/server/api/Messages.java @@ -21,15 +21,14 @@ import com.juick.Message; import com.juick.Tag; import com.juick.User; import com.juick.server.Utils; -import com.juick.server.component.MessageReadEvent; -import com.juick.server.helpers.CommandResult; +import com.juick.service.component.MessageReadEvent; +import com.juick.model.CommandResult; import com.juick.server.util.HttpBadRequestException; import com.juick.server.util.HttpNotFoundException; import com.juick.server.util.UserUtils; import com.juick.service.MessagesService; import com.juick.service.TagService; import com.juick.service.UserService; -import com.juick.service.security.entities.JuickUser; import org.apache.commons.io.IOUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; 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 index 0b34f275..b0d64292 100644 --- a/juick-server/src/main/java/com/juick/server/api/Notifications.java +++ b/juick-server/src/main/java/com/juick/server/api/Notifications.java @@ -21,7 +21,7 @@ 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.model.AnonymousUser; import com.juick.server.util.HttpBadRequestException; import com.juick.server.util.HttpForbiddenException; import com.juick.service.MessagesService; 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 index d3619662..80be92f0 100644 --- a/juick-server/src/main/java/com/juick/server/api/PM.java +++ b/juick-server/src/main/java/com/juick/server/api/PM.java @@ -18,9 +18,9 @@ package com.juick.server.api; import com.juick.User; -import com.juick.server.component.MessageEvent; -import com.juick.server.helpers.AnonymousUser; -import com.juick.server.helpers.PrivateChats; +import com.juick.service.component.MessageEvent; +import com.juick.model.AnonymousUser; +import com.juick.model.PrivateChats; import com.juick.server.util.*; import com.juick.service.PMQueriesService; import com.juick.service.UserService; 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 index 99d118c3..d6f085bb 100644 --- a/juick-server/src/main/java/com/juick/server/api/Post.java +++ b/juick-server/src/main/java/com/juick/server/api/Post.java @@ -22,7 +22,7 @@ import com.juick.Reaction; import com.juick.Status; import com.juick.User; import com.juick.server.CommandsManager; -import com.juick.server.helpers.CommandResult; +import com.juick.model.CommandResult; import com.juick.server.util.*; import com.juick.service.MessagesService; import com.juick.service.SubscriptionService; 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 index 38e71e3a..7a8e572a 100644 --- a/juick-server/src/main/java/com/juick/server/api/Tags.java +++ b/juick-server/src/main/java/com/juick/server/api/Tags.java @@ -18,7 +18,7 @@ package com.juick.server.api; import com.juick.User; -import com.juick.server.helpers.TagStats; +import com.juick.model.TagStats; import com.juick.server.util.UserUtils; import com.juick.service.TagService; import org.springframework.http.MediaType; 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 index de5c05c2..7686d722 100644 --- a/juick-server/src/main/java/com/juick/server/api/Users.java +++ b/juick-server/src/main/java/com/juick/server/api/Users.java @@ -18,8 +18,8 @@ package com.juick.server.api; import com.juick.User; -import com.juick.server.helpers.ApplicationStatus; -import com.juick.server.helpers.UserInfo; +import com.juick.model.ApplicationStatus; +import com.juick.model.UserInfo; import com.juick.server.util.HttpForbiddenException; import com.juick.server.util.HttpNotFoundException; import com.juick.service.CrosspostService; diff --git a/juick-server/src/main/java/com/juick/server/api/rss/RepliesView.java b/juick-server/src/main/java/com/juick/server/api/rss/RepliesView.java index b67e8b44..f9d7109e 100644 --- a/juick-server/src/main/java/com/juick/server/api/rss/RepliesView.java +++ b/juick-server/src/main/java/com/juick/server/api/rss/RepliesView.java @@ -17,7 +17,7 @@ package com.juick.server.api.rss; -import com.juick.server.helpers.ResponseReply; +import com.juick.model.ResponseReply; import com.juick.util.MessageUtils; import com.rometools.modules.mediarss.MediaEntryModuleImpl; import com.rometools.modules.mediarss.MediaModule; diff --git a/juick-server/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java b/juick-server/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java new file mode 100644 index 00000000..23a35384 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java @@ -0,0 +1,63 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.server.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Created by vitalyster on 28.06.2016. + */ +@Configuration +public class BaseWebConfiguration implements WebMvcConfigurer, SchedulingConfigurer { + + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.setUseSuffixPatternMatch(false); + } + + @Bean + public ResourceUrlEncodingFilter resourceUrlEncodingFilter() { + return new ResourceUrlEncodingFilter(); + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.setScheduler(taskExecutor()); + } + + @Bean(destroyMethod="shutdown") + public Executor taskExecutor() { + return Executors.newScheduledThreadPool(100); + } + + @Bean + public ExecutorService executorService() { + return Executors.newCachedThreadPool(); + } +} diff --git a/juick-server/src/main/java/com/juick/server/configuration/StorageConfiguration.java b/juick-server/src/main/java/com/juick/server/configuration/StorageConfiguration.java new file mode 100644 index 00000000..4101f37d --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/configuration/StorageConfiguration.java @@ -0,0 +1,20 @@ +package com.juick.server.configuration; + +import com.juick.service.ImagesService; +import com.juick.service.ImagesServiceImpl; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class StorageConfiguration { + + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String imgDir; + @Bean + public ImagesService imagesService() { + return new ImagesServiceImpl(imgDir, tmpDir); + } +} diff --git a/juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java b/juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java index 16d32ee4..f829a999 100644 --- a/juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java +++ b/juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java @@ -47,8 +47,6 @@ import java.util.Collections; */ @Configuration @EnableCaching -@Import({ BaseWebConfiguration.class, SecurityConfig.class, SapeConfiguration.class, - StorageConfiguration.class}) public class WwwAppConfiguration implements WebMvcConfigurer { @Inject private UserService userService; diff --git a/juick-server/src/main/java/com/juick/server/helpers/annotation/UserCommand.java b/juick-server/src/main/java/com/juick/server/helpers/annotation/UserCommand.java new file mode 100644 index 00000000..4f07001c --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/helpers/annotation/UserCommand.java @@ -0,0 +1,50 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.server.helpers.annotation; + +import org.apache.commons.lang3.StringUtils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by oxpa on 22.03.16. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserCommand { + /** + * + * @return a command pattern + */ + String pattern() default StringUtils.EMPTY; + + /** + * + * @return pattern flags + */ + int patternFlags() default 0; + + /** + * + * @return a string used in HELP command output. Basically, only 1 string + */ + String help() default StringUtils.EMPTY; +} diff --git a/juick-server/src/main/java/com/juick/server/util/HttpBadRequestException.java b/juick-server/src/main/java/com/juick/server/util/HttpBadRequestException.java new file mode 100644 index 00000000..242f2b09 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/util/HttpBadRequestException.java @@ -0,0 +1,32 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.server.util; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Created by vt on 24/11/2016. + */ +@ResponseStatus(value = HttpStatus.BAD_REQUEST) +public class HttpBadRequestException extends RuntimeException { + public HttpBadRequestException() { + super("the request was bad", null, false, false); + } +} diff --git a/juick-server/src/main/java/com/juick/server/util/HttpForbiddenException.java b/juick-server/src/main/java/com/juick/server/util/HttpForbiddenException.java new file mode 100644 index 00000000..3251ca38 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/util/HttpForbiddenException.java @@ -0,0 +1,33 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.server.util; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Created by vt on 24/11/2016. + */ +@ResponseStatus(value = HttpStatus.FORBIDDEN) +public class HttpForbiddenException extends RuntimeException { + public HttpForbiddenException() { + super(StringUtils.EMPTY, null, false, false); + } + +} diff --git a/juick-server/src/main/java/com/juick/server/util/HttpNotFoundException.java b/juick-server/src/main/java/com/juick/server/util/HttpNotFoundException.java new file mode 100644 index 00000000..f66ece8b --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/util/HttpNotFoundException.java @@ -0,0 +1,32 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.server.util; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Created by vt on 24/11/2016. + */ +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class HttpNotFoundException extends RuntimeException { + public HttpNotFoundException() { + super(StringUtils.EMPTY, null, false, false); + } +} diff --git a/juick-server/src/main/java/com/juick/server/util/HttpUtils.java b/juick-server/src/main/java/com/juick/server/util/HttpUtils.java new file mode 100644 index 00000000..9f356aa5 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/util/HttpUtils.java @@ -0,0 +1,110 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ +package com.juick.server.util; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Iterator; +import java.util.UUID; + +/** + * + * @author Ugnich Anton + */ +public class HttpUtils { + private static final Logger logger = LoggerFactory.getLogger(HttpUtils.class); + + public static URI receiveMultiPartFile(MultipartFile attach, String tmpDir) throws IOException { + if (attach != null && !attach.isEmpty()) { + ImageInputStream iis = ImageIO.createImageInputStream(attach.getInputStream()); + Iterator<ImageReader> readers = ImageIO.getImageReaders(iis); + + String format = StringUtils.EMPTY; + while (readers.hasNext()) { + ImageReader read = readers.next(); + format = read.getFormatName(); + } + String attachmentType = attachmentTypeFromFormat(format); + if (attachmentType.equals("jpg") || attachmentType.equals("png")) { + String attachmentFName = DigestUtils.md5Hex(UUID.randomUUID().toString()) + "." + attachmentType; + try { + Files.write(Paths.get(tmpDir, attachmentFName), + attach.getBytes()); + return URI.create(String.format("juick://%s", attachmentFName)); + } catch (IOException e) { + logger.warn("file receive error", e); + } + } + logger.warn("file type is unknown: {}", attach.getOriginalFilename()); + } + return URI.create(StringUtils.EMPTY); + } + + private static String attachmentTypeFromFormat(String format) throws IOException { + if (format != null && format.equals("JPEG")) { + return "jpg"; + } else if (format != null && format.equals("png")) { + return "png"; + } else { + throw new IOException("Wrong file type: " + format); + } + } + + public static URI downloadImage(URL url, String tmpDir) throws IOException { + ImageInputStream iis = ImageIO.createImageInputStream(url.openStream()); + Iterator<ImageReader> readers = ImageIO.getImageReaders(iis); + + String format = StringUtils.EMPTY; + while (readers.hasNext()) { + ImageReader read = readers.next(); + format = read.getFormatName(); + } + URLConnection urlConn; + try { + urlConn = url.openConnection(); + } catch (IOException e) { + logger.error(String.format("Failed open url: %s", url.toString())); + throw e; + } + + try (InputStream is = new BufferedInputStream(urlConn.getInputStream())) { + String attachmentType = attachmentTypeFromFormat(format); + + String attachmentFName = DigestUtils.md5Hex(UUID.randomUUID().toString()) + "." + attachmentType; + Files.copy(is, Paths.get(tmpDir, attachmentFName)); + return URI.create(String.format("juick://%s", attachmentFName)); + } catch (IOException e) { + logger.error(String.format("Failed download image by url: %s", url.toString()), e); + throw e; + } + } +} diff --git a/juick-server/src/main/java/com/juick/server/util/ImageUtils.java b/juick-server/src/main/java/com/juick/server/util/ImageUtils.java new file mode 100644 index 00000000..d16faf8f --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/util/ImageUtils.java @@ -0,0 +1,175 @@ + +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.server.util; + +import com.juick.Attachment; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.Imaging; +import org.apache.commons.imaging.common.ImageMetadata; +import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata; +import org.apache.commons.imaging.formats.tiff.TiffField; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.imgscalr.Scalr; +import org.imgscalr.Scalr.Rotation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.FileImageInputStream; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Iterator; + +public class ImageUtils { + private static final Logger logger = LoggerFactory.getLogger(ImageUtils.class); + + private String imgDir; + private String tmpDir; + + public ImageUtils(String imgDir, String tmpDir) { + this.imgDir = imgDir; + this.tmpDir = tmpDir; + } +/** + * Returns <code>BufferedImage</code>, same as <code>ImageIO.read()</code> does. + * + * <p>JPEG images with EXIF metadata are rotated according to Orientation tag. + * + * @param imageFile a <code>File</code> to read from. + */ + private static BufferedImage readImageWithOrientation(File imageFile) + throws IOException { + + BufferedImage image = ImageIO.read(imageFile); + if (!FilenameUtils.getExtension(imageFile.getName()).equals("jpg")) { + return image; + } + + try { + ImageMetadata metadata = Imaging.getMetadata(imageFile); + + if (metadata instanceof JpegImageMetadata) { + JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; + TiffField orientationField = jpegMetadata.findEXIFValue(TiffTagConstants.TIFF_TAG_ORIENTATION); + + if (orientationField != null) { + int orientation = orientationField.getIntValue(); + switch (orientation) { + case TiffTagConstants.ORIENTATION_VALUE_ROTATE_90_CW: + image = Scalr.rotate(image, Rotation.CW_90); + break; + case TiffTagConstants.ORIENTATION_VALUE_ROTATE_180: + image = Scalr.rotate(image, Rotation.CW_180); + break; + case TiffTagConstants.ORIENTATION_VALUE_ROTATE_270_CW: + image = Scalr.rotate(image, Rotation.CW_270); + break; + case TiffTagConstants.ORIENTATION_VALUE_MIRROR_HORIZONTAL: + image = Scalr.rotate(image, Rotation.FLIP_HORZ); + break; + case TiffTagConstants.ORIENTATION_VALUE_MIRROR_VERTICAL: + image = Scalr.rotate(image, Rotation.FLIP_VERT); + break; + case TiffTagConstants.ORIENTATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_90_CW: + image = Scalr.rotate(Scalr.rotate(image, Rotation.FLIP_HORZ), Rotation.CW_90); + break; + case TiffTagConstants.ORIENTATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_270_CW: + image = Scalr.rotate(Scalr.rotate(image, Rotation.FLIP_HORZ), Rotation.CW_270); + break; + case TiffTagConstants.ORIENTATION_VALUE_HORIZONTAL_NORMAL: + default: + // do nothing + break; + } + } + } + } catch (ImageReadException e) { + // failed to read metadata. + // nothing to do here, return image as is. + } + + return image; + } + + public void saveImageWithPreviews(String tempFilename, String outputFilename) + throws IOException { + String ext = FilenameUtils.getExtension(outputFilename); + + Path outputImagePath = Paths.get(imgDir, "p", outputFilename); + // this throws strange exceptions + // Files.move(Paths.get(tmpDir, tempFilename), outputImagePath); + FileUtils.moveFile(Paths.get(tmpDir, tempFilename).toFile(), outputImagePath.toFile()); + BufferedImage originalImage = readImageWithOrientation(outputImagePath.toFile()); + + int width = originalImage.getWidth(); + int height = originalImage.getHeight(); + int maxDimension = (width > height) ? width : height; + BufferedImage image1024 = (maxDimension > 1024) ? Scalr.resize(originalImage, 1024) : originalImage; + BufferedImage image0512 = (maxDimension > 512) ? Scalr.resize(originalImage, 512) : originalImage; + BufferedImage image0160 = (maxDimension > 160) ? Scalr.resize(originalImage, 160) : originalImage; + ImageIO.write(image1024, ext, Paths.get(imgDir, "photos-1024", outputFilename).toFile()); + ImageIO.write(image0512, ext, Paths.get(imgDir, "photos-512", outputFilename).toFile()); + ImageIO.write(image0160, ext, Paths.get(imgDir, "ps", outputFilename).toFile()); + } + + public void saveAvatar(String tempFilename, int uid) + throws IOException { + String ext = FilenameUtils.getExtension(tempFilename); + String originalName = String.format("%s.%s", uid, ext); + Path originalPath = Paths.get(imgDir, "ao", originalName); + Files.move(Paths.get(tmpDir, tempFilename), originalPath, StandardCopyOption.REPLACE_EXISTING); + BufferedImage originalImage = ImageIO.read(originalPath.toFile()); + + String targetExt = "png"; + String targetName = String.format("%s.%s", uid, targetExt); + ImageIO.write(Scalr.resize(originalImage, 96), targetExt, Paths.get(imgDir, "a", targetName).toFile()); + ImageIO.write(Scalr.resize(originalImage, 32), targetExt, Paths.get(imgDir, "as", targetName).toFile()); + } + public Attachment getAttachment(File imgFile) throws IOException { + Attachment attachment = new Attachment(); + try (ImageInputStream stream = ImageIO.createImageInputStream(imgFile)) { + Iterator<ImageReader> iter = ImageIO.getImageReaders(stream); + while (iter.hasNext()) { + ImageReader reader = iter.next(); + try { + reader.setInput(stream); + attachment.setWidth(reader.getWidth(reader.getMinIndex())); + attachment.setHeight(reader.getHeight(reader.getMinIndex())); + return attachment; + } catch (Exception e) { + logger.debug("Error reading {}, trying next reader", imgFile.getAbsolutePath()); + } finally { + reader.dispose(); + } + } + } + + logger.warn("Not a known image file {}", imgFile.getAbsolutePath()); + return attachment; + } +}
\ No newline at end of file diff --git a/juick-server/src/main/java/com/juick/server/util/TagUtils.java b/juick-server/src/main/java/com/juick/server/util/TagUtils.java new file mode 100644 index 00000000..cb828933 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/util/TagUtils.java @@ -0,0 +1,42 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.server.util; + +import com.juick.Tag; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Created by aalexeev on 11/13/16. + */ +public class TagUtils { + private TagUtils() { + throw new IllegalStateException(); + } + + public static String toString(final List<Tag> tags) { + if (CollectionUtils.isEmpty(tags)) + return StringUtils.EMPTY; + + return tags.stream().map(t -> "*" + t.getName()) + .collect(Collectors.joining(" ")); + } +} diff --git a/juick-server/src/main/java/com/juick/server/util/UserUtils.java b/juick-server/src/main/java/com/juick/server/util/UserUtils.java new file mode 100644 index 00000000..1adc85ab --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/util/UserUtils.java @@ -0,0 +1,55 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.server.util; + +import com.juick.User; +import com.juick.model.AnonymousUser; +import com.juick.service.security.entities.JuickUser; +import javax.annotation.Nonnull; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Created by aalexeev on 11/14/16. + */ +public class UserUtils { + private UserUtils() { + throw new IllegalStateException(); + } + + public static Authentication getAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); + } + + public static Object getPrincipal(final Authentication authentication) { + return authentication == null ? null : authentication.getPrincipal(); + } + + @Nonnull + public static User getCurrentUser() { + Object principal = getPrincipal(getAuthentication()); + + if (principal instanceof JuickUser) + return ((JuickUser) principal).getUser(); + + if (principal instanceof User) + return (User) principal; + + return AnonymousUser.INSTANCE; + } +} diff --git a/juick-server/src/main/java/com/juick/server/util/WebUtils.java b/juick-server/src/main/java/com/juick/server/util/WebUtils.java new file mode 100644 index 00000000..9dd628ee --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/util/WebUtils.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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.server.util; + +import java.util.regex.Pattern; + +/** + * Created by aalexeev on 11/28/16. + */ +public class WebUtils { + private WebUtils() { + throw new IllegalStateException(); + } + + private static final Pattern USER_NAME_PATTERN = Pattern.compile("[a-zA-Z-_\\d]{2,16}"); + + private static final Pattern POST_NUMBER_PATTERN = Pattern.compile("-?\\d+"); + + private static final Pattern JID_PATTERN = Pattern.compile("^[a-zA-Z0-9\\\\-\\\\_\\\\@\\\\.]{6,64}$"); + + + public static boolean isPostNumber(final String aString) { + return aString != null && POST_NUMBER_PATTERN.matcher(aString).matches(); + } + + public static boolean isNotPostNumber(final String aString) { + return !isPostNumber(aString); + } + + public static boolean isUserName(final String aString) { + return aString != null && USER_NAME_PATTERN.matcher(aString).matches(); + } + + public static boolean isNotUserName(final String aString) { + return !isUserName(aString); + } + + public static boolean isJid(final String aString) { + return aString != null && JID_PATTERN.matcher(aString).matches(); + } + + public static boolean isNotJid(final String aString) { + return !isJid(aString); + } + + +} diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java b/juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java index 9e364ff8..c476e5c2 100644 --- a/juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java +++ b/juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java @@ -19,8 +19,8 @@ package com.juick.server.www.controllers; import com.fasterxml.jackson.databind.ObjectMapper; import com.juick.Message; import com.juick.User; -import com.juick.server.helpers.AnonymousUser; -import com.juick.server.helpers.CommandResult; +import com.juick.model.AnonymousUser; +import com.juick.model.CommandResult; import com.juick.server.util.*; import com.juick.server.www.WebApp; import com.juick.service.*; diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/Settings.java b/juick-server/src/main/java/com/juick/server/www/controllers/Settings.java index f2ecccf6..6405b3bd 100644 --- a/juick-server/src/main/java/com/juick/server/www/controllers/Settings.java +++ b/juick-server/src/main/java/com/juick/server/www/controllers/Settings.java @@ -16,8 +16,8 @@ */ package com.juick.server.www.controllers; -import com.juick.server.helpers.NotifyOpts; -import com.juick.server.helpers.UserInfo; +import com.juick.model.NotifyOpts; +import com.juick.model.UserInfo; import com.juick.server.util.*; import com.juick.service.*; import org.apache.commons.lang3.RandomStringUtils; @@ -25,7 +25,6 @@ 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.ApplicationEventPublisher; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.GetMapping; @@ -45,7 +44,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java b/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java index 93d3946a..3cea3e34 100644 --- a/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java +++ b/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java @@ -24,7 +24,7 @@ import com.github.scribejava.core.builder.ServiceBuilder; import com.github.scribejava.core.model.*; import com.github.scribejava.core.oauth.OAuth10aService; import com.github.scribejava.core.oauth.OAuth20Service; -import com.juick.facebook.User; +import com.juick.model.facebook.User; import com.juick.server.util.HttpBadRequestException; import com.juick.server.util.UserUtils; import com.juick.service.CrosspostService; @@ -32,7 +32,7 @@ import com.juick.service.EmailService; import com.juick.service.TelegramService; import com.juick.service.UserService; import com.juick.server.www.Utils; -import com.juick.vk.UsersResponse; +import com.juick.model.vk.UsersResponse; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.HmacAlgorithms; import org.apache.commons.codec.digest.HmacUtils; @@ -216,8 +216,8 @@ public class SocialLogin { OAuth1AccessToken accessToken = oAuthService.getAccessToken(requestToken, verifier); OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, TWITTER_VERIFY_URL); oAuthService.signRequest(accessToken, oAuthRequest); - com.juick.twitter.User twitterUser = jsonMapper.readValue(oAuthService.execute(oAuthRequest).getBody(), - com.juick.twitter.User.class); + com.juick.model.twitter.User twitterUser = jsonMapper.readValue(oAuthService.execute(oAuthRequest).getBody(), + com.juick.model.twitter.User.class); if (userService.linkTwitterAccount(user, accessToken.getToken(), accessToken.getTokenSecret(), twitterUser.getScreenName())) { response.setStatus(HttpServletResponse.SC_FOUND); @@ -265,7 +265,7 @@ public class SocialLogin { vkService.signRequest(token, meRequest); String graph = vkService.execute(meRequest).getBody(); - com.juick.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0); + com.juick.model.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0); String vkName = jsonUser.getFirstName() + " " + jsonUser.getLastName(); String vkLink = jsonUser.getScreenName(); diff --git a/juick-server/src/main/java/com/juick/service/CrosspostServiceImpl.java b/juick-server/src/main/java/com/juick/service/CrosspostServiceImpl.java index 14bdc7e2..47d1870b 100644 --- a/juick-server/src/main/java/com/juick/service/CrosspostServiceImpl.java +++ b/juick-server/src/main/java/com/juick/service/CrosspostServiceImpl.java @@ -18,7 +18,7 @@ package com.juick.service; import com.juick.ExternalToken; -import com.juick.server.helpers.ApplicationStatus; +import com.juick.model.ApplicationStatus; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.springframework.dao.EmptyResultDataAccessException; diff --git a/juick-server/src/main/java/com/juick/service/ImagesServiceImpl.java b/juick-server/src/main/java/com/juick/service/ImagesServiceImpl.java new file mode 100644 index 00000000..67c8360e --- /dev/null +++ b/juick-server/src/main/java/com/juick/service/ImagesServiceImpl.java @@ -0,0 +1,82 @@ +package com.juick.service; + +import com.juick.Attachment; +import com.juick.Message; +import com.juick.Photo; +import com.juick.server.util.ImageUtils; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; + +public class ImagesServiceImpl implements ImagesService { + private ImageUtils imageUtils; + private String imgDir; + private String tmpDir; + public ImagesServiceImpl(String imgDir, String tmpDir) { + this.imgDir = imgDir; + this.tmpDir = tmpDir; + imageUtils = new ImageUtils(imgDir, tmpDir); + } + @Override + public void setAttachmentMetadata(String baseUrl, Message msg) throws Exception { + if (!StringUtils.isEmpty(msg.getAttachmentType())) { + Photo photo = new Photo(); + if (msg.getRid()> 0) { + photo.setSmall(String.format("%sphotos-512/%d-%d.%s", baseUrl, msg.getMid(), msg.getRid(), msg.getAttachmentType())); + photo.setMedium(String.format("%sphotos-1024/%d-%d.%s", baseUrl, msg.getMid(), msg.getRid(), msg.getAttachmentType())); + photo.setThumbnail(String.format("%sps/%d-%d.%s", baseUrl, msg.getMid(), msg.getRid(), msg.getAttachmentType())); + } else { + photo.setSmall(String.format("%sphotos-512/%d.%s", baseUrl, msg.getMid(), msg.getAttachmentType())); + photo.setMedium(String.format("%sphotos-1024/%d.%s", baseUrl, msg.getMid(), msg.getAttachmentType())); + photo.setThumbnail(String.format("%sps/%d.%s", baseUrl, msg.getMid(), msg.getAttachmentType())); + } + msg.setPhoto(photo); + String imageName = String.format("%s.%s", msg.getMid(), msg.getAttachmentType()); + if (msg.getRid() > 0) { + imageName = String.format("%s-%s.%s", msg.getMid(), msg.getRid(), msg.getAttachmentType()); + } + File fullImage = Paths.get(imgDir, "p", imageName).toFile(); + File mediumImage = Paths.get(imgDir, "photos-1024", imageName).toFile(); + File smallImage = Paths.get(imgDir, "photos-512", imageName).toFile(); + File thumbnailImage = Paths.get(imgDir, "ps", imageName).toFile(); + StringBuilder builder = new StringBuilder(); + builder.append(baseUrl); + builder.append(msg.getAttachmentType().equals("mp4") ? "video" : "p"); + builder.append("/").append(msg.getMid()); + if (msg.getRid() > 0) { + builder.append("-").append(msg.getRid()); + } + builder.append(".").append(msg.getAttachmentType()); + String originalUrl = builder.toString(); + + Attachment original = imageUtils.getAttachment(fullImage); + original.setUrl(originalUrl); + + Attachment medium = imageUtils.getAttachment(mediumImage); + medium.setUrl(photo.getMedium()); + original.setMedium(medium); + + Attachment small = imageUtils.getAttachment(smallImage); + small.setUrl(photo.getSmall()); + original.setSmall(small); + + Attachment thumb = imageUtils.getAttachment(thumbnailImage); + thumb.setUrl(photo.getMedium()); + original.setThumbnail(thumb); + + msg.setAttachment(original); + } + } + + @Override + public void saveImageWithPreviews(String tempFilename, String outputFilename) throws IOException { + imageUtils.saveImageWithPreviews(tempFilename, outputFilename); + } + + @Override + public void saveAvatar(String tempFilename, int uid) throws IOException { + imageUtils.saveAvatar(tempFilename, uid); + } +} diff --git a/juick-server/src/main/java/com/juick/service/MessagesServiceImpl.java b/juick-server/src/main/java/com/juick/service/MessagesServiceImpl.java index 0abe9309..b0707232 100644 --- a/juick-server/src/main/java/com/juick/service/MessagesServiceImpl.java +++ b/juick-server/src/main/java/com/juick/service/MessagesServiceImpl.java @@ -18,10 +18,9 @@ package com.juick.service; import com.juick.*; -import com.juick.server.helpers.PrivacyOpts; -import com.juick.server.helpers.ResponseReply; +import com.juick.model.PrivacyOpts; +import com.juick.model.ResponseReply; import com.juick.server.util.HttpNotFoundException; -import com.juick.service.security.entities.JuickUser; import com.juick.util.MessageUtils; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; diff --git a/juick-server/src/main/java/com/juick/service/SubscriptionServiceImpl.java b/juick-server/src/main/java/com/juick/service/SubscriptionServiceImpl.java index 1dee379f..492fef1c 100644 --- a/juick-server/src/main/java/com/juick/service/SubscriptionServiceImpl.java +++ b/juick-server/src/main/java/com/juick/service/SubscriptionServiceImpl.java @@ -20,7 +20,7 @@ package com.juick.service; import com.juick.Message; import com.juick.Tag; import com.juick.User; -import com.juick.server.helpers.NotifyOpts; +import com.juick.model.NotifyOpts; import org.apache.commons.lang3.StringUtils; import org.springframework.dao.DuplicateKeyException; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; diff --git a/juick-server/src/main/java/com/juick/service/TagServiceImpl.java b/juick-server/src/main/java/com/juick/service/TagServiceImpl.java index aa6c3713..42159d3b 100644 --- a/juick-server/src/main/java/com/juick/service/TagServiceImpl.java +++ b/juick-server/src/main/java/com/juick/service/TagServiceImpl.java @@ -19,7 +19,7 @@ package com.juick.service; import com.juick.Tag; import com.juick.User; -import com.juick.server.helpers.TagStats; +import com.juick.model.TagStats; import com.juick.util.StreamUtils; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; diff --git a/juick-server/src/main/java/com/juick/service/UserServiceImpl.java b/juick-server/src/main/java/com/juick/service/UserServiceImpl.java index bd72bed9..82e9a124 100644 --- a/juick-server/src/main/java/com/juick/service/UserServiceImpl.java +++ b/juick-server/src/main/java/com/juick/service/UserServiceImpl.java @@ -19,9 +19,9 @@ package com.juick.service; import com.juick.Message; import com.juick.User; -import com.juick.server.helpers.AnonymousUser; -import com.juick.server.helpers.Auth; -import com.juick.server.helpers.UserInfo; +import com.juick.model.AnonymousUser; +import com.juick.model.Auth; +import com.juick.model.UserInfo; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; diff --git a/juick-server/src/main/java/com/juick/service/security/HashParamAuthenticationFilter.java b/juick-server/src/main/java/com/juick/service/security/HashParamAuthenticationFilter.java new file mode 100644 index 00000000..9215d09a --- /dev/null +++ b/juick-server/src/main/java/com/juick/service/security/HashParamAuthenticationFilter.java @@ -0,0 +1,103 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.service.security; + +import com.juick.User; +import com.juick.service.security.entities.JuickUser; +import com.juick.service.UserService; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.RememberMeAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.WebUtils; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Created by aalexeev on 4/5/17. + */ +public class HashParamAuthenticationFilter extends OncePerRequestFilter { + public static final String PARAM_NAME = "hash"; + + private final UserService userService; + private final RememberMeServices rememberMeServices; + + + public HashParamAuthenticationFilter( + final UserService userService, + final RememberMeServices rememberMeServices) { + Assert.notNull(userService, "userService should not be null"); + Assert.notNull(rememberMeServices, "rememberMeServices should not be null"); + + this.userService = userService; + this.rememberMeServices = rememberMeServices; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + String hash = getHashFromRequest(request); + + if (hash != null && authenticationIsRequired()) { + User user = userService.getUserByHash(hash); + + if (!user.isAnonymous()) { + User userWithPassword = userService.getUserByName(user.getName()); + userWithPassword.setAuthHash(userService.getHashByUID(userWithPassword.getUid())); + Authentication authentication = new RememberMeAuthenticationToken( + ((AbstractRememberMeServices)rememberMeServices).getKey(), new JuickUser(userWithPassword), JuickUser.USER_AUTHORITY); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + rememberMeServices.loginSuccess(request, response, authentication); + } + } + + filterChain.doFilter(request, response); + } + + private boolean authenticationIsRequired() { + Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication(); + + return existingAuth == null || + !existingAuth.isAuthenticated() || + existingAuth instanceof AnonymousAuthenticationToken; + } + + private String getHashFromRequest(HttpServletRequest request) { + String paramHash = request.getParameter(PARAM_NAME); + Cookie cookieHash = WebUtils.getCookie(request, PARAM_NAME); + + if (paramHash == null && cookieHash != null) { + return cookieHash.getValue(); + } + return paramHash; + } +} diff --git a/juick-server/src/main/java/com/juick/service/security/JuickUserDetailsService.java b/juick-server/src/main/java/com/juick/service/security/JuickUserDetailsService.java new file mode 100644 index 00000000..59425fab --- /dev/null +++ b/juick-server/src/main/java/com/juick/service/security/JuickUserDetailsService.java @@ -0,0 +1,53 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.service.security; + +import com.juick.service.UserService; +import com.juick.service.security.entities.JuickUser; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.util.Assert; + +/** + * Created by aalexeev on 11/28/16. + */ +public class JuickUserDetailsService implements UserDetailsService { + private final UserService userService; + + public JuickUserDetailsService(final UserService userService) { + Assert.notNull(userService, "UserService must be initialized"); + this.userService = userService; + } + + @Override + public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { + if (StringUtils.isBlank(username)) + throw new UsernameNotFoundException("Invalid user name " + username); + + com.juick.User user = userService.getUserByName(username); + + if (!user.isAnonymous()) { + user.setAuthHash(userService.getHashByUID(user.getUid())); + return new JuickUser(user); + } + + throw new UsernameNotFoundException("The username " + username + " is not found"); + } +} diff --git a/juick-server/src/main/java/com/juick/service/security/NullUserDetailsService.java b/juick-server/src/main/java/com/juick/service/security/NullUserDetailsService.java new file mode 100644 index 00000000..91acefa3 --- /dev/null +++ b/juick-server/src/main/java/com/juick/service/security/NullUserDetailsService.java @@ -0,0 +1,33 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.service.security; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +/** + * Created by aalexeev on 11/28/16. + */ +public class NullUserDetailsService implements UserDetailsService { + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + throw new UsernameNotFoundException( + "loadUserByUsername called for NullUserDetailsService, user " + username + "can not be found"); + } +} diff --git a/juick-server/src/main/java/com/juick/service/security/deprecated/CookieSimpleHashRememberMeServices.java b/juick-server/src/main/java/com/juick/service/security/deprecated/CookieSimpleHashRememberMeServices.java new file mode 100644 index 00000000..e385d7dd --- /dev/null +++ b/juick-server/src/main/java/com/juick/service/security/deprecated/CookieSimpleHashRememberMeServices.java @@ -0,0 +1,130 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.service.security.deprecated; + +import com.juick.User; +import com.juick.service.security.entities.JuickUser; +import com.juick.service.UserService; +import com.juick.service.security.NullUserDetailsService; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; +import org.springframework.security.web.authentication.rememberme.InvalidCookieException; +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Optional; + +/** + * Created by aalexeev on 11/28/16. + * + * @deprecated not recommended use for secure reasons + */ +@Deprecated +public class CookieSimpleHashRememberMeServices extends AbstractRememberMeServices implements RememberMeServices { + private static final Logger logger = LoggerFactory.getLogger(CookieSimpleHashRememberMeServices.class); + + private static final String COOKIE_PARAM_NAME = "hash"; + + private final UserService userService; + + public CookieSimpleHashRememberMeServices( + final String key, final UserService userService, final Environment environment) { + super(key, new NullUserDetailsService()); + + Assert.notNull(userService); + Assert.notNull(environment); + + this.userService = userService; + + setCookieName(COOKIE_PARAM_NAME); + setCookieDomain(environment.getProperty("web_domain", "localhost")); + setAlwaysRemember(true); + } + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + super.logout(request, response, authentication); + userService.deleteLoginForUser(authentication.getName()); + } + + @Override + protected void onLoginSuccess( + HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { + String username = successfulAuthentication.getName(); + + logger.debug("Creating new persistent login for user {}", username); + + try { + int uid = userService.getUIDbyName(username); + + Assert.isTrue(uid > 0); + + String hash = RandomStringUtils.randomAlphanumeric(16).toUpperCase(); + + userService.setLoginForUser(uid, hash); + + setCookie(new String[]{hash}, getTokenValiditySeconds(), request, response); + } catch (Exception e) { + logger.error("Failed to save cookies", e); + } + } + + @Override + protected UserDetails processAutoLoginCookie( + String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) + throws RememberMeAuthenticationException, UsernameNotFoundException { + String hash = cookieTokens[0]; + + if (StringUtils.isBlank(hash)) { + hash = request.getParameter("hash"); + } + if (StringUtils.isBlank(hash)) { + throw new InvalidCookieException("Cookie is invalid and hash parameter not found"); + } + + int uid = userService.getUIDbyHash(hash); + if (uid <= 0) + throw new UsernameNotFoundException("User not found by hash, cookies" + cookieTokens); + + Optional<User> userOptional = userService.getUserByUID(uid); + + Assert.isTrue(userOptional.isPresent()); + + return new JuickUser(userService.getUserByName(userOptional.get().getName())); + } + + @Override + protected String[] decodeCookie(String cookieValue) throws InvalidCookieException { + return new String[]{cookieValue}; + } + + @Override + protected String encodeCookie(String[] cookieTokens) { + return cookieTokens != null && cookieTokens.length > 0 ? cookieTokens[0] : StringUtils.EMPTY; + } +} diff --git a/juick-server/src/main/java/com/juick/service/security/deprecated/RequestParamHashRememberMeServices.java b/juick-server/src/main/java/com/juick/service/security/deprecated/RequestParamHashRememberMeServices.java new file mode 100644 index 00000000..3631e5a4 --- /dev/null +++ b/juick-server/src/main/java/com/juick/service/security/deprecated/RequestParamHashRememberMeServices.java @@ -0,0 +1,88 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.service.security.deprecated; + +import com.juick.User; +import com.juick.service.security.entities.JuickUser; +import com.juick.service.UserService; +import com.juick.service.security.NullUserDetailsService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Created by aalexeev on 11/30/16. + * + * @deprecated for security reasons + */ +@Deprecated +public class RequestParamHashRememberMeServices extends AbstractRememberMeServices implements RememberMeServices { + private static final String PARAM_NAME = "hash"; + + private final UserService userService; + + public RequestParamHashRememberMeServices(String key, UserService userService) { + super(key, new NullUserDetailsService()); + + Assert.notNull(userService); + this.userService = userService; + setAlwaysRemember(false); + } + + @Override + protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { + // do nothing + } + + @Override + protected boolean rememberMeRequested(HttpServletRequest request, String parameter) { + return false; // always false + } + + @Override + protected void cancelCookie(HttpServletRequest request, HttpServletResponse response) { + // do nothing + } + + @Override + protected String extractRememberMeCookie(HttpServletRequest request) { + return PARAM_NAME; // return any not blank value + } + + @Override + protected UserDetails processAutoLoginCookie( + String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) + throws RememberMeAuthenticationException, UsernameNotFoundException { + String hash = request.getParameter(PARAM_NAME); + + if (StringUtils.isNotBlank(hash)) { + User user = userService.getUserByHash(hash); + if (!user.isAnonymous()) + return new JuickUser(userService.getUserByName(user.getName())); + } + throw new UsernameNotFoundException("User not found by hash " + hash); + } +} diff --git a/juick-server/src/main/java/com/juick/service/security/entities/JuickUser.java b/juick-server/src/main/java/com/juick/service/security/entities/JuickUser.java new file mode 100644 index 00000000..c43f112f --- /dev/null +++ b/juick-server/src/main/java/com/juick/service/security/entities/JuickUser.java @@ -0,0 +1,93 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.service.security.entities; + +import com.juick.User; +import com.juick.model.AnonymousUser; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Created by aalexeev on 11/21/16. + */ +public class JuickUser implements UserDetails { + static final GrantedAuthority ROLE_USER = new SimpleGrantedAuthority("ROLE_USER"); + static final GrantedAuthority ROLE_ANONYMOUS = new SimpleGrantedAuthority("ROLE_ANONYMOUS"); + + public static final List<GrantedAuthority> USER_AUTHORITY = Collections.singletonList(ROLE_USER); + public static final List<GrantedAuthority> ANONYMOUS_AUTHORITY = Collections.singletonList(ROLE_ANONYMOUS); + + public static final JuickUser ANONYMOUS_USER = new JuickUser(AnonymousUser.INSTANCE, ANONYMOUS_AUTHORITY); + + private final com.juick.User user; + private final Collection<? extends GrantedAuthority> authorities; + + public JuickUser(com.juick.User user) { + this(user, USER_AUTHORITY); + } + + public JuickUser(com.juick.User user, Collection<? extends GrantedAuthority> authorities) { + this.user = user; + this.authorities = authorities; + } + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return "{noop}" + user.getCredentials(); + } + + @Override + public String getUsername() { + return user.getName(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return StringUtils.isNotBlank(user.getCredentials()); + } + + @Override + public boolean isCredentialsNonExpired() { + return isAccountNonLocked(); + } + + @Override + public boolean isEnabled() { + return !user.isBanned() && isCredentialsNonExpired(); + } + + public User getUser() { + return user; + } +} |