From a608baeed738894433aacfa041e2617f60ce959f Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Sat, 4 Apr 2020 01:15:01 +0300 Subject: Initialize all components from configuration --- src/main/java/com/juick/API.java | 2 +- src/main/java/com/juick/ActivityPubManager.java | 405 +++++++++++++ src/main/java/com/juick/CommandsManager.java | 590 +++++++++++++++++++ src/main/java/com/juick/EmailManager.java | 202 +++++++ src/main/java/com/juick/KeystoreManager.java | 92 +++ src/main/java/com/juick/ServerManager.java | 184 ++++++ src/main/java/com/juick/SignatureManager.java | 169 ++++++ src/main/java/com/juick/TelegramBotManager.java | 465 +++++++++++++++ src/main/java/com/juick/TopManager.java | 71 +++ src/main/java/com/juick/TwitterManager.java | 116 ++++ src/main/java/com/juick/XMPPManager.java | 644 +++++++++++++++++++++ .../java/com/juick/adapters/SimpleDateAdapter.java | 40 -- .../config/ActivityPubClientErrorHandler.java | 52 ++ .../java/com/juick/config/ActivityPubConfig.java | 79 +++ src/main/java/com/juick/config/MailConfig.java | 32 + src/main/java/com/juick/config/RssConfig.java | 39 ++ src/main/java/com/juick/config/SapeConfig.java | 39 ++ src/main/java/com/juick/config/SecurityConfig.java | 224 +++++++ .../com/juick/config/SignInWithAppleConfig.java | 45 ++ src/main/java/com/juick/config/StorageConfig.java | 37 ++ src/main/java/com/juick/config/TelegramConfig.java | 32 + src/main/java/com/juick/config/TwitterConfig.java | 32 + src/main/java/com/juick/config/WebConfig.java | 181 ++++++ src/main/java/com/juick/config/XMPPConfig.java | 41 ++ .../com/juick/formatters/PlainTextFormatter.java | 105 ---- src/main/java/com/juick/model/Message.java | 2 +- .../java/com/juick/server/ActivityPubManager.java | 407 ------------- .../java/com/juick/server/CommandsManager.java | 594 ------------------- src/main/java/com/juick/server/EmailManager.java | 202 ------- .../java/com/juick/server/KeystoreManager.java | 92 --- src/main/java/com/juick/server/ServerManager.java | 186 ------ .../java/com/juick/server/SignatureManager.java | 171 ------ .../java/com/juick/server/TelegramBotManager.java | 465 --------------- src/main/java/com/juick/server/TopManager.java | 73 --- src/main/java/com/juick/server/TwitterManager.java | 118 ---- src/main/java/com/juick/server/Utils.java | 45 -- src/main/java/com/juick/server/XMPPManager.java | 644 --------------------- .../configuration/ActivityPubClientConfig.java | 72 --- .../ActivityPubClientErrorHandler.java | 52 -- .../server/configuration/ApiAppConfiguration.java | 49 -- .../server/configuration/BaseWebConfiguration.java | 56 -- .../server/configuration/MailConfiguration.java | 32 - .../server/configuration/SapeConfiguration.java | 39 -- .../juick/server/configuration/SecurityConfig.java | 224 ------- .../configuration/SignInWithAppleConfig.java | 45 -- .../server/configuration/StorageConfiguration.java | 37 -- .../juick/server/configuration/TelegramConfig.java | 32 - .../server/configuration/WwwAppConfiguration.java | 139 ----- .../com/juick/server/configuration/XMPPConfig.java | 42 -- .../server/helpers/HeaderRequestInterceptor.java | 43 -- .../server/helpers/annotation/UserCommand.java | 50 -- .../juick/server/util/HttpBadRequestException.java | 31 - .../juick/server/util/HttpForbiddenException.java | 33 -- .../juick/server/util/HttpNotFoundException.java | 32 - src/main/java/com/juick/server/util/HttpUtils.java | 115 ---- .../java/com/juick/server/util/ImageUtils.java | 173 ------ src/main/java/com/juick/server/util/TagUtils.java | 42 -- src/main/java/com/juick/server/util/WebUtils.java | 62 -- .../java/com/juick/server/xmpp/JidConverter.java | 32 - .../com/juick/server/xmpp/iq/MessageQuery.java | 27 - .../com/juick/server/xmpp/iq/package-info.java | 25 - .../java/com/juick/service/ImagesServiceImpl.java | 2 +- src/main/java/com/juick/service/UserService.java | 4 - .../java/com/juick/service/UserServiceImpl.java | 26 - .../HTTPSignatureAuthenticationFilter.java | 2 +- .../com/juick/util/HeaderRequestInterceptor.java | 43 ++ .../com/juick/util/HttpBadRequestException.java | 31 + .../com/juick/util/HttpForbiddenException.java | 33 ++ .../java/com/juick/util/HttpNotFoundException.java | 32 + src/main/java/com/juick/util/HttpUtils.java | 115 ++++ src/main/java/com/juick/util/ImageUtils.java | 173 ++++++ src/main/java/com/juick/util/TagUtils.java | 42 ++ src/main/java/com/juick/util/WebUtils.java | 79 +++ .../com/juick/util/adapters/SimpleDateAdapter.java | 40 ++ .../com/juick/util/annotation/UserCommand.java | 50 ++ .../juick/util/formatters/PlainTextFormatter.java | 105 ++++ .../java/com/juick/util/xmpp/JidConverter.java | 32 + .../java/com/juick/util/xmpp/iq/MessageQuery.java | 27 + .../java/com/juick/util/xmpp/iq/package-info.java | 25 + .../java/com/juick/www/api/ApiSocialLogin.java | 2 +- src/main/java/com/juick/www/api/Messages.java | 8 +- src/main/java/com/juick/www/api/Notifications.java | 2 +- src/main/java/com/juick/www/api/PM.java | 6 +- src/main/java/com/juick/www/api/Post.java | 12 +- src/main/java/com/juick/www/api/Service.java | 10 +- src/main/java/com/juick/www/api/Users.java | 8 +- .../java/com/juick/www/api/activity/Profile.java | 12 +- .../java/com/juick/www/api/webfinger/Resource.java | 2 +- .../juick/www/api/webhooks/TelegramWebhook.java | 2 +- src/main/java/com/juick/www/controllers/Help.java | 2 +- .../java/com/juick/www/controllers/Settings.java | 4 +- .../java/com/juick/www/controllers/SignUp.java | 4 +- src/main/java/com/juick/www/controllers/Site.java | 13 +- .../com/juick/www/controllers/SocialLogin.java | 10 +- .../java/com/juick/www/filters/AnythingFilter.java | 2 +- src/main/java/com/juick/www/rss/Feeds.java | 2 +- .../juick/config/DataSourceAutoConfiguration.java | 49 ++ .../com/juick/config/SwaggerConfiguration.java | 45 ++ .../juick/config/TestActivityConfiguration.java | 36 ++ .../configuration/DataSourceAutoConfiguration.java | 49 -- .../server/configuration/SwaggerConfiguration.java | 45 -- .../configuration/TestActivityConfiguration.java | 36 -- .../java/com/juick/server/tests/ServerTests.java | 10 +- 103 files changed, 4786 insertions(+), 4847 deletions(-) create mode 100644 src/main/java/com/juick/ActivityPubManager.java create mode 100644 src/main/java/com/juick/CommandsManager.java create mode 100644 src/main/java/com/juick/EmailManager.java create mode 100644 src/main/java/com/juick/KeystoreManager.java create mode 100644 src/main/java/com/juick/ServerManager.java create mode 100644 src/main/java/com/juick/SignatureManager.java create mode 100644 src/main/java/com/juick/TelegramBotManager.java create mode 100644 src/main/java/com/juick/TopManager.java create mode 100644 src/main/java/com/juick/TwitterManager.java create mode 100644 src/main/java/com/juick/XMPPManager.java delete mode 100644 src/main/java/com/juick/adapters/SimpleDateAdapter.java create mode 100644 src/main/java/com/juick/config/ActivityPubClientErrorHandler.java create mode 100644 src/main/java/com/juick/config/ActivityPubConfig.java create mode 100644 src/main/java/com/juick/config/MailConfig.java create mode 100644 src/main/java/com/juick/config/RssConfig.java create mode 100644 src/main/java/com/juick/config/SapeConfig.java create mode 100644 src/main/java/com/juick/config/SecurityConfig.java create mode 100644 src/main/java/com/juick/config/SignInWithAppleConfig.java create mode 100644 src/main/java/com/juick/config/StorageConfig.java create mode 100644 src/main/java/com/juick/config/TelegramConfig.java create mode 100644 src/main/java/com/juick/config/TwitterConfig.java create mode 100644 src/main/java/com/juick/config/WebConfig.java create mode 100644 src/main/java/com/juick/config/XMPPConfig.java delete mode 100644 src/main/java/com/juick/formatters/PlainTextFormatter.java delete mode 100644 src/main/java/com/juick/server/ActivityPubManager.java delete mode 100644 src/main/java/com/juick/server/CommandsManager.java delete mode 100644 src/main/java/com/juick/server/EmailManager.java delete mode 100644 src/main/java/com/juick/server/KeystoreManager.java delete mode 100644 src/main/java/com/juick/server/ServerManager.java delete mode 100644 src/main/java/com/juick/server/SignatureManager.java delete mode 100644 src/main/java/com/juick/server/TelegramBotManager.java delete mode 100644 src/main/java/com/juick/server/TopManager.java delete mode 100644 src/main/java/com/juick/server/TwitterManager.java delete mode 100644 src/main/java/com/juick/server/Utils.java delete mode 100644 src/main/java/com/juick/server/XMPPManager.java delete mode 100644 src/main/java/com/juick/server/configuration/ActivityPubClientConfig.java delete mode 100644 src/main/java/com/juick/server/configuration/ActivityPubClientErrorHandler.java delete mode 100644 src/main/java/com/juick/server/configuration/ApiAppConfiguration.java delete mode 100644 src/main/java/com/juick/server/configuration/BaseWebConfiguration.java delete mode 100644 src/main/java/com/juick/server/configuration/MailConfiguration.java delete mode 100644 src/main/java/com/juick/server/configuration/SapeConfiguration.java delete mode 100644 src/main/java/com/juick/server/configuration/SecurityConfig.java delete mode 100644 src/main/java/com/juick/server/configuration/SignInWithAppleConfig.java delete mode 100644 src/main/java/com/juick/server/configuration/StorageConfiguration.java delete mode 100644 src/main/java/com/juick/server/configuration/TelegramConfig.java delete mode 100644 src/main/java/com/juick/server/configuration/WwwAppConfiguration.java delete mode 100644 src/main/java/com/juick/server/configuration/XMPPConfig.java delete mode 100644 src/main/java/com/juick/server/helpers/HeaderRequestInterceptor.java delete mode 100644 src/main/java/com/juick/server/helpers/annotation/UserCommand.java delete mode 100644 src/main/java/com/juick/server/util/HttpBadRequestException.java delete mode 100644 src/main/java/com/juick/server/util/HttpForbiddenException.java delete mode 100644 src/main/java/com/juick/server/util/HttpNotFoundException.java delete mode 100644 src/main/java/com/juick/server/util/HttpUtils.java delete mode 100644 src/main/java/com/juick/server/util/ImageUtils.java delete mode 100644 src/main/java/com/juick/server/util/TagUtils.java delete mode 100644 src/main/java/com/juick/server/util/WebUtils.java delete mode 100644 src/main/java/com/juick/server/xmpp/JidConverter.java delete mode 100644 src/main/java/com/juick/server/xmpp/iq/MessageQuery.java delete mode 100644 src/main/java/com/juick/server/xmpp/iq/package-info.java create mode 100644 src/main/java/com/juick/util/HeaderRequestInterceptor.java create mode 100644 src/main/java/com/juick/util/HttpBadRequestException.java create mode 100644 src/main/java/com/juick/util/HttpForbiddenException.java create mode 100644 src/main/java/com/juick/util/HttpNotFoundException.java create mode 100644 src/main/java/com/juick/util/HttpUtils.java create mode 100644 src/main/java/com/juick/util/ImageUtils.java create mode 100644 src/main/java/com/juick/util/TagUtils.java create mode 100644 src/main/java/com/juick/util/WebUtils.java create mode 100644 src/main/java/com/juick/util/adapters/SimpleDateAdapter.java create mode 100644 src/main/java/com/juick/util/annotation/UserCommand.java create mode 100644 src/main/java/com/juick/util/formatters/PlainTextFormatter.java create mode 100644 src/main/java/com/juick/util/xmpp/JidConverter.java create mode 100644 src/main/java/com/juick/util/xmpp/iq/MessageQuery.java create mode 100644 src/main/java/com/juick/util/xmpp/iq/package-info.java create mode 100644 src/test/java/com/juick/config/DataSourceAutoConfiguration.java create mode 100644 src/test/java/com/juick/config/SwaggerConfiguration.java create mode 100644 src/test/java/com/juick/config/TestActivityConfiguration.java delete mode 100644 src/test/java/com/juick/server/configuration/DataSourceAutoConfiguration.java delete mode 100644 src/test/java/com/juick/server/configuration/SwaggerConfiguration.java delete mode 100644 src/test/java/com/juick/server/configuration/TestActivityConfiguration.java diff --git a/src/main/java/com/juick/API.java b/src/main/java/com/juick/API.java index dbe00ad9..608bd78e 100644 --- a/src/main/java/com/juick/API.java +++ b/src/main/java/com/juick/API.java @@ -25,7 +25,7 @@ import org.springframework.context.annotation.ComponentScan; @SpringBootApplication @EnableAutoConfiguration(exclude = { MailSenderAutoConfiguration.class }) -@ComponentScan(basePackages = {"com.juick.server", "com.juick.service", "com.juick.www"}) +@ComponentScan(basePackages = {"com.juick.config", "com.juick.service", "com.juick.www"}) public class API { public static void main(String[] args) { SpringApplication.run(API.class, args); diff --git a/src/main/java/com/juick/ActivityPubManager.java b/src/main/java/com/juick/ActivityPubManager.java new file mode 100644 index 00000000..e3b1ac8e --- /dev/null +++ b/src/main/java/com/juick/ActivityPubManager.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick; + +import com.juick.model.Message; +import com.juick.model.Reaction; +import com.juick.model.User; +import com.juick.util.formatters.PlainTextFormatter; +import com.juick.model.Tag; +import com.juick.www.api.SystemActivity.ActivityType; +import com.juick.www.api.activity.model.Context; +import com.juick.www.api.activity.model.activities.*; +import com.juick.www.api.activity.model.objects.Hashtag; +import com.juick.www.api.activity.model.objects.Image; +import com.juick.www.api.activity.model.objects.Mention; +import com.juick.www.api.activity.model.objects.Note; +import com.juick.www.api.activity.model.objects.Person; +import com.juick.util.HttpBadRequestException; +import com.juick.util.HttpUtils; +import com.juick.service.MessagesService; +import com.juick.service.SocialService; +import com.juick.service.UserService; +import com.juick.service.activities.*; +import com.juick.service.component.*; +import com.juick.util.MessageUtils; +import com.mitchellbosecke.pebble.PebbleEngine; +import com.mitchellbosecke.pebble.template.PebbleTemplate; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class ActivityPubManager implements ActivityListener, NotificationListener { + private static final Logger logger = LoggerFactory.getLogger("ActivityPub"); + @Inject + private SignatureManager signatureManager; + @Inject + private SocialService socialService; + @Inject + private UserService userService; + @Inject + private MessagesService messagesService; + @Inject + private PebbleEngine pebbleEngine; + @Value("${ap_base_uri:http://localhost:8080/}") + private String baseUri; + @Value("${service_user:juick}") + private String serviceUsername; + + private User serviceUser; + + @PostConstruct + public void init() { + serviceUser = userService.getUserByName(serviceUsername); + } + + @Override + public void processFollowEvent(@Nonnull FollowEvent followEvent) { + String acct = (String)followEvent.getRequest().getObject(); + logger.info("received follower request to {}", acct); + User followedUser = socialService.getUserByAccountUri(acct); + if (!followedUser.isAnonymous()) { + // automatically accept follower requests + Person me = (Person) signatureManager.getContext(URI.create(acct)).get(); + Person follower = (Person) signatureManager.getContext(URI.create(followEvent.getRequest().getActor())).get(); + Accept accept = new Accept(); + accept.setActor(me.getId()); + accept.setObject(followEvent.getRequest()); + try { + signatureManager.post(me, follower, accept); + socialService.addFollower(followedUser, follower.getId()); + logger.info("Follower added for {}", followedUser.getName()); + } catch (IOException e) { + logger.info("activitypub exception", e); + } + } + } + + @Override + public void undoFollowEvent(UndoFollowEvent event) { + String actor = event.getActor(); + String me = event.getObject(); + logger.info("{} stopping to follow {}", actor, me); + User followedUser = socialService.getUserByAccountUri(me); + if (!followedUser.isAnonymous()) { + socialService.removeFollower(followedUser, actor); + } + } + + @Override + public void deleteUserEvent(DeleteUserEvent event) { + String acct = event.getUserUri(); + logger.info("Deleting {} from followers", acct); + socialService.removeAccount(acct); + } + + @Override + public void deleteMessageEvent(DeleteMessageEvent event) { + Message msg = event.getMessage(); + User user = msg.getUser(); + String userUri = personUri(user); + Note note = makeNote(msg); + Person me = (Person) signatureManager.getContext(URI.create(userUri)).get(); + socialService.getFollowers(user).forEach(acct -> { + Person follower = (Person) signatureManager.getContext(URI.create(acct)).get(); + Delete delete = new Delete(); + delete.setId(note.getId()); + delete.setActor(me.getId()); + delete.setPublished(note.getPublished()); + delete.setObject(note); + try { + logger.info("Deletion to follower {}", follower.getId()); + signatureManager.post(me, follower, delete); + } catch (IOException e) { + logger.warn("activitypub exception", e); + } + }); + } + + @Override + public void processAnnounceEvent(AnnounceEvent event) { + UriComponents uriComponents = UriComponentsBuilder.fromUriString(event.getMessageUri()).build(); + List segments = uriComponents.getPathSegments(); + if (segments.get(0).equals("n")) { + String[] ids = segments.get(1).split("-", 2); + if (ids.length == 2 && Integer.parseInt(ids[1]) == 0) { + // only messages + logger.info("{} recommends {}", event.getActorUri(), Integer.valueOf(ids[0])); + messagesService.likeMessage(Integer.parseInt(ids[0]), 0, Reaction.LIKE, event.getActorUri()); + } + } + } + + @Override + public void undoAnnounceEvent(UndoAnnounceEvent event) { + UriComponents uriComponents = UriComponentsBuilder.fromUriString(event.getMessageUri()).build(); + List segments = uriComponents.getPathSegments(); + if (segments.get(0).equals("n")) { + String[] ids = segments.get(1).split("-", 2); + if (ids.length == 2 && Integer.parseInt(ids[1]) == 0) { + // only messages + logger.info("{} stop recommending {}", event.getActorUri(), Integer.valueOf(ids[0])); + messagesService.likeMessage(Integer.parseInt(ids[0]), 0, null, event.getActorUri()); + } + } + } + + @Override + public void processUpdateEvent(UpdateEvent event) { + String objectUri = event.getMessageUri(); + User user = event.getUser(); + String userUri = personUri(user); + Person me = (Person) signatureManager.getContext(URI.create(userUri)).get(); + socialService.getFollowers(user).forEach(acct -> { + Person follower = (Person) signatureManager.getContext(URI.create(acct)).get(); + Update update = new Update(); + update.setId(objectUri + "#update"); + update.setActor(me.getId()); + update.setObject(objectUri); + try { + logger.info("Update to follower {}", follower.getId()); + signatureManager.post(me, follower, update); + } catch (IOException e) { + logger.warn("activitypub exception", e); + } + }); + } + + @Override + public void processSystemEvent(SystemEvent systemEvent) { + ActivityType type = systemEvent.getActivity().getType(); + if (type.equals(ActivityType.message)) { + processMessage(systemEvent.getActivity().getMessage()); + } else if (type.equals(ActivityType.like)) { + if (systemEvent.getActivity().getFrom().equals(serviceUser)) { + processTop(systemEvent.getActivity().getMessage()); + } + } + } + private void processMessage(Message msg) { + if (MessageUtils.isPM(msg) || msg.isService()) { + return; + } + User user = msg.getUser(); + String userUri = personUri(user); + Note note = makeNote(msg); + Person me = (Person) signatureManager.getContext(URI.create(userUri)).get(); + Set subscribers = new HashSet<>(socialService.getFollowers(user)); + if (MessageUtils.isReply(msg) && msg.getTo().getUri().toASCIIString().length() > 0) { + String replier = msg.getTo().getUri().toASCIIString(); + subscribers.add(replier); + List cc = new ArrayList<>(note.getCc()); + cc.add(replier); + note.setCc(cc); + } + subscribers.addAll(note.getCc()); + subscribers.forEach(acct -> { + Optional context = signatureManager.getContext(URI.create(acct)); + if (context.isPresent() && context.get() instanceof Person) { + Person follower = (Person)context.get(); + Create create = new Create(); + create.setId(note.getId()); + create.setActor(me.getId()); + create.setPublished(note.getPublished()); + create.setObject(note); + try { + signatureManager.post(me, follower, create); + } catch (IOException e) { + logger.warn("activitypub exception", e); + } + } + }); + } + + public String inboxUri() { + UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); + return uri.replacePath("/api/inbox").toUriString(); + } + + public String outboxUri(User user) { + UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); + return uri.replacePath(String.format("/u/%s/blog/toc", user.getName())).toUriString(); + } + + public String personUri(User user) { + if (user.getUri().toString().length() > 0) { + return user.getUri().toASCIIString(); + } + UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); + return uri.replacePath(String.format("/u/%s", user.getName())).toUriString(); + } + public String personWebUri(User user) { + UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); + return uri.replacePath(String.format("/%s/", user.getName())).toUriString(); + } + + public String followersUri(User user) { + UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); + return uri.replacePath(String.format("/u/%s/followers/toc", user.getName())).toUriString(); + } + + public String followingUri(User user) { + UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); + return uri.replacePath(String.format("/u/%s/following/toc", user.getName())).toUriString(); + } + public String messageUri(Message msg) { + return messageUri(msg.getMid(), msg.getRid()); + } + public String messageUri(int mid, int rid) { + UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); + uri.replacePath(String.format("/n/%d-%d", mid, rid)); + return uri.toUriString(); + } + public String tagUri(Tag tag) { + UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); + return uri.replacePath(String.format("/t/%s", tag.getName())).toUriString(); + } + + public String postId(String messageUri) { + UriComponents uri = UriComponentsBuilder.fromUriString(messageUri).build(); + return uri.getPath().substring(uri.getPath().lastIndexOf('/') + 1).replace("-", "/"); + } + + public Note makeNote(Message msg) { + Note note = new Note(); + note.setId(messageUri(msg)); + note.setUrl(PlainTextFormatter.formatUrl(msg)); + note.setAttributedTo(personUri(msg.getUser())); + if (MessageUtils.isReply(msg)) { + if (msg.getReplyToUri().toASCIIString().length() > 0) { + note.setInReplyTo(msg.getReplyToUri().toASCIIString()); + } else { + note.setInReplyTo(messageUri(msg.getMid(), msg.getReplyto())); + } + } + if (MessageUtils.isPM(msg)) { + note.setTo(Collections.singletonList(personUri(msg.getTo()))); + } else { + note.setTo(Collections.singletonList(Context.ACTIVITYSTREAMS_PUBLIC)); + note.setCc(Collections.singletonList(followersUri(msg.getUser()))); + } + note.setPublished(msg.getCreated()); + if (StringUtils.isNotBlank(msg.getAttachmentType())) { + Image attachment = new Image(); + attachment.setId(msg.getAttachment().getMedium().getUrl()); + attachment.setUrl(msg.getAttachment().getMedium().getUrl()); + attachment.setMediaType(HttpUtils.mediaType(msg.getAttachmentType())); + note.setAttachment(Collections.singletonList(attachment)); + } + note.setTags(msg.getTags().stream().map(t -> new Hashtag(tagUri(t), t.getName())).collect(Collectors.toList())); + if (msg.getReplyToUri() != null && msg.getReplyToUri().toASCIIString().length() > 0) { + Optional noteContext = signatureManager.getContext(msg.getReplyToUri()); + if (noteContext.isPresent()) { + Note activity = (Note) noteContext.get(); + Optional personContext = signatureManager.getContext(URI.create(activity.getAttributedTo())); + if (personContext.isPresent()) { + Person person = (Person) personContext.get(); + note.getTags().add(new Mention(person.getUrl(), person.getPreferredUsername())); + msg.getTo().setName(person.getPreferredUsername()); + note.setInReplyTo(activity.getInReplyTo()); + } + } + } else if (MessageUtils.isReply(msg)) { + note.getTags().add(new Mention(personWebUri(msg.getTo()), msg.getTo().getName())); + } + MessageUtils.getGlobalMentions(msg).forEach(m -> { + // @user@server.tld -> user@server.tld + Optional personContext = signatureManager.discoverPerson(m.substring(1)); + if (personContext.isPresent()) { + Person person = (Person) personContext.get(); + note.getTags().add(new Mention(person.getUrl(), person.getPreferredUsername())); + List cc = new ArrayList<>(note.getCc()); + cc.add(person.getId()); + note.setCc(cc); + } + }); + if (msg.isHtml()) { + note.setContent(msg.getText()); + } else { + PebbleTemplate noteTemplate = pebbleEngine.getTemplate("layouts/note"); + Map context = new HashMap<>(); + context.put("msg", msg); + context.put("baseUri", baseUri); + try { + Writer writer = new StringWriter(); + noteTemplate.evaluate(writer, context); + note.setContent(writer.toString()); + } catch (IOException e) { + logger.warn("template not rendered, falling back"); + note.setContent(MessageUtils.formatMessage(StringUtils.defaultString(msg.getText()))); + } + } + return note; + } + + @Override + public void processPingEvent(PingEvent pingEvent) { + + } + + private void processTop(Message message) { + Note note = makeNote(message); + Announce announce = new Announce(); + announce.setId(note.getId() + "#top"); + announce.setActor(personUri(serviceUser)); + announce.setTo(Collections.singletonList(Context.ACTIVITYSTREAMS_PUBLIC)); + announce.setObject(note); + Person me = (Person) signatureManager.getContext(URI.create(announce.getActor())).get(); + socialService.getFollowers(serviceUser).forEach(acct -> { + var follower = signatureManager.getContext(URI.create(acct)); + follower.ifPresentOrElse((person) -> { + try { + logger.info("Announcing top: {}", message.getMid()); + signatureManager.post(me, (Person)person, announce); + } catch (IOException e) { + logger.warn("activitypub exception", e); + } + }, () -> logger.warn("Follower not found: {}", acct)); + }); + } + public User personToUser(URI uri) throws HttpBadRequestException { + Person person = (Person) signatureManager.getContext(uri).orElseThrow(HttpBadRequestException::new); + User user = new User(); + user.setUri(URI.create(person.getId())); + user.setName(person.getPreferredUsername()); + if (person.getIcon() != null) { + user.setAvatar(person.getIcon().getUrl()); + } + return user; + } +} diff --git a/src/main/java/com/juick/CommandsManager.java b/src/main/java/com/juick/CommandsManager.java new file mode 100644 index 00000000..f7c37b8b --- /dev/null +++ b/src/main/java/com/juick/CommandsManager.java @@ -0,0 +1,590 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick; + +import com.juick.model.Message; +import com.juick.model.Tag; +import com.juick.model.User; +import com.juick.util.formatters.PlainTextFormatter; +import com.juick.model.CommandResult; +import com.juick.model.TagStats; +import com.juick.www.api.SystemActivity; +import com.juick.util.annotation.UserCommand; +import com.juick.util.HttpUtils; +import com.juick.www.WebApp; +import com.juick.service.*; +import com.juick.service.activities.DeleteMessageEvent; +import com.juick.service.component.*; +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.apache.commons.text.StringEscapeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import java.io.IOException; +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 + */ +public class CommandsManager { + private static final Logger logger = LoggerFactory.getLogger(CommandsManager.class); + @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; + @Inject + private ApplicationEventPublisher applicationEventPublisher; + @Inject + private ImagesService imagesService; + @Inject + private WebApp webApp; + @Inject + private ActivityPubManager activityPubManager; + + public CommandResult processCommand(@Nonnull User user, String data, @Nonnull URI attachment) throws Exception { + if (!user.isAnonymous()) { + userService.updateLastSeen(user); + } + String strippedData = StringUtils.stripStart(data, null); + if (strippedData.startsWith("?OTR")) { + return CommandResult.fromString("?OTR Error: we are not using OTR"); + } + String input = StringEscapeUtils.unescapeHtml4(MessageUtils.stripNonSafeUrls(strippedData)); + Optional 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 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> tags = tagService.fromString(input); + if (tags.getRight().size() > 5) { + return CommandResult.fromString("Sorry, 5 tags maximum."); + } + // new message + String body = tags.getLeft().trim(); + if (body.length() > 4096) { + return CommandResult.fromString("Sorry, 4096 characters maximum."); + } + 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); + } + if (StringUtils.isEmpty(body) && !haveAttachment) { + return CommandResult.fromString("Empty message"); + } + 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).orElseThrow(IllegalStateException::new); + msg.getUser().setAvatar(webApp.getAvatarUrl(msg.getUser())); + subscriptionService.subscribeMessage(msg, user); + + applicationEventPublisher.publishEvent(new SystemEvent(this, SystemActivity.read(user, msg))); + applicationEventPublisher.publishEvent(new SystemEvent(this, + SystemActivity.message(user, msg, subscriptionService.getSubscribedUsers(msg.getUser().getUid(), msg)))); + 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]); + user_to.setAvatar(webApp.getAvatarUrl(user_to)); + if (!user_to.isAnonymous()) { + if (!userService.isInBLAny(user_to.getUid(), user_from.getUid())) { + if (pmQueriesService.createPM(user_from.getUid(), user_to.getUid(), body)) { + Message jmsg = new Message(); + jmsg.setUser(user_from); + jmsg.setTo(user_to); + jmsg.setText(body); + applicationEventPublisher.publishEvent(new SystemEvent(this, + SystemActivity.message(user_from, 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 blusers = userService.getUserBLUsers(user_from.getUid()); + List 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 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 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 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 friends = userService.getUserFriends(currentUser.getUid()); + List 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 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) { + Optional msg = messagesService.getMessage(mid); + if (msg.isPresent()) { + if (msg.get().getUser() == user) { + return CommandResult.fromString("You can't recommend your own messages."); + } + MessagesService.RecommendStatus status = messagesService.recommendMessage(mid, user.getUid(), user.getUri().toASCIIString()); + switch (status) { + case Added: + applicationEventPublisher.publishEvent(new SystemEvent(this, SystemActivity.like(user, msg.get(), + subscriptionService.getUsersSubscribedToUserRecommendations( + user.getUid(), msg.get())))); + 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 SystemEvent(this, + SystemActivity.follow(user, Collections.singletonList(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 were 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 were 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(@Nonnull User user, URI attachment, String... args) { + boolean subscribe = args[0].equalsIgnoreCase("s"); + int mid = NumberUtils.toInt(args[1], 0); + Optional msg = messagesService.getMessage(mid); + if (msg.isPresent()) { + if (subscribe) { + if (subscriptionService.subscribeMessage(msg.get(), user)) { + applicationEventPublisher.publishEvent( + new SystemEvent(this, SystemActivity.read(user, msg.get()))); + return CommandResult.fromString("Subscribed"); + } + } else { + if (subscriptionService.unSubscribeMessage(mid, user.getUid())) { + return CommandResult.fromString("Unsubscribed from #" + mid); + } + return CommandResult.fromString("You were 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() && !blogUser.isBanned()) { + List 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(@Nonnull 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"); + } + Optional msg = messagesService.getMessage(mid); + if (msg.isPresent()) { + if (showReplies) { + List replies = messagesService.getReplies(user, mid); + applicationEventPublisher.publishEvent( + new SystemEvent(this, SystemActivity.read(user, msg.get()))); + replies.add(0, msg.get()); + return CommandResult.fromString(replies.stream() + .map(PlainTextFormatter::formatPostSummary).collect(Collectors.joining("\n"))); + } + return CommandResult.fromString(PlainTextFormatter.formatPost(msg.get())); + } + 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); + 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 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]); + Optional message = messagesService.getMessage(mid); + if (message.isPresent() && messagesService.deleteMessage(user.getUid(), mid)) { + applicationEventPublisher.publishEvent(new DeleteMessageEvent(this, message.get())); + 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.parseInt(args[0]); + int rid = Integer.parseInt(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 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(@Nonnull 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]); + Optional msg = messagesService.getMessage(mid); + if (!msg.isPresent()) { + return CommandResult.fromString("Message not found"); + } + if (rid > 0) { + Message reply = messagesService.getReply(mid, rid); + if (reply == null) { + return CommandResult.fromString("Reply not found"); + } + } + Pair> messageTags = tagService.fromString(txt); + if (user.getUid() == msg.get().getUser().getUid() && rid == 0 && messageTags.getRight().size() > 0) { + if (!CollectionUtils.isEqualCollection(tagService.updateTags(mid, messageTags.getRight()), msg.get().getTags())) { + return CommandResult.fromString("Tags are updated"); + } else { + return CommandResult.fromString("Tags are NOT updated (5 tags maximum?)"); + } + } else { + if (txt.length() > 4096) { + return CommandResult.fromString("Sorry, 4096 characters maximum."); + } + boolean haveAttachment = StringUtils.isNotEmpty(attachment.toString()); + String attachmentFName = null; + String attachmentType = null; + if (haveAttachment) { + if (attachment.getScheme().equals("juick")) { + attachmentFName = attachment.getHost(); + attachmentType = attachmentFName.substring(attachmentFName.length() - 3); + } else { + try { + attachmentFName = HttpUtils.downloadImage(attachment.toURL(), tmpDir).getHost(); + attachmentType = attachmentFName.substring(attachmentFName.length() - 3); + } catch (IOException e) { + logger.warn("Can not download {}", attachment.toURL()); + } + } + } + boolean attachmentProcessed = !haveAttachment || StringUtils.isNotEmpty(attachmentType); + String messageText = attachmentProcessed ? txt : String.format("%s %s", txt, attachment.toASCIIString()); + int newrid = messagesService.createReply(mid, rid, user, messageText, attachmentType); + if (newrid > 0) { + if (haveAttachment && attachmentProcessed) { + String fname = String.format("%d-%d.%s", mid, newrid, attachmentType); + imagesService.saveImageWithPreviews(attachmentFName, fname); + } + applicationEventPublisher.publishEvent( + new SystemEvent(this, SystemActivity.read(user, msg.get()))); + Message original = messagesService.getMessage(mid).orElseThrow(IllegalStateException::new); + subscriptionService.subscribeMessage(original, user); + Message reply = messagesService.getReply(mid, newrid); + if (reply.getUser().isAnonymous()) { + reply.setUser(activityPubManager.personToUser(reply.getUser().getUri())); + } else { + reply.getUser().setAvatar(webApp.getAvatarUrl(reply.getUser())); + } + applicationEventPublisher.publishEvent( + new SystemEvent(this, + SystemActivity.message(reply.getUser(), 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))); + } else { + return CommandResult.fromString("Message is read-only"); + } + } + } + + String printMessages(User visitor, List mids, boolean crop) { + return messagesService.getMessages(visitor, mids).stream() + .sorted(Collections.reverseOrder()) + .map(PlainTextFormatter::formatPostSummary).collect(Collectors.joining("\n\n")); + } +} diff --git a/src/main/java/com/juick/EmailManager.java b/src/main/java/com/juick/EmailManager.java new file mode 100644 index 00000000..e5b527f4 --- /dev/null +++ b/src/main/java/com/juick/EmailManager.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.model.Message; +import com.juick.model.User; +import com.juick.www.api.SystemActivity; +import com.juick.util.HttpBadRequestException; +import com.juick.www.WebApp; +import com.juick.service.EmailService; +import com.juick.service.MessagesService; +import com.juick.service.UserService; +import com.juick.service.component.*; +import com.juick.util.MessageUtils; +import com.mitchellbosecke.pebble.PebbleEngine; +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 javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import java.util.*; + +import static com.juick.util.formatters.PlainTextFormatter.formatPost; +import static com.juick.util.formatters.PlainTextFormatter.formatUrl; + +public class EmailManager implements NotificationListener { + + public static final String MSGID_PATTERN = "\\.|@|<"; + + private static final Logger logger = LoggerFactory.getLogger(EmailManager.class); + @Inject + private EmailService emailService; + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private PebbleEngine pebbleEngine; + @Inject + private ObjectMapper jsonMapper; + @Inject + private WebApp webApp; + @Value("${service_email:}") + private String serviceEmail; + + @Override + public void processSystemEvent(@Nonnull SystemEvent systemEvent) { + var activity = systemEvent.getActivity(); + var msg = activity.getMessage(); + var subscribers = activity.getTo(); + if (activity.getType().equals(SystemActivity.ActivityType.message)) { + processMessage(msg, subscribers); + } + try { + var eventHeader = Collections.singletonMap("X-Event-Version", "1.0"); + sendEmail("noreply@juick.com", serviceEmail, "New system event", + jsonMapper.writeValueAsString(systemEvent.getActivity()), null, eventHeader); + } catch (JsonProcessingException e) { + logger.warn("JSON exception", e); + } + } + private void processMessage(Message msg, List subscribedUsers) { + if (msg.isService()) { + return; + } + if (MessageUtils.isPM(msg)) { + String subject = String.format("Private message from %s", msg.getUser().getName()); + emailService.getEmails(msg.getTo().getUid(), true).forEach(email -> emailNotify(email, subject, msg)); + } else if (MessageUtils.isReply(msg)) { + Message originalMessage = messagesService.getMessage(msg.getMid()).orElseThrow(IllegalStateException::new); + String subject = String.format("New reply to %s", originalMessage.getUser().getName()); + subscribedUsers.stream() + .flatMap(user -> emailService.getEmails(user.getUid(), true).stream()) + .forEach(email -> emailNotify(email, subject, msg)); + } else { + String subject = String.format("New message from %s", msg.getUser().getName()); + subscribedUsers + .forEach(user -> emailService.getEmails(user.getUid(), true) + .forEach(email -> emailNotify(email, subject, msg))); + } + } + + @Override + public void processPingEvent(PingEvent pingEvent) { + + } + + @Async + @EventListener + public void processMailVerificationEvent(MailVerificationEvent mailVerificationEvent) { + if (!sendEmail("noreply@juick.com", mailVerificationEvent.getEmail(), "Juick authorization link", + String.format("Follow link to attach this email to Juick account:\n" + + "http://juick.com/settings?page=auth-email&code=%s\n\n" + + "If you don't know, what this mean - just ignore this mail.\n", mailVerificationEvent.getCode()), + StringUtils.EMPTY, Collections.emptyMap())) { + throw new HttpBadRequestException(); + } + } + @Async + @EventListener + public void processAccountVerificationEvent(AccountVerificationEvent accountVerificationEvent) { + String signupUrl = String.format("Follow this link to create Juick account: https://juick.com/signup?type=email&hash=%s", accountVerificationEvent.getCode()); + sendEmail("noreply@juick.com", accountVerificationEvent.getEmail(), "Juick registration", signupUrl, StringUtils.EMPTY, Collections.emptyMap()); + } + + private void emailNotify(String email, String subject, Message msg) { + Map headers = new HashMap<>(); + if (!MessageUtils.isPM(msg)) { + headers.put("Message-ID", String.format("<%d.%d@juick.com>", msg.getMid(), msg.getRid())); + } + if (MessageUtils.isReply(msg)) { + if (msg.getReplyto() > 0) { + Message replyto = messagesService.getReply(msg.getMid(), msg.getReplyto()); + headers.put("In-Reply-To", String.format("<%d.%d@juick.com>", replyto.getMid(), replyto.getRid())); + } else { + Message original = messagesService.getMessage(msg.getMid()).orElseThrow(IllegalStateException::new); + headers.put("In-Reply-To", String.format("<%d.%d@juick.com>", original.getMid(), original.getRid())); + } + } + String plainText = webApp.renderPlaintext(formatPost(msg), formatUrl(msg)).orElseThrow(IllegalStateException::new); + String hash = userService.getHashByUID(userService.getUserByEmail(email).getUid()); + String htmlText = webApp.renderHtml(MessageUtils.formatHtml(msg), formatUrl(msg), msg, hash).orElseThrow(IllegalStateException::new); + sendEmail(StringUtils.EMPTY, email, subject, plainText, htmlText, headers); + } + public boolean sendEmail(String from, String to, String subject, String textPart, String htmlPart, Map messageHeaders) { + Properties prop = System.getProperties(); + prop.put("mail.smtp.starttls.enable", "true"); + Session session = Session.getDefaultInstance(prop); + try { + Transport transport = session.getTransport("smtp"); + MimeMessage message = new MimeMessage(session) { + protected void updateMessageID() throws MessagingException { + for (Map.Entry entry: messageHeaders.entrySet()) { + setHeader(entry.getKey(), entry.getValue()); + } + } + }; + String fromAddress = StringUtils.isNotEmpty(from) ? from : "juick@juick.com"; + message.setFrom(fromAddress); + message.addRecipient(javax.mail.Message.RecipientType.TO, new InternetAddress(to)); + message.setSubject(subject); + MimeBodyPart textBodyPart = new MimeBodyPart(); + textBodyPart.setContent(textPart, "text/plain; charset=UTF-8"); + + Multipart multipart = new MimeMultipart("alternative"); + multipart.addBodyPart(textBodyPart); + if (StringUtils.isNotBlank(htmlPart)) { + MimeBodyPart htmlBodyPart = new MimeBodyPart(); + htmlBodyPart.setContent(htmlPart, "text/html; charset=UTF-8"); + multipart.addBodyPart(htmlBodyPart); + } + message.setContent(multipart); + User emailUser = userService.getUserByEmail(to); + if (!emailUser.isAnonymous()) { + message.setHeader("List-Id", "Juick notifications "); + message.setHeader("List-Post", ""); + message.setHeader("List-Owner", ""); + message.setHeader("List-Archive", ""); + message.setHeader("List-Unsubscribe", + String.format(",", + userService.getHashByUID(emailUser.getUid()))); + message.setHeader("List-Unsubscribe-Post", "List-Unsubscribe=One-Click"); + } + message.saveChanges(); + transport.connect(); + transport.sendMessage(message, message.getAllRecipients()); + return true; + } catch (MessagingException ex) { + logger.error("mail exception", ex); + return false; + } + } +} diff --git a/src/main/java/com/juick/KeystoreManager.java b/src/main/java/com/juick/KeystoreManager.java new file mode 100644 index 00000000..50576255 --- /dev/null +++ b/src/main/java/com/juick/KeystoreManager.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick; + +import com.juick.www.api.activity.model.objects.Person; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.Resource; +import org.springframework.util.Base64Utils; + +import javax.net.ssl.KeyManagerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; + +public class KeystoreManager { + private static final Logger logger = LoggerFactory.getLogger("ActivityPub"); + + private String keystorePassword; + + private KeyStore ks; + + public KeystoreManager(Resource keystore, String keystorePassword) { + this.keystorePassword = keystorePassword; + try (InputStream ksIs = keystore.getInputStream()) { + ks = KeyStore.getInstance("PKCS12"); + ks.load(ksIs, keystorePassword.toCharArray()); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory + .getDefaultAlgorithm()); + kmf.init(ks, keystorePassword.toCharArray()); + } catch (IOException | KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | CertificateException e) { + logger.error("Keystore error", e); + } + } + + private KeyPair getKeyPair() { + Key privateKey; + try { + privateKey = ks.getKey("1", keystorePassword.toCharArray()); + Certificate certificate = ks.getCertificate("1"); + return new KeyPair(certificate.getPublicKey(), (PrivateKey) privateKey); + } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { + e.printStackTrace(); + } + return null; + } + public PrivateKey getPrivateKey() { + return getKeyPair().getPrivate(); + } + public PublicKey getPublicKey() { + return getKeyPair().getPublic(); + } + public String getPublicKeyPem() { + String[] key = Base64Utils.encodeToString(getKeyPair().getPublic().getEncoded()).split("(?<=\\G.{64})"); + return String.format("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", + String.join("\n", key)); + } + public static PublicKey publicKeyOf(Person person) { + String pubkeyPem = person.getPublicKey().getPublicKeyPem(); + String[] rawKey = pubkeyPem.split("\\n"); + String pubkeyData = String.join("", Arrays.asList(rawKey).subList(1, rawKey.length - 1)); + try{ + byte[] byteKey = Base64Utils.decodeFromString(pubkeyData); + X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(byteKey); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(X509publicKey); + } + catch(Exception e){ + e.printStackTrace(); + } + return null; + } +} diff --git a/src/main/java/com/juick/ServerManager.java b/src/main/java/com/juick/ServerManager.java new file mode 100644 index 00000000..f8f8b8c6 --- /dev/null +++ b/src/main/java/com/juick/ServerManager.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick; + +import com.juick.model.Message; +import com.juick.model.User; +import com.juick.model.AnonymousUser; +import com.juick.www.api.SystemActivity; +import com.juick.service.MessagesService; +import com.juick.service.SubscriptionService; +import com.juick.service.UserService; +import com.juick.service.component.*; +import com.juick.util.MessageUtils; +import org.apache.commons.collections4.ListUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +/** + * @author Ugnich Anton + */ +public class ServerManager implements NotificationListener { + private static final Logger logger = LoggerFactory.getLogger("Session"); + + @Inject + private MessagesService messagesService; + @Inject + private SubscriptionService subscriptionService; + @Inject + private UserService userService; + private final CopyOnWriteArrayList sessions = new CopyOnWriteArrayList<>(); + + @Value("${service_user:juick}") + private String serviceUsername; + + private User serviceUser; + + @PostConstruct + public void init() { + serviceUser = userService.getUserByName(serviceUsername); + } + + + private void onJuickPM(final User to, final Message jmsg) { + messageEvent(jmsg, Arrays.asList(to, jmsg.getUser())); + } + + private void onJuickMessagePost(final Message jmsg, List subscribedUsers) { + messageEvent(jmsg, subscribedUsers); + messageEvent(jmsg, Collections.singletonList(AnonymousUser.INSTANCE)); + } + + private void onJuickMessageReply(final Message jmsg, final List subscribedUsers) { + messageEvent(jmsg, subscribedUsers); + messageEvent(jmsg, Collections.singletonList(AnonymousUser.INSTANCE)); + } + + @Override + public void processSystemEvent(@Nonnull SystemEvent systemEvent) { + var activity = systemEvent.getActivity(); + var from = activity.getFrom(); + var message = activity.getMessage(); + var subscribers = activity.getTo(); + if (activity.getType().equals(SystemActivity.ActivityType.message)) { + processMessage(from, message, subscribers); + } else if (activity.getType().equals(SystemActivity.ActivityType.like)) { + if (from.equals(serviceUser)) { + processTop(message); + } + } + } + private void processMessage(User from, Message jmsg, List subscribers) { + List subscribedUsers = ListUtils.union(subscribers, Collections.singletonList(jmsg.getUser())); + if (jmsg.isService()) { + logger.info("Message read event from {} for {}", from.getUid(), jmsg.getMid()); + readEvent(jmsg, Collections.singletonList(serviceUser)); + return; + } + if (MessageUtils.isPM(jmsg)) { + onJuickPM(jmsg.getTo(), jmsg); + } else if (!MessageUtils.isReply(jmsg)) { + onJuickMessagePost(jmsg, subscribedUsers); + } else { + // to get quote and attachment + Message op = messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); + subscriptionService.getUsersSubscribedToComments(op, jmsg, true).stream() + .filter(u -> userService.isReplyToBL(u, jmsg)) + .forEach(b -> messagesService.setLastReadComment(b, jmsg.getMid(), jmsg.getRid())); + onJuickMessageReply(jmsg, subscribedUsers); + } + messageEvent(jmsg, Collections.singletonList(serviceUser)); + } + + @Override + public void processPingEvent(PingEvent pingEvent) { + + } + + private void processTop(Message msg) { + User topUser = msg.getUser(); + topEvent(msg, Arrays.asList(topUser, serviceUser)); + } + private void topEvent(Message msg, List subscribers){ + sendSseEvent(msg, "top", subscribers); + } + + private void readEvent(Message msg, List subscribers){ + sendSseEvent(msg, "read", subscribers); + } + + private void messageEvent(Message msg, List subscribers) { + sendSseEvent(msg, "msg", subscribers); + } + + private void sendSseEvent(Message msg, String name, List subscribers) { + List deadEmitters = new ArrayList<>(); + this.sessions.stream().filter(s -> subscribers.contains(s.user)).forEach(session -> { + try { + SseEmitter.SseEventBuilder builder = SseEmitter.event() + .name(name) + .data(msg); + session.getEmitter().send(builder); + } catch (Exception e) { + deadEmitters.add(session); + } + }); + this.sessions.removeAll(deadEmitters); + } + + public static class EventSession { + private final User user; + private final SseEmitter emitter; + + public EventSession(User user, SseEmitter sseEmitter) { + this.user = user; + this.emitter = sseEmitter; + } + + public User getUser() { + return user; + } + + public SseEmitter getEmitter() { + return emitter; + } + } + + public CopyOnWriteArrayList getSessions() { + return sessions; + } + @Scheduled(fixedRate = 30000) + public void ping() { + Message ping = new Message(); + ping.setService(true); + sendSseEvent(ping, "ping", getSessions().stream().map(s -> s.user) + .distinct().collect(Collectors.toList())); + } +} diff --git a/src/main/java/com/juick/SignatureManager.java b/src/main/java/com/juick/SignatureManager.java new file mode 100644 index 00000000..959242f5 --- /dev/null +++ b/src/main/java/com/juick/SignatureManager.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.model.User; +import com.juick.model.AnonymousUser; +import com.juick.www.api.activity.model.Context; +import com.juick.www.api.activity.model.objects.Person; +import com.juick.www.api.webfinger.model.Account; +import com.juick.www.api.webfinger.model.Link; +import com.juick.service.UserService; +import com.juick.util.DateFormattersHolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; +import org.tomitribe.auth.signatures.Signature; +import org.tomitribe.auth.signatures.Signer; +import org.tomitribe.auth.signatures.Verifier; +import rocks.xmpp.addr.Jid; + +import javax.inject.Inject; +import java.io.IOException; +import java.net.URI; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static com.juick.www.api.activity.model.Context.ACTIVITY_MEDIA_TYPE; + +public class SignatureManager { + private static final Logger logger = LoggerFactory.getLogger("ActivityPub"); + @Inject + private KeystoreManager keystoreManager; + @Inject + private ObjectMapper jsonMapper; + @Inject + private UserService userService; + @Inject + private RestTemplate apClient; + + public void post(Person from, Person to, Context data) throws IOException { + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(to.getInbox()); + URI inbox = uriComponentsBuilder.build().toUri(); + Instant now = Instant.now(); + String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now); + String host = inbox.getPort() > 0 ? String.format("%s:%d", inbox.getHost(), inbox.getPort()) : inbox.getHost(); + String signatureString = addSignature(from, host, "POST", inbox.getPath(), requestDate); + + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.add("Content-Type", Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE); + requestHeaders.add("Date", requestDate); + requestHeaders.add("Host", host); + requestHeaders.add("Signature", signatureString); + HttpEntity request = new HttpEntity<>(Context.build(data), requestHeaders); + logger.info("Sending context to {}: {}", to.getId(), jsonMapper.writeValueAsString(data)); + ResponseEntity response = apClient.postForEntity(inbox, request, Void.class); + logger.info("Remote response: {}", response.getStatusCodeValue()); + } + + public String addSignature(Person from, String host, String method, String path, String dateString) throws IOException { + return addSignature(from, host, method, path, dateString, keystoreManager); + } + + public String addSignature(Person from, String host, String method, String path, String dateString, KeystoreManager keystoreManager) throws IOException { + Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null, + "(request-target)", "host", "date"); + Map headers = new HashMap<>(); + headers.put("host", host); + headers.put("date", dateString); + Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature); + Signature signature = signer.sign(method, path, headers); + // remove "Signature: " from result + return signature.toString().substring(10); + } + + public User verifySignature(String method, String path, Map headers) { + String signatureString = headers.get("signature"); + logger.info("Signature: {}", signatureString); + Signature signature = Signature.fromString(signatureString); + Optional context = getContext(UriComponentsBuilder.fromUriString(signature.getKeyId()) + .fragment(null).build().toUri()); + if (context.isPresent() && context.get() instanceof Person) { + Person person = (Person) context.get(); + Key key = KeystoreManager.publicKeyOf(person); + + Verifier verifier = new Verifier(key, signature); + try { + boolean result = verifier.verify(method, path, headers); + logger.info("signature of {} is valid: {}", signature.getKeyId(), result); + if (result) { + User user = new User(); + user.setUri(URI.create(person.getId())); + if (key.equals(keystoreManager.getPublicKey())) { + return userService.getUserByName(person.getName()); + } + return user; + } else { + return AnonymousUser.INSTANCE; + } + } catch (NoSuchAlgorithmException | SignatureException | IOException e) { + logger.warn("Invalid signature {}", signatureString); + } + } else { + logger.warn("Unknown keyId"); + } + return AnonymousUser.INSTANCE; + } + public Optional getContext(URI contextUri) { + try { + Context context = apClient.getForEntity(contextUri, Context.class).getBody(); + if (context == null) { + logger.warn("Cannot identify {}", contextUri); + return Optional.empty(); + } + return Optional.of(context); + } catch (Exception e) { + logger.warn("REST Exception on {}: {}", contextUri, e.getMessage()); + } + return Optional.empty(); + } + public Optional discoverPerson(String acct) { + Jid acctId = Jid.of(acct); + URI resourceUri = UriComponentsBuilder.fromPath("/.well-known/webfinger") + .host(acctId.getDomain()) + .scheme("https") + .queryParam("resource", String.format("%s", acctId.toEscapedString())).build().toUri(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Accept", "application/jrd+json"); + HttpEntity webfingerRequest = new HttpEntity<>(headers); + ResponseEntity response = apClient.exchange( + resourceUri, HttpMethod.GET, webfingerRequest, Account.class); + if (response.getStatusCode().is2xxSuccessful()) { + Account acctData = response.getBody(); + if (acctData != null) { + for (Link l : acctData.getLinks()) { + if (l.getRel().equals("self") && l.getType().equals(ACTIVITY_MEDIA_TYPE)) { + return getContext(URI.create(l.getHref())); + } + } + } + } + return Optional.empty(); + } +} diff --git a/src/main/java/com/juick/TelegramBotManager.java b/src/main/java/com/juick/TelegramBotManager.java new file mode 100644 index 00000000..3538a27b --- /dev/null +++ b/src/main/java/com/juick/TelegramBotManager.java @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick; + +import com.juick.model.User; +import com.juick.model.AnonymousUser; +import com.juick.model.CommandResult; +import com.juick.www.api.SystemActivity; +import com.juick.util.HttpUtils; +import com.juick.service.MessagesService; +import com.juick.service.TelegramService; +import com.juick.service.UserService; +import com.juick.service.component.SystemEvent; +import com.juick.service.component.NotificationListener; +import com.juick.service.component.PingEvent; +import com.juick.util.MessageUtils; +import com.pengrad.telegrambot.Callback; +import com.pengrad.telegrambot.TelegramBot; +import com.pengrad.telegrambot.UpdatesListener; +import com.pengrad.telegrambot.model.Message; +import com.pengrad.telegrambot.model.MessageEntity; +import com.pengrad.telegrambot.model.PhotoSize; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.ParseMode; +import com.pengrad.telegrambot.request.GetFile; +import com.pengrad.telegrambot.request.SendMessage; +import com.pengrad.telegrambot.request.SendPhoto; +import com.pengrad.telegrambot.request.SetWebhook; +import com.pengrad.telegrambot.response.GetFileResponse; +import com.pengrad.telegrambot.response.SendResponse; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.*; + +import static com.juick.util.formatters.PlainTextFormatter.formatPost; +import static com.juick.util.formatters.PlainTextFormatter.formatUrl; + +/** + * Created by vt on 12/05/16. + */ +public class TelegramBotManager implements NotificationListener { + private static final Logger logger = LoggerFactory.getLogger("Telegram"); + + private TelegramBot bot; + + @Value("${telegram_api_url:}") + private String apiUrl; + @Value("${telegram_file_api_url:}") + private String fileApiUrl; + @Value("${telegram_webhook_url:}") + private String webhookUrl; + @Value("${telegram_token:12345678}") + private String telegramToken; + @Value("${telegram_debug:false}") + private boolean telegramDebug; + @Inject + private TelegramService telegramService; + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private CommandsManager commandsManager; + @Inject + private ApplicationEventPublisher applicationEventPublisher; + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + @Value("${service_user:juick}") + private String serviceUser; + + private static final String MSG_LINK = "🔗"; + + @PostConstruct + public void init() { + TelegramBot.Builder tgBuilder = new TelegramBot.Builder(telegramToken); + if (StringUtils.isNotEmpty(apiUrl)) { + tgBuilder.apiUrl(apiUrl).fileApiUrl(fileApiUrl); + } + bot = tgBuilder.build(); + if (!telegramDebug) { + try { + SetWebhook webhook = new SetWebhook().url(webhookUrl); + if (!bot.execute(webhook).isOk()) { + logger.error("error setting webhook"); + } + } catch (Exception e) { + logger.warn("couldn't initialize telegram bot", e); + } + } else { + bot.setUpdatesListener(updates -> { + logger.info("got updates: {}", updates); + updates.forEach(this::processUpdate); + return UpdatesListener.CONFIRMED_UPDATES_ALL; + }); + } + } + + public void processUpdate(Update update) { + Message message = update.message(); + if (update.message() == null) { + message = update.editedMessage(); + if (message == null) { + logger.error("error parsing telegram update: {}", update); + return; + } + User user_from = userService.getUserByUID(telegramService.getUser(message.chat().id())).orElse(AnonymousUser.INSTANCE); + logger.info("Found juick user {}", user_from.getUid()); + Optional> originalMessageData = messagesService.findMessageByProperty("durovId", + String.valueOf(message.messageId())); + if (originalMessageData.isPresent()) { + int mid = originalMessageData.get().getLeft(); + int rid = originalMessageData.get().getRight(); + // TODO: this is copypaste from api, need switch to api + com.juick.model.Message originalMessage = rid == 0 ? messagesService.getMessage(mid).orElseThrow(IllegalStateException::new) + : messagesService.getReply(mid, rid); + User author = originalMessage.getUser(); + String newMessageText = StringUtils.defaultString(message.text()); + if (user_from.equals(author) && canUpdateMessage(originalMessage, newMessageText)) { + if (messagesService.updateMessage(mid, rid, newMessageText)) { + telegramNotify(message.chat().id(), "Message updated", new com.juick.model.Message()); + return; + } + } + telegramNotify(message.chat().id(), "Error updating message", new com.juick.model.Message()); + } + } else { + User user_from = userService.getUserByUID(telegramService.getUser(message.chat().id())).orElse(AnonymousUser.INSTANCE); + logger.info("Found juick user {}", user_from.getUid()); + + String username = message.from().username(); + if (username == null) { + username = message.from().firstName(); + } + if (!user_from.isAnonymous()) { + URI attachment = URI.create(StringUtils.EMPTY); + if (message.photo() != null) { + String fileId = Arrays.stream(message.photo()).max(Comparator.comparingInt(PhotoSize::fileSize)) + .orElse(new PhotoSize()).fileId(); + if (StringUtils.isNotEmpty(fileId)) { + GetFile request = new GetFile(fileId); + GetFileResponse response = bot.execute(request); + logger.info("got file {}", response.file()); + try { + URL fileURL = new URL(bot.getFullFilePath(response.file())); + attachment = HttpUtils.downloadImage(fileURL, tmpDir); + } catch (Exception e) { + logger.warn("attachment exception", e); + } + logger.info("received {}", attachment); + } + } + String text = message.text(); + if (StringUtils.isBlank(text)) { + text = message.caption(); + } + if (StringUtils.isBlank(text)) { + text = StringUtils.EMPTY; + } + if (StringUtils.isNotEmpty(text) || StringUtils.isNotEmpty(attachment.toString())) { + if (text.equalsIgnoreCase("LOGIN") + || text.equalsIgnoreCase("PING") + || text.equalsIgnoreCase("HELP") + || text.equalsIgnoreCase("/login") + || text.equalsIgnoreCase("/logout") + || text.equalsIgnoreCase("/start") + || text.equalsIgnoreCase("/help")) { + String msgUrl = "http://juick.com/login?hash=" + userService.getHashByUID(user_from.getUid()); + String msg = String.format("Hi, %s!\nYou can post messages and images to Juick there.\n" + + "Tap to [log into website](%s) to get more info", user_from.getName(), msgUrl); + telegramNotify(message.from().id().longValue(), msg, new com.juick.model.Message()); + } else { + Message replyMessage = message.replyToMessage(); + if (replyMessage != null) { + MessageEntity[] entities = replyMessage.entities(); + if (entities == null) { + entities = replyMessage.captionEntities(); + } + if (entities != null) { + Optional juickLink = Arrays.stream(entities) + .filter(this::isJuickLink) + .findFirst(); + if (juickLink.isPresent()) { + if (StringUtils.isNotEmpty(juickLink.get().url())) { + UriComponents uriComponents = UriComponentsBuilder.fromUriString( + juickLink.get().url()).build(); + String path = uriComponents.getPath(); + if (StringUtils.isNotEmpty(path) && path.length() > 1) { + int mid; + try { + mid = Integer.parseInt(path.substring(3)); + } catch (NumberFormatException e) { + logger.warn("wrong mid received"); + return; + } + String prefix = String.format("#%d ", mid); + if (StringUtils.isNotEmpty(uriComponents.getFragment())) { + int rid = Integer.parseInt(uriComponents.getFragment()); + prefix = String.format("#%d/%d ", mid, rid); + } + executeCommand(message.messageId(), message.from().id().longValue(), + user_from, prefix + text, attachment); + } else { + logger.warn("invalid path: {}", path); + } + } else { + logger.warn("invalid entity: {}", juickLink); + } + } else { + telegramNotify(message.from().id().longValue(), + "Can not reply to this message", replyMessage.messageId(), new com.juick.model.Message()); + } + } else { + telegramNotify(message.from().id().longValue(), + "Can not reply to this message", replyMessage.messageId(), new com.juick.model.Message()); + } + } else { + executeCommand(message.messageId(), message.from().id().longValue(), + user_from, text, attachment); + } + } + } + messagesService.getUnread(user_from).forEach(mid -> messagesService.setRead(user_from, mid)); + } else { + List chats = telegramService.getAnonymous(); + if (!chats.contains(message.chat().id())) { + logger.info("added chat with {}", username); + telegramService.createTelegramUser(message.from().id(), username); + } + telegramSignupNotify(message.from().id().longValue(), userService.getSignUpHashByTelegramID(message.from().id().longValue(), username)); + } + } + } + + /* + validate user input + */ + private boolean canUpdateMessage(com.juick.model.Message message, String newData) { + if (StringUtils.isEmpty(newData)) { + // allow empty text only when image is present + return StringUtils.isNotEmpty(message.getAttachmentType()); + } + return true; + } + + private void executeCommand(Integer messageId, Long userId, User user_from, String text, URI attachment) { + try { + CommandResult result = commandsManager.processCommand(user_from, text, attachment); + if (result.getNewMessage().isPresent()) { + com.juick.model.Message newMessage = result.getNewMessage().get(); + messagesService.setMessageProperty(newMessage.getMid(), newMessage.getRid(), "durovId", + String.valueOf(messageId)); + } + String messageTxt = StringUtils.isNotEmpty(result.getMarkdown()) ? result.getMarkdown() + : "Unknown error or unsupported command"; + telegramNotify(userId, messageTxt, new com.juick.model.Message()); + } catch (Exception e) { + logger.warn("telegram exception", e); + } + } + + private boolean isJuickLink(MessageEntity e) { + return e.offset() == 0 && e.type().equals(MessageEntity.Type.text_link) && e.length() == 2; + } + + public void telegramNotify(Long chatId, String msg, @Nonnull com.juick.model.Message source) { + telegramNotify(chatId, msg, 0, source); + } + + public void telegramNotify(Long chatId, String msg, Integer replyTo, @Nonnull com.juick.model.Message source) { + String attachment = MessageUtils.attachmentUrl(source); + boolean isSendTxt = true; + if (!StringUtils.isEmpty(attachment)) { + SendPhoto telegramPhoto = new SendPhoto(chatId, attachment); + if (replyTo > 0) { + telegramPhoto.replyToMessageId(replyTo); + } + telegramPhoto.parseMode(ParseMode.Markdown); + if(msg.length() < 1024) { + telegramPhoto.caption(msg); + isSendTxt = false; + } + bot.execute(telegramPhoto, new Callback<>() { + @Override + public void onResponse(SendPhoto request, SendResponse response) { + processTelegramResponse(chatId, response, source); + } + + @Override + public void onFailure(SendPhoto request, IOException e) { + logger.warn("telegram failure", e); + } + }); + } + if (isSendTxt){ + SendMessage telegramMessage = new SendMessage(chatId, msg); + if (replyTo > 0) { + telegramMessage.replyToMessageId(replyTo); + } + telegramMessage.parseMode(ParseMode.Markdown).disableWebPagePreview(true); + bot.execute(telegramMessage, new Callback<>() { + @Override + public void onResponse(SendMessage request, SendResponse response) { + processTelegramResponse(chatId, response, source); + } + + @Override + public void onFailure(SendMessage request, IOException e) { + logger.warn("telegram failure", e); + } + }); + } + } + + private void processTelegramResponse(Long chatId, SendResponse response, com.juick.model.Message source) { + int userId = telegramService.getUser(chatId); + if (!response.isOk()) { + if (response.errorCode() == 403) { + // remove from anonymous users + telegramService.getAnonymous().stream().filter(c -> c.equals(chatId)).findFirst().ifPresent( + d -> { + telegramService.deleteAnonymous(d); + logger.info("deleted {} chat", d); + } + ); + if (userId > 0) { + User userToDelete = userService.getUserByUID(userId) + .orElseThrow(IllegalStateException::new); + boolean status = telegramService.deleteTelegramUser(userToDelete.getUid()); + logger.info("deleting telegram id of @{} : {}", userToDelete.getName(), status); + } + } else { + logger.warn("error response, isOk: {}, errorCode: {}, description: {}", + response.isOk(), response.errorCode(), response.description()); + } + } else { + if (MessageUtils.isReply(source)) { + messagesService.setLastReadComment(userService.getUserByUID(userId) + .orElseThrow(IllegalStateException::new), source.getMid(), source.getRid()); + User user = userService.getUserByUID(userId).orElseThrow(IllegalStateException::new); + userService.updateLastSeen(user); + applicationEventPublisher.publishEvent( + new SystemEvent(this, SystemActivity.read(user, source))); + } + } + } + + public void telegramSignupNotify(Long telegramId, String hash) { + bot.execute(new SendMessage(telegramId, + String.format("You are subscribed to all Juick messages. " + + "[Create or link](http://juick.com/signup?type=durov&hash=%s) " + + "an existing Juick account to get your subscriptions and ability to post messages", hash)) + .parseMode(ParseMode.Markdown), new Callback<>() { + @Override + public void onResponse(SendMessage request, SendResponse response) { + logger.info("got response: {}", response.message()); + } + + @Override + public void onFailure(SendMessage request, IOException e) { + logger.warn("telegram failure", e); + } + }); + } + + @Override + public void processSystemEvent(SystemEvent systemEvent) { + var activity = systemEvent.getActivity(); + var type = activity.getType(); + if (type.equals(SystemActivity.ActivityType.message)) { + processMessage(activity.getMessage(), activity.getTo()); + } else if (type.equals(SystemActivity.ActivityType.like)) { + if (systemEvent.getActivity().getFrom().getName().equals(serviceUser)) { + processTop(systemEvent.getActivity().getMessage()); + } else { + processLike(activity.getFrom(), activity.getMessage(), activity.getTo()); + } + } else if (type.equals(SystemActivity.ActivityType.follow)) { + processFollow(activity.getFrom(), activity.getTo()); + } + } + private void processMessage(com.juick.model.Message jmsg, List subscribedUsers) { + if (jmsg.isService()) { + return; + } + String msgUrl = formatUrl(jmsg); + if (MessageUtils.isPM(jmsg)) { + telegramService.getTelegramIdentifiers(Collections.singletonList(jmsg.getTo())) + .forEach(c -> telegramNotify(c, formatPost(jmsg, true), jmsg)); + } else if (MessageUtils.isReply(jmsg)) { + String fmsg = String.format("[%s](%s) %s", MSG_LINK, msgUrl, formatPost(jmsg, true)); + telegramService.getTelegramIdentifiers( + subscribedUsers + ).forEach(c -> telegramNotify(c, fmsg, jmsg)); + } else { + String msg = String.format("[%s](%s) %s", MSG_LINK, msgUrl, formatPost(jmsg, true)); + + List users = telegramService.getTelegramIdentifiers(subscribedUsers); + List chats = telegramService.getAnonymous(); + // registered subscribed users + + users.forEach(c -> telegramNotify(c, msg, jmsg)); + // anonymous + chats.stream().filter(u -> telegramService.getUser(u) == 0).forEach(c -> telegramNotify(c, msg, jmsg)); + } + } + + private void processLike(User liker, com.juick.model.Message message, List subscribers) { + if (!liker.getName().equals(serviceUser)) { + logger.info("Like received in tg listener"); + if (!userService.isInBLAny(message.getUser().getUid(), liker.getUid())) { + telegramService.getTelegramIdentifiers(Collections.singletonList(message.getUser())) + .forEach(c -> telegramNotify(c, String.format("%s recommends your [post](%s)", + MessageUtils.getMarkdownUser(liker), formatUrl(message)), new com.juick.model.Message())); + } + telegramService.getTelegramIdentifiers(subscribers) + .forEach(c -> telegramNotify(c, String.format("%s recommends you someone's [post](%s)", + MessageUtils.getMarkdownUser(liker), formatUrl(message)), new com.juick.model.Message())); + } + } + + @Override + public void processPingEvent(PingEvent pingEvent) { + + } + + private void processTop(com.juick.model.Message message) { + telegramService.getTelegramIdentifiers(Collections.singletonList(message.getUser())) + .forEach(c -> telegramNotify(c, String.format("Your [post](%s) became popular!", + formatUrl(message)), new com.juick.model.Message())); + } + + private void processFollow(User subscriber, List target) { + telegramService.getTelegramIdentifiers(target) + .forEach(c -> telegramNotify(c, String.format("%s subscribed to your blog", + MessageUtils.getMarkdownUser(subscriber)), new com.juick.model.Message())); + } +} diff --git a/src/main/java/com/juick/TopManager.java b/src/main/java/com/juick/TopManager.java new file mode 100644 index 00000000..611585d3 --- /dev/null +++ b/src/main/java/com/juick/TopManager.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick; + +import com.juick.model.Message; +import com.juick.model.Tag; +import com.juick.model.User; +import com.juick.www.api.SystemActivity; +import com.juick.service.MessagesService; +import com.juick.service.UserService; +import com.juick.service.component.SystemEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.scheduling.annotation.Scheduled; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class TopManager { + private static Logger logger = LoggerFactory.getLogger(TopManager.class); + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private ApplicationEventPublisher applicationEventPublisher; + + @Value("${service_user:juick}") + private String serviceUsername; + + private User serviceUser; + + @PostConstruct + public void init() { + serviceUser = userService.getUserByName(serviceUsername); + } + + @Scheduled(fixedRate = 3600000) + public void updateTop() { + messagesService.getPopularCandidates().forEach(m -> { + Message jmsg = messagesService.getMessage(m).orElseThrow(IllegalStateException::new); + logger.info("added {} to popular", m); + messagesService.setMessagePopular(m, 1); + List tags = jmsg.getTags().stream().map(Tag::getName).map(String::toLowerCase).collect(Collectors.toList()); + if (!tags.contains("juick")) { + applicationEventPublisher.publishEvent(new SystemEvent(this, + SystemActivity.like(serviceUser, jmsg, Collections.emptyList()))); + } + }); + } +} diff --git a/src/main/java/com/juick/TwitterManager.java b/src/main/java/com/juick/TwitterManager.java new file mode 100644 index 00000000..9fc2b5c7 --- /dev/null +++ b/src/main/java/com/juick/TwitterManager.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick; + +import com.juick.model.Message; +import com.juick.model.User; +import com.juick.www.api.SystemActivity; +import com.juick.service.UserService; +import com.juick.service.component.*; +import com.juick.service.CrosspostService; +import com.juick.util.MessageUtils; +import org.apache.commons.lang3.SerializationUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import twitter4j.TwitterFactory; +import twitter4j.conf.ConfigurationBuilder; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +/** + * @author Ugnich Anton + */ +public class TwitterManager implements NotificationListener { + + private static Logger logger = LoggerFactory.getLogger(TwitterManager.class); + + @Inject + private CrosspostService crosspostService; + + @Value("${twitter_consumer_key:12345678}") + private String twitter_consumer_key; + @Value("${twitter_consumer_secret:secret}") + private String twitter_consumer_secret; + @Inject + private UserService userService; + + @Value("${service_user:juick}") + private String serviceUsername; + + private User serviceUser; + + @PostConstruct + public void init() { + serviceUser = userService.getUserByName(serviceUsername); + } + + void twitterPost(final Message jmsg) { + crosspostService.getTwitterToken(jmsg.getUser().getUid()).ifPresent(t -> { + String status = MessageUtils.getMessageHashTags(jmsg) + StringUtils.defaultString(jmsg.getText()); + if (status.length() > 253) { + status = status.substring(0, 252) + "…"; + } + status += " http://juick.com/" + jmsg.getMid(); + ConfigurationBuilder configurationBuilder = new ConfigurationBuilder() + .setDebugEnabled(true) + .setOAuthConsumerKey(twitter_consumer_key) + .setOAuthConsumerSecret(twitter_consumer_secret); + TwitterFactory tf = new TwitterFactory(configurationBuilder + .setOAuthAccessToken(t.getToken()) + .setOAuthAccessTokenSecret(t.getSecret()).build()); + try { + tf.getInstance().updateStatus(status); + } catch (Exception e) { + logger.info("Twitter exception", e); + } + }); + } + + @Override + public void processSystemEvent(SystemEvent systemEvent) { + var activity = systemEvent.getActivity(); + var msg = activity.getMessage(); + if (activity.getType().equals(SystemActivity.ActivityType.message)) { + if (MessageUtils.isPM(msg) || MessageUtils.isReply(msg) || msg.isService()) { + return; + } + if (StringUtils.isNotEmpty(crosspostService.getTwitterName(msg.getUser().getUid()))) { + if (msg.getTags().stream().noneMatch(t -> t.getName().equals("notwitter"))) { + twitterPost(msg); + } + } + } else if (activity.getType().equals(SystemActivity.ActivityType.like)) { + if (activity.getFrom().getName().equals(serviceUsername)) { + processTop(msg); + } + } + } + + @Override + public void processPingEvent(PingEvent pingEvent) { + + } + + private void processTop(Message message) { + Message jmsg = SerializationUtils.clone(message); + jmsg.setUser(serviceUser); + twitterPost(jmsg); + } +} diff --git a/src/main/java/com/juick/XMPPManager.java b/src/main/java/com/juick/XMPPManager.java new file mode 100644 index 00000000..f2d568af --- /dev/null +++ b/src/main/java/com/juick/XMPPManager.java @@ -0,0 +1,644 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick; + +import com.juick.model.User; +import com.juick.util.formatters.PlainTextFormatter; +import com.juick.model.CommandResult; +import com.juick.www.api.SystemActivity; +import com.juick.www.WebApp; +import com.juick.util.xmpp.iq.MessageQuery; +import com.juick.service.MessagesService; +import com.juick.service.PMQueriesService; +import com.juick.service.UserService; +import com.juick.service.component.*; +import com.juick.util.MessageUtils; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.XmppException; +import rocks.xmpp.core.session.Extension; +import rocks.xmpp.core.session.XmppSession; +import rocks.xmpp.core.session.XmppSessionConfiguration; +import rocks.xmpp.core.session.debug.LogbackDebugger; +import rocks.xmpp.core.stanza.AbstractIQHandler; +import rocks.xmpp.core.stanza.model.IQ; +import rocks.xmpp.core.stanza.model.Message; +import rocks.xmpp.core.stanza.model.Presence; +import rocks.xmpp.core.stanza.model.StanzaError; +import rocks.xmpp.core.stanza.model.client.ClientMessage; +import rocks.xmpp.core.stanza.model.client.ClientPresence; +import rocks.xmpp.core.stanza.model.errors.Condition; +import rocks.xmpp.extensions.caps.EntityCapabilitiesManager; +import rocks.xmpp.extensions.component.accept.ExternalComponent; +import rocks.xmpp.extensions.disco.ServiceDiscoveryManager; +import rocks.xmpp.extensions.disco.model.info.Identity; +import rocks.xmpp.extensions.filetransfer.FileTransfer; +import rocks.xmpp.extensions.filetransfer.FileTransferManager; +import rocks.xmpp.extensions.nick.model.Nickname; +import rocks.xmpp.extensions.oob.model.x.OobX; +import rocks.xmpp.extensions.ping.PingManager; +import rocks.xmpp.extensions.receipts.MessageDeliveryReceiptsManager; +import rocks.xmpp.extensions.vcard.temp.model.VCard; +import rocks.xmpp.extensions.version.SoftwareVersionManager; +import rocks.xmpp.extensions.version.model.SoftwareVersion; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; + +/** + * @author ugnich + */ +public class XMPPManager implements NotificationListener { + + private static final Logger logger = LoggerFactory.getLogger("XMPP"); + + private ExternalComponent xmpp; + @Inject + private CommandsManager commandsManager; + @Value("${xmppbot_jid:juick@localhost}") + private Jid jid; + @Value("${hostname:localhost}") + private String componentName; + @Value("${component_port:5347}") + private int componentPort; + @Value("${component_host:localhost}") + private String componentHost; + @Value("${xmpp_password:secret}") + private String password; + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + @Value("classpath:juick.png") + private Resource vCardImage; + + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private PMQueriesService pmQueriesService; + @Inject + private Executor applicationTaskExecutor; + @Value("${service_user:juick}") + private String serviceUsername; + @Inject + private WebApp webApp; + + private User serviceUser; + + @PostConstruct + public void init() { + logger.info("xmpp component start connecting to {}", componentPort); + XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() + .extensions(Extension.of(com.juick.model.Message.class), Extension.of(MessageQuery.class)) + .debugger(LogbackDebugger.class) + .defaultResponseTimeout(Duration.ofMillis(120000)) + .build(); + xmpp = ExternalComponent.create(componentName, password, configuration, componentHost, componentPort); + ServiceDiscoveryManager serviceDiscoveryManager = xmpp.getManager(ServiceDiscoveryManager.class); + serviceDiscoveryManager.addIdentity(Identity.clientBot().withName("Juick")); + EntityCapabilitiesManager entityCapabilitiesManager = xmpp.getManager(EntityCapabilitiesManager.class); + entityCapabilitiesManager.setNode("https://juick.com/caps"); + MessageDeliveryReceiptsManager messageDeliveryReceiptsManager = xmpp.getManager(MessageDeliveryReceiptsManager.class); + messageDeliveryReceiptsManager.setEnabled(true); + PingManager pingManager = xmpp.getManager(PingManager.class); + pingManager.setEnabled(true); + SoftwareVersionManager softwareVersionManager = xmpp.getManager(SoftwareVersionManager.class); + softwareVersionManager.setSoftwareVersion(new SoftwareVersion("Juick", "2.x", + System.getProperty("os.name", "generic"))); + VCard vCard = new VCard(); + vCard.setFormattedName("Juick"); + vCard.setBirthday(LocalDate.of(2008, 10, 22)); + try { + vCard.setUrl(new URL("http://juick.com/")); + vCard.setPhoto(new VCard.Image("image/png", IOUtils.toByteArray(vCardImage.getInputStream()))); + } catch (MalformedURLException e) { + logger.error("invalid url", e); + } catch (IOException e) { + logger.warn("invalid resource", e); + } + xmpp.addIQHandler(MessageQuery.class, iq -> { + Message warningMessage = new Message(iq.getFrom(), Message.Type.CHAT); + warningMessage.setFrom(jid); + warningMessage.setBody("Your XMPP client constantly polls us with XMPP query which is unsupported for years, please find http://juick.com/query#messages in your client code and remove that"); + xmpp.send(warningMessage); + return iq.createError(new StanzaError(Condition.BAD_REQUEST, "Please stop this spam")); + }); + xmpp.addIQHandler(VCard.class, new AbstractIQHandler(IQ.Type.GET) { + @Override + protected IQ processRequest(IQ iq) { + if (iq.getTo().equals(jid) || iq.getTo().asBareJid().equals(jid.asBareJid()) + || iq.getTo().asBareJid().toEscapedString().equals(jid.getDomain())) { + return iq.createResult(vCard); + } + User user = userService.getUserByName(iq.getTo().getLocal()); + if (!user.isAnonymous()) { + User info = userService.getUserInfo(user); + VCard userVCard = new VCard(); + userVCard.setFormattedName(info.getFullName()); + userVCard.setNickname(user.getName()); + try { + userVCard.setPhoto(new VCard.Image(URI.create(webApp.getAvatarUrl(user)))); + if (info.getUrl() != null) { + userVCard.setUrl(new URL(info.getUrl())); + } + } catch (MalformedURLException e) { + logger.warn("url exception", e); + } + return iq.createResult(userVCard); + } + return iq.createError(Condition.BAD_REQUEST); + } + }); + xmpp.addInboundMessageListener(e -> { + ClientMessage result = incomingMessage(e.getMessage()); + if (result != null) { + xmpp.send(result); + } + }); + FileTransferManager fileTransferManager = xmpp.getManager(FileTransferManager.class); + fileTransferManager.addFileTransferOfferListener(e -> { + try { + List allowedTypes = new ArrayList<>() {{ + add("png"); + add("jpg"); + }}; + String attachmentExtension = FilenameUtils.getExtension(e.getName()).toLowerCase(); + String targetFilename = String.format("%s.%s", + DigestUtils.md5Hex(String.format("%s-%s", + e.getInitiator().toString(), e.getSessionId()).getBytes()), attachmentExtension); + if (allowedTypes.contains(attachmentExtension)) { + Path filePath = Paths.get(tmpDir, targetFilename); + FileTransfer ft = e.accept(filePath).get(); + ft.addFileTransferStatusListener(st -> { + logger.debug("{}: received {} of {}", e.getName(), st.getBytesTransferred(), e.getSize()); + if (st.getStatus().equals(FileTransfer.Status.COMPLETED)) { + logger.info("transfer completed"); + try { + Jid initiator = e.getInitiator(); + ClientMessage result = incomingMessageJuick( + userService.getUserByJID(initiator.asBareJid().toEscapedString()), initiator, + jid.getLocal(), StringUtils.defaultString(e.getDescription()).trim(), URI.create(String.format("juick://%s", targetFilename))); + if (result != null) { + xmpp.send(result); + } + } catch (Exception e1) { + logger.error("ft error", e1); + } + + } else if (st.getStatus().equals(FileTransfer.Status.FAILED)) { + logger.info("transfer failed", ft.getException()); + Message msg = new Message(); + msg.setType(Message.Type.CHAT); + msg.setFrom(jid); + msg.setTo(e.getInitiator()); + msg.setBody("File transfer failed, please report to us"); + xmpp.sendMessage(msg); + } else if (st.getStatus().equals(FileTransfer.Status.CANCELED)) { + logger.info("transfer cancelled"); + } + }); + ft.transfer(); + logger.info("transfer started"); + } else { + e.reject(); + logger.info("transfer rejected"); + } + } catch (IOException | InterruptedException | ExecutionException e1) { + logger.error("ft error", e1); + } + }); + xmpp.addConnectionListener(event -> { + if (event.getType().equals(rocks.xmpp.core.session.ConnectionEvent.Type.RECONNECTION_SUCCEEDED)) { + logger.info("component connected"); + } + }); + xmpp.addSessionStatusListener(event -> { + logger.info("event: " + event.getStatus(), event.getThrowable()); + if (event.getStatus().equals(XmppSession.Status.AUTHENTICATED)) { + logger.info("Authenticated, broadcasting..."); + broadcastPresence(null); + } + }); + xmpp.addInboundPresenceListener(event -> { + incomingPresence(event.getPresence()); + }); + applicationTaskExecutor.execute(() -> { + try { + xmpp.connect(); + } catch (XmppException e) { + logger.warn("xmpp exception", e); + } + }); + serviceUser = userService.getUserByName(serviceUsername); + } + + private void sendJuickMessage(com.juick.model.Message jmsg, List users) { + List jids = new ArrayList<>(); + + for (User user : users) { + jids.addAll(userService.getJIDsbyUID(user.getUid())); + } + com.juick.model.Message fullMsg = messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); + String txt = "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(fullMsg) + "\n"; + String attachmentUrl = MessageUtils.attachmentUrl(fullMsg); + if (StringUtils.isNotEmpty(attachmentUrl)) { + txt += attachmentUrl + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; + txt += "#" + jmsg.getMid() + " http://juick.com/m/" + jmsg.getMid(); + + Nickname nick = new Nickname("@" + jmsg.getUser().getName()); + + Message msg = new Message(); + msg.setFrom(jid); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.setThread("juick-" + jmsg.getMid()); + msg.addExtension(jmsg); + msg.addExtension(nick); + if (StringUtils.isNotEmpty(attachmentUrl)) { + try { + OobX oob = new OobX(new URI(attachmentUrl)); + msg.addExtension(oob); + } catch (URISyntaxException e) { + logger.warn("uri exception", e); + } + } + for (String jid : jids) { + msg.setTo(Jid.of(jid)); + xmpp.send(ClientMessage.from(msg)); + } + } + + private void sendJuickComment(com.juick.model.Message jmsg, List users) { + String replyQuote; + String replyTo; + + com.juick.model.Message replyMessage = jmsg.getReplyto() > 0 ? messagesService.getReply(jmsg.getMid(), jmsg.getReplyto()) + : messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); + replyTo = replyMessage.getUser().getName(); + com.juick.model.Message fullReply = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); + replyQuote = StringUtils.defaultString(fullReply.getReplyQuote()); + + String txt = "Reply by @" + jmsg.getUser().getName() + ":\n" + replyQuote + "\n@" + replyTo + " "; + String attachmentUrl = MessageUtils.attachmentUrl(fullReply); + if (StringUtils.isNotEmpty(attachmentUrl)) { + txt += attachmentUrl + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n" + "#" + jmsg.getMid() + "/" + jmsg.getRid() + " http://juick.com/m/" + jmsg.getMid() + "#" + jmsg.getRid(); + + Message msg = new Message(); + msg.setFrom(jid); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.addExtension(jmsg); + for (User user : users) { + for (String jid : userService.getJIDsbyUID(user.getUid())) { + msg.setTo(Jid.of(jid)); + xmpp.send(ClientMessage.from(msg)); + } + } + } + + @Override + public void processSystemEvent(SystemEvent systemEvent) { + var activity = systemEvent.getActivity(); + var type = activity.getType(); + if (type.equals(SystemActivity.ActivityType.message)) { + processMessage(activity.getMessage(), activity.getTo()); + } else if (type.equals(SystemActivity.ActivityType.like)) { + if (systemEvent.getActivity().getFrom().equals(serviceUser)) { + processTop(systemEvent.getActivity().getMessage()); + } else { + processLike(activity.getFrom(), activity.getMessage(), activity.getTo()); + } + } + } + private void processMessage(com.juick.model.Message msg, List subscribers) { + if (msg.isService()) { + return; + } + if (MessageUtils.isPM(msg)) { + userService.getJIDsbyUID(msg.getTo().getUid()) + .forEach(userJid -> { + Message mm = new Message(); + mm.setTo(Jid.of(userJid)); + mm.setType(Message.Type.CHAT); + boolean inroster = pmQueriesService.havePMinRoster(msg.getUser().getUid(), userJid); + if (inroster) { + mm.setFrom(Jid.of(msg.getUser().getName(), "juick.com", "Juick")); + mm.setBody(msg.getText()); + } else { + mm.setFrom(jid); + mm.setBody("Private message from @" + msg.getUser().getName() + ":\n" + msg.getText()); + } + xmpp.send(ClientMessage.from(mm)); + }); + } else if (MessageUtils.isReply(msg)) { + sendJuickComment(msg, subscribers); + } + else { + sendJuickMessage(msg, subscribers); + } + } + + private ClientMessage makeReply(Jid jidTo, String txt) { + Message reply = new Message(); + reply.setFrom(jid); + reply.setTo(jidTo); + reply.setType(Message.Type.CHAT); + reply.setBody(txt); + return ClientMessage.from(reply); + } + + public void processLike(User liker, com.juick.model.Message jmsg, List users) { + if (!userService.isInBLAny(jmsg.getUser().getUid(), liker.getUid())) { + userService.getJIDsbyUID(jmsg.getUser().getUid()).forEach(authorJid -> { + Message xmppMessage = new Message(); + xmppMessage.setFrom(jid); + xmppMessage.setTo(Jid.of(authorJid)); + xmppMessage.setType(Message.Type.CHAT); + xmppMessage.addExtension(jmsg); + xmppMessage.setBody(String.format("%s recommended your post #%d. %s", + liker.getName(), jmsg.getMid(), PlainTextFormatter.formatUrl(jmsg))); + xmpp.send(ClientMessage.from(xmppMessage)); + }); + } + + String txt = "Recommended by @" + liker.getName() + ":\n"; + txt += "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(jmsg) + "\n"; + String attachmentUrl = MessageUtils.attachmentUrl(jmsg); + if (StringUtils.isNotEmpty(attachmentUrl)) { + txt += attachmentUrl + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; + txt += "#" + jmsg.getMid(); + if (jmsg.getReplies() > 0) { + if (jmsg.getReplies() % 10 == 1 && jmsg.getReplies() % 100 != 11) { + txt += " (" + jmsg.getReplies() + " reply)"; + } else { + txt += " (" + jmsg.getReplies() + " replies)"; + } + } + txt += " http://juick.com/m/" + jmsg.getMid(); + + Nickname nick = new Nickname("@" + jmsg.getUser().getName()); + + Message msg = new Message(); + msg.setFrom(jid); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.setThread("juick-" + jmsg.getMid()); + msg.addExtension(jmsg); + msg.addExtension(nick); + if (StringUtils.isNotEmpty(attachmentUrl)) { + try { + OobX oob = new OobX(new URI(attachmentUrl)); + msg.addExtension(oob); + } catch (URISyntaxException e) { + logger.warn("uri exception", e); + } + } + + for (User user : users) { + for (String jid : userService.getJIDsbyUID(user.getUid())) { + msg.setTo(Jid.of(jid)); + xmpp.send(ClientMessage.from(msg)); + } + } + } + + @Override + public void processPingEvent(PingEvent pingEvent) { + userService.getJIDsbyUID(pingEvent.getPinger().getUid()) + .forEach(userJid -> { + Presence p = new Presence(Jid.of(userJid)); + p.setFrom(jid); + p.setPriority((byte) 10); + xmpp.send(ClientPresence.from(p)); + }); + } + + public void processTop(com.juick.model.Message message) { + try { + commandsManager.processCommand(serviceUser, String.format("! #%d", message.getMid()), URI.create(StringUtils.EMPTY)); + } catch (Exception e) { + logger.warn("XMPP error", e); + } + } + + private void incomingPresence(Presence p) { + final String username = p.getTo().getLocal(); + final boolean toJuick = username.equals(jid.getLocal()); + + if (p.getType() == null) { + Presence reply = new Presence(); + reply.setFrom(p.getTo().asBareJid()); + reply.setTo(p.getFrom().asBareJid()); + reply.setType(Presence.Type.UNSUBSCRIBE); + xmpp.send(ClientPresence.from(reply)); + } else if (p.getType().equals(Presence.Type.PROBE)) { + int uid_to = 0; + if (!toJuick) { + uid_to = userService.getUIDbyName(username); + } else { + User visitor = userService.getUserByJID(p.getFrom().asBareJid().toEscapedString()); + if (visitor != null) { + userService.updateLastSeen(visitor); + } + } + + if (toJuick || uid_to > 0) { + Presence reply = new Presence(); + reply.setFrom(p.getTo().withResource(jid.getResource())); + reply.setTo(p.getFrom()); + reply.setPriority((byte)10); + if (!userService.getActiveJIDs().contains(p.getFrom().asBareJid().toEscapedString())) { + reply.setStatus("Send ON to enable notifications"); + } + xmpp.send(ClientPresence.from(reply)); + } else { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.ERROR); + reply.setId(p.getId()); + reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); + xmpp.send(ClientPresence.from(reply)); + } + } else if (p.getType().equals(Presence.Type.SUBSCRIBE)) { + boolean canSubscribe = false; + if (toJuick) { + canSubscribe = true; + } else { + int uid_to = userService.getUIDbyName(username); + if (uid_to > 0) { + pmQueriesService.addPMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); + canSubscribe = true; + } + } + if (canSubscribe) { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.SUBSCRIBED); + xmpp.send(ClientPresence.from(reply)); + + reply.setFrom(reply.getFrom().withResource(jid.getResource())); + reply.setPriority((byte) 10); + reply.setType(null); + xmpp.send(ClientPresence.from(reply)); + } else { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.ERROR); + reply.setId(p.getId()); + reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); + xmpp.send(ClientPresence.from(reply)); + } + } else if (p.getType().equals(Presence.Type.UNSUBSCRIBE)) { + if (!toJuick) { + int uid_to = userService.getUIDbyName(username); + if (uid_to > 0) { + pmQueriesService.removePMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); + } + } + + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.UNSUBSCRIBED); + xmpp.send(ClientPresence.from(reply)); + } + } + + public ClientMessage incomingMessage(Message msg) { + if (msg.getType() != null && msg.getType().equals(Message.Type.ERROR)) { + StanzaError error = msg.getError(); + if (error != null && error.getCondition().equals(Condition.RESOURCE_CONSTRAINT)) { + // offline query is full, deactivating this jid + if (userService.setActiveStatusForJID(msg.getFrom().toEscapedString(), UserService.ActiveStatus.Inactive)) { + logger.info("{} is inactive now", msg.getFrom()); + return null; + } + } + return null; + } + Jid to = msg.getTo(); + if (to.getDomain().equals(xmpp.getDomain().toEscapedString()) || to.equals(this.jid)) { + User user_from = userService.getUserByJID(msg.getFrom().asBareJid().toEscapedString()); + if ((user_from == null || user_from.isAnonymous()) && !msg.getFrom().equals(jid)) { + String signuphash = userService.getSignUpHashByJID(msg.getFrom().asBareJid().toEscapedString()); + return makeReply(msg.getFrom(), "Для того, чтобы начать пользоваться сервисом, пожалуйста пройдите быструю регистрацию: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nTo start using Juick, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); + } + URI attachment = URI.create(StringUtils.EMPTY); + OobX oobX = msg.getExtension(OobX.class); + if (oobX != null) { + attachment = oobX.getUri(); + } + try { + return incomingMessageJuick(user_from, msg.getFrom(), msg.getTo().getLocal(), StringUtils.defaultString(msg.getBody()).trim(), attachment); + } catch (Exception e1) { + logger.warn("message exception", e1); + } + } + ClientMessage errorMessage = ClientMessage.from(msg); + errorMessage.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); + return errorMessage; + } + private ClientMessage incomingMessageJuick(User user_from, Jid from, String to, String command, @Nonnull URI attachment) { + if (StringUtils.isBlank(command) && attachment.toString().isEmpty()) { + return null; + } + + messagesService.getUnread(user_from).forEach(mid -> messagesService.setRead(user_from, mid)); + + int commandlen = command.length(); + + // COMPATIBILITY + if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) { + command = command.substring(3); + } + + if (!jid.getLocal().equals(to)) { + // PM + if (!StringUtils.isEmpty(command)) { + commandsManager.commandPM(user_from, null, to, command); + return null; + } + } + + try { + CommandResult result = commandsManager.processCommand(user_from, command, attachment); + if (StringUtils.isNotBlank(result.getText())) { + return makeReply(from, result.getText()); + } + } catch (Exception e) { + logger.warn("xmpp command exception", e); + return makeReply(from, "Error processing command"); + } + return null; + } + + private void broadcastPresence(Presence.Type type) { + Presence presence = new Presence(); + presence.setFrom(jid); + if (type != null) { + presence.setType(type); + } + userService.getActiveJIDs().forEach(j -> { + try { + presence.setTo(Jid.of(j)); + xmpp.send(ClientPresence.from(presence)); + } catch (IllegalArgumentException ex) { + logger.warn("Invalid jid: {}", j, ex); + } + }); + } + + @PreDestroy + public void close() throws Exception { + broadcastPresence(Presence.Type.UNAVAILABLE); + if (xmpp != null) { + xmpp.close(); + } + } +} diff --git a/src/main/java/com/juick/adapters/SimpleDateAdapter.java b/src/main/java/com/juick/adapters/SimpleDateAdapter.java deleted file mode 100644 index 8f75fb7c..00000000 --- a/src/main/java/com/juick/adapters/SimpleDateAdapter.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.adapters; - -import com.juick.util.DateFormattersHolder; - -import javax.xml.bind.annotation.adapters.XmlAdapter; -import java.time.Instant; - -/** - * Created by vitalyster on 15.11.2016. - */ - -public class SimpleDateAdapter extends XmlAdapter { - - @Override - public String marshal(Instant v) { - return DateFormattersHolder.getMessageFormatterInstance().format(v); - } - - @Override - public Instant unmarshal(String v) { - return DateFormattersHolder.getMessageFormatterInstance().parse(v); - } -} diff --git a/src/main/java/com/juick/config/ActivityPubClientErrorHandler.java b/src/main/java/com/juick/config/ActivityPubClientErrorHandler.java new file mode 100644 index 00000000..cd67fb5e --- /dev/null +++ b/src/main/java/com/juick/config/ActivityPubClientErrorHandler.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.juick.service.activities.DeleteUserEvent; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.client.DefaultResponseErrorHandler; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; + +@Component +public class ActivityPubClientErrorHandler extends DefaultResponseErrorHandler { + private static final Logger logger = LoggerFactory.getLogger("ActivityPub"); + + @Inject + private ApplicationEventPublisher applicationEventPublisher; + @Override + public void handleError(URI contextUri, HttpMethod method, @Nonnull ClientHttpResponse response) throws IOException { + logger.warn("HTTP ERROR {} {} : {}", response.getStatusCode().value(), + response.getStatusText(), IOUtils.toString(response.getBody(), StandardCharsets.UTF_8)); + if (response.getStatusCode().equals(HttpStatus.GONE)) { + logger.warn("Server report {} is gone, deleting", contextUri.toASCIIString()); + applicationEventPublisher.publishEvent(new DeleteUserEvent(this, contextUri.toASCIIString())); + } + } +} diff --git a/src/main/java/com/juick/config/ActivityPubConfig.java b/src/main/java/com/juick/config/ActivityPubConfig.java new file mode 100644 index 00000000..0411d0c7 --- /dev/null +++ b/src/main/java/com/juick/config/ActivityPubConfig.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.ActivityPubManager; +import com.juick.SignatureManager; +import com.juick.www.api.activity.model.Activity; +import com.juick.util.HeaderRequestInterceptor; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +import javax.inject.Inject; +import java.net.URI; +import java.util.Collections; + +@Configuration +public class ActivityPubConfig { + @Inject + ActivityPubClientErrorHandler activityPubClientErrorHandler; + @Inject + ObjectMapper jsonMapper; + @Bean + public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() { + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(jsonMapper); + return converter; + } + @Bean + public ActivityPubManager activityPubManager() { + return new ActivityPubManager(); + } + @Bean + public RestTemplate apClient() { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory() { + @Override + protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) { + HttpClientContext context = HttpClientContext.create(); + context.setRequestConfig(getRequestConfig()); + return context; + } + RequestConfig getRequestConfig() { + RequestConfig.Builder builder = RequestConfig.custom() + .setCookieSpec(CookieSpecs.IGNORE_COOKIES); + return builder.build(); + } + }); + restTemplate.getMessageConverters().add(0, mappingJacksonHttpMessageConverter()); + restTemplate.setErrorHandler(activityPubClientErrorHandler); + restTemplate.setInterceptors(Collections.singletonList( + new HeaderRequestInterceptor("Accept", Activity.ACTIVITY_MEDIA_TYPE))); + return restTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/juick/config/MailConfig.java b/src/main/java/com/juick/config/MailConfig.java new file mode 100644 index 00000000..209796be --- /dev/null +++ b/src/main/java/com/juick/config/MailConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.juick.EmailManager; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty("service_email") +public class MailConfig { + @Bean + public EmailManager emailManager() { + return new EmailManager(); + } +} diff --git a/src/main/java/com/juick/config/RssConfig.java b/src/main/java/com/juick/config/RssConfig.java new file mode 100644 index 00000000..9619f2f7 --- /dev/null +++ b/src/main/java/com/juick/config/RssConfig.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.juick.www.rss.MessagesView; +import com.juick.www.rss.RepliesView; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.view.feed.AbstractRssFeedView; + +/** + * Created by aalexeev on 11/12/16. + */ +@Configuration +public class RssConfig { + @Bean + AbstractRssFeedView messagesView() { + return new MessagesView(); + } + @Bean + AbstractRssFeedView repliesView() { + return new RepliesView(); + } +} diff --git a/src/main/java/com/juick/config/SapeConfig.java b/src/main/java/com/juick/config/SapeConfig.java new file mode 100644 index 00000000..279cc88f --- /dev/null +++ b/src/main/java/com/juick/config/SapeConfig.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import ru.sape.Sape; + +/** + * Created by vitalyster on 29.03.2017. + */ +@Configuration +@ConditionalOnProperty("sape_user") +public class SapeConfig { + @Value("${sape_user:}") + private String token; + + @Bean + public Sape sape() { + return new Sape(token, "juick.com", 2000, 3600); + } +} diff --git a/src/main/java/com/juick/config/SecurityConfig.java b/src/main/java/com/juick/config/SecurityConfig.java new file mode 100644 index 00000000..8f6325f6 --- /dev/null +++ b/src/main/java/com/juick/config/SecurityConfig.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.juick.SignatureManager; +import com.juick.service.UserService; +import com.juick.service.security.HTTPSignatureAuthenticationFilter; +import com.juick.service.security.HashParamAuthenticationFilter; +import com.juick.service.security.JuickUserDetailsService; +import com.juick.service.security.entities.JuickUser; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.NullRememberMeServices; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; +import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import javax.annotation.Resource; +import javax.inject.Inject; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +/** + * Created by aalexeev on 11/21/16. + */ +@EnableWebSecurity +public class SecurityConfig { + @Resource + private UserService userService; + + private static final String COOKIE_NAME = "juick-remember-me"; + + @Bean + public UserDetailsService userDetailsService() { + return new JuickUserDetailsService(userService); + } + @Bean + static CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(Collections.singletonList("*")); + configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE")); + configuration.setAllowedHeaders(Collections.singletonList("*")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/api/**", configuration); + source.registerCorsConfiguration("/u/**", configuration); + source.registerCorsConfiguration("/n/**", configuration); + return source; + } + + @Configuration + @Order(1) + public static class ApiConfig extends WebSecurityConfigurerAdapter { + @Value("${auth_remember_me_key:secret}") + private String rememberMeKey; + @Resource + private UserService userService; + @Resource + private SignatureManager signatureManager; + ApiConfig() { + super(true); + } + @Bean + RememberMeServices apiTokenServices() { + return new NullRememberMeServices(); + } + @Bean + public HashParamAuthenticationFilter apiAuthenticationFilter() { + return new HashParamAuthenticationFilter(userService, apiTokenServices()); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.antMatcher("/api/**") + .addFilterBefore(apiAuthenticationFilter(), BasicAuthenticationFilter.class) + .addFilterBefore(new HTTPSignatureAuthenticationFilter(signatureManager, userService), BasicAuthenticationFilter.class) + .authorizeRequests() + .antMatchers(HttpMethod.OPTIONS).permitAll() + .antMatchers("/api/", "/api/messages", "/api/avatar", "/api/messages/discussions", + "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", + "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", + "/api/_google", "/api/_applelogin", "/api/signup", "/api/inbox", "/api/events", "/api/info/**", + "/api/nodeinfo/2.0").permitAll() + .anyRequest().hasRole("USER") + .and() + .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) + .and() + .httpBasic().authenticationEntryPoint(juickAuthenticationEntryPoint()) + .and().cors().configurationSource(corsConfigurationSource()) + .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and().exceptionHandling().authenticationEntryPoint(juickAuthenticationEntryPoint()) + .and() + .rememberMe() + .alwaysRemember(true) + .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(6 * 30)) + .rememberMeServices(apiTokenServices()) + .key(rememberMeKey) + .and() + .headers().defaultsDisabled().cacheControl(); + } + + @Bean + public AuthenticationEntryPoint juickAuthenticationEntryPoint() { + var entryPoint = new BasicAuthenticationEntryPoint(); + entryPoint.setRealmName("Juick"); + return entryPoint; + } + + @Override + public void configure(WebSecurity web) { + web.debug(false); + web.ignoring().antMatchers("/api/v2/api-docs", "/api/configuration/ui", "/api/swagger-resources/**", + "/api/configuration/**", "/swagger-ui.html", "/webjars/**", "/h2-console/**"); + } + } + + @Configuration + public static class WebConfig extends WebSecurityConfigurerAdapter { + @Value("${auth_remember_me_key:secret}") + private String rememberMeKey; + @Value("${web_domain:localhost}") + private String webDomain; + @Resource + private UserService userService; + @Inject + private UserDetailsService userDetailsService; + @Bean + @Qualifier("www") + public HashParamAuthenticationFilter wwwAuthenticationFilter() { + return new HashParamAuthenticationFilter(userService, hashCookieServices()); + } + @Bean + @Qualifier("www") + public RememberMeServices hashCookieServices() { + TokenBasedRememberMeServices services = new TokenBasedRememberMeServices( + rememberMeKey, userDetailsService); + + services.setCookieName(COOKIE_NAME); + services.setCookieDomain(webDomain); + services.setAlwaysRemember(true); + services.setTokenValiditySeconds(6 * 30 * 24 * 3600); + services.setUseSecureCookie(false); // TODO set true if https is supports + + return services; + } + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .addFilterBefore(wwwAuthenticationFilter(), BasicAuthenticationFilter.class) + .authorizeRequests() + .antMatchers("/settings", "/pm/**", "/**/bl", "/_twitter", "/post", "/post2", "/comment") + .authenticated() + .antMatchers("/actuator/**").hasRole("ADMIN") + .anyRequest().permitAll() + .and() + .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) + .and().cors().configurationSource(corsConfigurationSource()) + .and().sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .invalidSessionUrl("/") + .and() + .logout() + .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) + .invalidateHttpSession(true) + .logoutUrl("/logout") + .logoutSuccessUrl("/") + .deleteCookies("hash", COOKIE_NAME) + .and() + .formLogin() + .loginPage("/login") + .permitAll() + .defaultSuccessUrl("/") + .loginProcessingUrl("/login") + .usernameParameter("username") + .passwordParameter("password") + .failureUrl("/login?error=1") + .and() + .rememberMe() + .rememberMeCookieDomain(webDomain).key(rememberMeKey) + .rememberMeServices(hashCookieServices()) + .and() + .csrf().disable() + .headers().defaultsDisabled().cacheControl(); + } + @Override + public void configure(WebSecurity web) { + web.debug(false); + web.ignoring().antMatchers("/style*.css", "/scripts*.js", "/h2-console/**", "/.well-known/**"); + } + } +} diff --git a/src/main/java/com/juick/config/SignInWithAppleConfig.java b/src/main/java/com/juick/config/SignInWithAppleConfig.java new file mode 100644 index 00000000..0b41cb7e --- /dev/null +++ b/src/main/java/com/juick/config/SignInWithAppleConfig.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.github.scribejava.apis.AppleClientSecretGenerator; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +@Configuration +public class SignInWithAppleConfig { + @Value("${apple_app_id:com.example.app}") + private String appId; + @Value("${apple_team_id:teamid}") + private String teamId; + @Value("${apple_key_id:keyid}") + private String keyId; + @Value("${apple_key_path:classpath:testkey.p8}") + private Resource keyPath; + + @Bean + public AppleClientSecretGenerator clientSecretGenerator() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { + return new AppleClientSecretGenerator(appId, teamId, keyId, keyPath.getFile().toPath()); + } +} diff --git a/src/main/java/com/juick/config/StorageConfig.java b/src/main/java/com/juick/config/StorageConfig.java new file mode 100644 index 00000000..d46b0a4f --- /dev/null +++ b/src/main/java/com/juick/config/StorageConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +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 StorageConfig { + + @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/src/main/java/com/juick/config/TelegramConfig.java b/src/main/java/com/juick/config/TelegramConfig.java new file mode 100644 index 00000000..fb0abd4f --- /dev/null +++ b/src/main/java/com/juick/config/TelegramConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.juick.TelegramBotManager; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(name = "telegram_token") +public class TelegramConfig { + @Bean + public TelegramBotManager telegramBotManager() { + return new TelegramBotManager(); + } +} diff --git a/src/main/java/com/juick/config/TwitterConfig.java b/src/main/java/com/juick/config/TwitterConfig.java new file mode 100644 index 00000000..22540dc5 --- /dev/null +++ b/src/main/java/com/juick/config/TwitterConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.juick.TwitterManager; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty("twitter_consumer_key") +public class TwitterConfig { + @Bean + public TwitterManager twitterManager() { + return new TwitterManager(); + } +} diff --git a/src/main/java/com/juick/config/WebConfig.java b/src/main/java/com/juick/config/WebConfig.java new file mode 100644 index 00000000..506c4dcf --- /dev/null +++ b/src/main/java/com/juick/config/WebConfig.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.juick.*; +import com.juick.service.HelpService; +import com.mitchellbosecke.pebble.PebbleEngine; +import com.mitchellbosecke.pebble.extension.FormatterExtension; +import com.mitchellbosecke.pebble.loader.ClasspathLoader; +import com.mitchellbosecke.pebble.loader.Loader; +import com.mitchellbosecke.pebble.spring.extension.SpringExtension; +import com.mitchellbosecke.pebble.spring.servlet.PebbleViewResolver; +import com.overzealous.remark.Options; +import com.overzealous.remark.Remark; +import org.apache.commons.codec.CharEncoding; +import org.commonmark.ext.autolink.AutolinkExtension; +import org.commonmark.node.Link; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.http.CacheControl; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; +import org.springframework.web.servlet.resource.VersionResourceResolver; + +import java.net.MalformedURLException; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +/** + * Created by aalexeev on 11/22/16. + */ +@Configuration +@EnableCaching +@EnableAsync(proxyTargetClass = true) +@EnableScheduling +public class WebConfig implements WebMvcConfigurer { + @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String imgDir; + @Bean + public CaffeineCacheManager cacheManager() { + return new CaffeineCacheManager("help"); + } + + @Bean + public HelpService helpService() { + return new HelpService("help"); + } + + @Bean + public Parser cmParser() { + return Parser.builder().extensions(Collections.singletonList(AutolinkExtension.create())).build(); + } + @Bean + public HtmlRenderer helpRenderer() { + return HtmlRenderer.builder() + .attributeProviderFactory(context -> (node, tagName, attributes) -> { + if (node instanceof Link) { + Link link = (Link) node; + if (link.getDestination().startsWith("/")) { + String destination = "/" + helpService().getHelpPath() + link.getDestination(); + link.setDestination(destination); + attributes.put("href", destination); + } + } + }) + .build(); + } + @Bean + public Loader templateLoader() { + return new ClasspathLoader(); + } + + @Bean + public SpringExtension springExtension() { + return new SpringExtension(); + } + + @Bean + public PebbleEngine pebbleEngine() { + boolean devToolsArePresent = false; + try { + Class.forName("org.springframework.boot.devtools.livereload.Connection"); + devToolsArePresent = true; + } catch (ClassNotFoundException e) { + // release mode + } + return new PebbleEngine.Builder() + .loader(this.templateLoader()) + .cacheActive(!devToolsArePresent) + .extension(springExtension()) + .extension(new FormatterExtension()) + .newLineTrimming(false) + .strictVariables(true) + .build(); + } + + @Value("${keystore:classpath:juick-test-key.p12}") + private Resource keystore; + @Value("${keystore_password:secret}") + private String keystorePassword; + @Bean + public ResourceUrlEncodingFilter resourceUrlEncodingFilter() { + return new ResourceUrlEncodingFilter(); + } + @Bean + public KeystoreManager keystoreManager() { + return new KeystoreManager(keystore, keystorePassword); + } + @Bean + public Remark remarkConverter() { + Options options = new Options(); + options.inlineLinks = true; + return new Remark(options); + } + @Bean + public CommandsManager commandsManager() { + return new CommandsManager(); + } + @Bean + public ServerManager serverManager() { + return new ServerManager(); + } + @Bean + public SignatureManager signatureManager() { + return new SignatureManager(); + } + @Bean + public TopManager topManager() { + return new TopManager(); + } + + @Bean + public ViewResolver viewResolver() { + PebbleViewResolver viewResolver = new PebbleViewResolver(); + viewResolver.setPrefix("templates"); + viewResolver.setSuffix(".html"); + viewResolver.setPebbleEngine(pebbleEngine()); + viewResolver.setCharacterEncoding(CharEncoding.UTF_8); + return viewResolver; + } + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + try { + registry + .addResourceHandler("/**", "/i/a/**") + .addResourceLocations("classpath:/static/", Paths.get(imgDir, "/a/").toUri().toURL().toString()) + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)) + .resourceChain(false) + .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**", "/i/a/**")); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/com/juick/config/XMPPConfig.java b/src/main/java/com/juick/config/XMPPConfig.java new file mode 100644 index 00000000..8a8fa4cc --- /dev/null +++ b/src/main/java/com/juick/config/XMPPConfig.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.juick.XMPPManager; +import com.juick.util.xmpp.JidConverter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; + +@Configuration +@ConditionalOnProperty("xmppbot_jid") +public class XMPPConfig { + @Bean + public static ConversionService conversionService() { + DefaultFormattingConversionService cs = new DefaultFormattingConversionService(); + cs.addConverter(new JidConverter()); + return cs; + } + @Bean + public XMPPManager xmppConnection() { + return new XMPPManager(); + } +} diff --git a/src/main/java/com/juick/formatters/PlainTextFormatter.java b/src/main/java/com/juick/formatters/PlainTextFormatter.java deleted file mode 100644 index 21cabac2..00000000 --- a/src/main/java/com/juick/formatters/PlainTextFormatter.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.formatters; - -import com.juick.model.Message; -import com.juick.util.MessageUtils; -import org.apache.commons.lang3.StringUtils; -import org.ocpsoft.prettytime.PrettyTime; - -import java.util.Date; -import java.util.Locale; - -/** - * Created by vitalyster on 12.10.2016. - */ -public class PlainTextFormatter { - static PrettyTime pt = new PrettyTime(new Locale("en")); - - public static String formatPost(Message jmsg) { - return formatPost(jmsg, false); - } - - public static String formatPost(Message jmsg, boolean markdown) { - StringBuilder sb = new StringBuilder(); - String title = MessageUtils.isReply(jmsg) ? "Reply by @" : MessageUtils.isPM(jmsg) ? "Private message from @" : "@"; - String subtitle = MessageUtils.isReply(jmsg) ? markdown ? MessageUtils.escapeMarkdown(StringUtils.defaultString(jmsg.getReplyQuote())) - : jmsg.getReplyQuote() - : markdown ? MessageUtils.getMessageHashTags(jmsg) : MessageUtils.getTagsString(jmsg); - sb.append(title).append(markdown ? MessageUtils.getMarkdownUser(jmsg.getUser()) : jmsg.getUser().getName()).append(":\n") - .append(subtitle).append("\n"); - if (markdown) { - sb.append(MessageUtils.formatMarkdownText(jmsg)); - } else { - sb.append(StringUtils.defaultString(jmsg.getText())); - } - sb.append("\n"); - if (!markdown && StringUtils.isNotEmpty(jmsg.getAttachmentType())) { - sb.append(MessageUtils.attachmentUrl(jmsg)); - } - return sb.toString(); - } - - public static String formatPostSummary(Message m) { - int cropLength = 384; - String timeAgo = pt.format(Date.from(m.getCreated())); - String repliesCount = m.getReplies() == 1 ? "; 1 reply" : m.getReplies() == 0 ? "" - : String.format("; %d replies", m.getReplies()); - StringBuilder sb = new StringBuilder(); - String txt = StringUtils.defaultString(m.getText()); - String attachmentUrl = MessageUtils.attachmentUrl(m); - if (StringUtils.isNotEmpty(attachmentUrl)) { - sb.append(attachmentUrl).append("\n"); - } - if (txt.length() >= cropLength) { - sb.append(StringUtils.substring(txt, 0, cropLength)).append(" [...]"); - } else { - sb.append(txt); - } - return String.format("@%s:%s\n%s\n#%s (%s%s) %s", - m.getUser().getName(), MessageUtils.getTagsString(m), sb.toString(), formatPostNumber(m), timeAgo, repliesCount, formatUrl(m)); - } - - public static String formatUrl(Message jmsg) { - if (MessageUtils.isReply(jmsg)) { - return String.format("https://juick.com/m/%d#%d", jmsg.getMid(), jmsg.getRid()); - } else if (MessageUtils.isPM(jmsg)) { - return "https://juick.com/pm/inbox"; - } - return "https://juick.com/m/" + jmsg.getMid(); - } - - public static String markdownUrl(String url, String description) { - if (StringUtils.isNotBlank(description)) { - return String.format("[%s](%s)", description, url); - } else { - return url; - } - } - - public static String formatPostNumber(Message jmsg) { - if (jmsg.getRid() > 0) { - return String.format("%d/%d", jmsg.getMid(), jmsg.getRid()); - } - return String.format("%d", jmsg.getMid()); - } - - public static String formatTwitterCard(Message jmsg) { - return MessageUtils.getMessageHashTags(jmsg) + StringUtils.defaultString(jmsg.getText()); - } -} diff --git a/src/main/java/com/juick/model/Message.java b/src/main/java/com/juick/model/Message.java index b1460957..e53afbfe 100644 --- a/src/main/java/com/juick/model/Message.java +++ b/src/main/java/com/juick/model/Message.java @@ -19,7 +19,7 @@ package com.juick.model; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.juick.adapters.SimpleDateAdapter; +import com.juick.util.adapters.SimpleDateAdapter; import org.apache.commons.lang3.builder.ToStringBuilder; import javax.annotation.Nonnull; diff --git a/src/main/java/com/juick/server/ActivityPubManager.java b/src/main/java/com/juick/server/ActivityPubManager.java deleted file mode 100644 index 50af506b..00000000 --- a/src/main/java/com/juick/server/ActivityPubManager.java +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright (C) 2008-2020, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server; - -import com.juick.model.Message; -import com.juick.model.Reaction; -import com.juick.model.User; -import com.juick.formatters.PlainTextFormatter; -import com.juick.model.Tag; -import com.juick.www.api.SystemActivity.ActivityType; -import com.juick.www.api.activity.model.Context; -import com.juick.www.api.activity.model.activities.*; -import com.juick.www.api.activity.model.objects.Hashtag; -import com.juick.www.api.activity.model.objects.Image; -import com.juick.www.api.activity.model.objects.Mention; -import com.juick.www.api.activity.model.objects.Note; -import com.juick.www.api.activity.model.objects.Person; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpUtils; -import com.juick.service.MessagesService; -import com.juick.service.SocialService; -import com.juick.service.UserService; -import com.juick.service.activities.*; -import com.juick.service.component.*; -import com.juick.util.MessageUtils; -import com.mitchellbosecke.pebble.PebbleEngine; -import com.mitchellbosecke.pebble.template.PebbleTemplate; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - -import javax.annotation.Nonnull; -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; -import java.net.URI; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -@Component -public class ActivityPubManager implements ActivityListener, NotificationListener { - private static final Logger logger = LoggerFactory.getLogger("ActivityPub"); - @Inject - private SignatureManager signatureManager; - @Inject - private SocialService socialService; - @Inject - private UserService userService; - @Inject - private MessagesService messagesService; - @Inject - private PebbleEngine pebbleEngine; - @Value("${ap_base_uri:http://localhost:8080/}") - private String baseUri; - @Value("${service_user:juick}") - private String serviceUsername; - - private User serviceUser; - - @PostConstruct - public void init() { - serviceUser = userService.getUserByName(serviceUsername); - } - - @Override - public void processFollowEvent(@Nonnull FollowEvent followEvent) { - String acct = (String)followEvent.getRequest().getObject(); - logger.info("received follower request to {}", acct); - User followedUser = socialService.getUserByAccountUri(acct); - if (!followedUser.isAnonymous()) { - // automatically accept follower requests - Person me = (Person) signatureManager.getContext(URI.create(acct)).get(); - Person follower = (Person) signatureManager.getContext(URI.create(followEvent.getRequest().getActor())).get(); - Accept accept = new Accept(); - accept.setActor(me.getId()); - accept.setObject(followEvent.getRequest()); - try { - signatureManager.post(me, follower, accept); - socialService.addFollower(followedUser, follower.getId()); - logger.info("Follower added for {}", followedUser.getName()); - } catch (IOException e) { - logger.info("activitypub exception", e); - } - } - } - - @Override - public void undoFollowEvent(UndoFollowEvent event) { - String actor = event.getActor(); - String me = event.getObject(); - logger.info("{} stopping to follow {}", actor, me); - User followedUser = socialService.getUserByAccountUri(me); - if (!followedUser.isAnonymous()) { - socialService.removeFollower(followedUser, actor); - } - } - - @Override - public void deleteUserEvent(DeleteUserEvent event) { - String acct = event.getUserUri(); - logger.info("Deleting {} from followers", acct); - socialService.removeAccount(acct); - } - - @Override - public void deleteMessageEvent(DeleteMessageEvent event) { - Message msg = event.getMessage(); - User user = msg.getUser(); - String userUri = personUri(user); - Note note = makeNote(msg); - Person me = (Person) signatureManager.getContext(URI.create(userUri)).get(); - socialService.getFollowers(user).forEach(acct -> { - Person follower = (Person) signatureManager.getContext(URI.create(acct)).get(); - Delete delete = new Delete(); - delete.setId(note.getId()); - delete.setActor(me.getId()); - delete.setPublished(note.getPublished()); - delete.setObject(note); - try { - logger.info("Deletion to follower {}", follower.getId()); - signatureManager.post(me, follower, delete); - } catch (IOException e) { - logger.warn("activitypub exception", e); - } - }); - } - - @Override - public void processAnnounceEvent(AnnounceEvent event) { - UriComponents uriComponents = UriComponentsBuilder.fromUriString(event.getMessageUri()).build(); - List segments = uriComponents.getPathSegments(); - if (segments.get(0).equals("n")) { - String[] ids = segments.get(1).split("-", 2); - if (ids.length == 2 && Integer.parseInt(ids[1]) == 0) { - // only messages - logger.info("{} recommends {}", event.getActorUri(), Integer.valueOf(ids[0])); - messagesService.likeMessage(Integer.parseInt(ids[0]), 0, Reaction.LIKE, event.getActorUri()); - } - } - } - - @Override - public void undoAnnounceEvent(UndoAnnounceEvent event) { - UriComponents uriComponents = UriComponentsBuilder.fromUriString(event.getMessageUri()).build(); - List segments = uriComponents.getPathSegments(); - if (segments.get(0).equals("n")) { - String[] ids = segments.get(1).split("-", 2); - if (ids.length == 2 && Integer.parseInt(ids[1]) == 0) { - // only messages - logger.info("{} stop recommending {}", event.getActorUri(), Integer.valueOf(ids[0])); - messagesService.likeMessage(Integer.parseInt(ids[0]), 0, null, event.getActorUri()); - } - } - } - - @Override - public void processUpdateEvent(UpdateEvent event) { - String objectUri = event.getMessageUri(); - User user = event.getUser(); - String userUri = personUri(user); - Person me = (Person) signatureManager.getContext(URI.create(userUri)).get(); - socialService.getFollowers(user).forEach(acct -> { - Person follower = (Person) signatureManager.getContext(URI.create(acct)).get(); - Update update = new Update(); - update.setId(objectUri + "#update"); - update.setActor(me.getId()); - update.setObject(objectUri); - try { - logger.info("Update to follower {}", follower.getId()); - signatureManager.post(me, follower, update); - } catch (IOException e) { - logger.warn("activitypub exception", e); - } - }); - } - - @Override - public void processSystemEvent(SystemEvent systemEvent) { - ActivityType type = systemEvent.getActivity().getType(); - if (type.equals(ActivityType.message)) { - processMessage(systemEvent.getActivity().getMessage()); - } else if (type.equals(ActivityType.like)) { - if (systemEvent.getActivity().getFrom().equals(serviceUser)) { - processTop(systemEvent.getActivity().getMessage()); - } - } - } - private void processMessage(Message msg) { - if (MessageUtils.isPM(msg) || msg.isService()) { - return; - } - User user = msg.getUser(); - String userUri = personUri(user); - Note note = makeNote(msg); - Person me = (Person) signatureManager.getContext(URI.create(userUri)).get(); - Set subscribers = new HashSet<>(socialService.getFollowers(user)); - if (MessageUtils.isReply(msg) && msg.getTo().getUri().toASCIIString().length() > 0) { - String replier = msg.getTo().getUri().toASCIIString(); - subscribers.add(replier); - List cc = new ArrayList<>(note.getCc()); - cc.add(replier); - note.setCc(cc); - } - subscribers.addAll(note.getCc()); - subscribers.forEach(acct -> { - Optional context = signatureManager.getContext(URI.create(acct)); - if (context.isPresent() && context.get() instanceof Person) { - Person follower = (Person)context.get(); - Create create = new Create(); - create.setId(note.getId()); - create.setActor(me.getId()); - create.setPublished(note.getPublished()); - create.setObject(note); - try { - signatureManager.post(me, follower, create); - } catch (IOException e) { - logger.warn("activitypub exception", e); - } - } - }); - } - - public String inboxUri() { - UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); - return uri.replacePath("/api/inbox").toUriString(); - } - - public String outboxUri(User user) { - UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); - return uri.replacePath(String.format("/u/%s/blog/toc", user.getName())).toUriString(); - } - - public String personUri(User user) { - if (user.getUri().toString().length() > 0) { - return user.getUri().toASCIIString(); - } - UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); - return uri.replacePath(String.format("/u/%s", user.getName())).toUriString(); - } - public String personWebUri(User user) { - UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); - return uri.replacePath(String.format("/%s/", user.getName())).toUriString(); - } - - public String followersUri(User user) { - UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); - return uri.replacePath(String.format("/u/%s/followers/toc", user.getName())).toUriString(); - } - - public String followingUri(User user) { - UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); - return uri.replacePath(String.format("/u/%s/following/toc", user.getName())).toUriString(); - } - public String messageUri(Message msg) { - return messageUri(msg.getMid(), msg.getRid()); - } - public String messageUri(int mid, int rid) { - UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); - uri.replacePath(String.format("/n/%d-%d", mid, rid)); - return uri.toUriString(); - } - public String tagUri(Tag tag) { - UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); - return uri.replacePath(String.format("/t/%s", tag.getName())).toUriString(); - } - - public String postId(String messageUri) { - UriComponents uri = UriComponentsBuilder.fromUriString(messageUri).build(); - return uri.getPath().substring(uri.getPath().lastIndexOf('/') + 1).replace("-", "/"); - } - - public Note makeNote(Message msg) { - Note note = new Note(); - note.setId(messageUri(msg)); - note.setUrl(PlainTextFormatter.formatUrl(msg)); - note.setAttributedTo(personUri(msg.getUser())); - if (MessageUtils.isReply(msg)) { - if (msg.getReplyToUri().toASCIIString().length() > 0) { - note.setInReplyTo(msg.getReplyToUri().toASCIIString()); - } else { - note.setInReplyTo(messageUri(msg.getMid(), msg.getReplyto())); - } - } - if (MessageUtils.isPM(msg)) { - note.setTo(Collections.singletonList(personUri(msg.getTo()))); - } else { - note.setTo(Collections.singletonList(Context.ACTIVITYSTREAMS_PUBLIC)); - note.setCc(Collections.singletonList(followersUri(msg.getUser()))); - } - note.setPublished(msg.getCreated()); - if (StringUtils.isNotBlank(msg.getAttachmentType())) { - Image attachment = new Image(); - attachment.setId(msg.getAttachment().getMedium().getUrl()); - attachment.setUrl(msg.getAttachment().getMedium().getUrl()); - attachment.setMediaType(HttpUtils.mediaType(msg.getAttachmentType())); - note.setAttachment(Collections.singletonList(attachment)); - } - note.setTags(msg.getTags().stream().map(t -> new Hashtag(tagUri(t), t.getName())).collect(Collectors.toList())); - if (msg.getReplyToUri() != null && msg.getReplyToUri().toASCIIString().length() > 0) { - Optional noteContext = signatureManager.getContext(msg.getReplyToUri()); - if (noteContext.isPresent()) { - Note activity = (Note) noteContext.get(); - Optional personContext = signatureManager.getContext(URI.create(activity.getAttributedTo())); - if (personContext.isPresent()) { - Person person = (Person) personContext.get(); - note.getTags().add(new Mention(person.getUrl(), person.getPreferredUsername())); - msg.getTo().setName(person.getPreferredUsername()); - note.setInReplyTo(activity.getInReplyTo()); - } - } - } else if (MessageUtils.isReply(msg)) { - note.getTags().add(new Mention(personWebUri(msg.getTo()), msg.getTo().getName())); - } - MessageUtils.getGlobalMentions(msg).forEach(m -> { - // @user@server.tld -> user@server.tld - Optional personContext = signatureManager.discoverPerson(m.substring(1)); - if (personContext.isPresent()) { - Person person = (Person) personContext.get(); - note.getTags().add(new Mention(person.getUrl(), person.getPreferredUsername())); - List cc = new ArrayList<>(note.getCc()); - cc.add(person.getId()); - note.setCc(cc); - } - }); - if (msg.isHtml()) { - note.setContent(msg.getText()); - } else { - PebbleTemplate noteTemplate = pebbleEngine.getTemplate("layouts/note"); - Map context = new HashMap<>(); - context.put("msg", msg); - context.put("baseUri", baseUri); - try { - Writer writer = new StringWriter(); - noteTemplate.evaluate(writer, context); - note.setContent(writer.toString()); - } catch (IOException e) { - logger.warn("template not rendered, falling back"); - note.setContent(MessageUtils.formatMessage(StringUtils.defaultString(msg.getText()))); - } - } - return note; - } - - @Override - public void processPingEvent(PingEvent pingEvent) { - - } - - private void processTop(Message message) { - Note note = makeNote(message); - Announce announce = new Announce(); - announce.setId(note.getId() + "#top"); - announce.setActor(personUri(serviceUser)); - announce.setTo(Collections.singletonList(Context.ACTIVITYSTREAMS_PUBLIC)); - announce.setObject(note); - Person me = (Person) signatureManager.getContext(URI.create(announce.getActor())).get(); - socialService.getFollowers(serviceUser).forEach(acct -> { - var follower = signatureManager.getContext(URI.create(acct)); - follower.ifPresentOrElse((person) -> { - try { - logger.info("Announcing top: {}", message.getMid()); - signatureManager.post(me, (Person)person, announce); - } catch (IOException e) { - logger.warn("activitypub exception", e); - } - }, () -> logger.warn("Follower not found: {}", acct)); - }); - } - public User personToUser(URI uri) throws HttpBadRequestException { - Person person = (Person) signatureManager.getContext(uri).orElseThrow(HttpBadRequestException::new); - User user = new User(); - user.setUri(URI.create(person.getId())); - user.setName(person.getPreferredUsername()); - if (person.getIcon() != null) { - user.setAvatar(person.getIcon().getUrl()); - } - return user; - } -} diff --git a/src/main/java/com/juick/server/CommandsManager.java b/src/main/java/com/juick/server/CommandsManager.java deleted file mode 100644 index bf907855..00000000 --- a/src/main/java/com/juick/server/CommandsManager.java +++ /dev/null @@ -1,594 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server; - -import com.juick.model.Message; -import com.juick.model.Tag; -import com.juick.model.User; -import com.juick.formatters.PlainTextFormatter; -import com.juick.model.CommandResult; -import com.juick.model.TagStats; -import com.juick.www.api.SystemActivity; -import com.juick.server.helpers.annotation.UserCommand; -import com.juick.server.util.HttpUtils; -import com.juick.www.WebApp; -import com.juick.service.*; -import com.juick.service.activities.DeleteMessageEvent; -import com.juick.service.component.*; -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.apache.commons.text.StringEscapeUtils; -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 javax.annotation.Nonnull; -import javax.inject.Inject; -import java.io.IOException; -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 { - private static final Logger logger = LoggerFactory.getLogger(CommandsManager.class); - @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; - @Inject - private WebApp webApp; - @Inject - private ActivityPubManager activityPubManager; - - public CommandResult processCommand(@Nonnull User user, String data, @Nonnull URI attachment) throws Exception { - if (!user.isAnonymous()) { - userService.updateLastSeen(user); - } - String strippedData = StringUtils.stripStart(data, null); - if (strippedData.startsWith("?OTR")) { - return CommandResult.fromString("?OTR Error: we are not using OTR"); - } - String input = StringEscapeUtils.unescapeHtml4(MessageUtils.stripNonSafeUrls(strippedData)); - Optional 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 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> tags = tagService.fromString(input); - if (tags.getRight().size() > 5) { - return CommandResult.fromString("Sorry, 5 tags maximum."); - } - // new message - String body = tags.getLeft().trim(); - if (body.length() > 4096) { - return CommandResult.fromString("Sorry, 4096 characters maximum."); - } - 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); - } - if (StringUtils.isEmpty(body) && !haveAttachment) { - return CommandResult.fromString("Empty message"); - } - 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).orElseThrow(IllegalStateException::new); - msg.getUser().setAvatar(webApp.getAvatarUrl(msg.getUser())); - subscriptionService.subscribeMessage(msg, user); - - applicationEventPublisher.publishEvent(new SystemEvent(this, SystemActivity.read(user, msg))); - applicationEventPublisher.publishEvent(new SystemEvent(this, - SystemActivity.message(user, msg, subscriptionService.getSubscribedUsers(msg.getUser().getUid(), msg)))); - 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]); - user_to.setAvatar(webApp.getAvatarUrl(user_to)); - if (!user_to.isAnonymous()) { - if (!userService.isInBLAny(user_to.getUid(), user_from.getUid())) { - if (pmQueriesService.createPM(user_from.getUid(), user_to.getUid(), body)) { - Message jmsg = new Message(); - jmsg.setUser(user_from); - jmsg.setTo(user_to); - jmsg.setText(body); - applicationEventPublisher.publishEvent(new SystemEvent(this, - SystemActivity.message(user_from, 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 blusers = userService.getUserBLUsers(user_from.getUid()); - List 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 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 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 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 friends = userService.getUserFriends(currentUser.getUid()); - List 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 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) { - Optional msg = messagesService.getMessage(mid); - if (msg.isPresent()) { - if (msg.get().getUser() == user) { - return CommandResult.fromString("You can't recommend your own messages."); - } - MessagesService.RecommendStatus status = messagesService.recommendMessage(mid, user.getUid(), user.getUri().toASCIIString()); - switch (status) { - case Added: - applicationEventPublisher.publishEvent(new SystemEvent(this, SystemActivity.like(user, msg.get(), - subscriptionService.getUsersSubscribedToUserRecommendations( - user.getUid(), msg.get())))); - 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 SystemEvent(this, - SystemActivity.follow(user, Collections.singletonList(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 were 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 were 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(@Nonnull User user, URI attachment, String... args) { - boolean subscribe = args[0].equalsIgnoreCase("s"); - int mid = NumberUtils.toInt(args[1], 0); - Optional msg = messagesService.getMessage(mid); - if (msg.isPresent()) { - if (subscribe) { - if (subscriptionService.subscribeMessage(msg.get(), user)) { - applicationEventPublisher.publishEvent( - new SystemEvent(this, SystemActivity.read(user, msg.get()))); - return CommandResult.fromString("Subscribed"); - } - } else { - if (subscriptionService.unSubscribeMessage(mid, user.getUid())) { - return CommandResult.fromString("Unsubscribed from #" + mid); - } - return CommandResult.fromString("You were 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() && !blogUser.isBanned()) { - List 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(@Nonnull 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"); - } - Optional msg = messagesService.getMessage(mid); - if (msg.isPresent()) { - if (showReplies) { - List replies = messagesService.getReplies(user, mid); - applicationEventPublisher.publishEvent( - new SystemEvent(this, SystemActivity.read(user, msg.get()))); - replies.add(0, msg.get()); - return CommandResult.fromString(replies.stream() - .map(PlainTextFormatter::formatPostSummary).collect(Collectors.joining("\n"))); - } - return CommandResult.fromString(PlainTextFormatter.formatPost(msg.get())); - } - 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); - 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 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]); - Optional message = messagesService.getMessage(mid); - if (message.isPresent() && messagesService.deleteMessage(user.getUid(), mid)) { - applicationEventPublisher.publishEvent(new DeleteMessageEvent(this, message.get())); - 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.parseInt(args[0]); - int rid = Integer.parseInt(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 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(@Nonnull 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]); - Optional msg = messagesService.getMessage(mid); - if (!msg.isPresent()) { - return CommandResult.fromString("Message not found"); - } - if (rid > 0) { - Message reply = messagesService.getReply(mid, rid); - if (reply == null) { - return CommandResult.fromString("Reply not found"); - } - } - Pair> messageTags = tagService.fromString(txt); - if (user.getUid() == msg.get().getUser().getUid() && rid == 0 && messageTags.getRight().size() > 0) { - if (!CollectionUtils.isEqualCollection(tagService.updateTags(mid, messageTags.getRight()), msg.get().getTags())) { - return CommandResult.fromString("Tags are updated"); - } else { - return CommandResult.fromString("Tags are NOT updated (5 tags maximum?)"); - } - } else { - if (txt.length() > 4096) { - return CommandResult.fromString("Sorry, 4096 characters maximum."); - } - boolean haveAttachment = StringUtils.isNotEmpty(attachment.toString()); - String attachmentFName = null; - String attachmentType = null; - if (haveAttachment) { - if (attachment.getScheme().equals("juick")) { - attachmentFName = attachment.getHost(); - attachmentType = attachmentFName.substring(attachmentFName.length() - 3); - } else { - try { - attachmentFName = HttpUtils.downloadImage(attachment.toURL(), tmpDir).getHost(); - attachmentType = attachmentFName.substring(attachmentFName.length() - 3); - } catch (IOException e) { - logger.warn("Can not download {}", attachment.toURL()); - } - } - } - boolean attachmentProcessed = !haveAttachment || StringUtils.isNotEmpty(attachmentType); - String messageText = attachmentProcessed ? txt : String.format("%s %s", txt, attachment.toASCIIString()); - int newrid = messagesService.createReply(mid, rid, user, messageText, attachmentType); - if (newrid > 0) { - if (haveAttachment && attachmentProcessed) { - String fname = String.format("%d-%d.%s", mid, newrid, attachmentType); - imagesService.saveImageWithPreviews(attachmentFName, fname); - } - applicationEventPublisher.publishEvent( - new SystemEvent(this, SystemActivity.read(user, msg.get()))); - Message original = messagesService.getMessage(mid).orElseThrow(IllegalStateException::new); - subscriptionService.subscribeMessage(original, user); - Message reply = messagesService.getReply(mid, newrid); - if (reply.getUser().isAnonymous()) { - reply.setUser(activityPubManager.personToUser(reply.getUser().getUri())); - } else { - reply.getUser().setAvatar(webApp.getAvatarUrl(reply.getUser())); - } - applicationEventPublisher.publishEvent( - new SystemEvent(this, - SystemActivity.message(reply.getUser(), 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))); - } else { - return CommandResult.fromString("Message is read-only"); - } - } - } - - String printMessages(User visitor, List mids, boolean crop) { - return messagesService.getMessages(visitor, mids).stream() - .sorted(Collections.reverseOrder()) - .map(PlainTextFormatter::formatPostSummary).collect(Collectors.joining("\n\n")); - } -} diff --git a/src/main/java/com/juick/server/EmailManager.java b/src/main/java/com/juick/server/EmailManager.java deleted file mode 100644 index 522f1db6..00000000 --- a/src/main/java/com/juick/server/EmailManager.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.juick.model.Message; -import com.juick.model.User; -import com.juick.www.api.SystemActivity; -import com.juick.server.util.HttpBadRequestException; -import com.juick.www.WebApp; -import com.juick.service.EmailService; -import com.juick.service.MessagesService; -import com.juick.service.UserService; -import com.juick.service.component.*; -import com.juick.util.MessageUtils; -import com.mitchellbosecke.pebble.PebbleEngine; -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 javax.annotation.Nonnull; -import javax.inject.Inject; -import javax.mail.MessagingException; -import javax.mail.Multipart; -import javax.mail.Session; -import javax.mail.Transport; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeBodyPart; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMultipart; -import java.util.*; - -import static com.juick.formatters.PlainTextFormatter.formatPost; -import static com.juick.formatters.PlainTextFormatter.formatUrl; - -public class EmailManager implements NotificationListener { - - public static final String MSGID_PATTERN = "\\.|@|<"; - - private static final Logger logger = LoggerFactory.getLogger(EmailManager.class); - @Inject - private EmailService emailService; - @Inject - private MessagesService messagesService; - @Inject - private UserService userService; - @Inject - private PebbleEngine pebbleEngine; - @Inject - private ObjectMapper jsonMapper; - @Inject - private WebApp webApp; - @Value("${service_email:}") - private String serviceEmail; - - @Override - public void processSystemEvent(@Nonnull SystemEvent systemEvent) { - var activity = systemEvent.getActivity(); - var msg = activity.getMessage(); - var subscribers = activity.getTo(); - if (activity.getType().equals(SystemActivity.ActivityType.message)) { - processMessage(msg, subscribers); - } - try { - var eventHeader = Collections.singletonMap("X-Event-Version", "1.0"); - sendEmail("noreply@juick.com", serviceEmail, "New system event", - jsonMapper.writeValueAsString(systemEvent.getActivity()), null, eventHeader); - } catch (JsonProcessingException e) { - logger.warn("JSON exception", e); - } - } - private void processMessage(Message msg, List subscribedUsers) { - if (msg.isService()) { - return; - } - if (MessageUtils.isPM(msg)) { - String subject = String.format("Private message from %s", msg.getUser().getName()); - emailService.getEmails(msg.getTo().getUid(), true).forEach(email -> emailNotify(email, subject, msg)); - } else if (MessageUtils.isReply(msg)) { - Message originalMessage = messagesService.getMessage(msg.getMid()).orElseThrow(IllegalStateException::new); - String subject = String.format("New reply to %s", originalMessage.getUser().getName()); - subscribedUsers.stream() - .flatMap(user -> emailService.getEmails(user.getUid(), true).stream()) - .forEach(email -> emailNotify(email, subject, msg)); - } else { - String subject = String.format("New message from %s", msg.getUser().getName()); - subscribedUsers - .forEach(user -> emailService.getEmails(user.getUid(), true) - .forEach(email -> emailNotify(email, subject, msg))); - } - } - - @Override - public void processPingEvent(PingEvent pingEvent) { - - } - - @Async - @EventListener - public void processMailVerificationEvent(MailVerificationEvent mailVerificationEvent) { - if (!sendEmail("noreply@juick.com", mailVerificationEvent.getEmail(), "Juick authorization link", - String.format("Follow link to attach this email to Juick account:\n" + - "http://juick.com/settings?page=auth-email&code=%s\n\n" + - "If you don't know, what this mean - just ignore this mail.\n", mailVerificationEvent.getCode()), - StringUtils.EMPTY, Collections.emptyMap())) { - throw new HttpBadRequestException(); - } - } - @Async - @EventListener - public void processAccountVerificationEvent(AccountVerificationEvent accountVerificationEvent) { - String signupUrl = String.format("Follow this link to create Juick account: https://juick.com/signup?type=email&hash=%s", accountVerificationEvent.getCode()); - sendEmail("noreply@juick.com", accountVerificationEvent.getEmail(), "Juick registration", signupUrl, StringUtils.EMPTY, Collections.emptyMap()); - } - - private void emailNotify(String email, String subject, Message msg) { - Map headers = new HashMap<>(); - if (!MessageUtils.isPM(msg)) { - headers.put("Message-ID", String.format("<%d.%d@juick.com>", msg.getMid(), msg.getRid())); - } - if (MessageUtils.isReply(msg)) { - if (msg.getReplyto() > 0) { - Message replyto = messagesService.getReply(msg.getMid(), msg.getReplyto()); - headers.put("In-Reply-To", String.format("<%d.%d@juick.com>", replyto.getMid(), replyto.getRid())); - } else { - Message original = messagesService.getMessage(msg.getMid()).orElseThrow(IllegalStateException::new); - headers.put("In-Reply-To", String.format("<%d.%d@juick.com>", original.getMid(), original.getRid())); - } - } - String plainText = webApp.renderPlaintext(formatPost(msg), formatUrl(msg)).orElseThrow(IllegalStateException::new); - String hash = userService.getHashByUID(userService.getUserByEmail(email).getUid()); - String htmlText = webApp.renderHtml(MessageUtils.formatHtml(msg), formatUrl(msg), msg, hash).orElseThrow(IllegalStateException::new); - sendEmail(StringUtils.EMPTY, email, subject, plainText, htmlText, headers); - } - public boolean sendEmail(String from, String to, String subject, String textPart, String htmlPart, Map messageHeaders) { - Properties prop = System.getProperties(); - prop.put("mail.smtp.starttls.enable", "true"); - Session session = Session.getDefaultInstance(prop); - try { - Transport transport = session.getTransport("smtp"); - MimeMessage message = new MimeMessage(session) { - protected void updateMessageID() throws MessagingException { - for (Map.Entry entry: messageHeaders.entrySet()) { - setHeader(entry.getKey(), entry.getValue()); - } - } - }; - String fromAddress = StringUtils.isNotEmpty(from) ? from : "juick@juick.com"; - message.setFrom(fromAddress); - message.addRecipient(javax.mail.Message.RecipientType.TO, new InternetAddress(to)); - message.setSubject(subject); - MimeBodyPart textBodyPart = new MimeBodyPart(); - textBodyPart.setContent(textPart, "text/plain; charset=UTF-8"); - - Multipart multipart = new MimeMultipart("alternative"); - multipart.addBodyPart(textBodyPart); - if (StringUtils.isNotBlank(htmlPart)) { - MimeBodyPart htmlBodyPart = new MimeBodyPart(); - htmlBodyPart.setContent(htmlPart, "text/html; charset=UTF-8"); - multipart.addBodyPart(htmlBodyPart); - } - message.setContent(multipart); - User emailUser = userService.getUserByEmail(to); - if (!emailUser.isAnonymous()) { - message.setHeader("List-Id", "Juick notifications "); - message.setHeader("List-Post", ""); - message.setHeader("List-Owner", ""); - message.setHeader("List-Archive", ""); - message.setHeader("List-Unsubscribe", - String.format(",", - userService.getHashByUID(emailUser.getUid()))); - message.setHeader("List-Unsubscribe-Post", "List-Unsubscribe=One-Click"); - } - message.saveChanges(); - transport.connect(); - transport.sendMessage(message, message.getAllRecipients()); - return true; - } catch (MessagingException ex) { - logger.error("mail exception", ex); - return false; - } - } -} diff --git a/src/main/java/com/juick/server/KeystoreManager.java b/src/main/java/com/juick/server/KeystoreManager.java deleted file mode 100644 index 0a66c2c8..00000000 --- a/src/main/java/com/juick/server/KeystoreManager.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server; - -import com.juick.www.api.activity.model.objects.Person; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.io.Resource; -import org.springframework.util.Base64Utils; - -import javax.net.ssl.KeyManagerFactory; -import java.io.IOException; -import java.io.InputStream; -import java.security.*; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.spec.X509EncodedKeySpec; -import java.util.Arrays; - -public class KeystoreManager { - private static final Logger logger = LoggerFactory.getLogger("ActivityPub"); - - private String keystorePassword; - - private KeyStore ks; - - public KeystoreManager(Resource keystore, String keystorePassword) { - this.keystorePassword = keystorePassword; - try (InputStream ksIs = keystore.getInputStream()) { - ks = KeyStore.getInstance("PKCS12"); - ks.load(ksIs, keystorePassword.toCharArray()); - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory - .getDefaultAlgorithm()); - kmf.init(ks, keystorePassword.toCharArray()); - } catch (IOException | KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | CertificateException e) { - logger.error("Keystore error", e); - } - } - - private KeyPair getKeyPair() { - Key privateKey; - try { - privateKey = ks.getKey("1", keystorePassword.toCharArray()); - Certificate certificate = ks.getCertificate("1"); - return new KeyPair(certificate.getPublicKey(), (PrivateKey) privateKey); - } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { - e.printStackTrace(); - } - return null; - } - public PrivateKey getPrivateKey() { - return getKeyPair().getPrivate(); - } - public PublicKey getPublicKey() { - return getKeyPair().getPublic(); - } - public String getPublicKeyPem() { - String[] key = Base64Utils.encodeToString(getKeyPair().getPublic().getEncoded()).split("(?<=\\G.{64})"); - return String.format("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", - String.join("\n", key)); - } - public static PublicKey publicKeyOf(Person person) { - String pubkeyPem = person.getPublicKey().getPublicKeyPem(); - String[] rawKey = pubkeyPem.split("\\n"); - String pubkeyData = String.join("", Arrays.asList(rawKey).subList(1, rawKey.length - 1)); - try{ - byte[] byteKey = Base64Utils.decodeFromString(pubkeyData); - X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(byteKey); - KeyFactory kf = KeyFactory.getInstance("RSA"); - return kf.generatePublic(X509publicKey); - } - catch(Exception e){ - e.printStackTrace(); - } - return null; - } -} diff --git a/src/main/java/com/juick/server/ServerManager.java b/src/main/java/com/juick/server/ServerManager.java deleted file mode 100644 index 1f11a9fb..00000000 --- a/src/main/java/com/juick/server/ServerManager.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.juick.server; - -import com.juick.model.Message; -import com.juick.model.User; -import com.juick.model.AnonymousUser; -import com.juick.www.api.SystemActivity; -import com.juick.service.MessagesService; -import com.juick.service.SubscriptionService; -import com.juick.service.UserService; -import com.juick.service.component.*; -import com.juick.util.MessageUtils; -import org.apache.commons.collections4.ListUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import javax.annotation.Nonnull; -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Collectors; - -/** - * @author Ugnich Anton - */ -@Component -public class ServerManager implements NotificationListener { - private static final Logger logger = LoggerFactory.getLogger("Session"); - - @Inject - private MessagesService messagesService; - @Inject - private SubscriptionService subscriptionService; - @Inject - private UserService userService; - private final CopyOnWriteArrayList sessions = new CopyOnWriteArrayList<>(); - - @Value("${service_user:juick}") - private String serviceUsername; - - private User serviceUser; - - @PostConstruct - public void init() { - serviceUser = userService.getUserByName(serviceUsername); - } - - - private void onJuickPM(final User to, final Message jmsg) { - messageEvent(jmsg, Arrays.asList(to, jmsg.getUser())); - } - - private void onJuickMessagePost(final Message jmsg, List subscribedUsers) { - messageEvent(jmsg, subscribedUsers); - messageEvent(jmsg, Collections.singletonList(AnonymousUser.INSTANCE)); - } - - private void onJuickMessageReply(final Message jmsg, final List subscribedUsers) { - messageEvent(jmsg, subscribedUsers); - messageEvent(jmsg, Collections.singletonList(AnonymousUser.INSTANCE)); - } - - @Override - public void processSystemEvent(@Nonnull SystemEvent systemEvent) { - var activity = systemEvent.getActivity(); - var from = activity.getFrom(); - var message = activity.getMessage(); - var subscribers = activity.getTo(); - if (activity.getType().equals(SystemActivity.ActivityType.message)) { - processMessage(from, message, subscribers); - } else if (activity.getType().equals(SystemActivity.ActivityType.like)) { - if (from.equals(serviceUser)) { - processTop(message); - } - } - } - private void processMessage(User from, Message jmsg, List subscribers) { - List subscribedUsers = ListUtils.union(subscribers, Collections.singletonList(jmsg.getUser())); - if (jmsg.isService()) { - logger.info("Message read event from {} for {}", from.getUid(), jmsg.getMid()); - readEvent(jmsg, Collections.singletonList(serviceUser)); - return; - } - if (MessageUtils.isPM(jmsg)) { - onJuickPM(jmsg.getTo(), jmsg); - } else if (!MessageUtils.isReply(jmsg)) { - onJuickMessagePost(jmsg, subscribedUsers); - } else { - // to get quote and attachment - Message op = messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); - subscriptionService.getUsersSubscribedToComments(op, jmsg, true).stream() - .filter(u -> userService.isReplyToBL(u, jmsg)) - .forEach(b -> messagesService.setLastReadComment(b, jmsg.getMid(), jmsg.getRid())); - onJuickMessageReply(jmsg, subscribedUsers); - } - messageEvent(jmsg, Collections.singletonList(serviceUser)); - } - - @Override - public void processPingEvent(PingEvent pingEvent) { - - } - - private void processTop(Message msg) { - User topUser = msg.getUser(); - topEvent(msg, Arrays.asList(topUser, serviceUser)); - } - private void topEvent(Message msg, List subscribers){ - sendSseEvent(msg, "top", subscribers); - } - - private void readEvent(Message msg, List subscribers){ - sendSseEvent(msg, "read", subscribers); - } - - private void messageEvent(Message msg, List subscribers) { - sendSseEvent(msg, "msg", subscribers); - } - - private void sendSseEvent(Message msg, String name, List subscribers) { - List deadEmitters = new ArrayList<>(); - this.sessions.stream().filter(s -> subscribers.contains(s.user)).forEach(session -> { - try { - SseEmitter.SseEventBuilder builder = SseEmitter.event() - .name(name) - .data(msg); - session.getEmitter().send(builder); - } catch (Exception e) { - deadEmitters.add(session); - } - }); - this.sessions.removeAll(deadEmitters); - } - - public static class EventSession { - private final User user; - private final SseEmitter emitter; - - public EventSession(User user, SseEmitter sseEmitter) { - this.user = user; - this.emitter = sseEmitter; - } - - public User getUser() { - return user; - } - - public SseEmitter getEmitter() { - return emitter; - } - } - - public CopyOnWriteArrayList getSessions() { - return sessions; - } - @Scheduled(fixedRate = 30000) - public void ping() { - Message ping = new Message(); - ping.setService(true); - sendSseEvent(ping, "ping", getSessions().stream().map(s -> s.user) - .distinct().collect(Collectors.toList())); - } -} diff --git a/src/main/java/com/juick/server/SignatureManager.java b/src/main/java/com/juick/server/SignatureManager.java deleted file mode 100644 index 602b4285..00000000 --- a/src/main/java/com/juick/server/SignatureManager.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.juick.model.User; -import com.juick.model.AnonymousUser; -import com.juick.www.api.activity.model.Context; -import com.juick.www.api.activity.model.objects.Person; -import com.juick.www.api.webfinger.model.Account; -import com.juick.www.api.webfinger.model.Link; -import com.juick.service.UserService; -import com.juick.util.DateFormattersHolder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponentsBuilder; -import org.tomitribe.auth.signatures.Signature; -import org.tomitribe.auth.signatures.Signer; -import org.tomitribe.auth.signatures.Verifier; -import rocks.xmpp.addr.Jid; - -import javax.inject.Inject; -import java.io.IOException; -import java.net.URI; -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static com.juick.www.api.activity.model.Context.ACTIVITY_MEDIA_TYPE; - -@Component -public class SignatureManager { - private static final Logger logger = LoggerFactory.getLogger("ActivityPub"); - @Inject - private KeystoreManager keystoreManager; - @Inject - private ObjectMapper jsonMapper; - @Inject - private UserService userService; - @Inject - private RestTemplate apClient; - - public void post(Person from, Person to, Context data) throws IOException { - UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(to.getInbox()); - URI inbox = uriComponentsBuilder.build().toUri(); - Instant now = Instant.now(); - String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now); - String host = inbox.getPort() > 0 ? String.format("%s:%d", inbox.getHost(), inbox.getPort()) : inbox.getHost(); - String signatureString = addSignature(from, host, "POST", inbox.getPath(), requestDate); - - HttpHeaders requestHeaders = new HttpHeaders(); - requestHeaders.add("Content-Type", Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE); - requestHeaders.add("Date", requestDate); - requestHeaders.add("Host", host); - requestHeaders.add("Signature", signatureString); - HttpEntity request = new HttpEntity<>(Context.build(data), requestHeaders); - logger.info("Sending context to {}: {}", to.getId(), jsonMapper.writeValueAsString(data)); - ResponseEntity response = apClient.postForEntity(inbox, request, Void.class); - logger.info("Remote response: {}", response.getStatusCodeValue()); - } - - public String addSignature(Person from, String host, String method, String path, String dateString) throws IOException { - return addSignature(from, host, method, path, dateString, keystoreManager); - } - - public String addSignature(Person from, String host, String method, String path, String dateString, KeystoreManager keystoreManager) throws IOException { - Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null, - "(request-target)", "host", "date"); - Map headers = new HashMap<>(); - headers.put("host", host); - headers.put("date", dateString); - Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature); - Signature signature = signer.sign(method, path, headers); - // remove "Signature: " from result - return signature.toString().substring(10); - } - - public User verifySignature(String method, String path, Map headers) { - String signatureString = headers.get("signature"); - logger.info("Signature: {}", signatureString); - Signature signature = Signature.fromString(signatureString); - Optional context = getContext(UriComponentsBuilder.fromUriString(signature.getKeyId()) - .fragment(null).build().toUri()); - if (context.isPresent() && context.get() instanceof Person) { - Person person = (Person) context.get(); - Key key = KeystoreManager.publicKeyOf(person); - - Verifier verifier = new Verifier(key, signature); - try { - boolean result = verifier.verify(method, path, headers); - logger.info("signature of {} is valid: {}", signature.getKeyId(), result); - if (result) { - User user = new User(); - user.setUri(URI.create(person.getId())); - if (key.equals(keystoreManager.getPublicKey())) { - return userService.getUserByName(person.getName()); - } - return user; - } else { - return AnonymousUser.INSTANCE; - } - } catch (NoSuchAlgorithmException | SignatureException | IOException e) { - logger.warn("Invalid signature {}", signatureString); - } - } else { - logger.warn("Unknown keyId"); - } - return AnonymousUser.INSTANCE; - } - public Optional getContext(URI contextUri) { - try { - Context context = apClient.getForEntity(contextUri, Context.class).getBody(); - if (context == null) { - logger.warn("Cannot identify {}", contextUri); - return Optional.empty(); - } - return Optional.of(context); - } catch (Exception e) { - logger.warn("REST Exception on {}: {}", contextUri, e.getMessage()); - } - return Optional.empty(); - } - public Optional discoverPerson(String acct) { - Jid acctId = Jid.of(acct); - URI resourceUri = UriComponentsBuilder.fromPath("/.well-known/webfinger") - .host(acctId.getDomain()) - .scheme("https") - .queryParam("resource", String.format("%s", acctId.toEscapedString())).build().toUri(); - HttpHeaders headers = new HttpHeaders(); - headers.add("Accept", "application/jrd+json"); - HttpEntity webfingerRequest = new HttpEntity<>(headers); - ResponseEntity response = apClient.exchange( - resourceUri, HttpMethod.GET, webfingerRequest, Account.class); - if (response.getStatusCode().is2xxSuccessful()) { - Account acctData = response.getBody(); - if (acctData != null) { - for (Link l : acctData.getLinks()) { - if (l.getRel().equals("self") && l.getType().equals(ACTIVITY_MEDIA_TYPE)) { - return getContext(URI.create(l.getHref())); - } - } - } - } - return Optional.empty(); - } -} diff --git a/src/main/java/com/juick/server/TelegramBotManager.java b/src/main/java/com/juick/server/TelegramBotManager.java deleted file mode 100644 index 3c38e5de..00000000 --- a/src/main/java/com/juick/server/TelegramBotManager.java +++ /dev/null @@ -1,465 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server; - -import com.juick.model.User; -import com.juick.model.AnonymousUser; -import com.juick.model.CommandResult; -import com.juick.www.api.SystemActivity; -import com.juick.server.util.HttpUtils; -import com.juick.service.MessagesService; -import com.juick.service.TelegramService; -import com.juick.service.UserService; -import com.juick.service.component.SystemEvent; -import com.juick.service.component.NotificationListener; -import com.juick.service.component.PingEvent; -import com.juick.util.MessageUtils; -import com.pengrad.telegrambot.Callback; -import com.pengrad.telegrambot.TelegramBot; -import com.pengrad.telegrambot.UpdatesListener; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.MessageEntity; -import com.pengrad.telegrambot.model.PhotoSize; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.GetFile; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendPhoto; -import com.pengrad.telegrambot.request.SetWebhook; -import com.pengrad.telegrambot.response.GetFileResponse; -import com.pengrad.telegrambot.response.SendResponse; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - -import javax.annotation.Nonnull; -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import java.io.IOException; -import java.net.URI; -import java.net.URL; -import java.util.*; - -import static com.juick.formatters.PlainTextFormatter.formatPost; -import static com.juick.formatters.PlainTextFormatter.formatUrl; - -/** - * Created by vt on 12/05/16. - */ -public class TelegramBotManager implements NotificationListener { - private static final Logger logger = LoggerFactory.getLogger("Telegram"); - - private TelegramBot bot; - - @Value("${telegram_api_url:}") - private String apiUrl; - @Value("${telegram_file_api_url:}") - private String fileApiUrl; - @Value("${telegram_webhook_url:}") - private String webhookUrl; - @Value("${telegram_token:12345678}") - private String telegramToken; - @Value("${telegram_debug:false}") - private boolean telegramDebug; - @Inject - private TelegramService telegramService; - @Inject - private MessagesService messagesService; - @Inject - private UserService userService; - @Inject - private CommandsManager commandsManager; - @Inject - private ApplicationEventPublisher applicationEventPublisher; - @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") - private String tmpDir; - @Value("${service_user:juick}") - private String serviceUser; - - private static final String MSG_LINK = "🔗"; - - @PostConstruct - public void init() { - TelegramBot.Builder tgBuilder = new TelegramBot.Builder(telegramToken); - if (StringUtils.isNotEmpty(apiUrl)) { - tgBuilder.apiUrl(apiUrl).fileApiUrl(fileApiUrl); - } - bot = tgBuilder.build(); - if (!telegramDebug) { - try { - SetWebhook webhook = new SetWebhook().url(webhookUrl); - if (!bot.execute(webhook).isOk()) { - logger.error("error setting webhook"); - } - } catch (Exception e) { - logger.warn("couldn't initialize telegram bot", e); - } - } else { - bot.setUpdatesListener(updates -> { - logger.info("got updates: {}", updates); - updates.forEach(this::processUpdate); - return UpdatesListener.CONFIRMED_UPDATES_ALL; - }); - } - } - - public void processUpdate(Update update) { - Message message = update.message(); - if (update.message() == null) { - message = update.editedMessage(); - if (message == null) { - logger.error("error parsing telegram update: {}", update); - return; - } - User user_from = userService.getUserByUID(telegramService.getUser(message.chat().id())).orElse(AnonymousUser.INSTANCE); - logger.info("Found juick user {}", user_from.getUid()); - Optional> originalMessageData = messagesService.findMessageByProperty("durovId", - String.valueOf(message.messageId())); - if (originalMessageData.isPresent()) { - int mid = originalMessageData.get().getLeft(); - int rid = originalMessageData.get().getRight(); - // TODO: this is copypaste from api, need switch to api - com.juick.model.Message originalMessage = rid == 0 ? messagesService.getMessage(mid).orElseThrow(IllegalStateException::new) - : messagesService.getReply(mid, rid); - User author = originalMessage.getUser(); - String newMessageText = StringUtils.defaultString(message.text()); - if (user_from.equals(author) && canUpdateMessage(originalMessage, newMessageText)) { - if (messagesService.updateMessage(mid, rid, newMessageText)) { - telegramNotify(message.chat().id(), "Message updated", new com.juick.model.Message()); - return; - } - } - telegramNotify(message.chat().id(), "Error updating message", new com.juick.model.Message()); - } - } else { - User user_from = userService.getUserByUID(telegramService.getUser(message.chat().id())).orElse(AnonymousUser.INSTANCE); - logger.info("Found juick user {}", user_from.getUid()); - - String username = message.from().username(); - if (username == null) { - username = message.from().firstName(); - } - if (!user_from.isAnonymous()) { - URI attachment = URI.create(StringUtils.EMPTY); - if (message.photo() != null) { - String fileId = Arrays.stream(message.photo()).max(Comparator.comparingInt(PhotoSize::fileSize)) - .orElse(new PhotoSize()).fileId(); - if (StringUtils.isNotEmpty(fileId)) { - GetFile request = new GetFile(fileId); - GetFileResponse response = bot.execute(request); - logger.info("got file {}", response.file()); - try { - URL fileURL = new URL(bot.getFullFilePath(response.file())); - attachment = HttpUtils.downloadImage(fileURL, tmpDir); - } catch (Exception e) { - logger.warn("attachment exception", e); - } - logger.info("received {}", attachment); - } - } - String text = message.text(); - if (StringUtils.isBlank(text)) { - text = message.caption(); - } - if (StringUtils.isBlank(text)) { - text = StringUtils.EMPTY; - } - if (StringUtils.isNotEmpty(text) || StringUtils.isNotEmpty(attachment.toString())) { - if (text.equalsIgnoreCase("LOGIN") - || text.equalsIgnoreCase("PING") - || text.equalsIgnoreCase("HELP") - || text.equalsIgnoreCase("/login") - || text.equalsIgnoreCase("/logout") - || text.equalsIgnoreCase("/start") - || text.equalsIgnoreCase("/help")) { - String msgUrl = "http://juick.com/login?hash=" + userService.getHashByUID(user_from.getUid()); - String msg = String.format("Hi, %s!\nYou can post messages and images to Juick there.\n" + - "Tap to [log into website](%s) to get more info", user_from.getName(), msgUrl); - telegramNotify(message.from().id().longValue(), msg, new com.juick.model.Message()); - } else { - Message replyMessage = message.replyToMessage(); - if (replyMessage != null) { - MessageEntity[] entities = replyMessage.entities(); - if (entities == null) { - entities = replyMessage.captionEntities(); - } - if (entities != null) { - Optional juickLink = Arrays.stream(entities) - .filter(this::isJuickLink) - .findFirst(); - if (juickLink.isPresent()) { - if (StringUtils.isNotEmpty(juickLink.get().url())) { - UriComponents uriComponents = UriComponentsBuilder.fromUriString( - juickLink.get().url()).build(); - String path = uriComponents.getPath(); - if (StringUtils.isNotEmpty(path) && path.length() > 1) { - int mid; - try { - mid = Integer.parseInt(path.substring(3)); - } catch (NumberFormatException e) { - logger.warn("wrong mid received"); - return; - } - String prefix = String.format("#%d ", mid); - if (StringUtils.isNotEmpty(uriComponents.getFragment())) { - int rid = Integer.parseInt(uriComponents.getFragment()); - prefix = String.format("#%d/%d ", mid, rid); - } - executeCommand(message.messageId(), message.from().id().longValue(), - user_from, prefix + text, attachment); - } else { - logger.warn("invalid path: {}", path); - } - } else { - logger.warn("invalid entity: {}", juickLink); - } - } else { - telegramNotify(message.from().id().longValue(), - "Can not reply to this message", replyMessage.messageId(), new com.juick.model.Message()); - } - } else { - telegramNotify(message.from().id().longValue(), - "Can not reply to this message", replyMessage.messageId(), new com.juick.model.Message()); - } - } else { - executeCommand(message.messageId(), message.from().id().longValue(), - user_from, text, attachment); - } - } - } - messagesService.getUnread(user_from).forEach(mid -> messagesService.setRead(user_from, mid)); - } else { - List chats = telegramService.getAnonymous(); - if (!chats.contains(message.chat().id())) { - logger.info("added chat with {}", username); - telegramService.createTelegramUser(message.from().id(), username); - } - telegramSignupNotify(message.from().id().longValue(), userService.getSignUpHashByTelegramID(message.from().id().longValue(), username)); - } - } - } - - /* - validate user input - */ - private boolean canUpdateMessage(com.juick.model.Message message, String newData) { - if (StringUtils.isEmpty(newData)) { - // allow empty text only when image is present - return StringUtils.isNotEmpty(message.getAttachmentType()); - } - return true; - } - - private void executeCommand(Integer messageId, Long userId, User user_from, String text, URI attachment) { - try { - CommandResult result = commandsManager.processCommand(user_from, text, attachment); - if (result.getNewMessage().isPresent()) { - com.juick.model.Message newMessage = result.getNewMessage().get(); - messagesService.setMessageProperty(newMessage.getMid(), newMessage.getRid(), "durovId", - String.valueOf(messageId)); - } - String messageTxt = StringUtils.isNotEmpty(result.getMarkdown()) ? result.getMarkdown() - : "Unknown error or unsupported command"; - telegramNotify(userId, messageTxt, new com.juick.model.Message()); - } catch (Exception e) { - logger.warn("telegram exception", e); - } - } - - private boolean isJuickLink(MessageEntity e) { - return e.offset() == 0 && e.type().equals(MessageEntity.Type.text_link) && e.length() == 2; - } - - public void telegramNotify(Long chatId, String msg, @Nonnull com.juick.model.Message source) { - telegramNotify(chatId, msg, 0, source); - } - - public void telegramNotify(Long chatId, String msg, Integer replyTo, @Nonnull com.juick.model.Message source) { - String attachment = MessageUtils.attachmentUrl(source); - boolean isSendTxt = true; - if (!StringUtils.isEmpty(attachment)) { - SendPhoto telegramPhoto = new SendPhoto(chatId, attachment); - if (replyTo > 0) { - telegramPhoto.replyToMessageId(replyTo); - } - telegramPhoto.parseMode(ParseMode.Markdown); - if(msg.length() < 1024) { - telegramPhoto.caption(msg); - isSendTxt = false; - } - bot.execute(telegramPhoto, new Callback<>() { - @Override - public void onResponse(SendPhoto request, SendResponse response) { - processTelegramResponse(chatId, response, source); - } - - @Override - public void onFailure(SendPhoto request, IOException e) { - logger.warn("telegram failure", e); - } - }); - } - if (isSendTxt){ - SendMessage telegramMessage = new SendMessage(chatId, msg); - if (replyTo > 0) { - telegramMessage.replyToMessageId(replyTo); - } - telegramMessage.parseMode(ParseMode.Markdown).disableWebPagePreview(true); - bot.execute(telegramMessage, new Callback<>() { - @Override - public void onResponse(SendMessage request, SendResponse response) { - processTelegramResponse(chatId, response, source); - } - - @Override - public void onFailure(SendMessage request, IOException e) { - logger.warn("telegram failure", e); - } - }); - } - } - - private void processTelegramResponse(Long chatId, SendResponse response, com.juick.model.Message source) { - int userId = telegramService.getUser(chatId); - if (!response.isOk()) { - if (response.errorCode() == 403) { - // remove from anonymous users - telegramService.getAnonymous().stream().filter(c -> c.equals(chatId)).findFirst().ifPresent( - d -> { - telegramService.deleteAnonymous(d); - logger.info("deleted {} chat", d); - } - ); - if (userId > 0) { - User userToDelete = userService.getUserByUID(userId) - .orElseThrow(IllegalStateException::new); - boolean status = telegramService.deleteTelegramUser(userToDelete.getUid()); - logger.info("deleting telegram id of @{} : {}", userToDelete.getName(), status); - } - } else { - logger.warn("error response, isOk: {}, errorCode: {}, description: {}", - response.isOk(), response.errorCode(), response.description()); - } - } else { - if (MessageUtils.isReply(source)) { - messagesService.setLastReadComment(userService.getUserByUID(userId) - .orElseThrow(IllegalStateException::new), source.getMid(), source.getRid()); - User user = userService.getUserByUID(userId).orElseThrow(IllegalStateException::new); - userService.updateLastSeen(user); - applicationEventPublisher.publishEvent( - new SystemEvent(this, SystemActivity.read(user, source))); - } - } - } - - public void telegramSignupNotify(Long telegramId, String hash) { - bot.execute(new SendMessage(telegramId, - String.format("You are subscribed to all Juick messages. " + - "[Create or link](http://juick.com/signup?type=durov&hash=%s) " + - "an existing Juick account to get your subscriptions and ability to post messages", hash)) - .parseMode(ParseMode.Markdown), new Callback<>() { - @Override - public void onResponse(SendMessage request, SendResponse response) { - logger.info("got response: {}", response.message()); - } - - @Override - public void onFailure(SendMessage request, IOException e) { - logger.warn("telegram failure", e); - } - }); - } - - @Override - public void processSystemEvent(SystemEvent systemEvent) { - var activity = systemEvent.getActivity(); - var type = activity.getType(); - if (type.equals(SystemActivity.ActivityType.message)) { - processMessage(activity.getMessage(), activity.getTo()); - } else if (type.equals(SystemActivity.ActivityType.like)) { - if (systemEvent.getActivity().getFrom().getName().equals(serviceUser)) { - processTop(systemEvent.getActivity().getMessage()); - } else { - processLike(activity.getFrom(), activity.getMessage(), activity.getTo()); - } - } else if (type.equals(SystemActivity.ActivityType.follow)) { - processFollow(activity.getFrom(), activity.getTo()); - } - } - private void processMessage(com.juick.model.Message jmsg, List subscribedUsers) { - if (jmsg.isService()) { - return; - } - String msgUrl = formatUrl(jmsg); - if (MessageUtils.isPM(jmsg)) { - telegramService.getTelegramIdentifiers(Collections.singletonList(jmsg.getTo())) - .forEach(c -> telegramNotify(c, formatPost(jmsg, true), jmsg)); - } else if (MessageUtils.isReply(jmsg)) { - String fmsg = String.format("[%s](%s) %s", MSG_LINK, msgUrl, formatPost(jmsg, true)); - telegramService.getTelegramIdentifiers( - subscribedUsers - ).forEach(c -> telegramNotify(c, fmsg, jmsg)); - } else { - String msg = String.format("[%s](%s) %s", MSG_LINK, msgUrl, formatPost(jmsg, true)); - - List users = telegramService.getTelegramIdentifiers(subscribedUsers); - List chats = telegramService.getAnonymous(); - // registered subscribed users - - users.forEach(c -> telegramNotify(c, msg, jmsg)); - // anonymous - chats.stream().filter(u -> telegramService.getUser(u) == 0).forEach(c -> telegramNotify(c, msg, jmsg)); - } - } - - private void processLike(User liker, com.juick.model.Message message, List subscribers) { - if (!liker.getName().equals(serviceUser)) { - logger.info("Like received in tg listener"); - if (!userService.isInBLAny(message.getUser().getUid(), liker.getUid())) { - telegramService.getTelegramIdentifiers(Collections.singletonList(message.getUser())) - .forEach(c -> telegramNotify(c, String.format("%s recommends your [post](%s)", - MessageUtils.getMarkdownUser(liker), formatUrl(message)), new com.juick.model.Message())); - } - telegramService.getTelegramIdentifiers(subscribers) - .forEach(c -> telegramNotify(c, String.format("%s recommends you someone's [post](%s)", - MessageUtils.getMarkdownUser(liker), formatUrl(message)), new com.juick.model.Message())); - } - } - - @Override - public void processPingEvent(PingEvent pingEvent) { - - } - - private void processTop(com.juick.model.Message message) { - telegramService.getTelegramIdentifiers(Collections.singletonList(message.getUser())) - .forEach(c -> telegramNotify(c, String.format("Your [post](%s) became popular!", - formatUrl(message)), new com.juick.model.Message())); - } - - private void processFollow(User subscriber, List target) { - telegramService.getTelegramIdentifiers(target) - .forEach(c -> telegramNotify(c, String.format("%s subscribed to your blog", - MessageUtils.getMarkdownUser(subscriber)), new com.juick.model.Message())); - } -} diff --git a/src/main/java/com/juick/server/TopManager.java b/src/main/java/com/juick/server/TopManager.java deleted file mode 100644 index 15abb6cc..00000000 --- a/src/main/java/com/juick/server/TopManager.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server; - -import com.juick.model.Message; -import com.juick.model.Tag; -import com.juick.model.User; -import com.juick.www.api.SystemActivity; -import com.juick.service.MessagesService; -import com.juick.service.UserService; -import com.juick.service.component.SystemEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -@Component -public class TopManager { - private static Logger logger = LoggerFactory.getLogger(TopManager.class); - @Inject - private MessagesService messagesService; - @Inject - private UserService userService; - @Inject - private ApplicationEventPublisher applicationEventPublisher; - - @Value("${service_user:juick}") - private String serviceUsername; - - private User serviceUser; - - @PostConstruct - public void init() { - serviceUser = userService.getUserByName(serviceUsername); - } - - @Scheduled(fixedRate = 3600000) - public void updateTop() { - messagesService.getPopularCandidates().forEach(m -> { - Message jmsg = messagesService.getMessage(m).orElseThrow(IllegalStateException::new); - logger.info("added {} to popular", m); - messagesService.setMessagePopular(m, 1); - List tags = jmsg.getTags().stream().map(Tag::getName).map(String::toLowerCase).collect(Collectors.toList()); - if (!tags.contains("juick")) { - applicationEventPublisher.publishEvent(new SystemEvent(this, - SystemActivity.like(serviceUser, jmsg, Collections.emptyList()))); - } - }); - } -} diff --git a/src/main/java/com/juick/server/TwitterManager.java b/src/main/java/com/juick/server/TwitterManager.java deleted file mode 100644 index e424784c..00000000 --- a/src/main/java/com/juick/server/TwitterManager.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.juick.server; - -import com.juick.model.Message; -import com.juick.model.User; -import com.juick.www.api.SystemActivity; -import com.juick.service.UserService; -import com.juick.service.component.*; -import com.juick.service.CrosspostService; -import com.juick.util.MessageUtils; -import org.apache.commons.lang3.SerializationUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import twitter4j.TwitterFactory; -import twitter4j.conf.ConfigurationBuilder; - -import javax.annotation.PostConstruct; -import javax.inject.Inject; - -/** - * @author Ugnich Anton - */ -@Component -public class TwitterManager implements NotificationListener { - - private static Logger logger = LoggerFactory.getLogger(TwitterManager.class); - - @Inject - private CrosspostService crosspostService; - - @Value("${twitter_consumer_key:12345678}") - private String twitter_consumer_key; - @Value("${twitter_consumer_secret:secret}") - private String twitter_consumer_secret; - @Inject - private UserService userService; - - @Value("${service_user:juick}") - private String serviceUsername; - - private User serviceUser; - - @PostConstruct - public void init() { - serviceUser = userService.getUserByName(serviceUsername); - } - - void twitterPost(final Message jmsg) { - crosspostService.getTwitterToken(jmsg.getUser().getUid()).ifPresent(t -> { - String status = MessageUtils.getMessageHashTags(jmsg) + StringUtils.defaultString(jmsg.getText()); - if (status.length() > 253) { - status = status.substring(0, 252) + "…"; - } - status += " http://juick.com/" + jmsg.getMid(); - ConfigurationBuilder configurationBuilder = new ConfigurationBuilder() - .setDebugEnabled(true) - .setOAuthConsumerKey(twitter_consumer_key) - .setOAuthConsumerSecret(twitter_consumer_secret); - TwitterFactory tf = new TwitterFactory(configurationBuilder - .setOAuthAccessToken(t.getToken()) - .setOAuthAccessTokenSecret(t.getSecret()).build()); - try { - tf.getInstance().updateStatus(status); - } catch (Exception e) { - logger.info("Twitter exception", e); - } - }); - } - - @Override - public void processSystemEvent(SystemEvent systemEvent) { - var activity = systemEvent.getActivity(); - var msg = activity.getMessage(); - if (activity.getType().equals(SystemActivity.ActivityType.message)) { - if (MessageUtils.isPM(msg) || MessageUtils.isReply(msg) || msg.isService()) { - return; - } - if (StringUtils.isNotEmpty(crosspostService.getTwitterName(msg.getUser().getUid()))) { - if (msg.getTags().stream().noneMatch(t -> t.getName().equals("notwitter"))) { - twitterPost(msg); - } - } - } else if (activity.getType().equals(SystemActivity.ActivityType.like)) { - if (activity.getFrom().getName().equals(serviceUsername)) { - processTop(msg); - } - } - } - - @Override - public void processPingEvent(PingEvent pingEvent) { - - } - - private void processTop(Message message) { - Message jmsg = SerializationUtils.clone(message); - jmsg.setUser(serviceUser); - twitterPost(jmsg); - } -} diff --git a/src/main/java/com/juick/server/Utils.java b/src/main/java/com/juick/server/Utils.java deleted file mode 100644 index 58662c71..00000000 --- a/src/main/java/com/juick/server/Utils.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.juick.server; - -import javax.servlet.http.HttpServletRequest; -import java.util.Optional; - -/** - * - * @author Ugnich Anton - */ -public class Utils { - - - public static String encodeSphinx(String str) { - return str.replaceAll("@", "\\\\@") - .replaceAll("\\'", "\\\\'") - .replaceAll("=", "\\\\\\\\="); - } - /** - * Returns the viewName to return for coming back to the sender url - * - * @param request Instance of {@link HttpServletRequest} or use an injected instance - * @return Optional with the view name. Recomended to use an alternativa url with - * {@link Optional#orElse(java.lang.Object)} - */ - public static Optional getPreviousPageByRequest(HttpServletRequest request) - { - return Optional.ofNullable(request.getHeader("Referer")); - } -} diff --git a/src/main/java/com/juick/server/XMPPManager.java b/src/main/java/com/juick/server/XMPPManager.java deleted file mode 100644 index 32f1b94e..00000000 --- a/src/main/java/com/juick/server/XMPPManager.java +++ /dev/null @@ -1,644 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server; - -import com.juick.model.User; -import com.juick.formatters.PlainTextFormatter; -import com.juick.model.CommandResult; -import com.juick.www.api.SystemActivity; -import com.juick.www.WebApp; -import com.juick.server.xmpp.iq.MessageQuery; -import com.juick.service.MessagesService; -import com.juick.service.PMQueriesService; -import com.juick.service.UserService; -import com.juick.service.component.*; -import com.juick.util.MessageUtils; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.Resource; -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.XmppException; -import rocks.xmpp.core.session.Extension; -import rocks.xmpp.core.session.XmppSession; -import rocks.xmpp.core.session.XmppSessionConfiguration; -import rocks.xmpp.core.session.debug.LogbackDebugger; -import rocks.xmpp.core.stanza.AbstractIQHandler; -import rocks.xmpp.core.stanza.model.IQ; -import rocks.xmpp.core.stanza.model.Message; -import rocks.xmpp.core.stanza.model.Presence; -import rocks.xmpp.core.stanza.model.StanzaError; -import rocks.xmpp.core.stanza.model.client.ClientMessage; -import rocks.xmpp.core.stanza.model.client.ClientPresence; -import rocks.xmpp.core.stanza.model.errors.Condition; -import rocks.xmpp.extensions.caps.EntityCapabilitiesManager; -import rocks.xmpp.extensions.component.accept.ExternalComponent; -import rocks.xmpp.extensions.disco.ServiceDiscoveryManager; -import rocks.xmpp.extensions.disco.model.info.Identity; -import rocks.xmpp.extensions.filetransfer.FileTransfer; -import rocks.xmpp.extensions.filetransfer.FileTransferManager; -import rocks.xmpp.extensions.nick.model.Nickname; -import rocks.xmpp.extensions.oob.model.x.OobX; -import rocks.xmpp.extensions.ping.PingManager; -import rocks.xmpp.extensions.receipts.MessageDeliveryReceiptsManager; -import rocks.xmpp.extensions.vcard.temp.model.VCard; -import rocks.xmpp.extensions.version.SoftwareVersionManager; -import rocks.xmpp.extensions.version.model.SoftwareVersion; - -import javax.annotation.Nonnull; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import javax.inject.Inject; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; - -/** - * @author ugnich - */ -public class XMPPManager implements NotificationListener { - - private static final Logger logger = LoggerFactory.getLogger("XMPP"); - - private ExternalComponent xmpp; - @Inject - private CommandsManager commandsManager; - @Value("${xmppbot_jid:juick@localhost}") - private Jid jid; - @Value("${hostname:localhost}") - private String componentName; - @Value("${component_port:5347}") - private int componentPort; - @Value("${component_host:localhost}") - private String componentHost; - @Value("${xmpp_password:secret}") - private String password; - @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") - private String tmpDir; - @Value("classpath:juick.png") - private Resource vCardImage; - - @Inject - private MessagesService messagesService; - @Inject - private UserService userService; - @Inject - private PMQueriesService pmQueriesService; - @Inject - private Executor applicationTaskExecutor; - @Value("${service_user:juick}") - private String serviceUsername; - @Inject - private WebApp webApp; - - private User serviceUser; - - @PostConstruct - public void init() { - logger.info("xmpp component start connecting to {}", componentPort); - XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() - .extensions(Extension.of(com.juick.model.Message.class), Extension.of(MessageQuery.class)) - .debugger(LogbackDebugger.class) - .defaultResponseTimeout(Duration.ofMillis(120000)) - .build(); - xmpp = ExternalComponent.create(componentName, password, configuration, componentHost, componentPort); - ServiceDiscoveryManager serviceDiscoveryManager = xmpp.getManager(ServiceDiscoveryManager.class); - serviceDiscoveryManager.addIdentity(Identity.clientBot().withName("Juick")); - EntityCapabilitiesManager entityCapabilitiesManager = xmpp.getManager(EntityCapabilitiesManager.class); - entityCapabilitiesManager.setNode("https://juick.com/caps"); - MessageDeliveryReceiptsManager messageDeliveryReceiptsManager = xmpp.getManager(MessageDeliveryReceiptsManager.class); - messageDeliveryReceiptsManager.setEnabled(true); - PingManager pingManager = xmpp.getManager(PingManager.class); - pingManager.setEnabled(true); - SoftwareVersionManager softwareVersionManager = xmpp.getManager(SoftwareVersionManager.class); - softwareVersionManager.setSoftwareVersion(new SoftwareVersion("Juick", "2.x", - System.getProperty("os.name", "generic"))); - VCard vCard = new VCard(); - vCard.setFormattedName("Juick"); - vCard.setBirthday(LocalDate.of(2008, 10, 22)); - try { - vCard.setUrl(new URL("http://juick.com/")); - vCard.setPhoto(new VCard.Image("image/png", IOUtils.toByteArray(vCardImage.getInputStream()))); - } catch (MalformedURLException e) { - logger.error("invalid url", e); - } catch (IOException e) { - logger.warn("invalid resource", e); - } - xmpp.addIQHandler(MessageQuery.class, iq -> { - Message warningMessage = new Message(iq.getFrom(), Message.Type.CHAT); - warningMessage.setFrom(jid); - warningMessage.setBody("Your XMPP client constantly polls us with XMPP query which is unsupported for years, please find http://juick.com/query#messages in your client code and remove that"); - xmpp.send(warningMessage); - return iq.createError(new StanzaError(Condition.BAD_REQUEST, "Please stop this spam")); - }); - xmpp.addIQHandler(VCard.class, new AbstractIQHandler(IQ.Type.GET) { - @Override - protected IQ processRequest(IQ iq) { - if (iq.getTo().equals(jid) || iq.getTo().asBareJid().equals(jid.asBareJid()) - || iq.getTo().asBareJid().toEscapedString().equals(jid.getDomain())) { - return iq.createResult(vCard); - } - User user = userService.getUserByName(iq.getTo().getLocal()); - if (!user.isAnonymous()) { - User info = userService.getUserInfo(user); - VCard userVCard = new VCard(); - userVCard.setFormattedName(info.getFullName()); - userVCard.setNickname(user.getName()); - try { - userVCard.setPhoto(new VCard.Image(URI.create(webApp.getAvatarUrl(user)))); - if (info.getUrl() != null) { - userVCard.setUrl(new URL(info.getUrl())); - } - } catch (MalformedURLException e) { - logger.warn("url exception", e); - } - return iq.createResult(userVCard); - } - return iq.createError(Condition.BAD_REQUEST); - } - }); - xmpp.addInboundMessageListener(e -> { - ClientMessage result = incomingMessage(e.getMessage()); - if (result != null) { - xmpp.send(result); - } - }); - FileTransferManager fileTransferManager = xmpp.getManager(FileTransferManager.class); - fileTransferManager.addFileTransferOfferListener(e -> { - try { - List allowedTypes = new ArrayList<>() {{ - add("png"); - add("jpg"); - }}; - String attachmentExtension = FilenameUtils.getExtension(e.getName()).toLowerCase(); - String targetFilename = String.format("%s.%s", - DigestUtils.md5Hex(String.format("%s-%s", - e.getInitiator().toString(), e.getSessionId()).getBytes()), attachmentExtension); - if (allowedTypes.contains(attachmentExtension)) { - Path filePath = Paths.get(tmpDir, targetFilename); - FileTransfer ft = e.accept(filePath).get(); - ft.addFileTransferStatusListener(st -> { - logger.debug("{}: received {} of {}", e.getName(), st.getBytesTransferred(), e.getSize()); - if (st.getStatus().equals(FileTransfer.Status.COMPLETED)) { - logger.info("transfer completed"); - try { - Jid initiator = e.getInitiator(); - ClientMessage result = incomingMessageJuick( - userService.getUserByJID(initiator.asBareJid().toEscapedString()), initiator, - jid.getLocal(), StringUtils.defaultString(e.getDescription()).trim(), URI.create(String.format("juick://%s", targetFilename))); - if (result != null) { - xmpp.send(result); - } - } catch (Exception e1) { - logger.error("ft error", e1); - } - - } else if (st.getStatus().equals(FileTransfer.Status.FAILED)) { - logger.info("transfer failed", ft.getException()); - Message msg = new Message(); - msg.setType(Message.Type.CHAT); - msg.setFrom(jid); - msg.setTo(e.getInitiator()); - msg.setBody("File transfer failed, please report to us"); - xmpp.sendMessage(msg); - } else if (st.getStatus().equals(FileTransfer.Status.CANCELED)) { - logger.info("transfer cancelled"); - } - }); - ft.transfer(); - logger.info("transfer started"); - } else { - e.reject(); - logger.info("transfer rejected"); - } - } catch (IOException | InterruptedException | ExecutionException e1) { - logger.error("ft error", e1); - } - }); - xmpp.addConnectionListener(event -> { - if (event.getType().equals(rocks.xmpp.core.session.ConnectionEvent.Type.RECONNECTION_SUCCEEDED)) { - logger.info("component connected"); - } - }); - xmpp.addSessionStatusListener(event -> { - logger.info("event: " + event.getStatus(), event.getThrowable()); - if (event.getStatus().equals(XmppSession.Status.AUTHENTICATED)) { - logger.info("Authenticated, broadcasting..."); - broadcastPresence(null); - } - }); - xmpp.addInboundPresenceListener(event -> { - incomingPresence(event.getPresence()); - }); - applicationTaskExecutor.execute(() -> { - try { - xmpp.connect(); - } catch (XmppException e) { - logger.warn("xmpp exception", e); - } - }); - serviceUser = userService.getUserByName(serviceUsername); - } - - private void sendJuickMessage(com.juick.model.Message jmsg, List users) { - List jids = new ArrayList<>(); - - for (User user : users) { - jids.addAll(userService.getJIDsbyUID(user.getUid())); - } - com.juick.model.Message fullMsg = messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); - String txt = "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(fullMsg) + "\n"; - String attachmentUrl = MessageUtils.attachmentUrl(fullMsg); - if (StringUtils.isNotEmpty(attachmentUrl)) { - txt += attachmentUrl + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; - txt += "#" + jmsg.getMid() + " http://juick.com/m/" + jmsg.getMid(); - - Nickname nick = new Nickname("@" + jmsg.getUser().getName()); - - Message msg = new Message(); - msg.setFrom(jid); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.setThread("juick-" + jmsg.getMid()); - msg.addExtension(jmsg); - msg.addExtension(nick); - if (StringUtils.isNotEmpty(attachmentUrl)) { - try { - OobX oob = new OobX(new URI(attachmentUrl)); - msg.addExtension(oob); - } catch (URISyntaxException e) { - logger.warn("uri exception", e); - } - } - for (String jid : jids) { - msg.setTo(Jid.of(jid)); - xmpp.send(ClientMessage.from(msg)); - } - } - - private void sendJuickComment(com.juick.model.Message jmsg, List users) { - String replyQuote; - String replyTo; - - com.juick.model.Message replyMessage = jmsg.getReplyto() > 0 ? messagesService.getReply(jmsg.getMid(), jmsg.getReplyto()) - : messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); - replyTo = replyMessage.getUser().getName(); - com.juick.model.Message fullReply = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); - replyQuote = StringUtils.defaultString(fullReply.getReplyQuote()); - - String txt = "Reply by @" + jmsg.getUser().getName() + ":\n" + replyQuote + "\n@" + replyTo + " "; - String attachmentUrl = MessageUtils.attachmentUrl(fullReply); - if (StringUtils.isNotEmpty(attachmentUrl)) { - txt += attachmentUrl + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n" + "#" + jmsg.getMid() + "/" + jmsg.getRid() + " http://juick.com/m/" + jmsg.getMid() + "#" + jmsg.getRid(); - - Message msg = new Message(); - msg.setFrom(jid); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.addExtension(jmsg); - for (User user : users) { - for (String jid : userService.getJIDsbyUID(user.getUid())) { - msg.setTo(Jid.of(jid)); - xmpp.send(ClientMessage.from(msg)); - } - } - } - - @Override - public void processSystemEvent(SystemEvent systemEvent) { - var activity = systemEvent.getActivity(); - var type = activity.getType(); - if (type.equals(SystemActivity.ActivityType.message)) { - processMessage(activity.getMessage(), activity.getTo()); - } else if (type.equals(SystemActivity.ActivityType.like)) { - if (systemEvent.getActivity().getFrom().equals(serviceUser)) { - processTop(systemEvent.getActivity().getMessage()); - } else { - processLike(activity.getFrom(), activity.getMessage(), activity.getTo()); - } - } - } - private void processMessage(com.juick.model.Message msg, List subscribers) { - if (msg.isService()) { - return; - } - if (MessageUtils.isPM(msg)) { - userService.getJIDsbyUID(msg.getTo().getUid()) - .forEach(userJid -> { - Message mm = new Message(); - mm.setTo(Jid.of(userJid)); - mm.setType(Message.Type.CHAT); - boolean inroster = pmQueriesService.havePMinRoster(msg.getUser().getUid(), userJid); - if (inroster) { - mm.setFrom(Jid.of(msg.getUser().getName(), "juick.com", "Juick")); - mm.setBody(msg.getText()); - } else { - mm.setFrom(jid); - mm.setBody("Private message from @" + msg.getUser().getName() + ":\n" + msg.getText()); - } - xmpp.send(ClientMessage.from(mm)); - }); - } else if (MessageUtils.isReply(msg)) { - sendJuickComment(msg, subscribers); - } - else { - sendJuickMessage(msg, subscribers); - } - } - - private ClientMessage makeReply(Jid jidTo, String txt) { - Message reply = new Message(); - reply.setFrom(jid); - reply.setTo(jidTo); - reply.setType(Message.Type.CHAT); - reply.setBody(txt); - return ClientMessage.from(reply); - } - - public void processLike(User liker, com.juick.model.Message jmsg, List users) { - if (!userService.isInBLAny(jmsg.getUser().getUid(), liker.getUid())) { - userService.getJIDsbyUID(jmsg.getUser().getUid()).forEach(authorJid -> { - Message xmppMessage = new Message(); - xmppMessage.setFrom(jid); - xmppMessage.setTo(Jid.of(authorJid)); - xmppMessage.setType(Message.Type.CHAT); - xmppMessage.addExtension(jmsg); - xmppMessage.setBody(String.format("%s recommended your post #%d. %s", - liker.getName(), jmsg.getMid(), PlainTextFormatter.formatUrl(jmsg))); - xmpp.send(ClientMessage.from(xmppMessage)); - }); - } - - String txt = "Recommended by @" + liker.getName() + ":\n"; - txt += "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(jmsg) + "\n"; - String attachmentUrl = MessageUtils.attachmentUrl(jmsg); - if (StringUtils.isNotEmpty(attachmentUrl)) { - txt += attachmentUrl + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; - txt += "#" + jmsg.getMid(); - if (jmsg.getReplies() > 0) { - if (jmsg.getReplies() % 10 == 1 && jmsg.getReplies() % 100 != 11) { - txt += " (" + jmsg.getReplies() + " reply)"; - } else { - txt += " (" + jmsg.getReplies() + " replies)"; - } - } - txt += " http://juick.com/m/" + jmsg.getMid(); - - Nickname nick = new Nickname("@" + jmsg.getUser().getName()); - - Message msg = new Message(); - msg.setFrom(jid); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.setThread("juick-" + jmsg.getMid()); - msg.addExtension(jmsg); - msg.addExtension(nick); - if (StringUtils.isNotEmpty(attachmentUrl)) { - try { - OobX oob = new OobX(new URI(attachmentUrl)); - msg.addExtension(oob); - } catch (URISyntaxException e) { - logger.warn("uri exception", e); - } - } - - for (User user : users) { - for (String jid : userService.getJIDsbyUID(user.getUid())) { - msg.setTo(Jid.of(jid)); - xmpp.send(ClientMessage.from(msg)); - } - } - } - - @Override - public void processPingEvent(PingEvent pingEvent) { - userService.getJIDsbyUID(pingEvent.getPinger().getUid()) - .forEach(userJid -> { - Presence p = new Presence(Jid.of(userJid)); - p.setFrom(jid); - p.setPriority((byte) 10); - xmpp.send(ClientPresence.from(p)); - }); - } - - public void processTop(com.juick.model.Message message) { - try { - commandsManager.processCommand(serviceUser, String.format("! #%d", message.getMid()), URI.create(StringUtils.EMPTY)); - } catch (Exception e) { - logger.warn("XMPP error", e); - } - } - - private void incomingPresence(Presence p) { - final String username = p.getTo().getLocal(); - final boolean toJuick = username.equals(jid.getLocal()); - - if (p.getType() == null) { - Presence reply = new Presence(); - reply.setFrom(p.getTo().asBareJid()); - reply.setTo(p.getFrom().asBareJid()); - reply.setType(Presence.Type.UNSUBSCRIBE); - xmpp.send(ClientPresence.from(reply)); - } else if (p.getType().equals(Presence.Type.PROBE)) { - int uid_to = 0; - if (!toJuick) { - uid_to = userService.getUIDbyName(username); - } else { - User visitor = userService.getUserByJID(p.getFrom().asBareJid().toEscapedString()); - if (visitor != null) { - userService.updateLastSeen(visitor); - } - } - - if (toJuick || uid_to > 0) { - Presence reply = new Presence(); - reply.setFrom(p.getTo().withResource(jid.getResource())); - reply.setTo(p.getFrom()); - reply.setPriority((byte)10); - if (!userService.getActiveJIDs().contains(p.getFrom().asBareJid().toEscapedString())) { - reply.setStatus("Send ON to enable notifications"); - } - xmpp.send(ClientPresence.from(reply)); - } else { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.ERROR); - reply.setId(p.getId()); - reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); - xmpp.send(ClientPresence.from(reply)); - } - } else if (p.getType().equals(Presence.Type.SUBSCRIBE)) { - boolean canSubscribe = false; - if (toJuick) { - canSubscribe = true; - } else { - int uid_to = userService.getUIDbyName(username); - if (uid_to > 0) { - pmQueriesService.addPMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); - canSubscribe = true; - } - } - if (canSubscribe) { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.SUBSCRIBED); - xmpp.send(ClientPresence.from(reply)); - - reply.setFrom(reply.getFrom().withResource(jid.getResource())); - reply.setPriority((byte) 10); - reply.setType(null); - xmpp.send(ClientPresence.from(reply)); - } else { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.ERROR); - reply.setId(p.getId()); - reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); - xmpp.send(ClientPresence.from(reply)); - } - } else if (p.getType().equals(Presence.Type.UNSUBSCRIBE)) { - if (!toJuick) { - int uid_to = userService.getUIDbyName(username); - if (uid_to > 0) { - pmQueriesService.removePMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); - } - } - - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.UNSUBSCRIBED); - xmpp.send(ClientPresence.from(reply)); - } - } - - public ClientMessage incomingMessage(Message msg) { - if (msg.getType() != null && msg.getType().equals(Message.Type.ERROR)) { - StanzaError error = msg.getError(); - if (error != null && error.getCondition().equals(Condition.RESOURCE_CONSTRAINT)) { - // offline query is full, deactivating this jid - if (userService.setActiveStatusForJID(msg.getFrom().toEscapedString(), UserService.ActiveStatus.Inactive)) { - logger.info("{} is inactive now", msg.getFrom()); - return null; - } - } - return null; - } - Jid to = msg.getTo(); - if (to.getDomain().equals(xmpp.getDomain().toEscapedString()) || to.equals(this.jid)) { - User user_from = userService.getUserByJID(msg.getFrom().asBareJid().toEscapedString()); - if ((user_from == null || user_from.isAnonymous()) && !msg.getFrom().equals(jid)) { - String signuphash = userService.getSignUpHashByJID(msg.getFrom().asBareJid().toEscapedString()); - return makeReply(msg.getFrom(), "Для того, чтобы начать пользоваться сервисом, пожалуйста пройдите быструю регистрацию: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nTo start using Juick, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); - } - URI attachment = URI.create(StringUtils.EMPTY); - OobX oobX = msg.getExtension(OobX.class); - if (oobX != null) { - attachment = oobX.getUri(); - } - try { - return incomingMessageJuick(user_from, msg.getFrom(), msg.getTo().getLocal(), StringUtils.defaultString(msg.getBody()).trim(), attachment); - } catch (Exception e1) { - logger.warn("message exception", e1); - } - } - ClientMessage errorMessage = ClientMessage.from(msg); - errorMessage.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); - return errorMessage; - } - private ClientMessage incomingMessageJuick(User user_from, Jid from, String to, String command, @Nonnull URI attachment) { - if (StringUtils.isBlank(command) && attachment.toString().isEmpty()) { - return null; - } - - messagesService.getUnread(user_from).forEach(mid -> messagesService.setRead(user_from, mid)); - - int commandlen = command.length(); - - // COMPATIBILITY - if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) { - command = command.substring(3); - } - - if (!jid.getLocal().equals(to)) { - // PM - if (!StringUtils.isEmpty(command)) { - commandsManager.commandPM(user_from, null, to, command); - return null; - } - } - - try { - CommandResult result = commandsManager.processCommand(user_from, command, attachment); - if (StringUtils.isNotBlank(result.getText())) { - return makeReply(from, result.getText()); - } - } catch (Exception e) { - logger.warn("xmpp command exception", e); - return makeReply(from, "Error processing command"); - } - return null; - } - - private void broadcastPresence(Presence.Type type) { - Presence presence = new Presence(); - presence.setFrom(jid); - if (type != null) { - presence.setType(type); - } - userService.getActiveJIDs().forEach(j -> { - try { - presence.setTo(Jid.of(j)); - xmpp.send(ClientPresence.from(presence)); - } catch (IllegalArgumentException ex) { - logger.warn("Invalid jid: {}", j, ex); - } - }); - } - - @PreDestroy - public void close() throws Exception { - broadcastPresence(Presence.Type.UNAVAILABLE); - if (xmpp != null) { - xmpp.close(); - } - } -} diff --git a/src/main/java/com/juick/server/configuration/ActivityPubClientConfig.java b/src/main/java/com/juick/server/configuration/ActivityPubClientConfig.java deleted file mode 100644 index 56edffa7..00000000 --- a/src/main/java/com/juick/server/configuration/ActivityPubClientConfig.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.juick.www.api.activity.model.Activity; -import com.juick.server.helpers.HeaderRequestInterceptor; -import org.apache.http.client.config.CookieSpecs; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.protocol.HttpClientContext; -import org.apache.http.protocol.HttpContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.web.client.RestTemplate; - -import javax.inject.Inject; -import java.net.URI; -import java.util.Collections; - -@Configuration -public class ActivityPubClientConfig { - @Inject - ActivityPubClientErrorHandler activityPubClientErrorHandler; - @Inject - ObjectMapper jsonMapper; - @Bean - public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() { - MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); - converter.setObjectMapper(jsonMapper); - return converter; - } - @Bean - public RestTemplate apClient() { - RestTemplate restTemplate = new RestTemplate(); - restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory() { - @Override - protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) { - HttpClientContext context = HttpClientContext.create(); - context.setRequestConfig(getRequestConfig()); - return context; - } - RequestConfig getRequestConfig() { - RequestConfig.Builder builder = RequestConfig.custom() - .setCookieSpec(CookieSpecs.IGNORE_COOKIES); - return builder.build(); - } - }); - restTemplate.getMessageConverters().add(0, mappingJacksonHttpMessageConverter()); - restTemplate.setErrorHandler(activityPubClientErrorHandler); - restTemplate.setInterceptors(Collections.singletonList( - new HeaderRequestInterceptor("Accept", Activity.ACTIVITY_MEDIA_TYPE))); - return restTemplate; - } -} \ No newline at end of file diff --git a/src/main/java/com/juick/server/configuration/ActivityPubClientErrorHandler.java b/src/main/java/com/juick/server/configuration/ActivityPubClientErrorHandler.java deleted file mode 100644 index edabadd7..00000000 --- a/src/main/java/com/juick/server/configuration/ActivityPubClientErrorHandler.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.juick.service.activities.DeleteUserEvent; -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.stereotype.Component; -import org.springframework.web.client.DefaultResponseErrorHandler; - -import javax.annotation.Nonnull; -import javax.inject.Inject; -import java.io.IOException; -import java.net.URI; -import java.nio.charset.StandardCharsets; - -@Component -public class ActivityPubClientErrorHandler extends DefaultResponseErrorHandler { - private static final Logger logger = LoggerFactory.getLogger("ActivityPub"); - - @Inject - private ApplicationEventPublisher applicationEventPublisher; - @Override - public void handleError(URI contextUri, HttpMethod method, @Nonnull ClientHttpResponse response) throws IOException { - logger.warn("HTTP ERROR {} {} : {}", response.getStatusCode().value(), - response.getStatusText(), IOUtils.toString(response.getBody(), StandardCharsets.UTF_8)); - if (response.getStatusCode().equals(HttpStatus.GONE)) { - logger.warn("Server report {} is gone, deleting", contextUri.toASCIIString()); - applicationEventPublisher.publishEvent(new DeleteUserEvent(this, contextUri.toASCIIString())); - } - } -} diff --git a/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java b/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java deleted file mode 100644 index 75d247bf..00000000 --- a/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.juick.www.rss.MessagesView; -import com.juick.www.rss.RepliesView; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.view.BeanNameViewResolver; -import org.springframework.web.servlet.view.feed.AbstractRssFeedView; - -/** - * Created by aalexeev on 11/12/16. - */ -@Configuration -@EnableAsync(proxyTargetClass = true) -@EnableScheduling -public class ApiAppConfiguration implements WebMvcConfigurer { - @Bean - public BeanNameViewResolver beanNameViewResolver() { - return new BeanNameViewResolver(); - } - @Bean - AbstractRssFeedView messagesView() { - return new MessagesView(); - } - @Bean - AbstractRssFeedView repliesView() { - return new RepliesView(); - } -} diff --git a/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java b/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java deleted file mode 100644 index e84c0c40..00000000 --- a/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.juick.server.KeystoreManager; -import com.overzealous.remark.Options; -import com.overzealous.remark.Remark; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.Resource; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; - -/** - * Created by vitalyster on 28.06.2016. - */ -@Configuration -public class BaseWebConfiguration implements WebMvcConfigurer { - - @Value("${keystore:classpath:juick-test-key.p12}") - private Resource keystore; - @Value("${keystore_password:secret}") - private String keystorePassword; - - @Bean - public ResourceUrlEncodingFilter resourceUrlEncodingFilter() { - return new ResourceUrlEncodingFilter(); - } - - @Bean - public KeystoreManager keystoreManager() { - return new KeystoreManager(keystore, keystorePassword); - } - @Bean - public Remark remarkConverter() { - Options options = new Options(); - options.inlineLinks = true; - return new Remark(options); - } -} diff --git a/src/main/java/com/juick/server/configuration/MailConfiguration.java b/src/main/java/com/juick/server/configuration/MailConfiguration.java deleted file mode 100644 index 31034339..00000000 --- a/src/main/java/com/juick/server/configuration/MailConfiguration.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2008-2020, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.juick.server.EmailManager; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@ConditionalOnProperty("service_email") -public class MailConfiguration { - @Bean - public EmailManager emailManager() { - return new EmailManager(); - } -} diff --git a/src/main/java/com/juick/server/configuration/SapeConfiguration.java b/src/main/java/com/juick/server/configuration/SapeConfiguration.java deleted file mode 100644 index 8892115d..00000000 --- a/src/main/java/com/juick/server/configuration/SapeConfiguration.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import ru.sape.Sape; - -/** - * Created by vitalyster on 29.03.2017. - */ -@Configuration -@ConditionalOnProperty("sape_user") -public class SapeConfiguration { - @Value("${sape_user:}") - private String token; - - @Bean - public Sape sape() { - return new Sape(token, "juick.com", 2000, 3600); - } -} diff --git a/src/main/java/com/juick/server/configuration/SecurityConfig.java b/src/main/java/com/juick/server/configuration/SecurityConfig.java deleted file mode 100644 index 0fab087f..00000000 --- a/src/main/java/com/juick/server/configuration/SecurityConfig.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright (C) 2008-2020, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.juick.server.SignatureManager; -import com.juick.service.UserService; -import com.juick.service.security.HTTPSignatureAuthenticationFilter; -import com.juick.service.security.HashParamAuthenticationFilter; -import com.juick.service.security.JuickUserDetailsService; -import com.juick.service.security.entities.JuickUser; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.http.HttpMethod; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.authentication.NullRememberMeServices; -import org.springframework.security.web.authentication.RememberMeServices; -import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; -import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import javax.annotation.Resource; -import javax.inject.Inject; -import java.util.Arrays; -import java.util.Collections; -import java.util.concurrent.TimeUnit; - -/** - * Created by aalexeev on 11/21/16. - */ -@EnableWebSecurity -public class SecurityConfig { - @Resource - private UserService userService; - - private static final String COOKIE_NAME = "juick-remember-me"; - - @Bean - public UserDetailsService userDetailsService() { - return new JuickUserDetailsService(userService); - } - @Bean - static CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - - configuration.setAllowedOrigins(Collections.singletonList("*")); - configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE")); - configuration.setAllowedHeaders(Collections.singletonList("*")); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/api/**", configuration); - source.registerCorsConfiguration("/u/**", configuration); - source.registerCorsConfiguration("/n/**", configuration); - return source; - } - - @Configuration - @Order(1) - public static class ApiConfig extends WebSecurityConfigurerAdapter { - @Value("${auth_remember_me_key:secret}") - private String rememberMeKey; - @Resource - private UserService userService; - @Resource - private SignatureManager signatureManager; - ApiConfig() { - super(true); - } - @Bean - RememberMeServices apiTokenServices() { - return new NullRememberMeServices(); - } - @Bean - public HashParamAuthenticationFilter apiAuthenticationFilter() { - return new HashParamAuthenticationFilter(userService, apiTokenServices()); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.antMatcher("/api/**") - .addFilterBefore(apiAuthenticationFilter(), BasicAuthenticationFilter.class) - .addFilterBefore(new HTTPSignatureAuthenticationFilter(signatureManager, userService), BasicAuthenticationFilter.class) - .authorizeRequests() - .antMatchers(HttpMethod.OPTIONS).permitAll() - .antMatchers("/api/", "/api/messages", "/api/avatar", "/api/messages/discussions", - "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", - "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", - "/api/_google", "/api/_applelogin", "/api/signup", "/api/inbox", "/api/events", "/api/info/**", - "/api/nodeinfo/2.0").permitAll() - .anyRequest().hasRole("USER") - .and() - .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) - .and() - .httpBasic().authenticationEntryPoint(juickAuthenticationEntryPoint()) - .and().cors().configurationSource(corsConfigurationSource()) - .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and().exceptionHandling().authenticationEntryPoint(juickAuthenticationEntryPoint()) - .and() - .rememberMe() - .alwaysRemember(true) - .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(6 * 30)) - .rememberMeServices(apiTokenServices()) - .key(rememberMeKey) - .and() - .headers().defaultsDisabled().cacheControl(); - } - - @Bean - public AuthenticationEntryPoint juickAuthenticationEntryPoint() { - var entryPoint = new BasicAuthenticationEntryPoint(); - entryPoint.setRealmName("Juick"); - return entryPoint; - } - - @Override - public void configure(WebSecurity web) { - web.debug(false); - web.ignoring().antMatchers("/api/v2/api-docs", "/api/configuration/ui", "/api/swagger-resources/**", - "/api/configuration/**", "/swagger-ui.html", "/webjars/**", "/h2-console/**"); - } - } - - @Configuration - public static class WebConfig extends WebSecurityConfigurerAdapter { - @Value("${auth_remember_me_key:secret}") - private String rememberMeKey; - @Value("${web_domain:localhost}") - private String webDomain; - @Resource - private UserService userService; - @Inject - private UserDetailsService userDetailsService; - @Bean - @Qualifier("www") - public HashParamAuthenticationFilter wwwAuthenticationFilter() { - return new HashParamAuthenticationFilter(userService, hashCookieServices()); - } - @Bean - @Qualifier("www") - public RememberMeServices hashCookieServices() { - TokenBasedRememberMeServices services = new TokenBasedRememberMeServices( - rememberMeKey, userDetailsService); - - services.setCookieName(COOKIE_NAME); - services.setCookieDomain(webDomain); - services.setAlwaysRemember(true); - services.setTokenValiditySeconds(6 * 30 * 24 * 3600); - services.setUseSecureCookie(false); // TODO set true if https is supports - - return services; - } - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .addFilterBefore(wwwAuthenticationFilter(), BasicAuthenticationFilter.class) - .authorizeRequests() - .antMatchers("/settings", "/pm/**", "/**/bl", "/_twitter", "/post", "/post2", "/comment") - .authenticated() - .antMatchers("/actuator/**").hasRole("ADMIN") - .anyRequest().permitAll() - .and() - .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) - .and().cors().configurationSource(corsConfigurationSource()) - .and().sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .invalidSessionUrl("/") - .and() - .logout() - .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) - .invalidateHttpSession(true) - .logoutUrl("/logout") - .logoutSuccessUrl("/") - .deleteCookies("hash", COOKIE_NAME) - .and() - .formLogin() - .loginPage("/login") - .permitAll() - .defaultSuccessUrl("/") - .loginProcessingUrl("/login") - .usernameParameter("username") - .passwordParameter("password") - .failureUrl("/login?error=1") - .and() - .rememberMe() - .rememberMeCookieDomain(webDomain).key(rememberMeKey) - .rememberMeServices(hashCookieServices()) - .and() - .csrf().disable() - .headers().defaultsDisabled().cacheControl(); - } - @Override - public void configure(WebSecurity web) { - web.debug(false); - web.ignoring().antMatchers("/style*.css", "/scripts*.js", "/h2-console/**", "/.well-known/**"); - } - } -} diff --git a/src/main/java/com/juick/server/configuration/SignInWithAppleConfig.java b/src/main/java/com/juick/server/configuration/SignInWithAppleConfig.java deleted file mode 100644 index 310c5899..00000000 --- a/src/main/java/com/juick/server/configuration/SignInWithAppleConfig.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.github.scribejava.apis.AppleClientSecretGenerator; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.Resource; - -import java.io.IOException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; - -@Configuration -public class SignInWithAppleConfig { - @Value("${apple_app_id:com.example.app}") - private String appId; - @Value("${apple_team_id:teamid}") - private String teamId; - @Value("${apple_key_id:keyid}") - private String keyId; - @Value("${apple_key_path:classpath:testkey.p8}") - private Resource keyPath; - - @Bean - public AppleClientSecretGenerator clientSecretGenerator() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { - return new AppleClientSecretGenerator(appId, teamId, keyId, keyPath.getFile().toPath()); - } -} diff --git a/src/main/java/com/juick/server/configuration/StorageConfiguration.java b/src/main/java/com/juick/server/configuration/StorageConfiguration.java deleted file mode 100644 index f4a80ece..00000000 --- a/src/main/java/com/juick/server/configuration/StorageConfiguration.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.juick.service.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/src/main/java/com/juick/server/configuration/TelegramConfig.java b/src/main/java/com/juick/server/configuration/TelegramConfig.java deleted file mode 100644 index c56d7d0e..00000000 --- a/src/main/java/com/juick/server/configuration/TelegramConfig.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.juick.server.TelegramBotManager; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@ConditionalOnProperty(name = "telegram_token") -public class TelegramConfig { - @Bean - public TelegramBotManager telegramBotManager() { - return new TelegramBotManager(); - } -} diff --git a/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java b/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java deleted file mode 100644 index 8e874e43..00000000 --- a/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.juick.service.HelpService; -import com.juick.service.TagService; -import com.juick.service.UserService; -import com.mitchellbosecke.pebble.PebbleEngine; -import com.mitchellbosecke.pebble.extension.FormatterExtension; -import com.mitchellbosecke.pebble.loader.ClasspathLoader; -import com.mitchellbosecke.pebble.loader.Loader; -import com.mitchellbosecke.pebble.spring.extension.SpringExtension; -import com.mitchellbosecke.pebble.spring.servlet.PebbleViewResolver; -import org.apache.commons.codec.CharEncoding; -import org.commonmark.ext.autolink.AutolinkExtension; -import org.commonmark.node.Link; -import org.commonmark.parser.Parser; -import org.commonmark.renderer.html.HtmlRenderer; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.cache.caffeine.CaffeineCacheManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.CacheControl; -import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.resource.VersionResourceResolver; - -import javax.inject.Inject; -import java.net.MalformedURLException; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.concurrent.TimeUnit; - -/** - * Created by aalexeev on 11/22/16. - */ -@Configuration -@EnableCaching -public class WwwAppConfiguration implements WebMvcConfigurer { - @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") - private String imgDir; - @Bean - public CaffeineCacheManager cacheManager() { - return new CaffeineCacheManager("help"); - } - - @Bean - public HelpService helpService() { - return new HelpService("help"); - } - - @Bean - public Parser cmParser() { - return Parser.builder().extensions(Collections.singletonList(AutolinkExtension.create())).build(); - } - @Bean - public HtmlRenderer helpRenderer() { - return HtmlRenderer.builder() - .attributeProviderFactory(context -> (node, tagName, attributes) -> { - if (node instanceof Link) { - Link link = (Link) node; - if (link.getDestination().startsWith("/")) { - String destination = "/" + helpService().getHelpPath() + link.getDestination(); - link.setDestination(destination); - attributes.put("href", destination); - } - } - }) - .build(); - } - @Bean - public Loader templateLoader() { - return new ClasspathLoader(); - } - - @Bean - public SpringExtension springExtension() { - return new SpringExtension(); - } - - @Bean - public PebbleEngine pebbleEngine() { - boolean devToolsArePresent = false; - try { - Class.forName("org.springframework.boot.devtools.livereload.Connection"); - devToolsArePresent = true; - } catch (ClassNotFoundException e) { - // release mode - } - return new PebbleEngine.Builder() - .loader(this.templateLoader()) - .cacheActive(!devToolsArePresent) - .extension(springExtension()) - .extension(new FormatterExtension()) - .newLineTrimming(false) - .strictVariables(true) - .build(); - } - - @Bean - public ViewResolver viewResolver() { - PebbleViewResolver viewResolver = new PebbleViewResolver(); - viewResolver.setPrefix("templates"); - viewResolver.setSuffix(".html"); - viewResolver.setPebbleEngine(pebbleEngine()); - viewResolver.setCharacterEncoding(CharEncoding.UTF_8); - return viewResolver; - } - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - try { - registry - .addResourceHandler("/**", "/i/a/**") - .addResourceLocations("classpath:/static/", Paths.get(imgDir, "/a/").toUri().toURL().toString()) - .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)) - .resourceChain(false) - .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**", "/i/a/**")); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - } -} diff --git a/src/main/java/com/juick/server/configuration/XMPPConfig.java b/src/main/java/com/juick/server/configuration/XMPPConfig.java deleted file mode 100644 index 62e19c71..00000000 --- a/src/main/java/com/juick/server/configuration/XMPPConfig.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.juick.server.XMPPManager; -import com.juick.server.xmpp.JidConverter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.convert.ConversionService; -import org.springframework.format.support.DefaultFormattingConversionService; - -@Configuration -@ConditionalOnProperty("xmppbot_jid") -public class XMPPConfig { - @Bean - public static ConversionService conversionService() { - DefaultFormattingConversionService cs = new DefaultFormattingConversionService(); - cs.addConverter(new JidConverter()); - return cs; - } - @Bean - public XMPPManager xmppConnection() { - return new XMPPManager(); - } -} diff --git a/src/main/java/com/juick/server/helpers/HeaderRequestInterceptor.java b/src/main/java/com/juick/server/helpers/HeaderRequestInterceptor.java deleted file mode 100644 index 8fb21ac5..00000000 --- a/src/main/java/com/juick/server/helpers/HeaderRequestInterceptor.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.helpers; - -import org.springframework.http.HttpRequest; -import org.springframework.http.client.ClientHttpRequestExecution; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.http.client.ClientHttpResponse; - -import java.io.IOException; - -public class HeaderRequestInterceptor implements ClientHttpRequestInterceptor { - - private final String headerName; - - private final String headerValue; - - public HeaderRequestInterceptor(String headerName, String headerValue) { - this.headerName = headerName; - this.headerValue = headerValue; - } - - @Override - public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { - request.getHeaders().set(headerName, headerValue); - return execution.execute(request, body); - } -} diff --git a/src/main/java/com/juick/server/helpers/annotation/UserCommand.java b/src/main/java/com/juick/server/helpers/annotation/UserCommand.java deleted file mode 100644 index d25810d2..00000000 --- a/src/main/java/com/juick/server/helpers/annotation/UserCommand.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.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/src/main/java/com/juick/server/util/HttpBadRequestException.java b/src/main/java/com/juick/server/util/HttpBadRequestException.java deleted file mode 100644 index 6dfc165e..00000000 --- a/src/main/java/com/juick/server/util/HttpBadRequestException.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.util; - -import org.springframework.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/src/main/java/com/juick/server/util/HttpForbiddenException.java b/src/main/java/com/juick/server/util/HttpForbiddenException.java deleted file mode 100644 index 0247f531..00000000 --- a/src/main/java/com/juick/server/util/HttpForbiddenException.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.util; - -import org.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/src/main/java/com/juick/server/util/HttpNotFoundException.java b/src/main/java/com/juick/server/util/HttpNotFoundException.java deleted file mode 100644 index dd5a2e1b..00000000 --- a/src/main/java/com/juick/server/util/HttpNotFoundException.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.util; - -import org.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/src/main/java/com/juick/server/util/HttpUtils.java b/src/main/java/com/juick/server/util/HttpUtils.java deleted file mode 100644 index beef5d60..00000000 --- a/src/main/java/com/juick/server/util/HttpUtils.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.juick.server.util; - -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.MediaType; -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 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 String mediaType(String attachmentType) { - return attachmentType.equals("jpg") ? MediaType.IMAGE_JPEG_VALUE : MediaType.IMAGE_PNG_VALUE; - } - - public static URI downloadImage(URL url, String tmpDir) throws IOException { - ImageInputStream iis = ImageIO.createImageInputStream(url.openStream()); - Iterator 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/src/main/java/com/juick/server/util/ImageUtils.java b/src/main/java/com/juick/server/util/ImageUtils.java deleted file mode 100644 index e06339ba..00000000 --- a/src/main/java/com/juick/server/util/ImageUtils.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.util; - -import com.juick.model.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.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 BufferedImage, same as ImageIO.read() does. - * - *

JPEG images with EXIF metadata are rotated according to Orientation tag. - * - * @param imageFile a File 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 | IOException 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 = Math.max(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 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/src/main/java/com/juick/server/util/TagUtils.java b/src/main/java/com/juick/server/util/TagUtils.java deleted file mode 100644 index 754d8020..00000000 --- a/src/main/java/com/juick/server/util/TagUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.util; - -import com.juick.model.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 tags) { - if (CollectionUtils.isEmpty(tags)) - return StringUtils.EMPTY; - - return tags.stream().map(t -> "*" + t.getName()) - .collect(Collectors.joining(" ")); - } -} diff --git a/src/main/java/com/juick/server/util/WebUtils.java b/src/main/java/com/juick/server/util/WebUtils.java deleted file mode 100644 index bc3ac63a..00000000 --- a/src/main/java/com/juick/server/util/WebUtils.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.util; - -import 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/src/main/java/com/juick/server/xmpp/JidConverter.java b/src/main/java/com/juick/server/xmpp/JidConverter.java deleted file mode 100644 index fdf80108..00000000 --- a/src/main/java/com/juick/server/xmpp/JidConverter.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.xmpp; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.Nullable; -import rocks.xmpp.addr.Jid; - -import javax.annotation.Nonnull; - -public class JidConverter implements Converter { - @Nullable - @Override - public Jid convert(@Nonnull String jidStr) { - return Jid.of(jidStr); - } -} diff --git a/src/main/java/com/juick/server/xmpp/iq/MessageQuery.java b/src/main/java/com/juick/server/xmpp/iq/MessageQuery.java deleted file mode 100644 index c973b624..00000000 --- a/src/main/java/com/juick/server/xmpp/iq/MessageQuery.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.xmpp.iq; - -import javax.xml.bind.annotation.XmlRootElement; - -@XmlRootElement(name = "query") -public class MessageQuery { - private MessageQuery() { - - } -} diff --git a/src/main/java/com/juick/server/xmpp/iq/package-info.java b/src/main/java/com/juick/server/xmpp/iq/package-info.java deleted file mode 100644 index 822fb8c4..00000000 --- a/src/main/java/com/juick/server/xmpp/iq/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2008-2019, 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 . - */ - -@XmlAccessorType(XmlAccessType.FIELD) -@XmlSchema(namespace = "http://juick.com/query#messages", elementFormDefault = XmlNsForm.QUALIFIED) -package com.juick.server.xmpp.iq; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlNsForm; -import javax.xml.bind.annotation.XmlSchema; \ No newline at end of file diff --git a/src/main/java/com/juick/service/ImagesServiceImpl.java b/src/main/java/com/juick/service/ImagesServiceImpl.java index abaec940..5884b0d0 100644 --- a/src/main/java/com/juick/service/ImagesServiceImpl.java +++ b/src/main/java/com/juick/service/ImagesServiceImpl.java @@ -20,7 +20,7 @@ package com.juick.service; import com.juick.model.Attachment; import com.juick.model.Message; import com.juick.model.Photo; -import com.juick.server.util.ImageUtils; +import com.juick.util.ImageUtils; import org.springframework.util.StringUtils; import java.io.File; diff --git a/src/main/java/com/juick/service/UserService.java b/src/main/java/com/juick/service/UserService.java index 4bd5486d..fbbab0ad 100644 --- a/src/main/java/com/juick/service/UserService.java +++ b/src/main/java/com/juick/service/UserService.java @@ -55,12 +55,8 @@ public interface UserService { List getJIDsbyUID(int uid); - int getUIDbyJID(String jid); - int getUIDbyName(String uname); - int getUIDbyHash(String hash); - @Nonnull User getUserByHash(String hash); diff --git a/src/main/java/com/juick/service/UserServiceImpl.java b/src/main/java/com/juick/service/UserServiceImpl.java index 23c55bbe..84ff1ff5 100644 --- a/src/main/java/com/juick/service/UserServiceImpl.java +++ b/src/main/java/com/juick/service/UserServiceImpl.java @@ -248,19 +248,6 @@ public class UserServiceImpl extends BaseJdbcService implements UserService { return getJdbcTemplate().queryForList("SELECT jid FROM jids WHERE user_id = ? AND active = 1", String.class, uid); } - @Transactional(readOnly = true) - @Override - public int getUIDbyJID(final String jid) { - if (StringUtils.isNotBlank(jid)) { - List list = getJdbcTemplate().queryForList( - "SELECT user_id FROM jids WHERE jid = ?", Integer.class, jid); - - if (!list.isEmpty()) - return list.get(0); - } - return 0; - } - @Transactional(readOnly = true) @Override public int getUIDbyName(final String uname) { @@ -274,19 +261,6 @@ public class UserServiceImpl extends BaseJdbcService implements UserService { return 0; } - @Transactional(readOnly = true) - @Override - public int getUIDbyHash(final String hash) { - if (StringUtils.isNotBlank(hash)) { - List list = getJdbcTemplate().queryForList( - "SELECT user_id FROM logins WHERE hash = ?", Integer.class, hash); - - if (!list.isEmpty()) - return list.get(0); - } - return 0; - } - @Transactional(readOnly = true) @Override public User getUserByHash(final String hash) { diff --git a/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java index 1b3cb936..9878df82 100644 --- a/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java +++ b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java @@ -18,7 +18,7 @@ package com.juick.service.security; import com.juick.model.User; -import com.juick.server.SignatureManager; +import com.juick.SignatureManager; import com.juick.service.UserService; import com.juick.service.security.entities.JuickUser; import org.apache.commons.lang3.StringUtils; diff --git a/src/main/java/com/juick/util/HeaderRequestInterceptor.java b/src/main/java/com/juick/util/HeaderRequestInterceptor.java new file mode 100644 index 00000000..22a3de06 --- /dev/null +++ b/src/main/java/com/juick/util/HeaderRequestInterceptor.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.util; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; + +public class HeaderRequestInterceptor implements ClientHttpRequestInterceptor { + + private final String headerName; + + private final String headerValue; + + public HeaderRequestInterceptor(String headerName, String headerValue) { + this.headerName = headerName; + this.headerValue = headerValue; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + request.getHeaders().set(headerName, headerValue); + return execution.execute(request, body); + } +} diff --git a/src/main/java/com/juick/util/HttpBadRequestException.java b/src/main/java/com/juick/util/HttpBadRequestException.java new file mode 100644 index 00000000..386b52df --- /dev/null +++ b/src/main/java/com/juick/util/HttpBadRequestException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.util; + +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/src/main/java/com/juick/util/HttpForbiddenException.java b/src/main/java/com/juick/util/HttpForbiddenException.java new file mode 100644 index 00000000..e2211574 --- /dev/null +++ b/src/main/java/com/juick/util/HttpForbiddenException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.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/src/main/java/com/juick/util/HttpNotFoundException.java b/src/main/java/com/juick/util/HttpNotFoundException.java new file mode 100644 index 00000000..f9d1ffbe --- /dev/null +++ b/src/main/java/com/juick/util/HttpNotFoundException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.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/src/main/java/com/juick/util/HttpUtils.java b/src/main/java/com/juick/util/HttpUtils.java new file mode 100644 index 00000000..46bb3e2d --- /dev/null +++ b/src/main/java/com/juick/util/HttpUtils.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.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.http.MediaType; +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 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 String mediaType(String attachmentType) { + return attachmentType.equals("jpg") ? MediaType.IMAGE_JPEG_VALUE : MediaType.IMAGE_PNG_VALUE; + } + + public static URI downloadImage(URL url, String tmpDir) throws IOException { + ImageInputStream iis = ImageIO.createImageInputStream(url.openStream()); + Iterator 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/src/main/java/com/juick/util/ImageUtils.java b/src/main/java/com/juick/util/ImageUtils.java new file mode 100644 index 00000000..38e1de08 --- /dev/null +++ b/src/main/java/com/juick/util/ImageUtils.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.util; + +import com.juick.model.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.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 BufferedImage, same as ImageIO.read() does. + * + *

JPEG images with EXIF metadata are rotated according to Orientation tag. + * + * @param imageFile a File 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 | IOException 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 = Math.max(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 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/src/main/java/com/juick/util/TagUtils.java b/src/main/java/com/juick/util/TagUtils.java new file mode 100644 index 00000000..2ec03e48 --- /dev/null +++ b/src/main/java/com/juick/util/TagUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.util; + +import com.juick.model.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 tags) { + if (CollectionUtils.isEmpty(tags)) + return StringUtils.EMPTY; + + return tags.stream().map(t -> "*" + t.getName()) + .collect(Collectors.joining(" ")); + } +} diff --git a/src/main/java/com/juick/util/WebUtils.java b/src/main/java/com/juick/util/WebUtils.java new file mode 100644 index 00000000..3a8c7620 --- /dev/null +++ b/src/main/java/com/juick/util/WebUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.util; + +import javax.servlet.http.HttpServletRequest; +import java.util.Optional; +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); + } + + public static String encodeSphinx(String str) { + return str.replaceAll("@", "\\\\@") + .replaceAll("\\'", "\\\\'") + .replaceAll("=", "\\\\\\\\="); + } + /** + * Returns the viewName to return for coming back to the sender url + * + * @param request Instance of {@link HttpServletRequest} or use an injected instance + * @return Optional with the view name. Recomended to use an alternativa url with + * {@link Optional#orElse(java.lang.Object)} + */ + public static Optional getPreviousPageByRequest(HttpServletRequest request) + { + return Optional.ofNullable(request.getHeader("Referer")); + } +} diff --git a/src/main/java/com/juick/util/adapters/SimpleDateAdapter.java b/src/main/java/com/juick/util/adapters/SimpleDateAdapter.java new file mode 100644 index 00000000..fd5f3b5a --- /dev/null +++ b/src/main/java/com/juick/util/adapters/SimpleDateAdapter.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.util.adapters; + +import com.juick.util.DateFormattersHolder; + +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.time.Instant; + +/** + * Created by vitalyster on 15.11.2016. + */ + +public class SimpleDateAdapter extends XmlAdapter { + + @Override + public String marshal(Instant v) { + return DateFormattersHolder.getMessageFormatterInstance().format(v); + } + + @Override + public Instant unmarshal(String v) { + return DateFormattersHolder.getMessageFormatterInstance().parse(v); + } +} diff --git a/src/main/java/com/juick/util/annotation/UserCommand.java b/src/main/java/com/juick/util/annotation/UserCommand.java new file mode 100644 index 00000000..29e40ca2 --- /dev/null +++ b/src/main/java/com/juick/util/annotation/UserCommand.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.util.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/src/main/java/com/juick/util/formatters/PlainTextFormatter.java b/src/main/java/com/juick/util/formatters/PlainTextFormatter.java new file mode 100644 index 00000000..b5d67030 --- /dev/null +++ b/src/main/java/com/juick/util/formatters/PlainTextFormatter.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.util.formatters; + +import com.juick.model.Message; +import com.juick.util.MessageUtils; +import org.apache.commons.lang3.StringUtils; +import org.ocpsoft.prettytime.PrettyTime; + +import java.util.Date; +import java.util.Locale; + +/** + * Created by vitalyster on 12.10.2016. + */ +public class PlainTextFormatter { + static PrettyTime pt = new PrettyTime(new Locale("en")); + + public static String formatPost(Message jmsg) { + return formatPost(jmsg, false); + } + + public static String formatPost(Message jmsg, boolean markdown) { + StringBuilder sb = new StringBuilder(); + String title = MessageUtils.isReply(jmsg) ? "Reply by @" : MessageUtils.isPM(jmsg) ? "Private message from @" : "@"; + String subtitle = MessageUtils.isReply(jmsg) ? markdown ? MessageUtils.escapeMarkdown(StringUtils.defaultString(jmsg.getReplyQuote())) + : jmsg.getReplyQuote() + : markdown ? MessageUtils.getMessageHashTags(jmsg) : MessageUtils.getTagsString(jmsg); + sb.append(title).append(markdown ? MessageUtils.getMarkdownUser(jmsg.getUser()) : jmsg.getUser().getName()).append(":\n") + .append(subtitle).append("\n"); + if (markdown) { + sb.append(MessageUtils.formatMarkdownText(jmsg)); + } else { + sb.append(StringUtils.defaultString(jmsg.getText())); + } + sb.append("\n"); + if (!markdown && StringUtils.isNotEmpty(jmsg.getAttachmentType())) { + sb.append(MessageUtils.attachmentUrl(jmsg)); + } + return sb.toString(); + } + + public static String formatPostSummary(Message m) { + int cropLength = 384; + String timeAgo = pt.format(Date.from(m.getCreated())); + String repliesCount = m.getReplies() == 1 ? "; 1 reply" : m.getReplies() == 0 ? "" + : String.format("; %d replies", m.getReplies()); + StringBuilder sb = new StringBuilder(); + String txt = StringUtils.defaultString(m.getText()); + String attachmentUrl = MessageUtils.attachmentUrl(m); + if (StringUtils.isNotEmpty(attachmentUrl)) { + sb.append(attachmentUrl).append("\n"); + } + if (txt.length() >= cropLength) { + sb.append(StringUtils.substring(txt, 0, cropLength)).append(" [...]"); + } else { + sb.append(txt); + } + return String.format("@%s:%s\n%s\n#%s (%s%s) %s", + m.getUser().getName(), MessageUtils.getTagsString(m), sb.toString(), formatPostNumber(m), timeAgo, repliesCount, formatUrl(m)); + } + + public static String formatUrl(Message jmsg) { + if (MessageUtils.isReply(jmsg)) { + return String.format("https://juick.com/m/%d#%d", jmsg.getMid(), jmsg.getRid()); + } else if (MessageUtils.isPM(jmsg)) { + return "https://juick.com/pm/inbox"; + } + return "https://juick.com/m/" + jmsg.getMid(); + } + + public static String markdownUrl(String url, String description) { + if (StringUtils.isNotBlank(description)) { + return String.format("[%s](%s)", description, url); + } else { + return url; + } + } + + public static String formatPostNumber(Message jmsg) { + if (jmsg.getRid() > 0) { + return String.format("%d/%d", jmsg.getMid(), jmsg.getRid()); + } + return String.format("%d", jmsg.getMid()); + } + + public static String formatTwitterCard(Message jmsg) { + return MessageUtils.getMessageHashTags(jmsg) + StringUtils.defaultString(jmsg.getText()); + } +} diff --git a/src/main/java/com/juick/util/xmpp/JidConverter.java b/src/main/java/com/juick/util/xmpp/JidConverter.java new file mode 100644 index 00000000..118c6711 --- /dev/null +++ b/src/main/java/com/juick/util/xmpp/JidConverter.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.util.xmpp; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.Nullable; +import rocks.xmpp.addr.Jid; + +import javax.annotation.Nonnull; + +public class JidConverter implements Converter { + @Nullable + @Override + public Jid convert(@Nonnull String jidStr) { + return Jid.of(jidStr); + } +} diff --git a/src/main/java/com/juick/util/xmpp/iq/MessageQuery.java b/src/main/java/com/juick/util/xmpp/iq/MessageQuery.java new file mode 100644 index 00000000..c1096e8e --- /dev/null +++ b/src/main/java/com/juick/util/xmpp/iq/MessageQuery.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.util.xmpp.iq; + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "query") +public class MessageQuery { + private MessageQuery() { + + } +} diff --git a/src/main/java/com/juick/util/xmpp/iq/package-info.java b/src/main/java/com/juick/util/xmpp/iq/package-info.java new file mode 100644 index 00000000..7a1694a5 --- /dev/null +++ b/src/main/java/com/juick/util/xmpp/iq/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2008-2019, 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 . + */ + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlSchema(namespace = "http://juick.com/query#messages", elementFormDefault = XmlNsForm.QUALIFIED) +package com.juick.util.xmpp.iq; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlNsForm; +import javax.xml.bind.annotation.XmlSchema; \ No newline at end of file diff --git a/src/main/java/com/juick/www/api/ApiSocialLogin.java b/src/main/java/com/juick/www/api/ApiSocialLogin.java index 6499b507..101f6b1f 100644 --- a/src/main/java/com/juick/www/api/ApiSocialLogin.java +++ b/src/main/java/com/juick/www/api/ApiSocialLogin.java @@ -34,7 +34,7 @@ import com.google.api.client.json.JsonFactory; import com.google.api.client.json.jackson2.JacksonFactory; import com.juick.model.AuthResponse; import com.juick.model.ext.facebook.User; -import com.juick.server.util.HttpBadRequestException; +import com.juick.util.HttpBadRequestException; import com.juick.service.CrosspostService; import com.juick.service.EmailService; import com.juick.service.UserService; diff --git a/src/main/java/com/juick/www/api/Messages.java b/src/main/java/com/juick/www/api/Messages.java index 59ed7c8f..de29c78c 100644 --- a/src/main/java/com/juick/www/api/Messages.java +++ b/src/main/java/com/juick/www/api/Messages.java @@ -20,10 +20,10 @@ package com.juick.www.api; import com.juick.model.Message; import com.juick.model.Tag; import com.juick.model.User; -import com.juick.server.Utils; +import com.juick.util.WebUtils; import com.juick.www.WebApp; import com.juick.model.CommandResult; -import com.juick.server.util.HttpBadRequestException; +import com.juick.util.HttpBadRequestException; import com.juick.service.MessagesService; import com.juick.service.TagService; import com.juick.service.UserService; @@ -119,7 +119,7 @@ public class Messages { } else if (daysback > 0) { mids = messagesService.getUserBlogAtDay(user.getUid(), 0, daysback); } else if (!StringUtils.isEmpty(search)) { - mids = messagesService.getUserSearch(visitor, user.getUid(), Utils.encodeSphinx(search), 0, page); + mids = messagesService.getUserSearch(visitor, user.getUid(), WebUtils.encodeSphinx(search), 0, page); } else { mids = messagesService.getUserBlog(user.getUid(), 0, before); } @@ -139,7 +139,7 @@ public class Messages { return NOT_FOUND; } } else if (!StringUtils.isEmpty(search)) { - mids = messagesService.getSearch(visitor, Utils.encodeSphinx(search), page); + mids = messagesService.getSearch(visitor, WebUtils.encodeSphinx(search), page); } else { mids = messagesService.getAll(visitor.getUid(), before); } diff --git a/src/main/java/com/juick/www/api/Notifications.java b/src/main/java/com/juick/www/api/Notifications.java index ca382246..4f6096ce 100644 --- a/src/main/java/com/juick/www/api/Notifications.java +++ b/src/main/java/com/juick/www/api/Notifications.java @@ -22,7 +22,7 @@ import com.juick.model.Message; import com.juick.model.Status; import com.juick.model.User; import com.juick.model.AnonymousUser; -import com.juick.server.util.HttpBadRequestException; +import com.juick.util.HttpBadRequestException; import com.juick.service.MessagesService; import com.juick.service.PushQueriesService; import com.juick.service.SubscriptionService; diff --git a/src/main/java/com/juick/www/api/PM.java b/src/main/java/com/juick/www/api/PM.java index b81dcc78..863a1055 100644 --- a/src/main/java/com/juick/www/api/PM.java +++ b/src/main/java/com/juick/www/api/PM.java @@ -22,9 +22,9 @@ import com.juick.model.Message; import com.juick.model.User; import com.juick.model.AnonymousUser; import com.juick.model.PrivateChats; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpForbiddenException; -import com.juick.server.util.WebUtils; +import com.juick.util.HttpBadRequestException; +import com.juick.util.HttpForbiddenException; +import com.juick.util.WebUtils; import com.juick.www.WebApp; import com.juick.service.PMQueriesService; import com.juick.service.UserService; diff --git a/src/main/java/com/juick/www/api/Post.java b/src/main/java/com/juick/www/api/Post.java index 3c1fbf6b..205c8c90 100644 --- a/src/main/java/com/juick/www/api/Post.java +++ b/src/main/java/com/juick/www/api/Post.java @@ -22,12 +22,12 @@ import com.juick.model.Reaction; import com.juick.model.Status; import com.juick.model.User; import com.juick.model.CommandResult; -import com.juick.server.ActivityPubManager; -import com.juick.server.CommandsManager; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpForbiddenException; -import com.juick.server.util.HttpNotFoundException; -import com.juick.server.util.HttpUtils; +import com.juick.ActivityPubManager; +import com.juick.CommandsManager; +import com.juick.util.HttpBadRequestException; +import com.juick.util.HttpForbiddenException; +import com.juick.util.HttpNotFoundException; +import com.juick.util.HttpUtils; import com.juick.service.MessagesService; import com.juick.service.UserService; import com.juick.service.activities.UpdateEvent; diff --git a/src/main/java/com/juick/www/api/Service.java b/src/main/java/com/juick/www/api/Service.java index cb918682..850acb9d 100644 --- a/src/main/java/com/juick/www/api/Service.java +++ b/src/main/java/com/juick/www/api/Service.java @@ -20,11 +20,11 @@ package com.juick.www.api; import com.juick.model.Message; import com.juick.model.User; import com.juick.model.CommandResult; -import com.juick.server.CommandsManager; -import com.juick.server.EmailManager; -import com.juick.server.ServerManager; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpForbiddenException; +import com.juick.CommandsManager; +import com.juick.EmailManager; +import com.juick.ServerManager; +import com.juick.util.HttpBadRequestException; +import com.juick.util.HttpForbiddenException; import com.juick.service.EmailService; import com.juick.service.MessagesService; import com.juick.service.UserService; diff --git a/src/main/java/com/juick/www/api/Users.java b/src/main/java/com/juick/www/api/Users.java index 06467b7d..f4c3a4c1 100644 --- a/src/main/java/com/juick/www/api/Users.java +++ b/src/main/java/com/juick/www/api/Users.java @@ -20,10 +20,10 @@ package com.juick.www.api; import com.juick.model.User; import com.juick.model.AnonymousUser; import com.juick.model.ApplicationStatus; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpNotFoundException; -import com.juick.server.util.HttpUtils; -import com.juick.server.util.WebUtils; +import com.juick.util.HttpBadRequestException; +import com.juick.util.HttpNotFoundException; +import com.juick.util.HttpUtils; +import com.juick.util.WebUtils; import com.juick.www.WebApp; import com.juick.service.*; import com.juick.service.component.MailVerificationEvent; diff --git a/src/main/java/com/juick/www/api/activity/Profile.java b/src/main/java/com/juick/www/api/activity/Profile.java index bdd7cab2..ef9b342f 100644 --- a/src/main/java/com/juick/www/api/activity/Profile.java +++ b/src/main/java/com/juick/www/api/activity/Profile.java @@ -20,12 +20,12 @@ package com.juick.www.api.activity; import com.fasterxml.jackson.databind.ObjectMapper; import com.juick.model.Message; import com.juick.model.User; -import com.juick.formatters.PlainTextFormatter; +import com.juick.util.formatters.PlainTextFormatter; import com.juick.model.CommandResult; -import com.juick.server.ActivityPubManager; -import com.juick.server.CommandsManager; -import com.juick.server.KeystoreManager; -import com.juick.server.SignatureManager; +import com.juick.ActivityPubManager; +import com.juick.CommandsManager; +import com.juick.KeystoreManager; +import com.juick.SignatureManager; import com.juick.www.api.activity.model.Activity; import com.juick.www.api.activity.model.Context; import com.juick.www.api.activity.model.activities.Announce; @@ -40,7 +40,7 @@ import com.juick.www.api.activity.model.objects.Note; import com.juick.www.api.activity.model.objects.OrderedCollection; import com.juick.www.api.activity.model.objects.OrderedCollectionPage; import com.juick.www.api.activity.model.objects.Person; -import com.juick.server.util.HttpNotFoundException; +import com.juick.util.HttpNotFoundException; import com.juick.www.WebApp; import com.juick.service.MessagesService; import com.juick.service.UserService; diff --git a/src/main/java/com/juick/www/api/webfinger/Resource.java b/src/main/java/com/juick/www/api/webfinger/Resource.java index 1529ea09..1e8b45e5 100644 --- a/src/main/java/com/juick/www/api/webfinger/Resource.java +++ b/src/main/java/com/juick/www/api/webfinger/Resource.java @@ -20,7 +20,7 @@ package com.juick.www.api.webfinger; import com.juick.model.User; import com.juick.www.api.webfinger.model.Account; import com.juick.www.api.webfinger.model.Link; -import com.juick.server.util.HttpNotFoundException; +import com.juick.util.HttpNotFoundException; import com.juick.service.UserService; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/juick/www/api/webhooks/TelegramWebhook.java b/src/main/java/com/juick/www/api/webhooks/TelegramWebhook.java index 32218b3a..8090f388 100644 --- a/src/main/java/com/juick/www/api/webhooks/TelegramWebhook.java +++ b/src/main/java/com/juick/www/api/webhooks/TelegramWebhook.java @@ -17,7 +17,7 @@ package com.juick.www.api.webhooks; -import com.juick.server.TelegramBotManager; +import com.juick.TelegramBotManager; import com.pengrad.telegrambot.BotUtils; import com.pengrad.telegrambot.model.Update; import org.apache.commons.io.IOUtils; diff --git a/src/main/java/com/juick/www/controllers/Help.java b/src/main/java/com/juick/www/controllers/Help.java index 7fc84060..f65e254a 100644 --- a/src/main/java/com/juick/www/controllers/Help.java +++ b/src/main/java/com/juick/www/controllers/Help.java @@ -18,7 +18,7 @@ package com.juick.www.controllers; import com.juick.model.User; -import com.juick.server.util.HttpNotFoundException; +import com.juick.util.HttpNotFoundException; import com.juick.service.HelpService; import com.juick.service.security.annotation.Visitor; import com.juick.www.WebApp; diff --git a/src/main/java/com/juick/www/controllers/Settings.java b/src/main/java/com/juick/www/controllers/Settings.java index 851e576b..69ddcd2f 100644 --- a/src/main/java/com/juick/www/controllers/Settings.java +++ b/src/main/java/com/juick/www/controllers/Settings.java @@ -18,8 +18,8 @@ package com.juick.www.controllers; import com.juick.model.User; import com.juick.model.NotifyOpts; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpUtils; +import com.juick.util.HttpBadRequestException; +import com.juick.util.HttpUtils; import com.juick.www.WebApp; import com.juick.service.*; import com.juick.service.security.annotation.Visitor; diff --git a/src/main/java/com/juick/www/controllers/SignUp.java b/src/main/java/com/juick/www/controllers/SignUp.java index 4e74d4c4..3b052e18 100644 --- a/src/main/java/com/juick/www/controllers/SignUp.java +++ b/src/main/java/com/juick/www/controllers/SignUp.java @@ -17,8 +17,8 @@ package com.juick.www.controllers; import com.juick.model.User; -import com.juick.server.util.HttpBadRequestException; -import com.juick.server.util.HttpForbiddenException; +import com.juick.util.HttpBadRequestException; +import com.juick.util.HttpForbiddenException; import com.juick.www.WebApp; import com.juick.service.CrosspostService; import com.juick.service.EmailService; diff --git a/src/main/java/com/juick/www/controllers/Site.java b/src/main/java/com/juick/www/controllers/Site.java index 61b84f71..a8cbc9bd 100644 --- a/src/main/java/com/juick/www/controllers/Site.java +++ b/src/main/java/com/juick/www/controllers/Site.java @@ -19,11 +19,10 @@ package com.juick.www.controllers; import com.juick.model.Message; import com.juick.model.Tag; import com.juick.model.User; -import com.juick.formatters.PlainTextFormatter; -import com.juick.server.Utils; -import com.juick.server.util.HttpForbiddenException; -import com.juick.server.util.HttpNotFoundException; -import com.juick.server.util.WebUtils; +import com.juick.util.formatters.PlainTextFormatter; +import com.juick.util.HttpForbiddenException; +import com.juick.util.HttpNotFoundException; +import com.juick.util.WebUtils; import com.juick.www.WebApp; import com.juick.service.*; import com.juick.service.security.annotation.Visitor; @@ -109,7 +108,7 @@ public class Site { if (paramSearch != null) { title = "Поиск: " + StringEscapeUtils.escapeHtml4(paramSearch); - mids = messagesService.getSearch(visitor, Utils.encodeSphinx(paramSearch), page); + mids = messagesService.getSearch(visitor, WebUtils.encodeSphinx(paramSearch), page); } else if (paramShow == null) { title = "Обсуждения"; mids = messagesService.getDiscussions(visitor.getUid(), paramTo); @@ -228,7 +227,7 @@ public class Site { mids = messagesService.getUserTag(user.getUid(), paramTag.TID, privacy, before); } else if (paramSearch != null) { title = "Блог " + user.getName() + ": " + StringEscapeUtils.escapeHtml4(paramSearch); - mids = messagesService.getUserSearch(visitor, user.getUid(), Utils.encodeSphinx(paramSearch), privacy, page); + mids = messagesService.getUserSearch(visitor, user.getUid(), WebUtils.encodeSphinx(paramSearch), privacy, page); } else { title = "Блог " + user.getName(); mids = messagesService.getUserBlog(user.getUid(), privacy, before); diff --git a/src/main/java/com/juick/www/controllers/SocialLogin.java b/src/main/java/com/juick/www/controllers/SocialLogin.java index 2bd89587..ffd785b7 100644 --- a/src/main/java/com/juick/www/controllers/SocialLogin.java +++ b/src/main/java/com/juick/www/controllers/SocialLogin.java @@ -24,13 +24,13 @@ import com.github.scribejava.core.oauth.OAuth10aService; import com.github.scribejava.core.oauth.OAuth20Service; import com.juick.model.ext.facebook.User; import com.juick.model.ext.vk.UsersResponse; -import com.juick.server.Utils; -import com.juick.server.util.HttpBadRequestException; +import com.juick.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.service.security.annotation.Visitor; +import com.juick.util.WebUtils; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.proc.BadJOSEException; import org.apache.commons.codec.digest.DigestUtils; @@ -140,7 +140,7 @@ public class SocialLogin { if (StringUtils.isBlank(code)) { String fbstate = UUID.randomUUID().toString(); if (StringUtils.isBlank(state)) { - state = Utils.getPreviousPageByRequest(request).orElse("https://juick.com/"); + state = WebUtils.getPreviousPageByRequest(request).orElse("https://juick.com/"); } crosspostService.addFacebookState(fbstate, state); return "redirect:" + facebookAuthService.getAuthorizationUrl(fbstate); @@ -282,7 +282,7 @@ public class SocialLogin { Cookie c = new Cookie("hash", userService.getHashByUID(uid)); c.setMaxAge(50 * 24 * 60 * 60); response.addCookie(c); - return "redirect:/" + Utils.getPreviousPageByRequest(request).orElse(StringUtils.EMPTY); + return "redirect:/" + WebUtils.getPreviousPageByRequest(request).orElse(StringUtils.EMPTY); } else { String loginhash = UUID.randomUUID().toString(); if (!crosspostService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) { @@ -312,7 +312,7 @@ public class SocialLogin { Cookie c = new Cookie("hash", userService.getHashByUID(uid)); c.setMaxAge(50 * 24 * 60 * 60); response.addCookie(c); - return "redirect:/" + Utils.getPreviousPageByRequest(request).orElse(StringUtils.EMPTY); + return "redirect:/" + WebUtils.getPreviousPageByRequest(request).orElse(StringUtils.EMPTY); } else { String username = StringUtils.defaultString(params.get("username"), params.get("first_name")); List chats = telegramService.getAnonymous(); diff --git a/src/main/java/com/juick/www/filters/AnythingFilter.java b/src/main/java/com/juick/www/filters/AnythingFilter.java index 8f9af7f6..461a2888 100644 --- a/src/main/java/com/juick/www/filters/AnythingFilter.java +++ b/src/main/java/com/juick/www/filters/AnythingFilter.java @@ -18,7 +18,7 @@ package com.juick.www.filters; import com.juick.model.User; -import com.juick.server.util.WebUtils; +import com.juick.util.WebUtils; import com.juick.service.MessagesService; import com.juick.service.UserService; import org.apache.commons.lang3.math.NumberUtils; diff --git a/src/main/java/com/juick/www/rss/Feeds.java b/src/main/java/com/juick/www/rss/Feeds.java index 42cadc5d..1817301e 100644 --- a/src/main/java/com/juick/www/rss/Feeds.java +++ b/src/main/java/com/juick/www/rss/Feeds.java @@ -18,7 +18,7 @@ package com.juick.www.rss; import com.juick.model.User; -import com.juick.server.util.HttpNotFoundException; +import com.juick.util.HttpNotFoundException; import com.juick.service.MessagesService; import com.juick.service.UserService; import com.juick.service.security.annotation.Visitor; diff --git a/src/test/java/com/juick/config/DataSourceAutoConfiguration.java b/src/test/java/com/juick/config/DataSourceAutoConfiguration.java new file mode 100644 index 00000000..b30de7fc --- /dev/null +++ b/src/test/java/com/juick/config/DataSourceAutoConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.config; + +import ch.vorburger.mariadb4j.springframework.MariaDB4jSpringService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; + +import javax.sql.DataSource; + +@Configuration +@ConfigurationProperties("spring.datasource") +@ConditionalOnProperty(value = "spring.datasource.platform", havingValue = "mysql") +public class DataSourceAutoConfiguration { + + @Bean + public MariaDB4jSpringService mariaDB4j() { + return new MariaDB4jSpringService(); + } + @Bean + @DependsOn("mariaDB4j") + public DataSource dataSource(DataSourceProperties dataSourceProperties) { + return DataSourceBuilder.create() + .driverClassName(dataSourceProperties.getDriverClassName()) + .url(dataSourceProperties.getUrl()) + .username(dataSourceProperties.getUsername()) + .password(dataSourceProperties.getPassword()) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/com/juick/config/SwaggerConfiguration.java b/src/test/java/com/juick/config/SwaggerConfiguration.java new file mode 100644 index 00000000..f92ef6c0 --- /dev/null +++ b/src/test/java/com/juick/config/SwaggerConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.google.common.base.Predicates; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.Collections; + +@Configuration +@EnableSwagger2 +public class SwaggerConfiguration { + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2) + .host("api.juick.com") + .select() + .apis(Predicates.not(Predicates.or(RequestHandlerSelectors.basePackage("org.springframework.boot"), RequestHandlerSelectors.basePackage("com.juick.server.www")))) + .paths(PathSelectors.any()).build().apiInfo(new ApiInfo("Juick API", "Juick REST API Documentation", + "2.0", "https://juick.com/help/tos", null, + "AGPLv3", "https://www.gnu.org/licenses/agpl-3.0.html", Collections.emptyList())); + } +} diff --git a/src/test/java/com/juick/config/TestActivityConfiguration.java b/src/test/java/com/juick/config/TestActivityConfiguration.java new file mode 100644 index 00000000..94b96bdf --- /dev/null +++ b/src/test/java/com/juick/config/TestActivityConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.config; + +import com.juick.KeystoreManager; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; + +import java.io.IOException; + +@Configuration +public class TestActivityConfiguration { + @Value("classpath:test.p12") + Resource keystoreFile; + @Bean + public KeystoreManager testKeystoreManager() throws IOException { + return new KeystoreManager(keystoreFile, "secret"); + } +} diff --git a/src/test/java/com/juick/server/configuration/DataSourceAutoConfiguration.java b/src/test/java/com/juick/server/configuration/DataSourceAutoConfiguration.java deleted file mode 100644 index bd0a3124..00000000 --- a/src/test/java/com/juick/server/configuration/DataSourceAutoConfiguration.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.juick.server.configuration; - -import ch.vorburger.mariadb4j.springframework.MariaDB4jSpringService; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.DependsOn; - -import javax.sql.DataSource; - -@Configuration -@ConfigurationProperties("spring.datasource") -@ConditionalOnProperty(value = "spring.datasource.platform", havingValue = "mysql") -public class DataSourceAutoConfiguration { - - @Bean - public MariaDB4jSpringService mariaDB4j() { - return new MariaDB4jSpringService(); - } - @Bean - @DependsOn("mariaDB4j") - public DataSource dataSource(DataSourceProperties dataSourceProperties) { - return DataSourceBuilder.create() - .driverClassName(dataSourceProperties.getDriverClassName()) - .url(dataSourceProperties.getUrl()) - .username(dataSourceProperties.getUsername()) - .password(dataSourceProperties.getPassword()) - .build(); - } -} \ No newline at end of file diff --git a/src/test/java/com/juick/server/configuration/SwaggerConfiguration.java b/src/test/java/com/juick/server/configuration/SwaggerConfiguration.java deleted file mode 100644 index ef63c69e..00000000 --- a/src/test/java/com/juick/server/configuration/SwaggerConfiguration.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.google.common.base.Predicates; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; - -import java.util.Collections; - -@Configuration -@EnableSwagger2 -public class SwaggerConfiguration { - @Bean - public Docket api() { - return new Docket(DocumentationType.SWAGGER_2) - .host("api.juick.com") - .select() - .apis(Predicates.not(Predicates.or(RequestHandlerSelectors.basePackage("org.springframework.boot"), RequestHandlerSelectors.basePackage("com.juick.server.www")))) - .paths(PathSelectors.any()).build().apiInfo(new ApiInfo("Juick API", "Juick REST API Documentation", - "2.0", "https://juick.com/help/tos", null, - "AGPLv3", "https://www.gnu.org/licenses/agpl-3.0.html", Collections.emptyList())); - } -} diff --git a/src/test/java/com/juick/server/configuration/TestActivityConfiguration.java b/src/test/java/com/juick/server/configuration/TestActivityConfiguration.java deleted file mode 100644 index 7c1aa8bf..00000000 --- a/src/test/java/com/juick/server/configuration/TestActivityConfiguration.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.juick.server.KeystoreManager; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.Resource; - -import java.io.IOException; - -@Configuration -public class TestActivityConfiguration { - @Value("classpath:test.p12") - Resource keystoreFile; - @Bean - public KeystoreManager testKeystoreManager() throws IOException { - return new KeystoreManager(keystoreFile, "secret"); - } -} diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index 80c8fe4e..51e17ecc 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -29,7 +29,9 @@ import com.gargoylesoftware.htmlunit.html.DomElement; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.github.scribejava.apis.AppleClientSecretGenerator; import com.jayway.jsonpath.JsonPath; -import com.juick.formatters.PlainTextFormatter; +import com.juick.*; +import com.juick.util.*; +import com.juick.util.formatters.PlainTextFormatter; import com.juick.model.*; import com.juick.server.*; import com.juick.www.api.SystemActivity; @@ -41,15 +43,11 @@ import com.juick.www.api.activity.model.objects.Note; import com.juick.www.api.activity.model.objects.Person; import com.juick.www.api.webfinger.model.Account; import com.juick.www.api.xnodeinfo2.model.NodeInfo; -import com.juick.server.util.HttpUtils; -import com.juick.server.util.ImageUtils; import com.juick.www.WebApp; import com.juick.service.*; import com.juick.service.activities.UpdateEvent; import com.juick.service.component.SystemEvent; import com.juick.test.util.MockUtils; -import com.juick.util.DateFormattersHolder; -import com.juick.util.MessageUtils; import com.mitchellbosecke.pebble.PebbleEngine; import com.mitchellbosecke.pebble.error.PebbleException; import com.mitchellbosecke.pebble.template.PebbleTemplate; @@ -1782,7 +1780,7 @@ public class ServerTests { @Test public void escapeSqlTests() { - String sql = String.format("SELECT * FROM table WHERE data='%s'", Utils.encodeSphinx("';-- DROP TABLE table")); + String sql = String.format("SELECT * FROM table WHERE data='%s'", WebUtils.encodeSphinx("';-- DROP TABLE table")); assertThat(sql, is("SELECT * FROM table WHERE data='\\';-- DROP TABLE table\'")); } -- cgit v1.2.3