From 7aaa3f9a29c280f01c677c918932620be45cdbd7 Mon Sep 17 00:00:00 2001
From: Vitaly Takmazov
Date: Thu, 8 Nov 2018 21:38:27 +0300
Subject: Merge everything into single Spring Boot application
---
src/main/assets/embed.js | 334 +++++
src/main/assets/logo.png | Bin 0 -> 2447 bytes
src/main/assets/logo@2x.png | Bin 0 -> 4822 bytes
src/main/assets/scripts.js | 935 ++++++++++++
src/main/assets/style.css | 956 ++++++++++++
src/main/java/com/cliqset/xrd/Alias.java | 62 +
src/main/java/com/cliqset/xrd/Expires.java | 58 +
src/main/java/com/cliqset/xrd/Link.java | 151 ++
src/main/java/com/cliqset/xrd/Property.java | 75 +
src/main/java/com/cliqset/xrd/Signature.java | 21 +
src/main/java/com/cliqset/xrd/Subject.java | 62 +
src/main/java/com/cliqset/xrd/Title.java | 68 +
src/main/java/com/cliqset/xrd/XRD.java | 166 +++
src/main/java/com/cliqset/xrd/XRDConstants.java | 26 +
src/main/java/com/cliqset/xrd/XRDException.java | 35 +
src/main/java/com/cliqset/xrd/package-info.java | 33 +
src/main/java/com/juick/ApiServer.java | 16 +
src/main/java/com/juick/Attachment.java | 58 +
src/main/java/com/juick/Chat.java | 27 +
src/main/java/com/juick/ExternalToken.java | 64 +
src/main/java/com/juick/Message.java | 378 +++++
src/main/java/com/juick/Photo.java | 53 +
src/main/java/com/juick/Reaction.java | 45 +
src/main/java/com/juick/Status.java | 49 +
src/main/java/com/juick/Tag.java | 73 +
src/main/java/com/juick/User.java | 226 +++
.../java/com/juick/adapters/SimpleDateAdapter.java | 40 +
.../com/juick/formatters/PlainTextFormatter.java | 97 ++
src/main/java/com/juick/model/AnonymousUser.java | 129 ++
.../java/com/juick/model/ApplicationStatus.java | 52 +
src/main/java/com/juick/model/Auth.java | 39 +
src/main/java/com/juick/model/CommandResult.java | 35 +
src/main/java/com/juick/model/NotifyOpts.java | 51 +
src/main/java/com/juick/model/PrivacyOpts.java | 46 +
src/main/java/com/juick/model/PrivateChats.java | 39 +
src/main/java/com/juick/model/ResponseReply.java | 98 ++
src/main/java/com/juick/model/TagStats.java | 46 +
src/main/java/com/juick/model/UserInfo.java | 60 +
src/main/java/com/juick/model/facebook/User.java | 125 ++
src/main/java/com/juick/model/twitter/User.java | 38 +
src/main/java/com/juick/model/vk/Token.java | 56 +
src/main/java/com/juick/model/vk/User.java | 65 +
.../java/com/juick/model/vk/UsersResponse.java | 38 +
src/main/java/com/juick/package-info.java | 35 +
.../java/com/juick/server/ActivityPubManager.java | 331 +++++
.../java/com/juick/server/CommandsManager.java | 540 +++++++
src/main/java/com/juick/server/EmailManager.java | 165 +++
.../java/com/juick/server/KeystoreManager.java | 92 ++
src/main/java/com/juick/server/ServerManager.java | 295 ++++
.../java/com/juick/server/SignatureManager.java | 113 ++
.../java/com/juick/server/TelegramBotManager.java | 412 ++++++
src/main/java/com/juick/server/TopManager.java | 54 +
src/main/java/com/juick/server/TwitterManager.java | 125 ++
src/main/java/com/juick/server/Utils.java | 45 +
.../java/com/juick/server/WebsocketManager.java | 174 +++
src/main/java/com/juick/server/XMPPConnection.java | 693 +++++++++
src/main/java/com/juick/server/XMPPServer.java | 429 ++++++
.../java/com/juick/server/api/ApiSocialLogin.java | 302 ++++
src/main/java/com/juick/server/api/Index.java | 54 +
src/main/java/com/juick/server/api/Messages.java | 201 +++
.../java/com/juick/server/api/Notifications.java | 221 +++
src/main/java/com/juick/server/api/PM.java | 116 ++
src/main/java/com/juick/server/api/Post.java | 245 ++++
src/main/java/com/juick/server/api/Service.java | 166 +++
src/main/java/com/juick/server/api/Tags.java | 54 +
src/main/java/com/juick/server/api/Users.java | 179 +++
.../com/juick/server/api/activity/Profile.java | 379 +++++
.../juick/server/api/activity/model/Activity.java | 23 +
.../juick/server/api/activity/model/Context.java | 123 ++
.../api/activity/model/activities/Accept.java | 6 +
.../api/activity/model/activities/Announce.java | 6 +
.../api/activity/model/activities/Block.java | 6 +
.../api/activity/model/activities/Create.java | 6 +
.../api/activity/model/activities/Delete.java | 6 +
.../api/activity/model/activities/Follow.java | 6 +
.../server/api/activity/model/activities/Like.java | 6 +
.../server/api/activity/model/activities/Undo.java | 6 +
.../server/api/activity/model/objects/Hashtag.java | 6 +
.../server/api/activity/model/objects/Image.java | 15 +
.../server/api/activity/model/objects/Key.java | 24 +
.../server/api/activity/model/objects/Link.java | 15 +
.../server/api/activity/model/objects/Mention.java | 12 +
.../server/api/activity/model/objects/Note.java | 64 +
.../activity/model/objects/OrderedCollection.java | 25 +
.../model/objects/OrderedCollectionPage.java | 58 +
.../server/api/activity/model/objects/Person.java | 87 ++
.../juick/server/api/apple/AppSiteAssociation.java | 49 +
.../com/juick/server/api/hostmeta/HostMeta.java | 25 +
src/main/java/com/juick/server/api/rss/Feeds.java | 75 +
.../com/juick/server/api/rss/MessagesView.java | 153 ++
.../java/com/juick/server/api/rss/RepliesView.java | 111 ++
.../server/api/rss/extension/JuickModule.java | 33 +
.../api/rss/extension/JuickModuleGenerator.java | 70 +
.../server/api/rss/extension/JuickModuleImpl.java | 54 +
.../api/rss/extension/JuickModuleParser.java | 42 +
.../com/juick/server/api/webfinger/Resource.java | 51 +
.../juick/server/api/webfinger/model/Account.java | 24 +
.../com/juick/server/api/webfinger/model/Link.java | 31 +
.../juick/server/api/webhooks/TelegramWebhook.java | 57 +
.../java/com/juick/server/api/xnodeinfo2/Info.java | 51 +
.../server/api/xnodeinfo2/model/NodeInfo.java | 54 +
.../juick/server/api/xnodeinfo2/model/Server.java | 40 +
.../server/api/xnodeinfo2/model/ServiceInfo.java | 24 +
.../juick/server/api/xnodeinfo2/model/Usage.java | 31 +
.../server/api/xnodeinfo2/model/UserStats.java | 31 +
.../configuration/ActivityPubClientConfig.java | 22 +
.../ActivityPubClientErrorHandler.java | 35 +
.../server/configuration/ApiAppConfiguration.java | 76 +
.../server/configuration/BaseWebConfiguration.java | 63 +
.../server/configuration/SapeConfiguration.java | 39 +
.../juick/server/configuration/SecurityConfig.java | 215 +++
.../server/configuration/StorageConfiguration.java | 20 +
.../juick/server/configuration/TelegramConfig.java | 15 +
.../server/configuration/WwwAppConfiguration.java | 120 ++
.../com/juick/server/configuration/XMPPConfig.java | 55 +
.../server/helpers/annotation/UserCommand.java | 50 +
.../juick/server/util/HttpBadRequestException.java | 32 +
.../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 | 175 +++
src/main/java/com/juick/server/util/TagUtils.java | 42 +
src/main/java/com/juick/server/util/UserUtils.java | 55 +
src/main/java/com/juick/server/util/WebUtils.java | 62 +
.../java/com/juick/server/www/HelpService.java | 69 +
src/main/java/com/juick/server/www/WebApp.java | 71 +
.../server/www/controllers/AnythingFilter.java | 69 +
.../com/juick/server/www/controllers/Help.java | 93 ++
.../com/juick/server/www/controllers/Login.java | 50 +
.../juick/server/www/controllers/MessagesWWW.java | 593 ++++++++
.../juick/server/www/controllers/NewMessage.java | 59 +
.../com/juick/server/www/controllers/Settings.java | 278 ++++
.../com/juick/server/www/controllers/SignUp.java | 172 +++
.../juick/server/www/controllers/SocialLogin.java | 329 +++++
.../java/com/juick/server/xmpp/JidConverter.java | 13 +
.../java/com/juick/server/xmpp/XMPPStatusPage.java | 32 +
.../com/juick/server/xmpp/helpers/XMPPStatus.java | 48 +
.../com/juick/server/xmpp/iq/MessageQuery.java | 10 +
.../com/juick/server/xmpp/iq/package-info.java | 8 +
.../com/juick/server/xmpp/router/Handshake.java | 39 +
.../java/com/juick/server/xmpp/router/Stream.java | 202 +++
.../server/xmpp/router/StreamComponentServer.java | 57 +
.../com/juick/server/xmpp/router/StreamError.java | 57 +
.../juick/server/xmpp/router/StreamFeatures.java | 95 ++
.../juick/server/xmpp/router/StreamHandler.java | 13 +
.../juick/server/xmpp/router/StreamNamespaces.java | 10 +
.../com/juick/server/xmpp/router/XMPPError.java | 73 +
.../com/juick/server/xmpp/router/XMPPRouter.java | 220 +++
.../com/juick/server/xmpp/router/XmlUtils.java | 88 ++
.../juick/server/xmpp/s2s/BasicXmppSession.java | 68 +
.../java/com/juick/server/xmpp/s2s/CacheEntry.java | 40 +
.../java/com/juick/server/xmpp/s2s/Connection.java | 158 ++
.../com/juick/server/xmpp/s2s/ConnectionIn.java | 231 +++
.../juick/server/xmpp/s2s/ConnectionListener.java | 16 +
.../com/juick/server/xmpp/s2s/ConnectionOut.java | 189 +++
.../java/com/juick/server/xmpp/s2s/DNSQueries.java | 65 +
.../com/juick/server/xmpp/s2s/StanzaListener.java | 28 +
.../juick/server/xmpp/s2s/util/DialbackUtils.java | 37 +
.../java/com/juick/service/ActivityPubService.java | 59 +
.../java/com/juick/service/BaseJdbcService.java | 41 +
.../java/com/juick/service/CrosspostService.java | 86 ++
.../com/juick/service/CrosspostServiceImpl.java | 282 ++++
src/main/java/com/juick/service/EmailService.java | 35 +
.../java/com/juick/service/EmailServiceImpl.java | 108 ++
src/main/java/com/juick/service/ImagesService.java | 24 +
.../java/com/juick/service/ImagesServiceImpl.java | 82 ++
.../java/com/juick/service/MessagesService.java | 145 ++
.../com/juick/service/MessagesServiceImpl.java | 1143 +++++++++++++++
.../java/com/juick/service/MessengerService.java | 14 +
.../com/juick/service/MessengerServiceImpl.java | 71 +
.../java/com/juick/service/PMQueriesService.java | 44 +
.../com/juick/service/PMQueriesServiceImpl.java | 149 ++
.../com/juick/service/PrivacyQueriesService.java | 34 +
.../juick/service/PrivacyQueriesServiceImpl.java | 63 +
.../java/com/juick/service/PushQueriesService.java | 50 +
.../com/juick/service/PushQueriesServiceImpl.java | 143 ++
src/main/java/com/juick/service/SearchService.java | 33 +
.../java/com/juick/service/ShowQueriesService.java | 31 +
.../com/juick/service/ShowQueriesServiceImpl.java | 62 +
src/main/java/com/juick/service/SocialService.java | 16 +
.../com/juick/service/SphinxSearchService.java | 97 ++
.../com/juick/service/SubscriptionService.java | 57 +
.../com/juick/service/SubscriptionServiceImpl.java | 229 +++
src/main/java/com/juick/service/TagService.java | 64 +
.../java/com/juick/service/TagServiceImpl.java | 277 ++++
.../java/com/juick/service/TelegramService.java | 40 +
.../com/juick/service/TelegramServiceImpl.java | 84 ++
src/main/java/com/juick/service/UserService.java | 137 ++
.../java/com/juick/service/UserServiceImpl.java | 668 +++++++++
.../juick/service/activities/ActivityListener.java | 19 +
.../service/activities/DeleteMessageEvent.java | 21 +
.../juick/service/activities/DeleteUserEvent.java | 20 +
.../com/juick/service/activities/FollowEvent.java | 21 +
.../juick/service/activities/UndoFollowEvent.java | 26 +
.../juick/service/component/DisconnectedEvent.java | 14 +
.../com/juick/service/component/LikeEvent.java | 36 +
.../com/juick/service/component/MessageEvent.java | 31 +
.../juick/service/component/MessageReadEvent.java | 30 +
.../service/component/NotificationListener.java | 25 +
.../com/juick/service/component/PingEvent.java | 21 +
.../juick/service/component/SubscribeEvent.java | 27 +
.../java/com/juick/service/component/TopEvent.java | 21 +
.../juick/service/component/UserUpdatedEvent.java | 23 +
.../security/HashParamAuthenticationFilter.java | 103 ++
.../service/security/JuickUserDetailsService.java | 53 +
.../service/security/NullUserDetailsService.java | 33 +
.../CookieSimpleHashRememberMeServices.java | 130 ++
.../RequestParamHashRememberMeServices.java | 88 ++
.../juick/service/security/entities/JuickUser.java | 93 ++
src/main/java/com/juick/util/DateFormatter.java | 57 +
.../java/com/juick/util/DateFormattersHolder.java | 75 +
src/main/java/com/juick/util/MessageUtils.java | 324 +++++
.../java/com/juick/util/PrettyTimeFormatter.java | 53 +
src/main/java/com/juick/util/StreamUtils.java | 38 +
.../pebble/extension/FormatterExtension.java | 38 +
.../extension/filters/FormatMessageFilter.java | 65 +
.../pebble/extension/filters/PrettyTimeFilter.java | 51 +
.../pebble/extension/filters/TagsListFilter.java | 43 +
.../pebble/extension/filters/TimestampFilter.java | 25 +
.../xmpp/core/session/debug/LogbackDebugger.java | 57 +
src/main/java/ru/sape/Sape.java | 23 +
src/main/java/ru/sape/SapeConnection.java | 108 ++
src/main/java/ru/sape/SapePageLinks.java | 76 +
src/main/resources/1x1.png | Bin 0 -> 95 bytes
src/main/resources/Transparent.gif | Bin 0 -> 42 bytes
.../db/migration/V1.10__favorites_user_uri.sql | 3 +
.../V1.11__increase pm timestamp precision.sql | 1 +
.../db/migration/V1.12__drop unused tables.sql | 5 +
.../db/migration/V1.13__drop unused tables.sql | 5 +
.../db/migration/V1.14__drop broken pm_streams.sql | 1 +
..._drop unused columns add ts for some tables.sql | 4 +
.../resources/db/migration/V1.16__last seen.sql | 2 +
.../db/migration/V1.1__Add updated_at field.sql | 2 +
.../db/migration/V1.2__Drop telegram_chats.sql | 2 +
...V1.3__Nullable user_id column in auth table.sql | 1 +
.../db/migration/V1.4__ActivityPub followers.sql | 7 +
.../db/migration/V1.5__Drop acct index.sql | 6 +
src/main/resources/db/migration/V1.6__user_uri.sql | 1 +
.../resources/db/migration/V1.7__reply_uri.sql | 1 +
.../resources/db/migration/V1.8__html reply.sql | 1 +
.../db/migration/V1.9__reply_uri_index.sql | 1 +
src/main/resources/errors.properties | 3 +
src/main/resources/errors_ru.properties | 3 +
src/main/resources/help | 1 +
src/main/resources/juick.png | Bin 0 -> 4298 bytes
src/main/resources/juick.sql | 947 ++++++++++++
src/main/resources/messages.properties | 80 +
src/main/resources/messages_ru.properties | 78 +
src/main/resources/pg_schema_wip | 1539 ++++++++++++++++++++
src/main/resources/rome.properties | 2 +
src/main/resources/schema.sql | 396 +++++
src/main/resources/static/favicon.png | Bin 0 -> 244 bytes
src/main/resources/static/logo.png | Bin 0 -> 1184 bytes
src/main/resources/static/tagscloud.png | Bin 0 -> 42316 bytes
src/main/resources/templates/layouts/content.html | 38 +
src/main/resources/templates/layouts/default.html | 16 +
src/main/resources/templates/layouts/minimal.html | 10 +
src/main/resources/templates/layouts/note.html | 5 +
src/main/resources/templates/views/404.html | 11 +
src/main/resources/templates/views/blog.html | 24 +
src/main/resources/templates/views/blog_tags.html | 10 +
src/main/resources/templates/views/help.html | 10 +
src/main/resources/templates/views/index.html | 29 +
src/main/resources/templates/views/login.html | 144 ++
.../resources/templates/views/login_success.html | 13 +
.../resources/templates/views/macros/tags.html | 11 +
.../resources/templates/views/partial/footer.html | 16 +
.../templates/views/partial/homecolumn.html | 25 +
.../resources/templates/views/partial/message.html | 76 +
.../templates/views/partial/navigation.html | 36 +
.../templates/views/partial/settings_tabs.html | 6 +
.../templates/views/partial/tagcolumn.html | 33 +
.../resources/templates/views/partial/tags.html | 3 +
.../templates/views/partial/usercolumn.html | 89 ++
.../templates/views/partial/usertags.html | 3 +
src/main/resources/templates/views/pm_inbox.html | 35 +
src/main/resources/templates/views/pm_sent.html | 33 +
src/main/resources/templates/views/post.html | 19 +
.../resources/templates/views/post_success.html | 19 +
.../resources/templates/views/settings_about.html | 20 +
.../templates/views/settings_auth-email.html | 9 +
.../resources/templates/views/settings_main.html | 151 ++
.../templates/views/settings_password.html | 17 +
.../templates/views/settings_privacy.html | 9 +
.../resources/templates/views/settings_result.html | 9 +
src/main/resources/templates/views/signup.html | 43 +
src/main/resources/templates/views/thread.html | 175 +++
src/main/resources/templates/views/users.html | 17 +
288 files changed, 27279 insertions(+)
create mode 100644 src/main/assets/embed.js
create mode 100644 src/main/assets/logo.png
create mode 100644 src/main/assets/logo@2x.png
create mode 100644 src/main/assets/scripts.js
create mode 100644 src/main/assets/style.css
create mode 100644 src/main/java/com/cliqset/xrd/Alias.java
create mode 100644 src/main/java/com/cliqset/xrd/Expires.java
create mode 100644 src/main/java/com/cliqset/xrd/Link.java
create mode 100644 src/main/java/com/cliqset/xrd/Property.java
create mode 100644 src/main/java/com/cliqset/xrd/Signature.java
create mode 100644 src/main/java/com/cliqset/xrd/Subject.java
create mode 100644 src/main/java/com/cliqset/xrd/Title.java
create mode 100644 src/main/java/com/cliqset/xrd/XRD.java
create mode 100644 src/main/java/com/cliqset/xrd/XRDConstants.java
create mode 100644 src/main/java/com/cliqset/xrd/XRDException.java
create mode 100644 src/main/java/com/cliqset/xrd/package-info.java
create mode 100644 src/main/java/com/juick/ApiServer.java
create mode 100644 src/main/java/com/juick/Attachment.java
create mode 100644 src/main/java/com/juick/Chat.java
create mode 100644 src/main/java/com/juick/ExternalToken.java
create mode 100644 src/main/java/com/juick/Message.java
create mode 100644 src/main/java/com/juick/Photo.java
create mode 100644 src/main/java/com/juick/Reaction.java
create mode 100644 src/main/java/com/juick/Status.java
create mode 100644 src/main/java/com/juick/Tag.java
create mode 100644 src/main/java/com/juick/User.java
create mode 100644 src/main/java/com/juick/adapters/SimpleDateAdapter.java
create mode 100644 src/main/java/com/juick/formatters/PlainTextFormatter.java
create mode 100644 src/main/java/com/juick/model/AnonymousUser.java
create mode 100644 src/main/java/com/juick/model/ApplicationStatus.java
create mode 100644 src/main/java/com/juick/model/Auth.java
create mode 100644 src/main/java/com/juick/model/CommandResult.java
create mode 100644 src/main/java/com/juick/model/NotifyOpts.java
create mode 100644 src/main/java/com/juick/model/PrivacyOpts.java
create mode 100644 src/main/java/com/juick/model/PrivateChats.java
create mode 100644 src/main/java/com/juick/model/ResponseReply.java
create mode 100644 src/main/java/com/juick/model/TagStats.java
create mode 100644 src/main/java/com/juick/model/UserInfo.java
create mode 100644 src/main/java/com/juick/model/facebook/User.java
create mode 100644 src/main/java/com/juick/model/twitter/User.java
create mode 100644 src/main/java/com/juick/model/vk/Token.java
create mode 100644 src/main/java/com/juick/model/vk/User.java
create mode 100644 src/main/java/com/juick/model/vk/UsersResponse.java
create mode 100644 src/main/java/com/juick/package-info.java
create mode 100644 src/main/java/com/juick/server/ActivityPubManager.java
create mode 100644 src/main/java/com/juick/server/CommandsManager.java
create mode 100644 src/main/java/com/juick/server/EmailManager.java
create mode 100644 src/main/java/com/juick/server/KeystoreManager.java
create mode 100644 src/main/java/com/juick/server/ServerManager.java
create mode 100644 src/main/java/com/juick/server/SignatureManager.java
create mode 100644 src/main/java/com/juick/server/TelegramBotManager.java
create mode 100644 src/main/java/com/juick/server/TopManager.java
create mode 100644 src/main/java/com/juick/server/TwitterManager.java
create mode 100644 src/main/java/com/juick/server/Utils.java
create mode 100644 src/main/java/com/juick/server/WebsocketManager.java
create mode 100644 src/main/java/com/juick/server/XMPPConnection.java
create mode 100644 src/main/java/com/juick/server/XMPPServer.java
create mode 100644 src/main/java/com/juick/server/api/ApiSocialLogin.java
create mode 100644 src/main/java/com/juick/server/api/Index.java
create mode 100644 src/main/java/com/juick/server/api/Messages.java
create mode 100644 src/main/java/com/juick/server/api/Notifications.java
create mode 100644 src/main/java/com/juick/server/api/PM.java
create mode 100644 src/main/java/com/juick/server/api/Post.java
create mode 100644 src/main/java/com/juick/server/api/Service.java
create mode 100644 src/main/java/com/juick/server/api/Tags.java
create mode 100644 src/main/java/com/juick/server/api/Users.java
create mode 100644 src/main/java/com/juick/server/api/activity/Profile.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/Activity.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/Context.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/activities/Accept.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/activities/Announce.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/activities/Block.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/activities/Create.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/activities/Delete.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/activities/Follow.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/activities/Like.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/activities/Undo.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/objects/Hashtag.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/objects/Image.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/objects/Key.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/objects/Link.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/objects/Mention.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/objects/Note.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/objects/OrderedCollection.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/objects/OrderedCollectionPage.java
create mode 100644 src/main/java/com/juick/server/api/activity/model/objects/Person.java
create mode 100644 src/main/java/com/juick/server/api/apple/AppSiteAssociation.java
create mode 100644 src/main/java/com/juick/server/api/hostmeta/HostMeta.java
create mode 100644 src/main/java/com/juick/server/api/rss/Feeds.java
create mode 100644 src/main/java/com/juick/server/api/rss/MessagesView.java
create mode 100644 src/main/java/com/juick/server/api/rss/RepliesView.java
create mode 100644 src/main/java/com/juick/server/api/rss/extension/JuickModule.java
create mode 100644 src/main/java/com/juick/server/api/rss/extension/JuickModuleGenerator.java
create mode 100644 src/main/java/com/juick/server/api/rss/extension/JuickModuleImpl.java
create mode 100644 src/main/java/com/juick/server/api/rss/extension/JuickModuleParser.java
create mode 100644 src/main/java/com/juick/server/api/webfinger/Resource.java
create mode 100644 src/main/java/com/juick/server/api/webfinger/model/Account.java
create mode 100644 src/main/java/com/juick/server/api/webfinger/model/Link.java
create mode 100644 src/main/java/com/juick/server/api/webhooks/TelegramWebhook.java
create mode 100644 src/main/java/com/juick/server/api/xnodeinfo2/Info.java
create mode 100644 src/main/java/com/juick/server/api/xnodeinfo2/model/NodeInfo.java
create mode 100644 src/main/java/com/juick/server/api/xnodeinfo2/model/Server.java
create mode 100644 src/main/java/com/juick/server/api/xnodeinfo2/model/ServiceInfo.java
create mode 100644 src/main/java/com/juick/server/api/xnodeinfo2/model/Usage.java
create mode 100644 src/main/java/com/juick/server/api/xnodeinfo2/model/UserStats.java
create mode 100644 src/main/java/com/juick/server/configuration/ActivityPubClientConfig.java
create mode 100644 src/main/java/com/juick/server/configuration/ActivityPubClientErrorHandler.java
create mode 100644 src/main/java/com/juick/server/configuration/ApiAppConfiguration.java
create mode 100644 src/main/java/com/juick/server/configuration/BaseWebConfiguration.java
create mode 100644 src/main/java/com/juick/server/configuration/SapeConfiguration.java
create mode 100644 src/main/java/com/juick/server/configuration/SecurityConfig.java
create mode 100644 src/main/java/com/juick/server/configuration/StorageConfiguration.java
create mode 100644 src/main/java/com/juick/server/configuration/TelegramConfig.java
create mode 100644 src/main/java/com/juick/server/configuration/WwwAppConfiguration.java
create mode 100644 src/main/java/com/juick/server/configuration/XMPPConfig.java
create mode 100644 src/main/java/com/juick/server/helpers/annotation/UserCommand.java
create mode 100644 src/main/java/com/juick/server/util/HttpBadRequestException.java
create mode 100644 src/main/java/com/juick/server/util/HttpForbiddenException.java
create mode 100644 src/main/java/com/juick/server/util/HttpNotFoundException.java
create mode 100644 src/main/java/com/juick/server/util/HttpUtils.java
create mode 100644 src/main/java/com/juick/server/util/ImageUtils.java
create mode 100644 src/main/java/com/juick/server/util/TagUtils.java
create mode 100644 src/main/java/com/juick/server/util/UserUtils.java
create mode 100644 src/main/java/com/juick/server/util/WebUtils.java
create mode 100644 src/main/java/com/juick/server/www/HelpService.java
create mode 100644 src/main/java/com/juick/server/www/WebApp.java
create mode 100644 src/main/java/com/juick/server/www/controllers/AnythingFilter.java
create mode 100644 src/main/java/com/juick/server/www/controllers/Help.java
create mode 100644 src/main/java/com/juick/server/www/controllers/Login.java
create mode 100644 src/main/java/com/juick/server/www/controllers/MessagesWWW.java
create mode 100644 src/main/java/com/juick/server/www/controllers/NewMessage.java
create mode 100644 src/main/java/com/juick/server/www/controllers/Settings.java
create mode 100644 src/main/java/com/juick/server/www/controllers/SignUp.java
create mode 100644 src/main/java/com/juick/server/www/controllers/SocialLogin.java
create mode 100644 src/main/java/com/juick/server/xmpp/JidConverter.java
create mode 100644 src/main/java/com/juick/server/xmpp/XMPPStatusPage.java
create mode 100644 src/main/java/com/juick/server/xmpp/helpers/XMPPStatus.java
create mode 100644 src/main/java/com/juick/server/xmpp/iq/MessageQuery.java
create mode 100644 src/main/java/com/juick/server/xmpp/iq/package-info.java
create mode 100644 src/main/java/com/juick/server/xmpp/router/Handshake.java
create mode 100644 src/main/java/com/juick/server/xmpp/router/Stream.java
create mode 100644 src/main/java/com/juick/server/xmpp/router/StreamComponentServer.java
create mode 100644 src/main/java/com/juick/server/xmpp/router/StreamError.java
create mode 100644 src/main/java/com/juick/server/xmpp/router/StreamFeatures.java
create mode 100644 src/main/java/com/juick/server/xmpp/router/StreamHandler.java
create mode 100644 src/main/java/com/juick/server/xmpp/router/StreamNamespaces.java
create mode 100644 src/main/java/com/juick/server/xmpp/router/XMPPError.java
create mode 100644 src/main/java/com/juick/server/xmpp/router/XMPPRouter.java
create mode 100644 src/main/java/com/juick/server/xmpp/router/XmlUtils.java
create mode 100644 src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java
create mode 100644 src/main/java/com/juick/server/xmpp/s2s/CacheEntry.java
create mode 100644 src/main/java/com/juick/server/xmpp/s2s/Connection.java
create mode 100644 src/main/java/com/juick/server/xmpp/s2s/ConnectionIn.java
create mode 100644 src/main/java/com/juick/server/xmpp/s2s/ConnectionListener.java
create mode 100644 src/main/java/com/juick/server/xmpp/s2s/ConnectionOut.java
create mode 100644 src/main/java/com/juick/server/xmpp/s2s/DNSQueries.java
create mode 100644 src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java
create mode 100644 src/main/java/com/juick/server/xmpp/s2s/util/DialbackUtils.java
create mode 100644 src/main/java/com/juick/service/ActivityPubService.java
create mode 100644 src/main/java/com/juick/service/BaseJdbcService.java
create mode 100644 src/main/java/com/juick/service/CrosspostService.java
create mode 100644 src/main/java/com/juick/service/CrosspostServiceImpl.java
create mode 100644 src/main/java/com/juick/service/EmailService.java
create mode 100644 src/main/java/com/juick/service/EmailServiceImpl.java
create mode 100644 src/main/java/com/juick/service/ImagesService.java
create mode 100644 src/main/java/com/juick/service/ImagesServiceImpl.java
create mode 100644 src/main/java/com/juick/service/MessagesService.java
create mode 100644 src/main/java/com/juick/service/MessagesServiceImpl.java
create mode 100644 src/main/java/com/juick/service/MessengerService.java
create mode 100644 src/main/java/com/juick/service/MessengerServiceImpl.java
create mode 100644 src/main/java/com/juick/service/PMQueriesService.java
create mode 100644 src/main/java/com/juick/service/PMQueriesServiceImpl.java
create mode 100644 src/main/java/com/juick/service/PrivacyQueriesService.java
create mode 100644 src/main/java/com/juick/service/PrivacyQueriesServiceImpl.java
create mode 100644 src/main/java/com/juick/service/PushQueriesService.java
create mode 100644 src/main/java/com/juick/service/PushQueriesServiceImpl.java
create mode 100644 src/main/java/com/juick/service/SearchService.java
create mode 100644 src/main/java/com/juick/service/ShowQueriesService.java
create mode 100644 src/main/java/com/juick/service/ShowQueriesServiceImpl.java
create mode 100644 src/main/java/com/juick/service/SocialService.java
create mode 100644 src/main/java/com/juick/service/SphinxSearchService.java
create mode 100644 src/main/java/com/juick/service/SubscriptionService.java
create mode 100644 src/main/java/com/juick/service/SubscriptionServiceImpl.java
create mode 100644 src/main/java/com/juick/service/TagService.java
create mode 100644 src/main/java/com/juick/service/TagServiceImpl.java
create mode 100644 src/main/java/com/juick/service/TelegramService.java
create mode 100644 src/main/java/com/juick/service/TelegramServiceImpl.java
create mode 100644 src/main/java/com/juick/service/UserService.java
create mode 100644 src/main/java/com/juick/service/UserServiceImpl.java
create mode 100644 src/main/java/com/juick/service/activities/ActivityListener.java
create mode 100644 src/main/java/com/juick/service/activities/DeleteMessageEvent.java
create mode 100644 src/main/java/com/juick/service/activities/DeleteUserEvent.java
create mode 100644 src/main/java/com/juick/service/activities/FollowEvent.java
create mode 100644 src/main/java/com/juick/service/activities/UndoFollowEvent.java
create mode 100644 src/main/java/com/juick/service/component/DisconnectedEvent.java
create mode 100644 src/main/java/com/juick/service/component/LikeEvent.java
create mode 100644 src/main/java/com/juick/service/component/MessageEvent.java
create mode 100644 src/main/java/com/juick/service/component/MessageReadEvent.java
create mode 100644 src/main/java/com/juick/service/component/NotificationListener.java
create mode 100644 src/main/java/com/juick/service/component/PingEvent.java
create mode 100644 src/main/java/com/juick/service/component/SubscribeEvent.java
create mode 100644 src/main/java/com/juick/service/component/TopEvent.java
create mode 100644 src/main/java/com/juick/service/component/UserUpdatedEvent.java
create mode 100644 src/main/java/com/juick/service/security/HashParamAuthenticationFilter.java
create mode 100644 src/main/java/com/juick/service/security/JuickUserDetailsService.java
create mode 100644 src/main/java/com/juick/service/security/NullUserDetailsService.java
create mode 100644 src/main/java/com/juick/service/security/deprecated/CookieSimpleHashRememberMeServices.java
create mode 100644 src/main/java/com/juick/service/security/deprecated/RequestParamHashRememberMeServices.java
create mode 100644 src/main/java/com/juick/service/security/entities/JuickUser.java
create mode 100644 src/main/java/com/juick/util/DateFormatter.java
create mode 100644 src/main/java/com/juick/util/DateFormattersHolder.java
create mode 100644 src/main/java/com/juick/util/MessageUtils.java
create mode 100644 src/main/java/com/juick/util/PrettyTimeFormatter.java
create mode 100644 src/main/java/com/juick/util/StreamUtils.java
create mode 100644 src/main/java/com/mitchellbosecke/pebble/extension/FormatterExtension.java
create mode 100644 src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java
create mode 100644 src/main/java/com/mitchellbosecke/pebble/extension/filters/PrettyTimeFilter.java
create mode 100644 src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java
create mode 100644 src/main/java/com/mitchellbosecke/pebble/extension/filters/TimestampFilter.java
create mode 100644 src/main/java/rocks/xmpp/core/session/debug/LogbackDebugger.java
create mode 100644 src/main/java/ru/sape/Sape.java
create mode 100644 src/main/java/ru/sape/SapeConnection.java
create mode 100644 src/main/java/ru/sape/SapePageLinks.java
create mode 100644 src/main/resources/1x1.png
create mode 100644 src/main/resources/Transparent.gif
create mode 100644 src/main/resources/db/migration/V1.10__favorites_user_uri.sql
create mode 100644 src/main/resources/db/migration/V1.11__increase pm timestamp precision.sql
create mode 100644 src/main/resources/db/migration/V1.12__drop unused tables.sql
create mode 100644 src/main/resources/db/migration/V1.13__drop unused tables.sql
create mode 100644 src/main/resources/db/migration/V1.14__drop broken pm_streams.sql
create mode 100644 src/main/resources/db/migration/V1.15__drop unused columns add ts for some tables.sql
create mode 100644 src/main/resources/db/migration/V1.16__last seen.sql
create mode 100644 src/main/resources/db/migration/V1.1__Add updated_at field.sql
create mode 100644 src/main/resources/db/migration/V1.2__Drop telegram_chats.sql
create mode 100644 src/main/resources/db/migration/V1.3__Nullable user_id column in auth table.sql
create mode 100644 src/main/resources/db/migration/V1.4__ActivityPub followers.sql
create mode 100644 src/main/resources/db/migration/V1.5__Drop acct index.sql
create mode 100644 src/main/resources/db/migration/V1.6__user_uri.sql
create mode 100644 src/main/resources/db/migration/V1.7__reply_uri.sql
create mode 100644 src/main/resources/db/migration/V1.8__html reply.sql
create mode 100644 src/main/resources/db/migration/V1.9__reply_uri_index.sql
create mode 100644 src/main/resources/errors.properties
create mode 100644 src/main/resources/errors_ru.properties
create mode 160000 src/main/resources/help
create mode 100644 src/main/resources/juick.png
create mode 100644 src/main/resources/juick.sql
create mode 100644 src/main/resources/messages.properties
create mode 100644 src/main/resources/messages_ru.properties
create mode 100644 src/main/resources/pg_schema_wip
create mode 100644 src/main/resources/rome.properties
create mode 100644 src/main/resources/schema.sql
create mode 100644 src/main/resources/static/favicon.png
create mode 100644 src/main/resources/static/logo.png
create mode 100644 src/main/resources/static/tagscloud.png
create mode 100644 src/main/resources/templates/layouts/content.html
create mode 100644 src/main/resources/templates/layouts/default.html
create mode 100644 src/main/resources/templates/layouts/minimal.html
create mode 100644 src/main/resources/templates/layouts/note.html
create mode 100644 src/main/resources/templates/views/404.html
create mode 100644 src/main/resources/templates/views/blog.html
create mode 100644 src/main/resources/templates/views/blog_tags.html
create mode 100644 src/main/resources/templates/views/help.html
create mode 100644 src/main/resources/templates/views/index.html
create mode 100644 src/main/resources/templates/views/login.html
create mode 100644 src/main/resources/templates/views/login_success.html
create mode 100644 src/main/resources/templates/views/macros/tags.html
create mode 100644 src/main/resources/templates/views/partial/footer.html
create mode 100644 src/main/resources/templates/views/partial/homecolumn.html
create mode 100644 src/main/resources/templates/views/partial/message.html
create mode 100644 src/main/resources/templates/views/partial/navigation.html
create mode 100644 src/main/resources/templates/views/partial/settings_tabs.html
create mode 100644 src/main/resources/templates/views/partial/tagcolumn.html
create mode 100644 src/main/resources/templates/views/partial/tags.html
create mode 100644 src/main/resources/templates/views/partial/usercolumn.html
create mode 100644 src/main/resources/templates/views/partial/usertags.html
create mode 100644 src/main/resources/templates/views/pm_inbox.html
create mode 100644 src/main/resources/templates/views/pm_sent.html
create mode 100644 src/main/resources/templates/views/post.html
create mode 100644 src/main/resources/templates/views/post_success.html
create mode 100644 src/main/resources/templates/views/settings_about.html
create mode 100644 src/main/resources/templates/views/settings_auth-email.html
create mode 100644 src/main/resources/templates/views/settings_main.html
create mode 100644 src/main/resources/templates/views/settings_password.html
create mode 100644 src/main/resources/templates/views/settings_privacy.html
create mode 100644 src/main/resources/templates/views/settings_result.html
create mode 100644 src/main/resources/templates/views/signup.html
create mode 100644 src/main/resources/templates/views/thread.html
create mode 100644 src/main/resources/templates/views/users.html
(limited to 'src/main')
diff --git a/src/main/assets/embed.js b/src/main/assets/embed.js
new file mode 100644
index 00000000..d4cbab8e
--- /dev/null
+++ b/src/main/assets/embed.js
@@ -0,0 +1,334 @@
+
+function insertAfter(newNode, referenceNode) {
+ referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
+}
+
+function setContent(containerNode, ...newNodes) {
+ removeAllFrom(containerNode);
+ newNodes.forEach(n => containerNode.appendChild(n));
+ return containerNode;
+}
+
+function removeAllFrom(fromNode) {
+ for (let c; c = fromNode.lastChild; ) { fromNode.removeChild(c); }
+}
+
+function htmlEscape(html) {
+ let textarea = document.createElement('textarea');
+ textarea.textContent = html;
+ return textarea.innerHTML;
+}
+
+// rules :: [{pr: number, re: RegExp, with: string}]
+// rules :: [{pr: number, re: RegExp, with: Function}]
+// rules :: [{pr: number, re: RegExp, brackets: true, with: [string, string]}]
+// rules :: [{pr: number, re: RegExp, brackets: true, with: [string, string, Function]}]
+function formatText(txt, rules) {
+ let idCounter = 0;
+ function nextId() { return idCounter++; }
+ function ft(txt, rules) {
+ let matches = rules.map(r => { r.re.lastIndex = 0; return [r, r.re.exec(txt)]; })
+ .filter(([,m]) => m !== null)
+ .sort(([r1,m1],[r2,m2]) => (r1.pr - r2.pr) || (m1.index - m2.index));
+ if (matches && matches.length > 0) {
+ let [rule, match] = matches[0];
+ let subsequentRules = rules.filter(r => r.pr >= rule.pr);
+ let idStr = `<>(${nextId()})<>`;
+ let outerStr = txt.substring(0, match.index) + idStr + txt.substring(rule.re.lastIndex);
+ let innerStr = (rule.brackets)
+ ? (() => { let [l ,r ,f] = rule.with; return l + ft((f ? f(match[1]) : match[1]), subsequentRules) + r; })()
+ : match[0].replace(rule.re, rule.with);
+ return ft(outerStr, subsequentRules).replace(idStr, innerStr);
+ }
+ return txt;
+ }
+ return ft(htmlEscape(txt), rules); // idStr above relies on the fact the text is escaped
+}
+
+function fixWwwLink(url) {
+ return url.replace(/^(?!([a-z]+:)?\/\/)/i, '//');
+}
+
+function makeNewNode(embedType, aNode, reResult) {
+ const withClasses = el => {
+ if (embedType.className) {
+ el.classList.add(...embedType.className.split(' '));
+ }
+ return el;
+ };
+ return embedType.makeNode(aNode, reResult, withClasses(document.createElement('div')));
+}
+
+function makeIframe(src, w, h, scrolling='no') {
+ let iframe = document.createElement('iframe');
+ iframe.style.width = w;
+ iframe.style.height = h;
+ iframe.frameBorder = 0;
+ iframe.scrolling = scrolling;
+ iframe.setAttribute('allowFullScreen', '');
+ iframe.src = src;
+ iframe.innerHTML = 'Cannot show iframes.';
+ return iframe;
+}
+
+function makeResizableToRatio(element, ratio) {
+ element.dataset['ratio'] = ratio;
+ makeResizable(element, w => w * element.dataset['ratio']);
+}
+
+// calcHeight :: Number -> Number -- calculate element height for a given width
+function makeResizable(element, calcHeight) {
+ const setHeight = el => {
+ if (document.body.contains(el) && (el.offsetWidth > 0)) {
+ el.style.height = (calcHeight(el.offsetWidth)).toFixed(2) + 'px';
+ }
+ };
+ window.addEventListener('resize', () => setHeight(element));
+ setHeight(element);
+}
+
+function extractDomain(url) {
+ const domainRe = /^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/\n]+)/i;
+ return domainRe.exec(url)[1];
+}
+
+function urlReplace(match, p1, p2, p3) {
+ let isBrackets = (p1 !== undefined);
+ return (isBrackets)
+ ? `${p1}`
+ : `${extractDomain(match)}`;
+}
+
+function urlReplaceInCode(match, p1, p2, p3) {
+ let isBrackets = (p1 !== undefined);
+ return (isBrackets)
+ ? `${match}`
+ : `${match}`;
+}
+
+function messageReplyReplace(messageId) {
+ return function(match, mid, rid) {
+ let replyPart = (rid && rid != '0') ? '#' + rid : '';
+ return `${match}`;
+ };
+}
+
+/**
+ * Given "txt" message in unescaped plaintext with Juick markup, this function
+ * returns escaped formatted HTML string.
+ *
+ * @param {string} txt
+ * @param {string} messageId - current message id
+ * @param {boolean} isCode
+ * @returns {string}
+ */
+function juickFormat(txt, messageId, isCode) {
+ const urlRe = /(?:\[([^\]\[]+)\](?:\[([^\]]+)\]|\(((?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))\))|\b(?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))/gi;
+ const bqReplace = m => m.replace(/^(?:>|>)\s?/gmi, '');
+ return (isCode)
+ ? formatText(txt, [
+ { pr: 1, re: urlRe, with: urlReplaceInCode },
+ { pr: 1, re: /\B(?:#(\d+))?(?:\/(\d+))?\b/g, with: messageReplyReplace(messageId) },
+ { pr: 1, re: /\B@([\w-]+)\b/gi, with: '@$1' },
+ ])
+ : formatText(txt, [
+ { pr: 0, re: /((?:^(?:>|>)\s?[\s\S]+?$\n?)+)/gmi, brackets: true, with: ['', '
', bqReplace] },
+ { pr: 1, re: urlRe, with: urlReplace },
+ { pr: 1, re: /\B(?:#(\d+))?(?:\/(\d+))?\b/g, with: messageReplyReplace(messageId) },
+ { pr: 1, re: /\B@([\w-]+)\b/gi, with: '@$1' },
+ { pr: 2, re: /\B\*([^\n]+?)\*((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] },
+ { pr: 2, re: /\B\/([^\n]+?)\/((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] },
+ { pr: 2, re: /\b\_([^\n]+?)\_((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] },
+ { pr: 3, re: /\n/g, with: '
' },
+ ]);
+}
+
+function getEmbeddableLinkTypes() {
+ return [
+ {
+ name: 'Jpeg and png images',
+ id: 'embed_jpeg_and_png_images',
+ className: 'picture compact',
+ ctsDefault: false,
+ re: /\.(jpe?g|png|svg)(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
+ div.innerHTML = ``;
+ return div;
+ }
+ },
+ {
+ name: 'Gif images',
+ id: 'embed_gif_images',
+ className: 'picture compact',
+ ctsDefault: true,
+ re: /\.gif(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
+ div.innerHTML = ``;
+ return div;
+ }
+ },
+ {
+ name: 'Video (webm, mp4, ogv)',
+ id: 'embed_webm_and_mp4_videos',
+ className: 'video compact',
+ ctsDefault: false,
+ re: /\.(webm|mp4|m4v|ogv)(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
+ div.innerHTML = ``;
+ return div;
+ }
+ },
+ {
+ name: 'Audio (mp3, ogg, weba, opus, m4a, oga, wav)',
+ id: 'embed_sound_files',
+ className: 'audio singleColumn',
+ ctsDefault: false,
+ re: /\.(mp3|ogg|weba|opus|m4a|oga|wav)(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
+ div.innerHTML = ``;
+ return div;
+ }
+ },
+ {
+ name: 'YouTube videos (and playlists)',
+ id: 'embed_youtube_videos',
+ className: 'youtube resizableV singleColumn',
+ ctsDefault: false,
+ re: /^(?:https?:)?\/\/(?:www\.|m\.|gaming\.)?(?:youtu(?:(?:\.be\/|be\.com\/(?:v|embed)\/)([-\w]+)|be\.com\/watch)((?:(?:\?|&(?:amp;)?)(?:\w+=[-\.\w]*[-\w]))*)|youtube\.com\/playlist\?list=([-\w]*)(&(amp;)?[-\w\?=]*)?)/i,
+ makeNode: function(aNode, reResult, div) {
+ let [url, v, args, plist] = reResult;
+ let iframeUrl;
+ if (plist) {
+ iframeUrl = '//www.youtube-nocookie.com/embed/videoseries?list=' + plist;
+ } else {
+ let pp = {}; args.replace(/^\?/, '')
+ .split('&')
+ .map(s => s.split('='))
+ .forEach(z => pp[z[0]] = z[1]);
+ let embedArgs = { rel: '0' };
+ if (pp.t) {
+ const tre = /^(?:(\d+)|(?:(\d+)h)?(?:(\d+)m)?(\d+)s|(?:(\d+)h)?(\d+)m|(\d+)h)$/i;
+ let [, t, h, m, s, h1, m1, h2] = tre.exec(pp.t);
+ embedArgs['start'] = (+t) || ((+(h || h1 || h2 || 0))*60*60 + (+(m || m1 || 0))*60 + (+(s || 0)));
+ }
+ if (pp.list) {
+ embedArgs['list'] = pp.list;
+ }
+ v = v || pp.v;
+ let argsStr = Object.keys(embedArgs)
+ .map(k => `${k}=${embedArgs[k]}`)
+ .join('&');
+ iframeUrl = `//www.youtube-nocookie.com/embed/${v}?${argsStr}`;
+ }
+ let iframe = makeIframe(iframeUrl, '100%', '360px');
+ iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0);
+ return setContent(div, iframe);
+ }
+ },
+ {
+ name: 'Vimeo videos',
+ id: 'embed_vimeo_videos',
+ className: 'vimeo resizableV',
+ ctsDefault: false,
+ re: /^(?:https?:)?\/\/(?:www\.)?(?:player\.)?vimeo\.com\/(?:video\/|album\/[\d]+\/video\/)?([\d]+)/i,
+ makeNode: function(aNode, reResult, div) {
+ let iframe = makeIframe('//player.vimeo.com/video/' + reResult[1], '100%', '360px');
+ iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0);
+ return setContent(div, iframe);
+ }
+ }
+ ];
+}
+
+function embedLink(aNode, linkTypes, container, afterNode) {
+ let anyEmbed = false;
+ let linkId = (aNode.href.replace(/^https?:/i, '').replace(/\'/gi,''));
+ let sameEmbed = container.querySelector(`*[data-linkid='${linkId}']`); // do not embed the same thing twice
+ if (sameEmbed === null) {
+ anyEmbed = [].some.call(linkTypes, function(linkType) {
+ let reResult = linkType.re.exec(aNode.href);
+ if (reResult) {
+ if (linkType.match && (linkType.match(aNode, reResult) === false)) { return false; }
+ let newNode = makeNewNode(linkType, aNode, reResult);
+ if (!newNode) { return false; }
+ newNode.setAttribute('data-linkid', linkId);
+ if (afterNode) {
+ insertAfter(newNode, afterNode);
+ } else {
+ container.appendChild(newNode);
+ }
+ aNode.classList.add('embedLink');
+ return true;
+ }
+ });
+ }
+ return anyEmbed;
+}
+
+function embedLinks(aNodes, container) {
+ let anyEmbed = false;
+ let embeddableLinkTypes = getEmbeddableLinkTypes();
+ Array.from(aNodes).forEach(aNode => {
+ let isEmbedded = embedLink(aNode, embeddableLinkTypes, container);
+ anyEmbed = anyEmbed || isEmbedded;
+ });
+ return anyEmbed;
+}
+
+/**
+ * Embed all the links inside element "x" that match to "allLinksSelector".
+ * All the embedded media is placed inside "div.embedContainer".
+ * "div.embedContainer" is inserted before an element matched by "beforeNodeSelector"
+ * if not present. Existing container is used otherwise.
+ *
+ * @param {Element} x
+ * @param {string} beforeNodeSelector
+ * @param {string} allLinksSelector
+ */
+export function embedLinksToX(x, beforeNodeSelector, allLinksSelector) {
+ let isCtsPost = false;
+ let allLinks = x.querySelectorAll(allLinksSelector);
+
+ let existingContainer = x.querySelector('div.embedContainer');
+ if (existingContainer) {
+ embedLinks(allLinks, existingContainer);
+ } else {
+ let embedContainer = document.createElement('div');
+ embedContainer.className = 'embedContainer';
+
+ let anyEmbed = embedLinks(allLinks, embedContainer);
+ if (anyEmbed) {
+ let beforeNode = x.querySelector(beforeNodeSelector);
+ x.insertBefore(embedContainer, beforeNode);
+ }
+ }
+}
+
+function embedLinksToArticles() {
+ let beforeNodeSelector = 'nav.l';
+ let allLinksSelector = 'p:not(.ir) a, pre a';
+ Array.from(document.querySelectorAll('#content article')).forEach(article => {
+ embedLinksToX(article, beforeNodeSelector, allLinksSelector);
+ });
+}
+
+function embedLinksToPost() {
+ let beforeNodeSelector = '.msg-txt + *';
+ let allLinksSelector = '.msg-txt a';
+ Array.from(document.querySelectorAll('#content .msg-cont')).forEach(msg => {
+ embedLinksToX(msg, beforeNodeSelector, allLinksSelector);
+ });
+}
+
+/**
+ * Embed all the links in all messages/replies on the page.
+ */
+export function embedAll() {
+ if (document.querySelector('#content article[data-mid]')) {
+ embedLinksToArticles();
+ } else {
+ embedLinksToPost();
+ }
+}
+
+export const format = juickFormat;
diff --git a/src/main/assets/logo.png b/src/main/assets/logo.png
new file mode 100644
index 00000000..4e0f6d56
Binary files /dev/null and b/src/main/assets/logo.png differ
diff --git a/src/main/assets/logo@2x.png b/src/main/assets/logo@2x.png
new file mode 100644
index 00000000..6febeaf9
Binary files /dev/null and b/src/main/assets/logo@2x.png differ
diff --git a/src/main/assets/scripts.js b/src/main/assets/scripts.js
new file mode 100644
index 00000000..9ee0639e
--- /dev/null
+++ b/src/main/assets/scripts.js
@@ -0,0 +1,935 @@
+require('element-closest');
+require('classlist.js');
+require('url-polyfill');
+require('formdata-polyfill');
+import { embedLinksToX, embedAll, format } from './embed';
+
+if (!('remove' in Element.prototype)) { // Firefox <23
+ Element.prototype.remove = function() {
+ if (this.parentNode) {
+ this.parentNode.removeChild(this);
+ }
+ };
+}
+
+NodeList.prototype.forEach = Array.prototype.forEach;
+HTMLCollection.prototype.forEach = Array.prototype.forEach;
+
+NodeList.prototype.filter = Array.prototype.filter;
+HTMLCollection.prototype.filter = Array.prototype.filter;
+
+Element.prototype.selectText = function() {
+ let d = document;
+ if (d.body.createTextRange) {
+ let range = d.body.createTextRange();
+ range.moveToElementText(this);
+ range.select();
+ } else if (window.getSelection) {
+ let selection = window.getSelection();
+ let rangeSel = d.createRange();
+ rangeSel.selectNodeContents(this);
+ selection.removeAllRanges();
+ selection.addRange(rangeSel);
+ }
+};
+
+function autosize(el) {
+ let offset = (!window.opera)
+ ? (el.offsetHeight - el.clientHeight)
+ : (el.offsetHeight + parseInt(window.getComputedStyle(el, null).getPropertyValue('border-top-width')));
+
+ let resize = function(el) {
+ el.style.height = 'auto';
+ el.style.height = (el.scrollHeight + offset) + 'px';
+ };
+
+ if (el.addEventListener) {
+ el.addEventListener('input', () => resize(el));
+ } else if (el.attachEvent) {
+ el.attachEvent('onkeyup', () => resize(el));
+ }
+}
+
+function evilIcon(name) {
+ return `
`;
+}
+
+/* eslint-disable only-ascii/only-ascii */
+const translations = {
+ 'en': {
+ 'message.inReplyTo': 'in reply to',
+ 'message.reply': 'Reply',
+ 'message.likeThisMessage?': 'Recommend this message?',
+ 'postForm.pleaseInputMessageText': 'Please input message text',
+ 'postForm.upload': 'Upload',
+ 'postForm.newMessage': 'New message...',
+ 'postForm.imageLink': 'Link to image',
+ 'postForm.imageFormats': 'JPG/PNG, up to 10 MB',
+ 'postForm.or': 'or',
+ 'postForm.tags': 'Tags (space separated)',
+ 'postForm.submit': 'Send',
+ 'comment.writeComment': 'Write a comment...',
+ 'shareDialog.linkToMessage': 'Link to message',
+ 'shareDialog.messageNumber': 'Message number',
+ 'shareDialog.share': 'Share',
+ 'loginDialog.pleaseIntroduceYourself': 'Please introduce yourself',
+ 'loginDialog.registeredAlready': 'Registered already?',
+ 'loginDialog.username': 'Username',
+ 'loginDialog.password': 'Password',
+ 'loginDialog.facebook': 'Login with Facebook',
+ 'loginDialog.vk': 'Login with VK',
+ 'loginDialog.email': 'Registration',
+ 'error.error': 'Error'
+ },
+ 'ru': {
+ 'message.inReplyTo': 'в ответ на',
+ 'message.reply': 'Ответить',
+ 'message.likeThisMessage?': 'Рекомендовать это сообщение?',
+ 'postForm.pleaseInputMessageText': 'Пожалуйста, введите текст сообщения',
+ 'postForm.upload': 'загрузить',
+ 'postForm.newMessage': 'Новое сообщение...',
+ 'postForm.imageLink': 'Ссылка на изображение',
+ 'postForm.imageFormats': 'JPG/PNG, до 10Мб',
+ 'postForm.or': 'или',
+ 'postForm.tags': 'Теги (через пробел)',
+ 'postForm.submit': 'Отправить',
+ 'comment.writeComment': 'Написать комментарий...',
+ 'shareDialog.linkToMessage': 'Ссылка на сообщение',
+ 'shareDialog.messageNumber': 'Номер сообщения',
+ 'shareDialog.share': 'Поделиться',
+ 'loginDialog.pleaseIntroduceYourself': 'Пожалуйста, представьтесь',
+ 'loginDialog.registeredAlready': 'Уже зарегистрированы?',
+ 'loginDialog.username': 'Имя пользователя',
+ 'loginDialog.password': 'Пароль',
+ 'loginDialog.facebook': 'Войти через Facebook',
+ 'loginDialog.vk': 'Войти через ВКонтакте',
+ 'loginDialog.email': 'Регистрация',
+ 'error.error': 'Ошибка'
+ }
+};
+/* eslint-enable only-ascii/only-ascii */
+
+function getLang() {
+ return (window.navigator.languages && window.navigator.languages[0])
+ || window.navigator.userLanguage
+ || window.navigator.language;
+}
+function i18n(key, lang = undefined) {
+ const fallbackLang = 'ru';
+ lang = lang || getLang().split('-')[0];
+ return (translations[lang] && translations[lang][key])
+ || translations[fallbackLang][key]
+ || key;
+}
+
+var es, pageTitle;
+
+function initES() {
+ if (!('EventSource' in window)) {
+ return;
+ }
+ let url = '/api/events';
+ let hash = document.getElementById('body').getAttribute('data-hash');
+ if (hash) {
+ url += '?hash=' + hash;
+ }
+
+ es = new EventSource(url);
+ es.onopen = function() {
+ console.log('online');
+ if (!document.querySelector('#wsthread')) {
+ var d = document.createElement('div');
+ d.id = 'wsthread';
+ d.addEventListener('click', nextReply);
+ document.querySelector('body').appendChild(d);
+ pageTitle = document.title;
+ }
+ };
+ es.addEventListener('msg', msg => {
+ try {
+ var jsonMsg = JSON.parse(msg.data);
+ console.log('data: ' + msg.data);
+ if (jsonMsg.service) {
+ return;
+ }
+ wsIncomingReply(jsonMsg);
+ } catch (err) {
+ console.log(err);
+ }
+ });
+}
+
+function wsIncomingReply(msg) {
+ let content = document.getElementById('content');
+ if (!content) { return; }
+ let pageMID = content.getAttribute('data-mid');
+ if (!pageMID || pageMID != msg.mid) { return; }
+ let msgNum = '/' + msg.rid;
+ if (msg.replyto > 0) {
+ msgNum += ` ${i18n('message.inReplyTo')} /${msg.replyto}`;
+ }
+ let photoDiv = (msg.attach == null) ? '' : `
+ `;
+ let msgContHtml = `
+
+
+
${format(msg.body, msg.mid, false)}
${photoDiv}
+
+
+
`;
+
+ let li = document.createElement('li');
+ li.setAttribute('class', 'msg reply-new');
+ li.setAttribute('id', msg.rid);
+ li.innerHTML = msgContHtml;
+ li.addEventListener('click', newReply);
+ li.addEventListener('mouseover', newReply);
+ li.querySelector('a.msg-reply-link').addEventListener('click', function(e) {
+ showCommentForm(msg.mid, msg.rid);
+ e.preventDefault();
+ });
+
+ embedLinksToX(li.querySelector('.msg-cont'), '.msg-links', '.msg-txt a');
+
+ document.getElementById('replies').appendChild(li);
+
+ updateRepliesCounter();
+}
+
+function newReply(e) {
+ var li = e.target;
+ li.classList.remove('reply-new');
+ li.removeEventListener('click', e);
+ li.removeEventListener('mouseover', e);
+ updateRepliesCounter();
+}
+
+function nextReply() {
+ var li = document.querySelector('#replies>li.reply-new');
+ if (li) {
+ li.classList.remove('reply-new');
+ li.removeEventListener('click', this);
+ li.children[0].scrollIntoView();
+ updateRepliesCounter();
+ }
+}
+
+function updateRepliesCounter() {
+ var replies = document.querySelectorAll('#replies>li.reply-new').length;
+ var wsthread = document.getElementById('wsthread');
+ if (replies) {
+ wsthread.textContent = replies;
+ wsthread.style.display = 'block';
+ document.title = '[' + replies + '] ' + pageTitle;
+ } else {
+ wsthread.style.display = 'none';
+ document.title = pageTitle;
+ }
+}
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+function postformListener(formEl, ev) {
+ if (ev.ctrlKey && (ev.keyCode == 10 || ev.keyCode == 13)) {
+ let form = formEl.closest('form');
+ if (!form.onsubmit || form.onsubmit()) {
+ form.querySelector('input[type="submit"]').click();
+ }
+ }
+}
+function closeDialogListener(ev) {
+ ev = ev || window.event;
+ if (ev.keyCode == 27) {
+ closeDialog();
+ }
+}
+
+function newMessage(evt) {
+ document.querySelectorAll('#newmessage .dialogtxt').forEach(t => {
+ t.remove();
+ });
+ if (document.querySelector('#newmessage textarea').value.length == 0
+ && document.querySelector('#newmessage .img').value.length == 0
+ && !document.querySelector('#newmessage input[type="file"]')) {
+ document.querySelector('#newmessage').insertAdjacentHTML('afterbegin', `${i18n('postForm.pleaseInputMessageText')}
`);
+ evt.preventDefault();
+ }
+}
+
+function handleErrors(response) {
+ if (!response.ok) {
+ throw Error(response.statusText);
+ }
+ return response;
+}
+
+function showCommentForm(mid, rid) {
+ let reply = document.getElementById(rid);
+ let formTarget = reply.querySelector('div.msg-cont .msg-comment-target');
+ if (formTarget) {
+ let formHtml = `
+ `;
+ formTarget.insertAdjacentHTML('afterend', formHtml);
+ formTarget.remove();
+
+ let form = reply.querySelector('form');
+ let submitButton = form.querySelector('input[type="submit"]');
+
+ let attachButton = form.querySelector('.msg-comment .attach-photo');
+ attachButton.addEventListener('click', e => attachCommentPhoto(e.target));
+
+ let textarea = form.querySelector('.msg-comment textarea');
+ textarea.addEventListener('keypress', e => postformListener(e.target, e));
+ autosize(textarea);
+
+ let validateMessage = () => {
+ let len = textarea.value.length;
+ if (len > 4096) { return 'Message is too long'; }
+ return '';
+ };
+ form.addEventListener('submit', e => {
+ let validationResult = validateMessage();
+ if (validationResult) {
+ e.preventDefault();
+ alert(validationResult);
+ return false;
+ }
+ submitButton.disabled = true;
+ let formData = new FormData(form);
+ fetch('/api/comment' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ method: 'POST',
+ body: formData,
+ credentials: 'omit'
+ }).then(handleErrors)
+ .then(response => {
+ if (response.ok) {
+ response.json().then(result => {
+ if (result.newMessage) {
+ window.location.href = new URL(`${mid}#${result.newMessage.rid}`, window.location.href);
+ } else {
+ alert(result.text);
+ }
+ window.location.reload(true);
+ });
+ }
+ }).catch(error => {
+ alert(error.message);
+ });
+ e.preventDefault();
+ });
+ }
+ reply.querySelector('.msg-comment textarea').focus();
+}
+
+function attachInput() {
+ let inp = document.createElement('input');
+ inp.setAttribute('type', 'file');
+ inp.setAttribute('name', 'attach');
+ inp.setAttribute('accept', 'image/jpeg,image/png');
+ inp.style.visibility = 'hidden';
+ return inp;
+}
+
+function attachCommentPhoto(div) {
+ let input = div.querySelector('input');
+ if (input) {
+ input.remove();
+ div.classList.remove('attach-photo-active');
+ } else {
+ let newInput = attachInput();
+ newInput.addEventListener('change', function() {
+ div.classList.add('attach-photo-active');
+ });
+ newInput.click();
+ div.appendChild(newInput);
+ }
+}
+
+function attachMessagePhoto(div) {
+ var f = div.closest('form'),
+ finput = f.querySelector('input[type="file"]');
+ if (!finput) {
+ var inp = attachInput();
+ inp.style.float = 'left';
+ inp.style.width = 0;
+ inp.style.height = 0;
+ inp.addEventListener('change', function() {
+ div.textContent = i18n('postForm.upload') + ' (✓)';
+ });
+ f.appendChild(inp);
+ inp.click();
+ } else {
+ finput.remove();
+ div.textContent = i18n('postForm.upload');
+ }
+}
+
+function showMessageLinksDialog(mid, rid) {
+ let hlink = window.location.protocol + '//juick.com/' + mid;
+ let mlink = '#' + mid;
+ if (rid > 0) {
+ hlink += '#' + rid;
+ mlink += '/' + rid;
+ }
+ let hlinkenc = encodeURIComponent(hlink);
+ let html = `
+
+ ${i18n('shareDialog.linkToMessage')}:
${hlink}
+ ${i18n('shareDialog.messageNumber')}:
${mlink}
+ ${i18n('shareDialog.share')}:
+
+
`;
+
+ openDialog(html);
+}
+
+function showPhotoDialog(fname) {
+ let width = window.innerWidth;
+ let height = window.innerHeight;
+ let minDimension = (width < height) ? width : height;
+ if (minDimension < 640) {
+ return true; // no dialog, open the link
+ } else if (minDimension < 1280) {
+ openDialog(``, true);
+ return false;
+ } else {
+ openDialog(``, true);
+ return false;
+ }
+}
+
+function openPostDialog() {
+ let newmessageTemplate = `
+
+ `;
+ return openDialog(newmessageTemplate);
+}
+
+function openDialog(html, image) {
+ var dialogHtml = `
+ `;
+ let body = document.querySelector('body');
+ body.classList.add('dialog-opened');
+ body.insertAdjacentHTML('afterbegin', dialogHtml);
+ if (image) {
+ let header = document.querySelector('#dialog_header');
+ header.classList.add('header_image');
+ }
+ document.addEventListener('keydown', closeDialogListener);
+ document.querySelector('#dialogb').addEventListener('click', closeDialog);
+ document.querySelector('#dialogc').addEventListener('click', closeDialog);
+}
+
+function closeDialog() {
+ let draft = document.querySelector('#newmessage textarea');
+ if (draft) {
+ window.draft = draft.value;
+ }
+ document.querySelector('body').classList.remove('dialog-opened');
+ document.querySelector('#dialogb').remove();
+ document.querySelector('#dialogt').remove();
+}
+
+function openSocialWindow(a) {
+ var w = window.open(a.href, 'juickshare', 'width=640,height=400');
+ if (window.focus) { w.focus(); }
+ return false;
+}
+
+function checkUsername() {
+ var uname = document.querySelector('#username').textContent,
+ style = document.querySelector('#username').style;
+ fetch('/api/users?uname=' + uname)
+ .then(handleErrors)
+ .then(function() {
+ style.background = '#FFCCCC';
+ })
+ .catch(function() {
+ style.background = '#CCFFCC';
+ });
+}
+
+/******************************************************************************/
+
+function openDialogLogin() {
+ let html = `
+ `;
+ openDialog(html);
+ return false;
+}
+
+/******************************************************************************/
+
+function resultMessage(str) {
+ var result = document.createElement('p');
+ result.textContent = str;
+ return result;
+}
+
+function likeMessage(e, mid) {
+ if (confirm(i18n('message.likeThisMessage?'))) {
+ fetch('/api/like?mid=' + mid
+ + '&hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ method: 'POST',
+ credentials: 'omit'
+ })
+ .then(handleErrors)
+ .then(function(response) {
+ if (response.ok) {
+ e.closest('article').appendChild(resultMessage('OK!'));
+ }
+ })
+ .catch(function() {
+ e.closest('article').appendChild(resultMessage(i18n('error.error')));
+ });
+ }
+ return false;
+}
+
+function subscribeMessage(e, mid) {
+ fetch('/api/subscribe?mid=' + mid
+ + '&hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ method: 'POST',
+ credentials: 'omit'
+ })
+ .then(handleErrors)
+ .then(function(response) {
+ if (response.ok) {
+ window.location.reload(true);
+ } else {
+ alert('Something went wrong :(');
+ }
+ })
+ .catch(error => {
+ alert(error.message);
+ });
+ return false;
+}
+
+/******************************************************************************/
+
+function setPopular(e, mid, popular) {
+ fetch('/api/messages/set_popular?mid=' + mid
+ + '&popular=' + popular
+ + '&hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ credentials: 'same-origin'
+ })
+ .then(handleErrors)
+ .then(function() {
+ e.closest('article').append(resultMessage('OK!'));
+ });
+ return false;
+}
+
+function setPrivacy(e, mid) {
+ fetch('/api/messages/set_privacy?mid=' + mid
+ + '&hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ credentials: 'same-origin'
+ })
+ .then(handleErrors)
+ .then(function() {
+ e.closest('article').append(resultMessage('OK!'));
+ });
+ return false;
+}
+
+function getTags() {
+ fetch('/api/tags?hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ credentials: 'omit'
+ })
+ .then(handleErrors)
+ .then(response => {
+ return response.json();
+ })
+ .then(json => {
+ let tags = json.map(t => t.tag);
+ let input = document.getElementById('tags_input');
+ });
+ return false;
+}
+
+function addTag(tag) {
+ document.forms['postmsg'].body.value = '*' + tag + ' ' + document.forms['postmsg'].body.value;
+ return false;
+}
+
+var users = {};
+
+function fetchUserUri(dataUri, callback) {
+ if (users[dataUri]) {
+ callback(users[dataUri]);
+ } else {
+ let data = new FormData();
+ data.append('uri', dataUri);
+ fetch('/u/', {
+ method: 'POST',
+ body: data
+ }).then(handleErrors)
+ .then(response => {
+ return response.json();
+ })
+ .then(json => {
+ users[dataUri] = json;
+ callback(json);
+ });
+ }
+}
+
+/******************************************************************************/
+
+function ready(fn) {
+ if (document.readyState != 'loading') {
+ fn();
+ } else {
+ document.addEventListener('DOMContentLoaded', fn);
+ }
+}
+
+ready(function() {
+ document.querySelectorAll('textarea').forEach((ta) => {
+ autosize(ta);
+ });
+
+ var insertPMButtons = function(e) {
+ e.target.classList.add('narrowpm');
+ e.target.parentNode.insertAdjacentHTML('afterend', '');
+ e.target.removeEventListener('click', insertPMButtons);
+ e.preventDefault();
+ };
+ document.querySelectorAll('textarea.replypm').forEach(function(e) {
+ e.addEventListener('click', insertPMButtons);
+ e.addEventListener('keypress', function(e) {
+ postformListener(e.target, e);
+ });
+ });
+ document.querySelectorAll('#postmsg textarea').forEach(function(e) {
+ e.addEventListener('keypress', function(e) {
+ postformListener(e.target, e);
+ });
+ });
+
+ var content = document.getElementById('content');
+ if (content) {
+ var pageMID = content.getAttribute('data-mid');
+ if (pageMID > 0) {
+ document.querySelectorAll('li.msg').forEach(li => {
+ let showReplyFormBtn = li.querySelector('.a-thread-comment');
+ if (showReplyFormBtn) {
+ showReplyFormBtn.addEventListener('click', function(e) {
+ showCommentForm(pageMID, li.id);
+ e.preventDefault();
+ });
+ }
+ });
+ let opMessage = document.querySelector('.msgthread');
+ if (opMessage) {
+ let replyTextarea = opMessage.querySelector('textarea.reply');
+ if (replyTextarea) {
+ replyTextarea.addEventListener('focus', e => showCommentForm(pageMID, 0));
+ replyTextarea.addEventListener('keypress', e => postformListener(e.target, e));
+ if (!window.location.hash) {
+ replyTextarea.focus();
+ }
+ }
+ }
+ }
+ }
+
+ var postmsg = document.getElementById('postmsg');
+ if (postmsg) {
+ document.querySelectorAll('a').filter(t => t.href.indexOf('?') >= 0).forEach(t => {
+ t.addEventListener('click', e => {
+ let params = new URLSearchParams(t.href.slice(t.href.indexOf('?') + 1));
+ if (params.has('tag')) {
+ addTag(params.get('tag'));
+ e.preventDefault();
+ }
+ });
+ });
+ postmsg.addEventListener('submit', e => {
+ let formData = new FormData(postmsg);
+ fetch('/api/post' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ method: 'POST',
+ body: formData,
+ credentials: 'omit'
+ }).then(handleErrors)
+ .then(response => {
+ if (response.ok) {
+ response.json().then(result => {
+ if (result.newMessage) {
+ window.location = new URL(`/m/${result.newMessage.mid}`, window.location.href);
+ } else {
+ alert(result.text);
+ }
+ });
+ } else {
+ alert('Something went wrong :(');
+ }
+ }).catch(error => {
+ alert(error.message);
+ });
+ e.preventDefault();
+ });
+ }
+ document.querySelectorAll('.pmmsg').forEach(pmmsg => {
+ pmmsg.addEventListener('submit', e => {
+ let formData = new FormData(pmmsg);
+ fetch('/api/pm' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ method: 'POST',
+ body: formData,
+ credentials: 'omit'
+ }).then(handleErrors)
+ .then(response => {
+ if (response.ok) {
+ response.json().then(result => {
+ if (result.to) {
+ window.location = new URL('/pm/sent', window.location.href);
+ } else {
+ alert('Something went wrong :(');
+ }
+ });
+ } else {
+ alert('Something went wrong :(');
+ }
+ }).catch(error => {
+ alert(error.message);
+ });
+ e.preventDefault();
+ });
+ });
+
+ document.querySelectorAll('.msg-menu').forEach(function(el) {
+ el.addEventListener('click', function(e) {
+ var reply = e.target.closest('li');
+ var rid = reply ? parseInt(reply.id) : 0;
+ var message = e.target.closest('section');
+ var mid = message.getAttribute('data-mid') || e.target.closest('article').getAttribute('data-mid');
+ showMessageLinksDialog(mid, rid);
+ e.preventDefault();
+ });
+ });
+ document.querySelectorAll('.l .a-privacy').forEach(function(e) {
+ e.addEventListener('click', function(e) {
+ setPrivacy(
+ e.target,
+ e.target.closest('article').getAttribute('data-mid'));
+ e.preventDefault();
+ });
+ });
+ document.querySelectorAll('.ir a[data-fname], .msg-media a[data-fname]').forEach(function(el) {
+ el.addEventListener('click', function(e) {
+ let fname = e.target.closest('[data-fname]').getAttribute('data-fname');
+ if (!showPhotoDialog(fname)) {
+ e.preventDefault();
+ }
+ });
+ });
+ document.querySelectorAll('.social a').forEach(function(e) {
+ e.addEventListener('click', function(e) {
+ openSocialWindow(e.target);
+ e.preventDefault();
+ });
+ });
+ var username = document.getElementById('username');
+ if (username) {
+ username.addEventListener('blur', function() {
+ checkUsername();
+ });
+ }
+
+ document.querySelectorAll('.l .a-like').forEach(function(e) {
+ e.addEventListener('click', function(e) {
+ likeMessage(
+ e.target,
+ e.target.closest('article').getAttribute('data-mid'));
+ e.preventDefault();
+ });
+ });
+ document.querySelectorAll('.l .a-sub').forEach(function(e) {
+ e.addEventListener('click', function(e) {
+ subscribeMessage(
+ e.target,
+ document.getElementById('content').getAttribute('data-mid'));
+ e.preventDefault();
+ });
+ });
+ document.querySelectorAll('.a-login').forEach(function(el) {
+ el.addEventListener('click', function(e) {
+ openDialogLogin();
+ e.preventDefault();
+ });
+ });
+ var unfoldall = document.getElementById('unfoldall');
+ if (unfoldall) {
+ unfoldall.addEventListener('click', function(e) {
+ document.querySelectorAll('#replies>li').forEach(function(e) {
+ e.style.display = 'block';
+ });
+ document.querySelectorAll('#replies .msg-comments').forEach(function(e) {
+ e.style.display = 'none';
+ });
+ e.preventDefault();
+ });
+ }
+ document.querySelectorAll('article').forEach(function(article) {
+ if (Array.prototype.some.call(
+ article.querySelectorAll('.msg-tags a'),
+ function(a) {
+ return a.textContent === 'NSFW';
+ }
+ )) {
+ article.classList.add('nsfw');
+ }
+ });
+ document.querySelectorAll('[data-uri]').forEach(el => {
+ let dataUri = el.getAttribute('data-uri');
+ if (dataUri) {
+ setTimeout(() => fetchUserUri(dataUri, user => {
+ let header = el.closest('.msg-header');
+ header.querySelectorAll('.a-username').forEach(a => {
+ a.setAttribute('href', user.uri);
+ let img = a.querySelector('img');
+ if (img && user.avatar) {
+ img.setAttribute('src', user.avatar);
+ img.setAttribute('alt', user.uname);
+ }
+ let textNode = a.childNodes[0];
+ if (textNode.nodeType === Node.TEXT_NODE && textNode.nodeValue.trim().length > 0) {
+ let uname = document.createTextNode(user.uname);
+ a.replaceChild(uname, a.firstChild);
+ }
+ });
+ }), 100);
+ }
+ });
+ document.querySelectorAll('[data-user-uri]').forEach(el => {
+ let dataUri = el.getAttribute('href');
+ if (dataUri) {
+ setTimeout(() => fetchUserUri(dataUri, user => {
+ let textNode = el.childNodes[0];
+ if (textNode.nodeType === Node.TEXT_NODE && textNode.nodeValue.trim().length > 0) {
+ let uname = document.createTextNode(`@${user.uname}`);
+ el.replaceChild(uname, el.firstChild);
+ }
+ }), 100);
+ }
+ });
+ initES();
+
+ embedAll();
+ var elSelector = 'header',
+ elClassHidden = 'header--hidden',
+ elClassBackground = 'header--background',
+ throttleTimeout = 500,
+ element = document.querySelector(elSelector);
+
+ if (element) {
+
+ var dHeight = 0,
+ wHeight = 0,
+ wScrollCurrent = 0,
+ wScrollBefore = 0,
+ wScrollDiff = 0,
+
+ throttle = function(delay, fn) {
+ var last, deferTimer;
+ return function() {
+ var context = this, args = arguments, now = +new Date;
+ if (last && now < last + delay) {
+ clearTimeout(deferTimer);
+ deferTimer = setTimeout(
+ function() {
+ last = now;
+ fn.apply(context, args);
+ },
+ delay);
+ } else {
+ last = now;
+ fn.apply(context, args);
+ }
+ };
+ };
+
+ window.addEventListener('scroll', throttle(throttleTimeout, function() {
+ dHeight = document.body.offsetHeight;
+ wHeight = window.innerHeight;
+ wScrollCurrent = window.pageYOffset;
+ wScrollDiff = wScrollBefore - wScrollCurrent;
+
+ if (wScrollCurrent <= 0) {
+ // scrolled to the very top; element sticks to the top
+ element.classList.remove(elClassHidden);
+ element.classList.remove(elClassBackground);
+ } else if (wScrollDiff > 0 && element.classList.contains(elClassHidden)) {
+ // scrolled up; element slides in
+ element.classList.remove(elClassHidden);
+ element.classList.add(elClassBackground);
+ } else if (wScrollDiff < 0) {
+ // scrolled down
+ if (wScrollCurrent + wHeight >= dHeight && element.classList.contains(elClassHidden)) {
+ // scrolled to the very bottom; element slides in
+ element.classList.remove(elClassHidden);
+ element.classList.add(elClassBackground);
+ } else {
+ // scrolled down; element slides out
+ element.classList.add(elClassHidden);
+ }
+ }
+
+ wScrollBefore = wScrollCurrent;
+ }));
+ }
+});
diff --git a/src/main/assets/style.css b/src/main/assets/style.css
new file mode 100644
index 00000000..02c59aad
--- /dev/null
+++ b/src/main/assets/style.css
@@ -0,0 +1,956 @@
+/* #region generic */
+
+html,
+body,
+div,
+h1,
+h2,
+ul,
+li,
+p,
+form,
+input,
+textarea,
+pre {
+ margin: 0;
+ padding: 0;
+}
+textarea {
+ overflow: auto;
+}
+html,
+input,
+textarea {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ font-size: 12pt;
+ -webkit-font-smoothing: subpixel-antialiased;
+}
+h1,
+h2 {
+ font-weight: normal;
+}
+ul {
+ list-style-type: none;
+}
+a {
+ color: #069;
+ text-decoration: none;
+}
+img,
+hr {
+ border: none;
+}
+hr {
+ background: #CCC;
+ height: 1px;
+ margin: 10px 0;
+}
+pre {
+ background: #1e2028;
+ color: #41b645;
+ overflow-x: auto;
+ padding: 6px 20px;
+ white-space: pre;
+}
+pre::selection {
+ background: #41b645;
+ color: #1e2028;
+}
+pre::-moz-selection {
+ background: #41b645;
+ color: #1e2028;
+}
+.u {
+ text-decoration: underline;
+}
+
+/* #endregion */
+
+/* #region overall layout */
+
+html {
+ background: #f8f8f8;
+ color: #222;
+}
+#wrapper {
+ margin: 0 auto;
+ width: 1000px;
+ margin-top: 52px;
+}
+#column {
+ float: left;
+ margin-left: 10px;
+ overflow: hidden;
+ padding-top: 10px;
+ width: 240px;
+}
+#content {
+ float: right;
+ margin: 12px 0 0 0;
+ width: 728px;
+}
+#minimal_content {
+ margin: 0 auto;
+ min-width: 310px;
+ width: auto;
+}
+*::selection {
+ background: #006699;
+ color: #fff;
+}
+body > header {
+ position: fixed;
+ top: 0;
+ width: 100%;
+ z-index: 10;
+ transition-duration: 0.5s;
+ transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ transition-property: transform;
+}
+@supports (backdrop-filter: blur(10px)) {
+ body > header--background {
+ background: rgba(255, 255, 255, 0.8);
+ backdrop-filter: blur(10px);
+ }
+}
+#header_wrapper {
+ margin: 0 auto;
+ width: 1000px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ padding: 4px;
+}
+.header--background {
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.28);
+ background: #fff;
+}
+.header--hidden {
+ transform: translateY(-100%);
+}
+#footer {
+ clear: both;
+ color: #999;
+ font-size: 10pt;
+ margin: 40px;
+ padding: 10px 0;
+}
+
+@media screen and (max-width: 850px) {
+ body {
+ text-size-adjust: 100%;
+ }
+ body,
+ #wrapper,
+ #topwrapper,
+ #content,
+ #footer {
+ float: none;
+ margin: 0 auto;
+ min-width: 310px;
+ width: auto;
+ }
+ #wrapper {
+ margin-top: 50px;
+ }
+ body > header {
+ margin-bottom: 15px;
+ }
+ #column {
+ float: none;
+ margin: 0 10px;
+ padding-top: 0;
+ width: auto;
+ }
+}
+
+/* #endregion */
+
+/* #region header internals */
+
+#logo {
+ height: 36px;
+ width: 110px;
+}
+#logo a {
+ background: url("logo@2x.png") no-repeat;
+ background-size: cover;
+ border: 0;
+ display: block;
+ height: 36px;
+ overflow: hidden;
+ text-indent: 100%;
+ white-space: nowrap;
+ width: 110px;
+}
+#global {
+ display: flex;
+}
+#global a {
+ color: #888;
+ display: inline-block;
+ font-size: 13pt;
+ padding: 14px 6px;
+}
+#global li {
+ display: inline-block;
+}
+#ctitle a {
+ padding: 14px;
+}
+#global li:hover,
+#ctitle a:hover,
+.l a:hover {
+ background-color: #fff;
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.16);
+ cursor: pointer;
+ transition: box-shadow 0.2s ease-in;
+}
+#search input {
+ background: #FFF;
+ border: 1px solid #ccc;
+ outline: none !important;
+ padding: 4px;
+ -webkit-appearance: none;
+ border-radius: 0;
+}
+
+/* #endregion */
+
+/* #region left column internals */
+
+.toolbar {
+ border-top: 1px solid #CCC;
+}
+
+#column ul,
+#column p,
+#column hr {
+ margin: 10px 0;
+}
+#column li > a {
+ display: block;
+ height: 100%;
+ padding: 6px;
+}
+#column li > a:hover {
+ background-color: #fff;
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.16);
+ transition: background-color 0.2s ease-in;
+}
+#column .margtop {
+ margin-top: 15px;
+}
+
+#column .tags {
+ background: #fff;
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.16);
+ line-height: 140%;
+ padding: 6px;
+ text-align: justify;
+}
+#column .inp {
+ background: #fff;
+ border: 1px solid #ddddd5;
+ outline: none !important;
+ padding: 4px;
+ width: 222px;
+}
+#column .tags h4 {
+ background: #eee;
+ border: 1px solid #eee;
+ color: #888;
+ display: block;
+ text-align: center;
+}
+#ctitle {
+ font-size: 14pt;
+}
+#ctitle img {
+ margin-right: 5px;
+ vertical-align: middle;
+ max-width: 48px;
+ max-height: 48px;
+}
+#ustats li {
+ font-size: 10pt;
+ margin: 3px 0;
+}
+#column table.iread {
+ width: 100%;
+}
+#column table.iread td {
+ text-align: center;
+}
+#column table.iread img {
+ height: 48px;
+ width: 48px;
+}
+
+/* #endregion */
+
+/* #region main content */
+#content > p,
+#content > h1,
+#content > h2,
+#minimal_content > p,
+#minimal_content > h1,
+#minimal_content > h2 {
+ margin: 1em 0;
+}
+.page {
+ background: #eee;
+ padding: 6px;
+ text-align: center;
+}
+
+.page a {
+ color: #888;
+}
+
+/* #endregion */
+
+/* #region article, message internals */
+
+article {
+ background: #fff;
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.16);
+ line-height: 140%;
+ margin-bottom: 10px;
+ padding: 20px;
+}
+article time {
+ color: #999;
+ font-size: 10pt;
+}
+article p {
+ clear: left;
+ margin: 5px 0 15px 0;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+article .ir {
+ text-align: center;
+}
+article .ir a {
+ cursor: zoom-in;
+ display: block;
+}
+article .ir img {
+ max-width: 100%;
+}
+article > nav.l,
+.msg-cont > nav.l {
+ border-top: 1px solid #eee;
+ display: flex;
+ justify-content: space-around;
+ font-size: 10pt;
+}
+article > nav.l a,
+.msg-cont > nav.l a {
+ color: #888;
+ margin-right: 15px;
+}
+article .likes {
+ padding-left: 20px;
+}
+article .replies {
+ margin-left: 18px;
+}
+article .tags {
+ margin-top: 3px;
+}
+.msg-tags {
+ margin-top: 12px;
+ min-height: 1px;
+}
+article .tags > a,
+.badge,
+.msg-tags > a {
+ background: #eee;
+ border: 1px solid #eee;
+ color: #888;
+ display: inline-block;
+ font-size: 10pt;
+ margin-bottom: 5px;
+ margin-right: 5px;
+ padding: 0 10px;
+}
+.l .msg-button {
+ align-items: center;
+ display: flex;
+ flex-basis: 0;
+ flex-direction: column;
+ flex-grow: 1;
+ padding-top: 12px;
+}
+.l .msg-button-icon {
+ font-weight: bold;
+}
+.msgthread {
+ margin-bottom: 0;
+}
+.msg-avatar {
+ float: left;
+ max-height: 48px;
+ margin-right: 10px;
+ max-width: 48px;
+}
+.msg-avatar img {
+ max-height: 48px;
+ vertical-align: top;
+ max-width: 48px;
+}
+.msg-cont {
+ background: #FFF;
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.16);
+ line-height: 140%;
+ margin-bottom: 12px;
+ padding: 20px;
+ width: 640px;
+}
+.reply-new .msg-cont {
+ border-right: 5px solid #0C0;
+}
+.msg-ts {
+ font-size: small;
+ vertical-align: top;
+}
+.msg-ts,
+.msg-ts > a {
+ color: #999;
+}
+.msg-txt {
+ clear: both;
+ margin: 0 0 12px;
+ padding-top: 10px;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+.msg-media {
+ text-align: center;
+}
+.msg-links {
+ color: #999;
+ font-size: small;
+ margin: 5px 0 0 0;
+}
+.msg-comments {
+ color: #AAA;
+ font-size: small;
+ margin-top: 10px;
+ overflow: hidden;
+ text-indent: 10px;
+}
+.ta-wrapper {
+ border: 1px solid #DDD;
+ display: flex;
+ flex-grow: 1;
+}
+.msg-comment {
+ display: flex;
+ width: 100%;
+ margin-top: 10px;
+}
+.msg-comment-hidden {
+ display: none;
+}
+.msg-comment textarea {
+ border: 0;
+ flex-grow: 1;
+ outline: none !important;
+ padding: 4px;
+ resize: vertical;
+ vertical-align: top;
+}
+.attach-photo {
+ cursor: pointer;
+}
+.attach-photo-active {
+ color: green;
+}
+.msg-comment input {
+ align-self: flex-start;
+ background: #EEE;
+ border: 1px solid #CCC;
+ color: #999;
+ margin: 0 0 0 6px;
+ position: -webkit-sticky;
+ position: sticky;
+ top: 70px;
+ vertical-align: top;
+ width: 50px;
+}
+.msg-recomms {
+ color: #AAA;
+ font-size: small;
+ margin-top: 10px;
+ overflow: hidden;
+ text-indent: 10px;
+}
+#replies .msg-txt,
+#private-messages .msg-txt {
+ margin: 0;
+}
+.title2 {
+ background: #fff;
+ margin: 20px 0;
+ padding: 10px 20px;
+ width: 640px;
+}
+.title2-right {
+ float: right;
+ line-height: 24px;
+}
+#content .title2 h2 {
+ font-size: x-large;
+ margin: 0;
+}
+
+@media screen and (max-width: 850px) {
+ #header_wrapper {
+ width: auto;
+ }
+ #global {
+ justify-content: space-around;
+ flex-grow: 1;
+ }
+ #search {
+ padding: 4px;
+ }
+ article {
+ overflow: auto;
+ }
+ article p {
+ margin: 10px 0 8px 0;
+ }
+ .msg,
+ .msg-cont {
+ min-width: 280px;
+ width: auto;
+ }
+ .msg-cont {
+ margin: 8px 0;
+ }
+ .msg-media {
+ overflow: auto;
+ }
+ .title2 h2 {
+ font-size: large;
+ }
+ .msg-comment {
+ flex-direction: column;
+ }
+ .msg-comment input {
+ align-self: flex-end;
+ margin: 6px 0 0 0;
+ width: 100px;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ #wrapper {
+ margin-top: 104px;
+ }
+ #search {
+ display: none;
+ }
+ #global a {
+ padding: 14px 2px;
+ font-size: 11pt;
+ }
+ .msg-cont > nav.l,
+ article > nav.l {
+ font-size: 9pt;
+ }
+ .msg-txt {
+ padding-top: 5px;
+ }
+ .title2 {
+ font-size: 11pt;
+ width: auto;
+ }
+ #content .title2 h2 {
+ font-size: 11pt;
+ }
+ .title2-right {
+ line-height: initial;
+ }
+}
+
+/* #endregion */
+
+/* #region user-generated texts */
+
+q:before,
+q:after {
+ content: "";
+}
+q,
+blockquote {
+ border-left: 3px solid #CCC;
+ color: #666;
+ display: block;
+ margin: 10px 0 10px 10px;
+ padding-left: 10px;
+}
+
+/* #endregion */
+
+/* #region new message form internals */
+
+#newmessage {
+ background: #E5E5E0;
+ margin-bottom: 20px;
+ padding: 15px;
+}
+#newmessage textarea {
+ border: 1px solid #CCC;
+ box-sizing: border-box;
+ margin: 0 0 5px 0;
+ margin-top: 20px;
+ max-height: 6em;
+ min-width: 280px;
+ padding: 4px;
+ width: 100%;
+}
+#newmessage input {
+ border: 1px solid #CCC;
+ margin: 5px 0;
+ padding: 2px 4px;
+}
+#newmessage .img {
+ width: 500px;
+}
+#newmessage .tags {
+ width: 500px;
+}
+#newmessage .subm {
+ background: #EEEEE5;
+ width: 150px;
+}
+@media screen and (max-width: 850px) {
+ #newmessage .img,
+ #newmessage .tags {
+ width: 100%;
+ }
+}
+
+/* #endregion */
+
+/* #region user lists */
+
+.users {
+ margin: 10px 0;
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+}
+.users > span {
+ overflow: hidden;
+ padding: 6px 0;
+ width: 200px;
+}
+.users img {
+ height: 32px;
+ margin-right: 6px;
+ vertical-align: middle;
+ width: 32px;
+}
+
+/* #endregion */
+
+/* #region signup form */
+
+.signup-h1 > img {
+ margin-right: 10px;
+ vertical-align: middle;
+}
+.signup-h1 {
+ font-size: x-large;
+ margin: 20px 0 10px 0;
+}
+.signup-h2 {
+ font-size: large;
+ margin: 10px 0 5px 0;
+}
+.signup-hr {
+ margin: 20px 0;
+}
+
+/* #endregion */
+
+/* #region PM */
+
+.newpm {
+ margin: 20px 60px 30px 60px;
+}
+.newpm textarea {
+ resize: vertical;
+ width: 100%;
+}
+.newpm-send input {
+ width: 100px;
+}
+
+/* #endregion */
+
+/* #region popup dialog (lightbox) */
+
+#dialogb {
+ background: #222;
+ height: 100%;
+ left: 0;
+ opacity: 0.6;
+ position: fixed;
+ top: 0;
+ width: 100%;
+ z-index: 10;
+}
+#dialogt {
+ height: 100%;
+ left: 0;
+ position: fixed;
+ top: 0;
+ width: 100%;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+#dialogw {
+ z-index: 11;
+ max-width: 96%;
+ max-height: calc(100% - 100px);
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+#dialogw a {
+ display: block;
+}
+#dialogw img {
+ max-height: 100%;
+ max-height: 90vh;
+ max-width: 100%;
+}
+#dialog_header {
+ width: 100%;
+ height: 44px;
+ position: fixed;
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: center;
+}
+.header_image {
+ background: rgba(0, 0, 0, 0.28);
+}
+#dialogc {
+ cursor: pointer;
+ color: #ccc;
+ padding-right: 6px;
+}
+.dialoglogin {
+ background: #fff;
+ padding: 25px;
+ width: 300px;
+}
+.dialog-opened {
+ overflow: hidden;
+}
+#signemail,
+#signfb,
+#signvk {
+ display: block;
+ line-height: 32px;
+ margin: 10px 0;
+ text-decoration: none;
+ width: 100%;
+}
+#signvk {
+ margin-bottom: 30px;
+}
+.dialoglogin form {
+ margin-top: 7px;
+}
+.signinput,
+.signsubmit {
+ border: 1px solid #CCC;
+ margin: 3px 0;
+ padding: 3px;
+}
+.signinput {
+ width: 292px;
+}
+.signsubmit {
+ width: 70px;
+}
+.dialogshare {
+ background: #fff;
+ min-width: 300px;
+ overflow: auto;
+ padding: 20px;
+}
+.dialogl {
+ background: #fff;
+ border: 1px solid #DDD;
+ margin: 3px 0 20px;
+ padding: 5px;
+}
+.dialogshare li {
+ float: left;
+ margin: 5px 10px 0 0;
+}
+.dialogshare a {
+ display: block;
+}
+.dialogtxt {
+ background: #fff;
+ padding: 20px;
+}
+
+@media screen and (max-width: 480px) {
+ .dialog-opened {
+ position: fixed;
+ width: 100%;
+ }
+}
+
+/* #endregion */
+
+/* #region misc */
+
+#wsthread {
+ background: #CCC;
+ bottom: 20px;
+ cursor: pointer;
+ display: none;
+ padding: 5px 10px;
+ position: fixed;
+ right: 20px;
+}
+.sharenew {
+ display: inline-block;
+ line-height: 32px;
+ min-height: 32px;
+ min-width: 200px;
+ padding: 0 12px 0 37px;
+}
+.icon {
+ margin-top: -2px;
+ vertical-align: middle;
+}
+.icon--ei-link {
+ margin-top: -1px;
+}
+.icon--ei-comment {
+ margin-top: -5px;
+}
+.newmessage {
+ /* textarea on the /post page */
+ border: 1px solid #DDD;
+ padding: 2px;
+ resize: vertical;
+ width: 100%;
+}
+
+/* #endregion */
+
+/* #region footer internals */
+
+#footer-social {
+ float: left;
+}
+#footer-social a {
+ border: 0;
+ display: inline-block;
+}
+#footer-left {
+ margin-left: 286px;
+ margin-right: 350px;
+}
+#footer-right {
+ float: right;
+}
+
+@media screen and (max-width: 850px) {
+ #footer {
+ margin: 0 10px;
+ }
+ #footer div {
+ float: none;
+ margin: 10px 0;
+ }
+}
+
+/* #endregion */
+
+/* #region settings */
+
+fieldset {
+ border: 1px dotted #ccc;
+ margin-top: 25px;
+}
+
+/* #endregion */
+
+/* #region embeds */
+
+.embedContainer {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ margin: 30px -3px 15px -3px;
+}
+.embedContainer > * {
+ box-sizing: border-box;
+ flex-grow: 1;
+ margin: 3px;
+ min-width: 49%;
+}
+.embedContainer > .compact {
+ flex-grow: 0;
+}
+.embedContainer .picture img {
+ display: block;
+}
+.embedContainer img,
+.embedContainer video {
+ max-width: 100%;
+ max-height: 80vh;
+}
+.embedContainer > .audio,
+.embedContainer > .youtube {
+ min-width: 90%;
+}
+.embedContainer audio {
+ width: 100%;
+}
+.embedContainer iframe {
+ overflow: hidden;
+ resize: vertical;
+ display: block;
+}
+
+/* #endregion */
+
+/* #region nsfw */
+
+article.nsfw .embedContainer img,
+article.nsfw .embedContainer video,
+article.nsfw .embedContainer iframe,
+article.nsfw .ir img {
+ opacity: 0.1;
+}
+article.nsfw .embedContainer img:hover,
+article.nsfw .embedContainer video:hover,
+article.nsfw .embedContainer iframe:hover,
+article.nsfw .ir img:hover {
+ opacity: 1;
+}
+
+/* #endregion */
diff --git a/src/main/java/com/cliqset/xrd/Alias.java b/src/main/java/com/cliqset/xrd/Alias.java
new file mode 100644
index 00000000..49e4052b
--- /dev/null
+++ b/src/main/java/com/cliqset/xrd/Alias.java
@@ -0,0 +1,62 @@
+/*
+ Copyright 2010 Cliqset Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package com.cliqset.xrd;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAnyAttribute;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+import javax.xml.namespace.QName;
+
+@XmlType(name="Alias", namespace=XRDConstants.XRD_NAMESPACE)
+@XmlAccessorType(XmlAccessType.FIELD)
+public class Alias {
+
+ @XmlAnyAttribute()
+ private Map unknownAttributes;
+
+ @XmlValue
+ private URI value;
+
+ public void setValue(URI value) {
+ this.value = value;
+ }
+
+ public URI getValue() {
+ return value;
+ }
+
+ public void setUnknownAttributes(Map unknownAttributes) {
+ this.unknownAttributes = unknownAttributes;
+ }
+
+ public Map getUnknownAttributes() {
+ if (null == this.unknownAttributes) {
+ this.unknownAttributes = new HashMap();
+ }
+ return unknownAttributes;
+ }
+
+ public boolean hasUnknownAttributes() {
+ return !(null == this.unknownAttributes || this.unknownAttributes.size() < 1);
+ }
+}
diff --git a/src/main/java/com/cliqset/xrd/Expires.java b/src/main/java/com/cliqset/xrd/Expires.java
new file mode 100644
index 00000000..b4bcdd24
--- /dev/null
+++ b/src/main/java/com/cliqset/xrd/Expires.java
@@ -0,0 +1,58 @@
+/*
+ Copyright 2010 Cliqset Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package com.cliqset.xrd;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.namespace.QName;
+import javax.xml.bind.annotation.*;
+
+@XmlType(name="Expires", namespace=XRDConstants.XRD_NAMESPACE)
+@XmlAccessorType(XmlAccessType.FIELD)
+public class Expires {
+
+ @XmlAnyAttribute
+ private Map unknownAttributes;
+
+ @XmlValue
+ private Date value;
+
+ public void setValue(Date value) {
+ this.value = value;
+ }
+
+ public Date getValue() {
+ return value;
+ }
+
+ public void setUnknownAttributes(Map unknownAttributes) {
+ this.unknownAttributes = unknownAttributes;
+ }
+
+ public Map getUnknownAttributes() {
+ if (null == this.unknownAttributes) {
+ this.unknownAttributes = new HashMap();
+ }
+ return unknownAttributes;
+ }
+
+ public boolean hasUnknownAttributes() {
+ return !(null == this.unknownAttributes || this.unknownAttributes.size() < 1);
+ }
+}
diff --git a/src/main/java/com/cliqset/xrd/Link.java b/src/main/java/com/cliqset/xrd/Link.java
new file mode 100644
index 00000000..ec8522f0
--- /dev/null
+++ b/src/main/java/com/cliqset/xrd/Link.java
@@ -0,0 +1,151 @@
+/*
+ Copyright 2010 Cliqset Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package com.cliqset.xrd;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.namespace.QName;
+import javax.xml.bind.annotation.XmlAnyElement;
+import javax.xml.bind.annotation.XmlAnyAttribute;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+
+import org.w3c.dom.Element;
+
+@XmlType(name="Link", namespace=XRDConstants.XRD_NAMESPACE)
+@XmlAccessorType(XmlAccessType.FIELD)
+public class Link {
+
+ @XmlAttribute(name="rel")
+ private URI rel;
+
+ @XmlAttribute(name="type")
+ private String type;
+
+ @XmlAttribute(name="href")
+ private URI href;
+
+ @XmlAttribute(name="template")
+ private String template;
+
+ @XmlElement(name="Title")
+ private List titles;
+
+ @XmlElement(name="Property")
+ private List properties;
+
+ @XmlAnyElement
+ private List unknownElements;
+
+ @XmlAnyAttribute
+ private Map unknownAttributes;
+
+ private URI processedTemplate;
+
+ public void setRel(URI rel) {
+ this.rel = rel;
+ }
+
+ public URI getRel() {
+ return rel;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setHref(URI href) {
+ this.href = href;
+ }
+
+ public URI getHref() {
+ return href;
+ }
+
+ public void setTemplate(String template) {
+ this.template = template;
+ }
+
+ public String getTemplate() {
+ return template;
+ }
+
+ public void setTitles(List titles) {
+ this.titles = titles;
+ }
+
+ public List getTitles() {
+ return titles;
+ }
+
+ public void setProperties(List properties) {
+ this.properties = properties;
+ }
+
+ public List getProperties() {
+ return properties;
+ }
+
+ public void setUnknownElements(List unknownElements) {
+ this.unknownElements = unknownElements;
+ }
+
+ public List getUnknownElements() {
+ return unknownElements;
+ }
+
+ public void setUnknownAttributes(Map unknownAttributes) {
+ this.unknownAttributes = unknownAttributes;
+ }
+
+ public Map getUnknownAttributes() {
+ if (null == this.unknownAttributes) {
+ this.unknownAttributes = new HashMap();
+ }
+ return unknownAttributes;
+ }
+
+ public boolean hasTemplate() {
+ return null != this.template;
+ }
+
+ public boolean hasHref() {
+ return null != this.href;
+ }
+
+ public boolean hasUnknownAttributes() {
+ return !(null == this.unknownAttributes || this.unknownAttributes.size() < 1);
+ }
+
+ public void setProcessedTemplate(URI processedTemplate) {
+ this.processedTemplate = processedTemplate;
+ }
+
+ public URI getProcessedTemplate() {
+ return processedTemplate;
+ }
+}
diff --git a/src/main/java/com/cliqset/xrd/Property.java b/src/main/java/com/cliqset/xrd/Property.java
new file mode 100644
index 00000000..35c7d0cc
--- /dev/null
+++ b/src/main/java/com/cliqset/xrd/Property.java
@@ -0,0 +1,75 @@
+/*
+ Copyright 2010 Cliqset Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package com.cliqset.xrd;
+
+import java.net.URI;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAnyAttribute;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+import javax.xml.namespace.QName;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@XmlType(name="Property", namespace=XRDConstants.XRD_NAMESPACE)
+@XmlAccessorType(XmlAccessType.FIELD)
+public class Property {
+
+ @XmlAttribute(name="type", required=true)
+ private URI type;
+
+ @XmlAnyAttribute()
+ private Map unknownAttributes;
+
+ @XmlValue()
+ private URI value;
+
+ public void setType(URI type) {
+ this.type = type;
+ }
+
+ public URI getType() {
+ return type;
+ }
+
+ public void setValue(URI value) {
+ this.value = value;
+ }
+
+ public URI getValue() {
+ return value;
+ }
+
+ public void setUnknownAttributes(Map unknownAttributes) {
+ this.unknownAttributes = unknownAttributes;
+ }
+
+ public Map getUnknownAttributes() {
+ if (null == this.unknownAttributes) {
+ this.unknownAttributes = new HashMap();
+ }
+ return unknownAttributes;
+ }
+
+ public boolean hasUnknownAttributes() {
+ return !(null == this.unknownAttributes || this.unknownAttributes.size() < 1);
+ }
+}
diff --git a/src/main/java/com/cliqset/xrd/Signature.java b/src/main/java/com/cliqset/xrd/Signature.java
new file mode 100644
index 00000000..f52f9218
--- /dev/null
+++ b/src/main/java/com/cliqset/xrd/Signature.java
@@ -0,0 +1,21 @@
+/*
+ Copyright 2010 Cliqset Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package com.cliqset.xrd;
+
+public class Signature {
+
+}
diff --git a/src/main/java/com/cliqset/xrd/Subject.java b/src/main/java/com/cliqset/xrd/Subject.java
new file mode 100644
index 00000000..f6815317
--- /dev/null
+++ b/src/main/java/com/cliqset/xrd/Subject.java
@@ -0,0 +1,62 @@
+/*
+ Copyright 2010 Cliqset Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package com.cliqset.xrd;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAnyAttribute;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+import javax.xml.namespace.QName;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+@XmlType(name="Subject", namespace=XRDConstants.XRD_NAMESPACE)
+@XmlAccessorType(XmlAccessType.FIELD)
+public class Subject {
+
+ @XmlAnyAttribute()
+ private Map unknownAttributes;
+
+ @XmlValue
+ private URI value;
+
+ public void setValue(URI value) {
+ this.value = value;
+ }
+
+ public URI getValue() {
+ return value;
+ }
+
+ public void setUnknownAttributes(Map unknownAttributes) {
+ this.unknownAttributes = unknownAttributes;
+ }
+
+ public Map getUnknownAttributes() {
+ if (null == this.unknownAttributes) {
+ this.unknownAttributes = new HashMap();
+ }
+ return unknownAttributes;
+ }
+
+ public boolean hasUnknownAttributes() {
+ return !(null == this.unknownAttributes || this.unknownAttributes.size() < 1);
+ }
+}
diff --git a/src/main/java/com/cliqset/xrd/Title.java b/src/main/java/com/cliqset/xrd/Title.java
new file mode 100644
index 00000000..7d6597bd
--- /dev/null
+++ b/src/main/java/com/cliqset/xrd/Title.java
@@ -0,0 +1,68 @@
+/*
+ Copyright 2010 Cliqset Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package com.cliqset.xrd;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.bind.annotation.*;
+import javax.xml.namespace.QName;
+
+@XmlType(name="Title", namespace=XRDConstants.XRD_NAMESPACE)
+@XmlAccessorType(XmlAccessType.FIELD)
+public class Title {
+
+ @XmlAttribute(name="lang", namespace=XRDConstants.XML_NAMESPACE)
+ private String lang;
+
+ @XmlAnyAttribute()
+ private Map unknownAttributes;
+
+ @XmlValue
+ private String value;
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setLang(String lang) {
+ this.lang = lang;
+ }
+
+ public String getLang() {
+ return lang;
+ }
+
+ public void setUnknownAttributes(Map unknownAttributes) {
+ this.unknownAttributes = unknownAttributes;
+ }
+
+ public Map getUnknownAttributes() {
+ if (null == this.unknownAttributes) {
+ this.unknownAttributes = new HashMap();
+ }
+ return unknownAttributes;
+ }
+
+ public boolean hasUnknownAttributes() {
+ return !(null == this.unknownAttributes || this.unknownAttributes.size() < 1);
+ }
+}
diff --git a/src/main/java/com/cliqset/xrd/XRD.java b/src/main/java/com/cliqset/xrd/XRD.java
new file mode 100644
index 00000000..393e977b
--- /dev/null
+++ b/src/main/java/com/cliqset/xrd/XRD.java
@@ -0,0 +1,166 @@
+/*
+ Copyright 2010 Cliqset Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package com.cliqset.xrd;
+
+import javax.xml.namespace.QName;
+import javax.xml.bind.annotation.*;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+
+import org.w3c.dom.Element;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+@XmlRootElement(name="XRD", namespace=XRDConstants.XRD_NAMESPACE)
+@XmlAccessorType(XmlAccessType.FIELD)
+public class XRD {
+
+ @XmlAttribute(name="id", namespace=XRDConstants.XML_NAMESPACE)
+ private String id;
+
+ @XmlAnyAttribute
+ private Map unknownAttributes;
+
+ @XmlElement(name="Expires", namespace=XRDConstants.XRD_NAMESPACE)
+ private Expires expires;
+
+ @XmlElement(name="Subject", namespace=XRDConstants.XRD_NAMESPACE)
+ private Subject subject;
+
+ @XmlElement(name="Alias", namespace=XRDConstants.XRD_NAMESPACE)
+ private List aliases;
+
+ @XmlElement(name="Property", namespace=XRDConstants.XRD_NAMESPACE)
+ private List properties;
+
+ @XmlElement(name="Link", namespace=XRDConstants.XRD_NAMESPACE)
+ private List links;
+
+ @XmlElement(name="Signature", namespace=XRDConstants.XML_SIG_NAMESPACE)
+ private List signatures;
+
+ @XmlAnyElement
+ private List unknownElements;
+
+ public void setExpires(Expires expires) {
+ this.expires = expires;
+ }
+
+ public Expires getExpires() {
+ return expires;
+ }
+
+ public void setSubject(Subject subject) {
+ this.subject = subject;
+ }
+
+ public Subject getSubject() {
+ return subject;
+ }
+
+ public void setAliases(List aliases) {
+ this.aliases = aliases;
+ }
+
+ public List getAliases() {
+ return aliases;
+ }
+
+ public void setProperties(List properties) {
+ this.properties = properties;
+ }
+
+ public List getProperties() {
+ return properties;
+ }
+
+ public void setLinks(List links) {
+ this.links = links;
+ }
+
+ public List getLinks() {
+ return links;
+ }
+
+ public void setSignatures(List signatures) {
+ this.signatures = signatures;
+ }
+
+ public List getSignatures() {
+ return signatures;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setUnknownAttributes(Map unknownAttributes) {
+ this.unknownAttributes = unknownAttributes;
+ }
+
+ public Map getUnknownAttributes() {
+ return unknownAttributes;
+ }
+
+ public void setUnknownElements(List unknownElements) {
+ this.unknownElements = unknownElements;
+ }
+
+ public List getUnknownElements() {
+ return unknownElements;
+ }
+
+ public boolean hasId() {
+ return null != this.id;
+ }
+
+ public boolean hasExpires() {
+ return null != this.expires;
+ }
+
+ public boolean hasSubject() {
+ return null != this.subject;
+ }
+
+ public boolean hasAliases() {
+ return null != this.aliases;
+ }
+
+ public boolean hasProperties() {
+ return null != this.properties;
+ }
+
+ public boolean hasLinks() {
+ return null != this.links;
+ }
+
+ public static XRD fromStream(InputStream stream) throws XRDException {
+ JAXBContext context;
+ try {
+ context = JAXBContext.newInstance(XRD.class);
+ return (XRD)context.createUnmarshaller().unmarshal(stream);
+ } catch (JAXBException e) {
+ throw new XRDException("Unable to deserialize stream into XRD", e);
+ }
+ }
+}
diff --git a/src/main/java/com/cliqset/xrd/XRDConstants.java b/src/main/java/com/cliqset/xrd/XRDConstants.java
new file mode 100644
index 00000000..39e3c584
--- /dev/null
+++ b/src/main/java/com/cliqset/xrd/XRDConstants.java
@@ -0,0 +1,26 @@
+/*
+ Copyright 2010 Cliqset Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package com.cliqset.xrd;
+
+public class XRDConstants {
+
+ public static final String XRD_NAMESPACE = "http://docs.oasis-open.org/ns/xri/xrd-1.0";
+ public static final String XML_SIG_NAMESPACE = "";
+ public static final String XML_NAMESPACE = "";
+
+ public static final String XRD_MEDIA_TYPE = "application/xrd+xml";
+}
diff --git a/src/main/java/com/cliqset/xrd/XRDException.java b/src/main/java/com/cliqset/xrd/XRDException.java
new file mode 100644
index 00000000..da1e6849
--- /dev/null
+++ b/src/main/java/com/cliqset/xrd/XRDException.java
@@ -0,0 +1,35 @@
+/*
+ Copyright 2010 Cliqset Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package com.cliqset.xrd;
+
+@SuppressWarnings("serial")
+public class XRDException extends Exception {
+
+ public XRDException() {}
+
+ public XRDException(String message) {
+ super(message);
+ }
+
+ public XRDException(Throwable cause) {
+ super(cause);
+ }
+
+ public XRDException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/cliqset/xrd/package-info.java b/src/main/java/com/cliqset/xrd/package-info.java
new file mode 100644
index 00000000..bd8f0146
--- /dev/null
+++ b/src/main/java/com/cliqset/xrd/package-info.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+@XmlSchema(
+ namespace=XRD_NAMESPACE,
+ elementFormDefault = XmlNsForm.QUALIFIED,
+ xmlns={
+ @XmlNs(prefix= StringUtils.EMPTY, namespaceURI=XRD_NAMESPACE)
+ }
+)
+package com.cliqset.xrd;
+
+import org.apache.commons.lang3.StringUtils;
+
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlNsForm;
+import javax.xml.bind.annotation.XmlSchema;
+
+import static com.cliqset.xrd.XRDConstants.XRD_NAMESPACE;
\ No newline at end of file
diff --git a/src/main/java/com/juick/ApiServer.java b/src/main/java/com/juick/ApiServer.java
new file mode 100644
index 00000000..fb2d9701
--- /dev/null
+++ b/src/main/java/com/juick/ApiServer.java
@@ -0,0 +1,16 @@
+package com.juick;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+
+@SpringBootApplication
+@EnableAutoConfiguration(exclude = { MailSenderAutoConfiguration.class })
+@ComponentScan(basePackages = {"com.juick.server", "com.juick.service"})
+public class ApiServer {
+ public static void main(String[] args) {
+ SpringApplication.run(ApiServer.class, args);
+ }
+}
diff --git a/src/main/java/com/juick/Attachment.java b/src/main/java/com/juick/Attachment.java
new file mode 100644
index 00000000..76f2995a
--- /dev/null
+++ b/src/main/java/com/juick/Attachment.java
@@ -0,0 +1,58 @@
+package com.juick;
+
+public class Attachment {
+ private String url;
+ private Integer height;
+ private Integer width;
+ private Attachment small;
+ private Attachment medium;
+ private Attachment thumbnail;
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public Integer getHeight() {
+ return height;
+ }
+
+ public void setHeight(Integer height) {
+ this.height = height;
+ }
+
+ public Integer getWidth() {
+ return width;
+ }
+
+ public void setWidth(Integer width) {
+ this.width = width;
+ }
+
+ public Attachment getSmall() {
+ return small;
+ }
+
+ public void setSmall(Attachment small) {
+ this.small = small;
+ }
+
+ public Attachment getMedium() {
+ return medium;
+ }
+
+ public void setMedium(Attachment medium) {
+ this.medium = medium;
+ }
+
+ public Attachment getThumbnail() {
+ return thumbnail;
+ }
+
+ public void setThumbnail(Attachment thumbnail) {
+ this.thumbnail = thumbnail;
+ }
+}
diff --git a/src/main/java/com/juick/Chat.java b/src/main/java/com/juick/Chat.java
new file mode 100644
index 00000000..59c0a2dc
--- /dev/null
+++ b/src/main/java/com/juick/Chat.java
@@ -0,0 +1,27 @@
+package com.juick;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import java.time.Instant;
+
+public class Chat extends User {
+ private Instant lastMessageTimestamp;
+ private String lastMessageText;
+
+ public Instant getLastMessageTimestamp() {
+ return lastMessageTimestamp;
+ }
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
+ public void setLastMessageTimestamp(Instant lastMessageTimestamp) {
+ this.lastMessageTimestamp = lastMessageTimestamp;
+ }
+
+ public String getLastMessageText() {
+ return lastMessageText;
+ }
+
+ public void setLastMessageText(String lastMessageText) {
+ this.lastMessageText = lastMessageText;
+ }
+}
diff --git a/src/main/java/com/juick/ExternalToken.java b/src/main/java/com/juick/ExternalToken.java
new file mode 100644
index 00000000..f6094478
--- /dev/null
+++ b/src/main/java/com/juick/ExternalToken.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Created by vitalyster on 22.11.2016.
+ */
+public class ExternalToken {
+ private String name;
+ private String type;
+ private String token;
+ private String secret;
+
+ @JsonCreator
+ public ExternalToken(@JsonProperty("name") String name,
+ @JsonProperty("type") String type,
+ @JsonProperty("token") String token,
+ @JsonProperty("secret") String secret) {
+ this.name = name;
+ this.type = type;
+ this.token = token;
+ this.secret = secret;
+ if (this.type == null) {
+ throw new IllegalStateException("Token must have type");
+ }
+ if (this.token == null) {
+ throw new IllegalStateException("Token must have value");
+ }
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getSecret() {
+ return secret;
+ }
+}
diff --git a/src/main/java/com/juick/Message.java b/src/main/java/com/juick/Message.java
new file mode 100644
index 00000000..bd2c91b5
--- /dev/null
+++ b/src/main/java/com/juick/Message.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.juick;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.juick.adapters.SimpleDateAdapter;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
+import javax.xml.bind.annotation.*;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.net.URI;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * @author Ugnich Anton
+ */
+@XmlRootElement(name = "juick", namespace = "http://juick.com/message")
+@XmlAccessorType()
+public class Message implements Comparable {
+ private int mid = 0;
+ private int rid = 0;
+ private int replyto = 0;
+ private String text = null;
+ private User user = null;
+ private final List tags;
+ private Instant ts;
+ private Instant updated;
+ private Instant updatedAt;
+ private boolean unread;
+ @JsonIgnore
+ private int privacy = 1;
+ @XmlTransient
+ @JsonIgnore
+ public boolean FriendsOnly = false;
+ @XmlTransient
+ @JsonIgnore
+ public boolean ReadOnly = false;
+ @XmlTransient
+ @JsonIgnore
+ public boolean Hidden = false;
+ @JsonIgnore
+ @XmlTransient
+ public boolean VisitorCanComment = true;
+ private int replies = 0;
+ private String repliesBy;
+ private String attachmentType;
+ @XmlTransient
+ private Photo photo;
+ @XmlTransient
+ private Attachment attachment;
+ private int likes;
+ private User to;
+ private String replyQuote;
+ @XmlTransient
+ private Set reactions;
+ private boolean service;
+ private URI replyUri;
+ private URI replyToUri;
+ private boolean html;
+
+ private Set recommendations;
+
+ public Message() {
+ tags = new ArrayList<>();
+ reactions = new HashSet<>();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .append("mid", mid)
+ .append("rid", rid)
+ .append("replyto", replyto)
+ .append("privacy", privacy)
+ .append("FriendsOnly", FriendsOnly)
+ .append("ReadOnly", ReadOnly)
+ .append("Hidden", Hidden)
+ .append("VisitorCanComment", VisitorCanComment)
+ .append("replies", replies)
+ .append("likes", likes)
+ .append("reactions", reactions)
+ .toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this)
+ return true;
+
+ if (!(obj instanceof Message))
+ return false;
+
+ Message jmsg = (Message) obj;
+ return (this.getMid() == jmsg.getMid() && this.getRid() == jmsg.getRid());
+ }
+
+ @Override
+ public int compareTo(Object obj) throws ClassCastException {
+ if (obj == this)
+ return 0;
+
+ if (!(obj instanceof Message))
+ throw new ClassCastException();
+
+ Message jmsg = (Message) obj;
+
+ int cmp = Integer.compare(jmsg.getMid(), getMid());
+
+ if (cmp == 0)
+ cmp = Integer.compare(getRid(), jmsg.getRid());
+
+ return cmp;
+ }
+
+ @JsonProperty("mid")
+ @XmlAttribute(name = "mid")
+ public int getMid() {
+ return mid;
+ }
+
+ public void setMid(int mid) {
+ this.mid = mid;
+ }
+
+ @JsonProperty("rid")
+ @XmlAttribute(name = "rid")
+ public int getRid() {
+ return rid;
+ }
+
+ public void setRid(int rid) {
+ this.rid = rid;
+ }
+
+ @XmlElement(name = "user", namespace = "http://juick.com/user")
+ public com.juick.User getUser() {
+ return user;
+ }
+
+ public void setUser(com.juick.User user) {
+ this.user = user;
+ }
+
+ @JsonProperty("body")
+ @XmlElement(name = "body")
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ @JsonProperty("timestamp")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
+ @XmlAttribute(name = "ts")
+ @XmlJavaTypeAdapter(SimpleDateAdapter.class)
+ public Instant getTimestamp() {
+ return ts;
+ }
+
+ public void setTimestamp(Instant timestamp) {
+ this.ts = timestamp;
+ }
+
+ @XmlElement(name = "to", namespace = "http://juick.com/user")
+ public User getTo() {
+ return to;
+ }
+
+ public void setTo(User to) {
+ this.to = to;
+ }
+
+ @XmlAttribute(name = "quote")
+ public String getReplyQuote() {
+ return replyQuote;
+ }
+
+ public void setReplyQuote(String quote) {
+ replyQuote = quote;
+ }
+
+ @JsonProperty("replyto")
+ @XmlAttribute(name = "replyto")
+ public int getReplyto() {
+ return replyto;
+ }
+
+ public void setReplyto(int replyto) {
+ this.replyto = replyto;
+ }
+
+ @JsonProperty("tags")
+ @XmlElement(name = "tag")
+ public List getTags() {
+ return tags;
+ }
+
+ public void setTags(List tags) {
+ this.tags.clear();
+ if (CollectionUtils.isNotEmpty(tags))
+ this.tags.addAll(tags);
+ }
+
+ @XmlAttribute
+ public int getPrivacy() {
+ return privacy;
+ }
+
+ public void setPrivacy(int privacy) {
+ this.privacy = privacy;
+ }
+
+ public Photo getPhoto() {
+ return photo;
+ }
+
+ public void setPhoto(Photo photo) {
+ this.photo = photo;
+ }
+
+ @XmlAttribute(name = "attach")
+ @JsonProperty("attach")
+ public String getAttachmentType() {
+ return attachmentType;
+ }
+
+ public void setAttachmentType(String attachmentType) {
+ this.attachmentType = attachmentType;
+ }
+
+ @XmlTransient
+ public int getReplies() {
+ return replies;
+ }
+
+ public void setReplies(int replies) {
+ this.replies = replies;
+ }
+
+ @XmlTransient
+ public int getLikes() {
+ return likes;
+ }
+
+ public void setLikes(int likes) {
+ this.likes = likes;
+ }
+
+ @JsonProperty("repliesby")
+ public String getRepliesBy() {
+ return repliesBy;
+ }
+
+ public void setRepliesBy(String repliesBy) {
+ this.repliesBy = repliesBy;
+ }
+
+ public Attachment getAttachment() {
+ return attachment;
+ }
+ public void setAttachment(Attachment attachment) {
+ this.attachment = attachment;
+ }
+
+ /**
+ * @return timestamp of the last comment
+ */
+ @XmlTransient
+ public Instant getUpdated() {
+ return updated;
+ }
+
+ public void setUpdated(Instant updated) {
+ this.updated = updated;
+ }
+
+ @XmlTransient
+ public boolean isUnread() {
+ return unread;
+ }
+
+ public void setUnread(boolean unread) {
+ this.unread = unread;
+ }
+
+
+ @XmlTransient
+ public Set getReactions() {
+ return reactions;
+ }
+
+ public void setReactions(Set reactions) {
+ this.reactions = reactions;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mid, rid);
+ }
+
+ public boolean isService() {
+ return service;
+ }
+
+ public void setService(boolean service) {
+ this.service = service;
+ }
+
+ public Set getRecommendations() {
+ return recommendations;
+ }
+
+ public void setRecommendations(Set recommendations) {
+ this.recommendations = recommendations;
+ }
+
+ /**
+ * @return timestamp of the last edit
+ */
+ @XmlTransient
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
+ @JsonProperty("updated_at")
+ public Instant getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public void setUpdatedAt(Instant updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+
+ public URI getReplyUri() {
+ return replyUri;
+ }
+
+ public void setReplyUri(URI replyUri) {
+ this.replyUri = replyUri;
+ }
+
+ public boolean isHtml() {
+ return html;
+ }
+
+ public void setHtml(boolean html) {
+ this.html = html;
+ }
+
+ public URI getReplyToUri() {
+ return replyToUri;
+ }
+
+ public void setReplyToUri(URI replyToUri) {
+ this.replyToUri = replyToUri;
+ }
+}
diff --git a/src/main/java/com/juick/Photo.java b/src/main/java/com/juick/Photo.java
new file mode 100644
index 00000000..06299610
--- /dev/null
+++ b/src/main/java/com/juick/Photo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick;
+
+/**
+ * Created by vitalyster on 30.11.2016.
+ */
+// used for compatibility
+@Deprecated
+public class Photo {
+ private String small;
+ private String medium;
+ private String thumbnail;
+
+ public String getSmall() {
+ return small;
+ }
+
+ public void setSmall(String small) {
+ this.small = small;
+ }
+
+ public String getMedium() {
+ return medium;
+ }
+
+ public void setMedium(String medium) {
+ this.medium = medium;
+ }
+
+ public String getThumbnail() {
+ return thumbnail;
+ }
+
+ public void setThumbnail(String thumbnail) {
+ this.thumbnail = thumbnail;
+ }
+}
diff --git a/src/main/java/com/juick/Reaction.java b/src/main/java/com/juick/Reaction.java
new file mode 100644
index 00000000..536ac241
--- /dev/null
+++ b/src/main/java/com/juick/Reaction.java
@@ -0,0 +1,45 @@
+package com.juick;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
+public class Reaction {
+
+ public static final int LIKE = 1;
+
+ private final int id;
+ private String description;
+ private int count;
+
+ public Reaction(int id) {
+ this.id = id;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .append("id", getId())
+ .append("description", description)
+ .append("count", count)
+ .build();
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ public void setCount(int count) {
+ this.count = count;
+ }
+
+ public int getId() {
+ return id;
+ }
+}
diff --git a/src/main/java/com/juick/Status.java b/src/main/java/com/juick/Status.java
new file mode 100644
index 00000000..d7983536
--- /dev/null
+++ b/src/main/java/com/juick/Status.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Created by vitalyster on 25.07.2016.
+ */
+public class Status {
+ private final String value;
+
+ public static final Status OK = new Status("ok");
+ public static final Status FAIL = new Status("Fail");
+ public static final Status ERROR = new Status("Error");
+
+ public static Status getStatus(final String stringStatus) {
+ return new Status(stringStatus);
+ }
+
+ private Status(String value) {
+ this.value = value;
+ }
+
+ @JsonProperty("status")
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return "value = " + value;
+ }
+}
diff --git a/src/main/java/com/juick/Tag.java b/src/main/java/com/juick/Tag.java
new file mode 100644
index 00000000..b93d0e76
--- /dev/null
+++ b/src/main/java/com/juick/Tag.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.juick;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import javax.xml.bind.annotation.*;
+import java.util.Comparator;
+import java.util.Objects;
+
+/**
+ * @author Ugnich Anton
+ */
+@XmlRootElement(name = "tag", namespace = "http://juick.com/message")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class Tag implements Comparable {
+ @XmlValue
+ private String name;
+
+ @XmlTransient
+ public int TID = 0;
+ @XmlTransient
+ public int SynonymID = 0;
+
+ public Tag() {
+ // required for (de)serialization
+ }
+
+ public Tag(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o == this ||
+ (o instanceof Tag) && Objects.equals(name, ((Tag) o).name);
+ }
+
+ @XmlTransient
+ @JsonValue
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public int compareTo(Tag o) {
+ return Objects.compare(name, o.getName(), Comparator.naturalOrder());
+ }
+}
diff --git a/src/main/java/com/juick/User.java b/src/main/java/com/juick/User.java
new file mode 100644
index 00000000..e2e45122
--- /dev/null
+++ b/src/main/java/com/juick/User.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.juick;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
+import javax.annotation.Nonnull;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlTransient;
+import java.net.URI;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author Ugnich Anton
+ */
+@XmlRootElement(name = "user", namespace = "http://juick.com/user")
+@XmlAccessorType()
+public class User {
+ private int uid;
+ private String name;
+ private Object avatar;
+ private String fullName;
+ private int messagesCount;
+ private String authHash;
+ private boolean banned;
+ private String credentials;
+ private List tokens;
+ private List read;
+ private List readers;
+ private List unread;
+ private URI uri;
+ private Instant seen;
+
+ public User() {
+ tokens = new ArrayList<>();
+ uri = URI.create(StringUtils.EMPTY);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == this ||
+ (obj instanceof User && ((User) obj).getUid() == this.getUid());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uid);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .append("uid", uid)
+ .append("name", name)
+ .append("fullName", fullName)
+ .append("messagesCount", messagesCount)
+ .append("banned", banned)
+ .toString();
+ }
+
+ @JsonProperty("uid")
+ @XmlAttribute(name = "uid")
+ public int getUid() {
+ return uid;
+ }
+
+ public void setUid(int uid) {
+ this.uid = uid;
+ }
+
+ @JsonProperty("uname")
+ @XmlAttribute(name = "uname")
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @JsonProperty("fullname")
+ @XmlTransient
+ public String getFullName() {
+ return fullName;
+ }
+
+ public void setFullName(String fullName) {
+ this.fullName = fullName;
+ }
+
+ @XmlTransient
+ @JsonIgnore
+ public String getAuthHash() {
+ return authHash;
+ }
+
+ public void setAuthHash(String authHash) {
+ this.authHash = authHash;
+ }
+
+ @JsonProperty("unreadCount")
+ @XmlTransient
+ public Integer getUnreadCount() {
+ return messagesCount;
+ }
+
+ public void setUnreadCount(Integer count) {
+ this.messagesCount = count;
+ }
+
+ @XmlTransient
+ public boolean isBanned() {
+ return banned;
+ }
+
+ public void setBanned(boolean banned) {
+ this.banned = banned;
+ }
+
+ public Object getAvatar() {
+ return avatar;
+ }
+
+ public void setAvatar(Object avatar) {
+ this.avatar = avatar;
+ }
+
+ @XmlTransient
+ @JsonIgnore
+ public String getCredentials() {
+ return credentials;
+ }
+
+ public void setCredentials(String credentials) {
+ this.credentials = credentials;
+ }
+
+ @XmlTransient
+ public int getMessagesCount() {
+ return messagesCount;
+ }
+
+ public void setMessagesCount(int messagesCount) {
+ this.messagesCount = messagesCount;
+ }
+
+ @XmlTransient
+ @JsonIgnore
+ public boolean isAnonymous() {
+ return false;
+ }
+
+ @Nonnull
+ public List getTokens() {
+ return tokens;
+ }
+
+ public void setTokens(List tokens) {
+ this.tokens = tokens;
+ }
+
+ public List getRead() {
+ return read;
+ }
+ public List getReaders() {
+ return readers;
+ }
+
+ public void setRead(List read) {
+ this.read = read;
+ }
+
+ public void setReaders(List readers) {
+ this.readers = readers;
+ }
+
+ public List getUnread() {
+ return unread;
+ }
+
+ public void setUnread(List unread) {
+ this.unread = unread;
+ }
+
+ @Nonnull
+ public URI getUri() {
+ if (uri == null) {
+ uri = URI.create(StringUtils.EMPTY);
+ }
+ return uri;
+ }
+
+ public void setUri(URI uri) {
+ this.uri = uri;
+ }
+
+ public Instant getSeen() {
+ return seen;
+ }
+
+ public void setSeen(Instant seen) {
+ this.seen = seen;
+ }
+}
diff --git a/src/main/java/com/juick/adapters/SimpleDateAdapter.java b/src/main/java/com/juick/adapters/SimpleDateAdapter.java
new file mode 100644
index 00000000..b8e08599
--- /dev/null
+++ b/src/main/java/com/juick/adapters/SimpleDateAdapter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.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) throws Exception {
+ return DateFormattersHolder.getMessageFormatterInstance().format(v);
+ }
+
+ @Override
+ public Instant unmarshal(String v) throws Exception {
+ return DateFormattersHolder.getMessageFormatterInstance().parse(v);
+ }
+}
diff --git a/src/main/java/com/juick/formatters/PlainTextFormatter.java b/src/main/java/com/juick/formatters/PlainTextFormatter.java
new file mode 100644
index 00000000..378a523f
--- /dev/null
+++ b/src/main/java/com/juick/formatters/PlainTextFormatter.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.formatters;
+
+import com.juick.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.getMarkdownTags(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.getTimestamp()));
+ 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(com.juick.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 formatPostNumber(com.juick.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/AnonymousUser.java b/src/main/java/com/juick/model/AnonymousUser.java
new file mode 100644
index 00000000..f4511194
--- /dev/null
+++ b/src/main/java/com/juick/model/AnonymousUser.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model;
+
+import com.juick.User;
+
+/**
+ * Created by aalexeev on 12/11/16.
+ */
+public final class AnonymousUser extends User {
+ public static final AnonymousUser INSTANCE = new AnonymousUser();
+
+ private AnonymousUser() {
+ super.setUid(getUid());
+ super.setName(getName());
+ super.setAvatar(getAvatar());
+ super.setFullName(getFullName());
+ super.setMessagesCount(getMessagesCount());
+ super.setAuthHash(getAuthHash());
+ super.setBanned(isBanned());
+ super.setCredentials(getCredentials());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == this || obj instanceof AnonymousUser;
+ }
+
+ @Override
+ public int getUid() {
+ return 0;
+ }
+
+ @Override
+ public String getName() {
+ return "Anonymous";
+ }
+
+ @Override
+ public String getFullName() {
+ return getName();
+ }
+
+ @Override
+ public String getAuthHash() {
+ return null;
+ }
+
+ @Override
+ public Integer getUnreadCount() {
+ return 0;
+ }
+
+ @Override
+ public boolean isBanned() {
+ return false;
+ }
+
+ @Override
+ public Object getAvatar() {
+ return null;
+ }
+
+ @Override
+ public String getCredentials() {
+ return null;
+ }
+
+ @Override
+ public int getMessagesCount() {
+ return 0;
+ }
+
+ @Override
+ public boolean isAnonymous() {
+ return true;
+ }
+
+ @Override
+ public void setUid(int uid) {
+ }
+
+ @Override
+ public void setName(String name) {
+ }
+
+ @Override
+ public void setFullName(String fullName) {
+ }
+
+ @Override
+ public void setAuthHash(String authHash) {
+ }
+
+ @Override
+ public void setUnreadCount(Integer count) {
+ }
+
+ @Override
+ public void setBanned(boolean banned) {
+ }
+
+ @Override
+ public void setAvatar(Object avatar) {
+ }
+
+ @Override
+ public void setCredentials(String credentials) {
+ }
+
+ @Override
+ public void setMessagesCount(int messagesCount) {
+ }
+}
diff --git a/src/main/java/com/juick/model/ApplicationStatus.java b/src/main/java/com/juick/model/ApplicationStatus.java
new file mode 100644
index 00000000..b18e12bf
--- /dev/null
+++ b/src/main/java/com/juick/model/ApplicationStatus.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
+/**
+ * Created by vt on 03/09/16.
+ */
+public class ApplicationStatus {
+ private boolean connected;
+ private boolean crosspostEnabled;
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .append("connected", connected)
+ .append("crosspostEnabled", crosspostEnabled)
+ .toString();
+ }
+
+ public boolean isConnected() {
+ return connected;
+ }
+
+ public void setConnected(boolean connected) {
+ this.connected = connected;
+ }
+
+ public boolean isCrosspostEnabled() {
+ return crosspostEnabled;
+ }
+
+ public void setCrosspostEnabled(boolean crosspostEnabled) {
+ this.crosspostEnabled = crosspostEnabled;
+ }
+}
diff --git a/src/main/java/com/juick/model/Auth.java b/src/main/java/com/juick/model/Auth.java
new file mode 100644
index 00000000..66125567
--- /dev/null
+++ b/src/main/java/com/juick/model/Auth.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model;
+
+/**
+ * Created by vt on 09/02/16.
+ */
+public class Auth {
+ private String account;
+ private String authCode;
+
+ public Auth(String account, String authCode) {
+ this.account = account;
+ this.authCode = authCode;
+ }
+
+ public String getAccount() {
+ return account;
+ }
+
+ public String getAuthCode() {
+ return authCode;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/juick/model/CommandResult.java b/src/main/java/com/juick/model/CommandResult.java
new file mode 100644
index 00000000..c310756c
--- /dev/null
+++ b/src/main/java/com/juick/model/CommandResult.java
@@ -0,0 +1,35 @@
+package com.juick.model;
+
+import com.juick.Message;
+
+import java.util.Optional;
+
+public class CommandResult {
+ private String text;
+ private String markdown;
+ private Message newMessage;
+
+ public String getText() {
+ return text;
+ }
+ public String getMarkdown() {
+ return markdown;
+ }
+
+ public Optional getNewMessage() {
+ return Optional.ofNullable(newMessage);
+ }
+ public static CommandResult build(Message newMessage, String text, String markdown) {
+ CommandResult result = new CommandResult();
+ result.newMessage = newMessage;
+ result.text = text;
+ result.markdown = markdown;
+ return result;
+ }
+ public static CommandResult fromString(String text) {
+ CommandResult result = new CommandResult();
+ result.text = text;
+ return result;
+ }
+
+}
diff --git a/src/main/java/com/juick/model/NotifyOpts.java b/src/main/java/com/juick/model/NotifyOpts.java
new file mode 100644
index 00000000..1c0e0aac
--- /dev/null
+++ b/src/main/java/com/juick/model/NotifyOpts.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model;
+
+/**
+ * Created by vt on 03/09/16.
+ */
+public class NotifyOpts {
+ private boolean repliesEnabled;
+ private boolean subscriptionsEnabled;
+ private boolean recommendationsEnabled;
+
+ public boolean isRepliesEnabled() {
+ return repliesEnabled;
+ }
+
+ public void setRepliesEnabled(boolean repliesEnabled) {
+ this.repliesEnabled = repliesEnabled;
+ }
+
+ public boolean isSubscriptionsEnabled() {
+ return subscriptionsEnabled;
+ }
+
+ public void setSubscriptionsEnabled(boolean subscriptionsEnabled) {
+ this.subscriptionsEnabled = subscriptionsEnabled;
+ }
+
+ public boolean isRecommendationsEnabled() {
+ return recommendationsEnabled;
+ }
+
+ public void setRecommendationsEnabled(boolean recommendationsEnabled) {
+ this.recommendationsEnabled = recommendationsEnabled;
+ }
+}
diff --git a/src/main/java/com/juick/model/PrivacyOpts.java b/src/main/java/com/juick/model/PrivacyOpts.java
new file mode 100644
index 00000000..52cbe588
--- /dev/null
+++ b/src/main/java/com/juick/model/PrivacyOpts.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model;
+
+/**
+ * Created by vt on 16/01/16.
+ */
+public class PrivacyOpts {
+ private int uid;
+ private int privacy;
+
+ public PrivacyOpts() {
+
+ }
+
+ public int getUid() {
+ return uid;
+ }
+
+ public void setUid(int uid) {
+ this.uid = uid;
+ }
+
+ public int getPrivacy() {
+ return privacy;
+ }
+
+ public void setPrivacy(int privacy) {
+ this.privacy = privacy;
+ }
+}
diff --git a/src/main/java/com/juick/model/PrivateChats.java b/src/main/java/com/juick/model/PrivateChats.java
new file mode 100644
index 00000000..b6bb48ab
--- /dev/null
+++ b/src/main/java/com/juick/model/PrivateChats.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.juick.Chat;
+
+import java.util.List;
+
+/**
+ * Created by vt on 24/11/2016.
+ */
+public class PrivateChats {
+ private List users;
+
+ @JsonProperty("pms")
+ public List getUsers() {
+ return users;
+ }
+
+ public void setUsers(List users) {
+ this.users = users;
+ }
+}
diff --git a/src/main/java/com/juick/model/ResponseReply.java b/src/main/java/com/juick/model/ResponseReply.java
new file mode 100644
index 00000000..183c6f72
--- /dev/null
+++ b/src/main/java/com/juick/model/ResponseReply.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model;
+
+import java.util.Date;
+
+/**
+ * Created by vitalyster on 13.12.2016.
+ */
+public class ResponseReply {
+ private String muname;
+ private int mid;
+ private int rid;
+ private String uname;
+ private String description;
+ private Date pubDate;
+ private String attachmentType;
+ private boolean html;
+
+ public String getMuname() {
+ return muname;
+ }
+
+ public void setMuname(String muname) {
+ this.muname = muname;
+ }
+
+ public int getMid() {
+ return mid;
+ }
+
+ public void setMid(int mid) {
+ this.mid = mid;
+ }
+
+ public int getRid() {
+ return rid;
+ }
+
+ public void setRid(int rid) {
+ this.rid = rid;
+ }
+
+ public String getUname() {
+ return uname;
+ }
+
+ public void setUname(String uname) {
+ this.uname = uname;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Date getPubDate() {
+ return pubDate;
+ }
+
+ public void setPubDate(Date pubDate) {
+ this.pubDate = pubDate;
+ }
+
+ public String getAttachmentType() {
+ return attachmentType;
+ }
+
+ public void setAttachmentType(String attachmentType) {
+ this.attachmentType = attachmentType;
+ }
+
+ public boolean isHtml() {
+ return html;
+ }
+
+ public void setHtml(boolean html) {
+ this.html = html;
+ }
+}
diff --git a/src/main/java/com/juick/model/TagStats.java b/src/main/java/com/juick/model/TagStats.java
new file mode 100644
index 00000000..da2f3f92
--- /dev/null
+++ b/src/main/java/com/juick/model/TagStats.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.juick.Tag;
+
+/**
+ * Created by vitalyster on 01.12.2016.
+ */
+public class TagStats {
+ private Tag tag;
+ private int usageCount;
+
+ public Tag getTag() {
+ return tag;
+ }
+
+ public void setTag(Tag tag) {
+ this.tag = tag;
+ }
+
+ @JsonProperty("messages")
+ public int getUsageCount() {
+ return usageCount;
+ }
+
+ public void setUsageCount(int usageCount) {
+ this.usageCount = usageCount;
+ }
+}
diff --git a/src/main/java/com/juick/model/UserInfo.java b/src/main/java/com/juick/model/UserInfo.java
new file mode 100644
index 00000000..ca5d75e0
--- /dev/null
+++ b/src/main/java/com/juick/model/UserInfo.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model;
+
+/**
+ * Created by vt on 03/09/16.
+ */
+public class UserInfo {
+ private String fullName;
+ private String country;
+ private String url;
+ private String description;
+
+ public String getFullName() {
+ return fullName;
+ }
+
+ public void setFullName(String fullName) {
+ this.fullName = fullName;
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/src/main/java/com/juick/model/facebook/User.java b/src/main/java/com/juick/model/facebook/User.java
new file mode 100644
index 00000000..80838de6
--- /dev/null
+++ b/src/main/java/com/juick/model/facebook/User.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model.facebook;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Created by vitalyster on 28.11.2016.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class User {
+ private String id;
+ private String name;
+ private String link;
+ private boolean verified;
+ private String firstName;
+ private String lastName;
+ private String gender;
+ private String locale;
+ private String timezone;
+ private String updatedTime;
+ private String email;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public boolean getVerified() {
+ return verified;
+ }
+
+ public void setVerified(boolean verified) {
+ this.verified = verified;
+ }
+
+ @JsonProperty("first_name")
+ public String getFirstName() {
+ return firstName;
+ }
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getGender() {
+ return gender;
+ }
+
+ public void setGender(String gender) {
+ this.gender = gender;
+ }
+
+ @JsonProperty("last_name")
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getLocale() {
+ return locale;
+ }
+
+ public void setLocale(String locale) {
+ this.locale = locale;
+ }
+
+ public String getTimezone() {
+ return timezone;
+ }
+
+ public void setTimezone(String timezone) {
+ this.timezone = timezone;
+ }
+
+ @JsonProperty("updated_time")
+ public String getUpdatedTime() {
+ return updatedTime;
+ }
+
+ public void setUpdatedTime(String updatedTime) {
+ this.updatedTime = updatedTime;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+}
diff --git a/src/main/java/com/juick/model/twitter/User.java b/src/main/java/com/juick/model/twitter/User.java
new file mode 100644
index 00000000..3c80eff4
--- /dev/null
+++ b/src/main/java/com/juick/model/twitter/User.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model.twitter;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Created by vitalyster on 28.11.2016.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class User {
+ private String screenName;
+
+ @JsonProperty("screen_name")
+ public String getScreenName() {
+ return screenName;
+ }
+
+ public void setScreenName(String screenName) {
+ this.screenName = screenName;
+ }
+}
diff --git a/src/main/java/com/juick/model/vk/Token.java b/src/main/java/com/juick/model/vk/Token.java
new file mode 100644
index 00000000..ed93a3ab
--- /dev/null
+++ b/src/main/java/com/juick/model/vk/Token.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model.vk;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Created by vitalyster on 28.11.2016.
+ */
+public class Token {
+ private Long userId;
+ private String accessToken;
+ private String expiresIn;
+
+ @JsonProperty("user_id")
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ @JsonProperty("access_token")
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public void setAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ @JsonProperty("expires_in")
+ public String getExpiresIn() {
+ return expiresIn;
+ }
+
+ public void setExpiresIn(String expiresIn) {
+ this.expiresIn = expiresIn;
+ }
+}
diff --git a/src/main/java/com/juick/model/vk/User.java b/src/main/java/com/juick/model/vk/User.java
new file mode 100644
index 00000000..aeb18285
--- /dev/null
+++ b/src/main/java/com/juick/model/vk/User.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model.vk;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Created by vitalyster on 28.11.2016.
+ */
+public class User {
+ private String id;
+ private String firstName;
+ private String lastName;
+ private String screenName;
+
+ @JsonProperty("first_name")
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ @JsonProperty("last_name")
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ @JsonProperty("screen_name")
+ public String getScreenName() {
+ return screenName;
+ }
+
+ public void setScreenName(String screenName) {
+ this.screenName = screenName;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+}
diff --git a/src/main/java/com/juick/model/vk/UsersResponse.java b/src/main/java/com/juick/model/vk/UsersResponse.java
new file mode 100644
index 00000000..67505703
--- /dev/null
+++ b/src/main/java/com/juick/model/vk/UsersResponse.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.model.vk;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * Created by vitalyster on 28.11.2016.
+ */
+public class UsersResponse {
+ private List users;
+
+ @JsonProperty("response")
+ public List getUsers() {
+ return users;
+ }
+
+ public void setUsers(List users) {
+ this.users = users;
+ }
+}
diff --git a/src/main/java/com/juick/package-info.java b/src/main/java/com/juick/package-info.java
new file mode 100644
index 00000000..c9023417
--- /dev/null
+++ b/src/main/java/com/juick/package-info.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * Created by vitalyster on 15.11.2016.
+ */
+@XmlSchema(
+ namespace="http://juick.com/message",
+ elementFormDefault = XmlNsForm.QUALIFIED,
+ xmlns={
+ @XmlNs(prefix= StringUtils.EMPTY, namespaceURI="http://juick.com/message"),
+ @XmlNs(prefix="user", namespaceURI="http://juick.com/user")
+ }
+)
+package com.juick;
+
+import org.apache.commons.lang3.StringUtils;
+
+import javax.xml.bind.annotation.XmlNs;
+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/server/ActivityPubManager.java b/src/main/java/com/juick/server/ActivityPubManager.java
new file mode 100644
index 00000000..4601f7d1
--- /dev/null
+++ b/src/main/java/com/juick/server/ActivityPubManager.java
@@ -0,0 +1,331 @@
+package com.juick.server;
+
+import com.juick.Message;
+import com.juick.User;
+import com.juick.formatters.PlainTextFormatter;
+import com.juick.server.api.activity.model.Context;
+import com.juick.server.api.activity.model.activities.Accept;
+import com.juick.server.api.activity.model.activities.Announce;
+import com.juick.server.api.activity.model.activities.Create;
+import com.juick.server.api.activity.model.activities.Delete;
+import com.juick.server.api.activity.model.objects.Hashtag;
+import com.juick.server.api.activity.model.objects.Image;
+import com.juick.server.api.activity.model.objects.Mention;
+import com.juick.server.api.activity.model.objects.Note;
+import com.juick.server.api.activity.model.objects.Person;
+import com.juick.server.util.HttpUtils;
+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.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(ActivityPubManager.class);
+ @Inject
+ private SignatureManager signatureManager;
+ @Inject
+ private SocialService socialService;
+ @Inject
+ private UserService userService;
+ @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 processMessageEvent(MessageEvent messageEvent) {
+ Message msg = messageEvent.getMessage();
+ if (MessageUtils.isPM(msg)) {
+ 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.forEach(acct -> {
+ Optional context = signatureManager.getContext(URI.create(acct));
+ if (context.isPresent()) {
+ 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 {
+ logger.info("Posting to subscriber {}", follower.getId());
+ 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(com.juick.Tag tag) {
+ UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri);
+ return uri.replacePath(String.format("/t/%s", tag.getName())).toUriString();
+ }
+
+ 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("https://www.w3.org/ns/activitystreams#Public"));
+ note.setCc(Collections.singletonList(followersUri(msg.getUser())));
+ }
+ note.setPublished(msg.getTimestamp());
+ 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 -> {
+ Hashtag hashtag = new Hashtag();
+ hashtag.setId(tagUri(t));
+ hashtag.setName(t.getName());
+ return hashtag;
+ }).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.getUrl());
+ 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 processSubscribeEvent(SubscribeEvent subscribeEvent) {
+
+ }
+
+ @Override
+ public void processLikeEvent(LikeEvent likeEvent) {
+
+ }
+
+ @Override
+ public void processPingEvent(PingEvent pingEvent) {
+
+ }
+
+ @Override
+ public void processMessageReadEvent(MessageReadEvent messageReadEvent) {
+
+ }
+
+ @Override
+ public void processTopEvent(TopEvent topEvent) {
+ Message message = topEvent.getMessage();
+ Note note = makeNote(message);
+ Announce announce = new Announce();
+ announce.setId(note.getId() + "#top");
+ announce.setActor(personUri(serviceUser));
+ announce.setObject(note);
+ Person me = (Person) signatureManager.getContext(URI.create(announce.getActor())).get();
+ socialService.getFollowers(serviceUser).forEach(acct -> {
+ Person follower = (Person) signatureManager.getContext(URI.create(acct)).get();
+ try {
+ logger.info("Announcing top: {}", message.getMid());
+ signatureManager.post(me, follower, announce);
+ } catch (IOException e) {
+ logger.warn("activitypub exception", e);
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/juick/server/CommandsManager.java b/src/main/java/com/juick/server/CommandsManager.java
new file mode 100644
index 00000000..82143482
--- /dev/null
+++ b/src/main/java/com/juick/server/CommandsManager.java
@@ -0,0 +1,540 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server;
+
+import com.juick.Message;
+import com.juick.Tag;
+import com.juick.User;
+import com.juick.formatters.PlainTextFormatter;
+import com.juick.service.activities.DeleteMessageEvent;
+import com.juick.service.component.*;
+import com.juick.model.CommandResult;
+import com.juick.model.TagStats;
+import com.juick.server.helpers.annotation.UserCommand;
+import com.juick.server.util.HttpUtils;
+import com.juick.service.*;
+import com.juick.util.MessageUtils;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.commons.lang3.reflect.MethodUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Nonnull;
+import javax.inject.Inject;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * @author ugnich
+ */
+@Component
+public class CommandsManager {
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private UserService userService;
+ @Inject
+ private TagService tagService;
+ @Inject
+ private PMQueriesService pmQueriesService;
+ @Inject
+ private ShowQueriesService showQueriesService;
+ @Inject
+ private PrivacyQueriesService privacyQueriesService;
+ @Inject
+ private SubscriptionService subscriptionService;
+ @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String tmpDir;
+ @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String imgDir;
+ @Inject
+ private ApplicationEventPublisher applicationEventPublisher;
+ @Inject
+ private ImagesService imagesService;
+
+ public CommandResult processCommand(User user, String data, @Nonnull URI attachment) throws Exception {
+ 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 = 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();
+ boolean haveAttachment = StringUtils.isNotEmpty(attachment.toString());
+ String attachmentFName = null;
+ String attachmentType = null;
+ if (haveAttachment) {
+ attachmentFName = attachment.getScheme().equals("juick") ? attachment.getHost()
+ : HttpUtils.downloadImage(attachment.toURL(), tmpDir).getHost();
+ attachmentType = attachmentFName.substring(attachmentFName.length() - 3);
+ }
+ int mid = messagesService.createMessage(user.getUid(), body, attachmentType, tags.getRight());
+ if (haveAttachment) {
+ String fname = String.format("%d.%s", mid, attachmentType);
+ imagesService.saveImageWithPreviews(attachmentFName, fname);
+ }
+ Message msg = messagesService.getMessage(mid);
+ subscriptionService.subscribeMessage(msg, user);
+
+ applicationEventPublisher.publishEvent(new MessageReadEvent(this, user, msg));
+ applicationEventPublisher.publishEvent(new MessageEvent(this, msg, subscriptionService.getSubscribedUsers(msg.getUser().getUid(), msg)));
+ return CommandResult.build(msg, "New message posted.\n#" + msg.getMid() + " https://juick.com/m/" + msg.getMid(), String.format("[New message](%s) posted", PlainTextFormatter.formatUrl(msg)));
+ }
+
+ @UserCommand(pattern = "^ping$", patternFlags = Pattern.CASE_INSENSITIVE,
+ help = "PING - returns you a PONG")
+ public CommandResult commandPing(User user, URI attachment, String[] input) {
+ applicationEventPublisher.publishEvent(new PingEvent(this, user));
+ return CommandResult.fromString("PONG");
+ }
+
+ @UserCommand(pattern = "^help$", patternFlags = Pattern.CASE_INSENSITIVE,
+ help = "HELP - returns this help message")
+ public CommandResult commandHelp(User user, URI attachment, String[] input) {
+ return CommandResult.fromString(Arrays.stream(getClass().getDeclaredMethods())
+ .filter(m -> m.isAnnotationPresent(UserCommand.class))
+ .map(m -> m.getAnnotation(UserCommand.class).help())
+ .collect(Collectors.joining("\n")));
+ }
+
+ @UserCommand(pattern = "^login$", patternFlags = Pattern.CASE_INSENSITIVE,
+ help = "LOGIN - log in to Juick website")
+ public CommandResult commandLogin(User user_from, URI attachment, String[] input) {
+ return CommandResult.fromString("http://juick.com/login?hash=" + userService.getHashByUID(user_from.getUid()));
+ }
+ @UserCommand(pattern = "^\\@(\\S+)\\s+([\\s\\S]+)$", help = "@username message - send PM to username")
+ public CommandResult commandPM(User user_from, URI attachment, String... arguments) {
+ String body = arguments[1];
+
+ User user_to = userService.getUserByName(arguments[0]);
+
+ if (!user_to.isAnonymous()) {
+ if (!userService.isInBLAny(user_to.getUid(), user_from.getUid())) {
+ if (pmQueriesService.createPM(user_from.getUid(), user_to.getUid(), body)) {
+ com.juick.Message jmsg = new com.juick.Message();
+ jmsg.setUser(user_from);
+ jmsg.setTo(user_to);
+ jmsg.setText(body);
+ applicationEventPublisher.publishEvent(new MessageEvent(this, jmsg, Collections.singletonList(user_to)));
+ return CommandResult.fromString("Private message sent");
+ }
+ }
+ }
+ return CommandResult.fromString("Error");
+ }
+ @UserCommand(pattern = "^bl$", patternFlags = Pattern.CASE_INSENSITIVE,
+ help = "BL - Show your blacklist")
+ public CommandResult commandBLShow(User user_from, URI attachment, String... arguments) {
+ List 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) {
+ com.juick.Message msg = messagesService.getMessage(mid);
+ if (msg != null) {
+ if (msg.getUser() == user) {
+ return CommandResult.fromString("You can't recommend your own messages.");
+ }
+ MessagesService.RecommendStatus status = messagesService.recommendMessage(mid, user.getUid());
+ switch (status) {
+ case Added:
+ applicationEventPublisher.publishEvent(new LikeEvent(this, user, msg,
+ subscriptionService.getUsersSubscribedToUserRecommendations(
+ user.getUid(), msg)));
+ return CommandResult.fromString("Message is added to your recommendations");
+ case Deleted:
+ return CommandResult.fromString("Message deleted from your recommendations.");
+ }
+ }
+ return CommandResult.fromString("Message not found");
+ }
+ return CommandResult.fromString("Message not found");
+ }
+ // TODO: target notification
+ @UserCommand(pattern = "^(s|u)\\s+\\@(\\S+)$", help = "S @username - subscribe to user" +
+ "\nU @username - unsubscribe from user", patternFlags = Pattern.CASE_INSENSITIVE)
+ public CommandResult commandSubscribeUser(User user, URI attachment, String... args) {
+ boolean subscribe = args[0].equalsIgnoreCase("s");
+ User toUser = userService.getUserByName(args[1]);
+ if (toUser.isAnonymous()) {
+ return CommandResult.fromString("User not found");
+ }
+ if (subscribe) {
+ if (subscriptionService.subscribeUser(user, toUser)) {
+ // TODO: already subscribed case
+ applicationEventPublisher.publishEvent(new SubscribeEvent(this, user, toUser));
+ return CommandResult.fromString("Subscribed to @" + toUser.getName());
+ }
+ } else {
+ if (subscriptionService.unSubscribeUser(user, toUser)) {
+ return CommandResult.fromString("Unsubscribed from @" + toUser.getName());
+ }
+ return CommandResult.fromString("You 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(User user, URI attachment, String... args) {
+ boolean subscribe = args[0].equalsIgnoreCase("s");
+ int mid = NumberUtils.toInt(args[1], 0);
+ Message msg = messagesService.getMessage(mid);
+ if (msg != null) {
+ if (subscribe) {
+ if (subscriptionService.subscribeMessage(msg, user)) {
+ applicationEventPublisher.publishEvent(
+ new MessageReadEvent(this, user, msg));
+ return CommandResult.fromString("Subscribed");
+ }
+ } else {
+ if (subscriptionService.unSubscribeMessage(mid, user.getUid())) {
+ return CommandResult.fromString("Unsubscribed from #" + mid);
+ }
+ return CommandResult.fromString("You 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()) {
+ 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(User user, URI attachment, String... arguments) {
+ boolean showReplies = arguments[1].length() > 0;
+ int mid = NumberUtils.toInt(arguments[0], 0);
+ if (mid == 0) {
+ return CommandResult.fromString("Error");
+ }
+ com.juick.Message msg = messagesService.getMessage(mid);
+ if (msg != null) {
+ if (showReplies) {
+ List replies = messagesService.getReplies(user, mid);
+ applicationEventPublisher.publishEvent(
+ new MessageReadEvent(this, user, msg));
+ replies.add(0, msg);
+ return CommandResult.fromString(String.join("\n",
+ replies.stream().map(PlainTextFormatter::formatPostSummary).collect(Collectors.toList())));
+ }
+ return CommandResult.fromString(PlainTextFormatter.formatPost(msg));
+ }
+ return CommandResult.fromString("Message not found");
+ }
+ @UserCommand(pattern = "^#(\\d+)\\/(\\d+)$", help = "#1234/5 - Show reply")
+ public CommandResult commandShowReply(User user, URI attachment, String... arguments) {
+ int mid = NumberUtils.toInt(arguments[0], 0);
+ int rid = NumberUtils.toInt(arguments[1], 0);
+ com.juick.Message reply = messagesService.getReply(mid, rid);
+ if (reply != null) {
+ return CommandResult.fromString(PlainTextFormatter.formatPost(reply));
+ }
+ return CommandResult.fromString("Reply not found");
+ }
+ @UserCommand(pattern = "^\\*(\\S+)(\\+?)$", help = "*tag - Show last messages with tag")
+ public CommandResult commandShowTag(User user, URI attachment, String... arguments) {
+ if (StringUtils.isNotEmpty(attachment.toString())) {
+ // new message with tag
+ return CommandResult.fromString(StringUtils.EMPTY);
+ }
+ Tag tag = tagService.getTag(arguments[0], false);
+ if (tag != null) {
+ // TODO: synonyms
+ List 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]);
+ Message message = messagesService.getMessage(mid);
+ if (message != null && messagesService.deleteMessage(user.getUid(), mid)) {
+ applicationEventPublisher.publishEvent(new DeleteMessageEvent(this, message));
+ return CommandResult.fromString("Message deleted");
+ }
+ return CommandResult.fromString("This is not your message");
+ }
+ @UserCommand(pattern = "^D #(\\d+)(\\.|\\-|\\/)(\\d+)$", help = "D #1234/5 - Delete comment", patternFlags = Pattern.CASE_INSENSITIVE)
+ public CommandResult commandDeleteReply(User user, URI attachment, String... args) {
+ int mid = Integer.valueOf(args[0]);
+ int rid = Integer.valueOf(args[2]);
+ if (messagesService.deleteReply(user.getUid(), mid, rid)) {
+ return CommandResult.fromString("Reply deleted");
+ } else {
+ return CommandResult.fromString("This is not your reply");
+ }
+ }
+ @UserCommand(pattern = "^(D L|DL|D LAST)$", help = "D L - Delete last message", patternFlags = Pattern.CASE_INSENSITIVE)
+ public CommandResult commandDeleteLast(User user, URI attachment, String... args) {
+ return CommandResult.fromString("Temporarily unavailable");
+ }
+ @UserCommand(pattern = "^\\?\\s+\\@([a-zA-Z0-9\\-\\.\\@]+)\\s+([\\s\\S]+)$", help = "? @user string - search in user messages")
+ public CommandResult commandSearch(User user, URI attachment, String... args) {
+ return CommandResult.fromString("Temporarily unavailable");
+ }
+ @UserCommand(pattern = "^\\?\\s+([\\s\\S]+)$", help = "? string - search in all messages")
+ public CommandResult commandSearchAll(User user, URI attachment, String... args) {
+ return CommandResult.fromString("Temporarily unavailable");
+ }
+ @UserCommand(pattern = "^(#+)$", help = "# - Show last messages from your feed (## - second page, ...)")
+ public CommandResult commandMyFeed(User user, URI attachment, String... arguments) {
+ // number of # is the page count
+ int page = arguments[0].length() - 1;
+ List mids = messagesService.getMyFeed(user.getUid(), page, false);
+ if (mids.size() > 0) {
+ return CommandResult.fromString("Your feed: \n" + printMessages(user, mids, true));
+ }
+ return CommandResult.fromString("Your feed is empty");
+ }
+ @UserCommand(pattern = "^(#|\\.)(\\d+)((\\.|\\-|\\/)(\\d+))?\\s([\\s\\S]+)?",
+ help = "#1234 *tag *tag2 - edit tags\n#1234 text - reply to message")
+ public CommandResult EditOrReply(User user, @Nonnull URI attachment, String... args) throws Exception {
+ int mid = NumberUtils.toInt(args[1]);
+ int rid = NumberUtils.toInt(args[4], 0);
+ String txt = StringUtils.defaultString(args[5]);
+ Message msg = messagesService.getMessage(mid);
+ Pair> messageTags = tagService.fromString(txt);
+ if (messageTags.getRight().size() > 0) {
+ if (user.getUid() != msg.getUser().getUid()) {
+ return CommandResult.fromString("It is not your message");
+ }
+ if (!CollectionUtils.isEqualCollection(tagService.updateTags(mid, messageTags.getRight()), msg.getTags())) {
+ return CommandResult.fromString("Tags are updated");
+ } else {
+ return CommandResult.fromString("Tags are NOT updated (5 tags maximum?)");
+ }
+ } else {
+ boolean haveAttachment = StringUtils.isNotEmpty(attachment.toString());
+ String attachmentFName = null;
+ String attachmentType = null;
+ if (haveAttachment) {
+ attachmentFName = attachment.getScheme().equals("juick") ? attachment.getHost()
+ : HttpUtils.downloadImage(attachment.toURL(), tmpDir).getHost();
+ attachmentType = attachmentFName.substring(attachmentFName.length() - 3);
+ }
+ int newrid = messagesService.createReply(mid, rid, user, txt, attachmentType);
+ if (haveAttachment) {
+ String fname = String.format("%d-%d.%s", mid, newrid, attachmentType);
+ imagesService.saveImageWithPreviews(attachmentFName, fname);
+ }
+ applicationEventPublisher.publishEvent(
+ new MessageReadEvent(this, user, msg));
+ Message original = messagesService.getMessage(mid);
+ subscriptionService.subscribeMessage(original, user);
+ Message reply = messagesService.getReply(mid, newrid);
+ applicationEventPublisher.publishEvent(new MessageEvent(this, reply, subscriptionService.getUsersSubscribedToComments(original, reply)));
+ return CommandResult.build(reply,"Reply posted.\n#" + mid + "/" + newrid + " "
+ + "https://juick.com/m/" + mid + "#" + newrid,
+ String.format("[Reply](%s) posted", PlainTextFormatter.formatUrl(reply)));
+ }
+ }
+
+ String printMessages(User visitor, List 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
new file mode 100644
index 00000000..1cdafac6
--- /dev/null
+++ b/src/main/java/com/juick/server/EmailManager.java
@@ -0,0 +1,165 @@
+package com.juick.server;
+
+import com.juick.Message;
+import com.juick.User;
+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 org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Nonnull;
+import javax.inject.Inject;
+import javax.mail.MessagingException;
+import javax.mail.Multipart;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import static com.juick.formatters.PlainTextFormatter.formatPost;
+import static com.juick.formatters.PlainTextFormatter.formatUrl;
+
+@Component
+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;
+ @Override
+ public void processMessageEvent(@Nonnull MessageEvent event) {
+ Message msg = event.getMessage();
+ List subscribedUsers = event.getUsers();
+ 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());
+ 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 processSubscribeEvent(SubscribeEvent subscribeEvent) {
+
+ }
+
+ @Override
+ public void processLikeEvent(LikeEvent likeEvent) {
+
+ }
+
+ @Override
+ public void processPingEvent(PingEvent pingEvent) {
+
+ }
+
+ @Override
+ public void processMessageReadEvent(MessageReadEvent messageReadEvent) {
+
+ }
+
+ @Override
+ public void processTopEvent(TopEvent topEvent) {
+
+ }
+
+ 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());
+ headers.put("In-Reply-To", String.format("<%d.%d@juick.com>", original.getMid(), original.getRid()));
+ }
+ }
+ String plainText = String.format("%s\n\n--\nYou are receiving this because you are subscribed to this user," +
+ " discussion, tag or mentioned. Reply to this email directly or view it on Juick: %s.",
+ formatPost(msg), formatUrl(msg));
+ String hash = userService.getHashByUID(userService.getUserByEmail(email).getUid());
+ String htmlText = String.format("%s
--
You are receiving this because you are subscribed to this user" +
+ ", discussion, tag or mentioned. Reply to this email directly or view it on Juick." +
+ "
Configure or disable notifications",
+ msg.isHtml() ? msg.getText() : MessageUtils.formatHtml(msg), formatUrl(msg),
+ msg.getMid(), msg.getRid(), hash, hash);
+ sendEmail(email, subject, plainText, htmlText, headers);
+ }
+ public void sendEmail(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());
+ }
+ }
+ };
+ message.setFrom(new InternetAddress("juick@juick.com"));
+ 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("https://juick.com/settings/unsubscribe?hash=%s",
+ userService.getHashByUID(emailUser.getUid())));
+ message.setHeader("List-Unsubscribe-Post", "List-Unsubscribe=One-Click");
+ }
+ message.saveChanges();
+ transport.connect();
+ transport.sendMessage(message, message.getAllRecipients());
+ } catch (MessagingException ex) {
+ logger.error("mail exception", ex);
+ }
+ }
+}
diff --git a/src/main/java/com/juick/server/KeystoreManager.java b/src/main/java/com/juick/server/KeystoreManager.java
new file mode 100644
index 00000000..97c3a224
--- /dev/null
+++ b/src/main/java/com/juick/server/KeystoreManager.java
@@ -0,0 +1,92 @@
+package com.juick.server;
+
+import com.juick.server.api.activity.model.objects.Person;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Base64Utils;
+
+import javax.annotation.PostConstruct;
+import javax.net.ssl.KeyManagerFactory;
+import java.io.FileInputStream;
+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;
+import java.util.stream.Collectors;
+
+@Component
+public class KeystoreManager {
+ private static final Logger logger = LoggerFactory.getLogger("com.juick.server");
+ @Value("${keystore:../juick.p12}")
+ private String keystore;
+ @Value("${keystore_password:secret}")
+ private String keystorePassword;
+
+ private KeyStore ks;
+
+ private KeyManagerFactory kmf;
+
+ @PostConstruct
+ public void init() {
+ try (InputStream ksIs = new FileInputStream(keystore)) {
+ ks = KeyStore.getInstance("PKCS12");
+ ks.load(ksIs, keystorePassword.toCharArray());
+ kmf = KeyManagerFactory.getInstance(KeyManagerFactory
+ .getDefaultAlgorithm());
+ kmf.init(ks, keystorePassword.toCharArray());
+ } catch (IOException | KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | CertificateException e) {
+ logger.error("Keystore error", e);
+ }
+ }
+
+ public KeyStore getKeystore() {
+ return ks;
+ }
+
+ public KeyManagerFactory getKeymanagerFactory() {
+ return kmf;
+ }
+
+ private KeyPair getKeyPair() {
+ Key privateKey = null;
+ 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",
+ Arrays.asList(key).stream().collect(Collectors.joining("\n")));
+ }
+ 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
new file mode 100644
index 00000000..ef848526
--- /dev/null
+++ b/src/main/java/com/juick/server/ServerManager.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.juick.server;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.juick.Message;
+import com.juick.User;
+import com.juick.model.AnonymousUser;
+import com.juick.service.MessagesService;
+import com.juick.service.SubscriptionService;
+import com.juick.service.UserService;
+import com.juick.service.component.LikeEvent;
+import com.juick.service.component.MessageEvent;
+import com.juick.service.component.MessageReadEvent;
+import com.juick.service.component.NotificationListener;
+import com.juick.service.component.PingEvent;
+import com.juick.service.component.SubscribeEvent;
+import com.juick.service.component.TopEvent;
+import com.juick.util.MessageUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import org.springframework.web.socket.TextMessage;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.io.IOException;
+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 Logger logger = LoggerFactory.getLogger(ServerManager.class);
+
+ @Inject
+ private ObjectMapper jsonMapper;
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private WebsocketManager wsHandler;
+ @Inject
+ private SubscriptionService subscriptionService;
+ @Inject
+ private UserService userService;
+ private 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 com.juick.Message jmsg) {
+ try {
+ String json = jsonMapper.writeValueAsString(jmsg);
+ synchronized (wsHandler.getClients()) {
+ wsHandler.getClients().stream().filter(c ->
+ (!c.legacy && c.visitor.getUid() == to.getUid()) || c.visitor.equals(serviceUser))
+ .forEach(c -> {
+ try {
+ logger.debug("sending pm to {}", c.visitor.getUid());
+ c.sendMessage(new TextMessage(json));
+ } catch (IOException e) {
+ logger.warn("ws error", e);
+ }
+ });
+ }
+ } catch (JsonProcessingException e) {
+ logger.warn("Invalid JSON", e);
+ }
+ messageEvent(jmsg, Collections.singletonList(to));
+ }
+
+ private void onJuickMessagePost(final com.juick.Message jmsg, List subscribedUsers) {
+ try {
+ String json = jsonMapper.writeValueAsString(jmsg);
+ List uids = subscribedUsers
+ .stream().map(User::getUid).collect(Collectors.toList());
+ synchronized (wsHandler.getClients()) {
+ wsHandler.getClients().stream().filter(c ->
+ (!c.legacy && c.visitor.isAnonymous()) // anonymous users
+ || c.visitor.equals(serviceUser) // services
+ || (!c.legacy && uids.contains(c.visitor.getUid()))) // subscriptions
+ .forEach(c -> {
+ try {
+ logger.debug("sending message to {}", c.visitor.getUid());
+ c.sendMessage(new TextMessage(json));
+ } catch (IOException e) {
+ logger.warn("ws error", e);
+ }
+ });
+ wsHandler.getClients().stream().filter(c ->
+ c.legacy && c.allMessages) // legacy all posts
+ .forEach(c -> {
+ try {
+ logger.debug("sending message to legacy client {}", c.visitor.getUid());
+ c.sendMessage(new TextMessage(json));
+ } catch (IOException e) {
+ logger.warn("ws error", e);
+ }
+ });
+ }
+ } catch (JsonProcessingException e) {
+ logger.warn("Invalid JSON", e);
+ }
+ messageEvent(jmsg, subscribedUsers);
+ messageEvent(jmsg, Collections.singletonList(AnonymousUser.INSTANCE));
+ }
+
+ private void onJuickMessageReply(final com.juick.Message jmsg, final List subscribedUsers) {
+ try {
+
+ String json = jsonMapper.writeValueAsString(jmsg);
+ com.juick.Message op = messagesService.getMessage(jmsg.getMid());
+ List threadUsers =
+ subscribedUsers
+ .stream().map(User::getUid).collect(Collectors.toList());
+ synchronized (wsHandler.getClients()) {
+ wsHandler.getClients().stream().filter(c ->
+ (!c.legacy && c.visitor.isAnonymous()) // anonymous users
+ || c.visitor.equals(serviceUser) // services
+ || (!c.legacy && threadUsers.contains(c.visitor.getUid()))) // subscriptions
+ .forEach(c -> {
+ try {
+ logger.debug("sending reply to {}", c.visitor.getUid());
+ c.sendMessage(new TextMessage(json));
+ } catch (IOException e) {
+ logger.warn("ws error", e);
+ }
+ });
+ wsHandler.getClients().stream().filter(c ->
+ (c.legacy && c.allReplies) || (c.legacy && c.MID == jmsg.getMid())) // legacy replies
+ .forEach(c -> {
+ try {
+ logger.debug("sending reply to legacy client {}", c.visitor.getUid());
+ c.sendMessage(new TextMessage(json));
+ } catch (IOException e) {
+ logger.warn("ws error", e);
+ }
+ });
+ }
+ } catch (JsonProcessingException e) {
+ logger.warn("Invalid JSON", e);
+ }
+ messageEvent(jmsg, subscribedUsers);
+ messageEvent(jmsg, Collections.singletonList(AnonymousUser.INSTANCE));
+ }
+
+ @Override
+ public void processMessageEvent(MessageEvent event) {
+ com.juick.Message jmsg = event.getMessage();
+ List subscribedUsers = event.getUsers();
+ if (jmsg.isService()) {
+ readEvent(jmsg, Collections.singletonList(serviceUser));
+ return;
+ }
+ if (MessageUtils.isPM(jmsg)) {
+ onJuickPM(jmsg.getTo(), jmsg);
+ } else if (!MessageUtils.isReply(jmsg)) {
+ // to get full message with attachment, etc.
+ onJuickMessagePost(messagesService.getMessage(jmsg.getMid()), subscribedUsers);
+ } else {
+ // to get quote and attachment
+ Message op = messagesService.getMessage(jmsg.getMid());
+ com.juick.Message reply = messagesService.getReply(jmsg.getMid(), jmsg.getRid());
+ subscriptionService.getUsersSubscribedToComments(op, reply, true).stream()
+ .filter(u -> userService.isReplyToBL(u, reply))
+ .forEach(b -> messagesService.setLastReadComment(b, reply.getMid(), reply.getRid()));
+ onJuickMessageReply(reply, subscribedUsers);
+ }
+ messageEvent(jmsg, Collections.singletonList(serviceUser));
+ }
+
+ @Override
+ public void processSubscribeEvent(SubscribeEvent subscribeEvent) {
+
+ }
+
+ @Override
+ public void processLikeEvent(LikeEvent likeEvent) {
+
+ }
+
+ @Override
+ public void processPingEvent(PingEvent pingEvent) {
+
+ }
+
+ @Override
+ public void processMessageReadEvent(MessageReadEvent messageReadEvent) {
+ User user = messageReadEvent.getUser();
+ Message source = messageReadEvent.getMessage();
+
+ logger.info("Message read event from {} for {}", user.getUid(), source.getMid());
+ Message serviceMessage = new Message();
+ serviceMessage.setService(true);
+ serviceMessage.setUser(user);
+ serviceMessage.setMid(source.getMid());
+ serviceMessage.setUnread(false);
+ wsHandler.getClients().stream().filter(c ->
+ (!c.legacy && c.visitor == user) || c.visitor.equals(serviceUser)
+ ).forEach(u -> {
+ try {
+ u.sendMessage(new TextMessage(jsonMapper.writeValueAsString(serviceMessage)));
+ } catch (IOException e) {
+ logger.error("JSON error", e);
+ }
+ });
+ readEvent(serviceMessage, Collections.singletonList(serviceUser));
+ }
+
+ @Override
+ public void processTopEvent(TopEvent topEvent) {
+ User topUser = topEvent.getMessage().getUser();
+ topEvent(topEvent.getMessage(), Arrays.asList(topUser, serviceUser));
+ }
+
+ public void topEvent(Message msg, List subscribers){
+ sendSseEvent(msg, "top", subscribers);
+ }
+
+ public void readEvent(Message msg, List subscribers){
+ sendSseEvent(msg, "read", subscribers);
+ }
+
+ public 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 User user;
+ private 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;
+ }
+}
diff --git a/src/main/java/com/juick/server/SignatureManager.java b/src/main/java/com/juick/server/SignatureManager.java
new file mode 100644
index 00000000..b3b7a301
--- /dev/null
+++ b/src/main/java/com/juick/server/SignatureManager.java
@@ -0,0 +1,113 @@
+package com.juick.server;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.juick.server.api.activity.model.Context;
+import com.juick.server.api.activity.model.objects.Person;
+import com.juick.server.api.webfinger.model.Account;
+import com.juick.server.api.webfinger.model.Link;
+import com.juick.util.DateFormattersHolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+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.server.api.activity.model.Context.ACTIVITY_MEDIA_TYPE;
+
+@Component
+public class SignatureManager {
+ private static final Logger logger = LoggerFactory.getLogger(ActivityPubManager.class);
+ @Inject
+ private KeystoreManager keystoreManager;
+ @Inject
+ private ObjectMapper jsonMapper;
+ @Inject
+ private ApplicationEventPublisher applicationEventPublisher;
+ @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);
+ Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null,
+ "(request-target)", "host", "date");
+ Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature);
+ Map headers = new HashMap<>();
+ headers.put("host", inbox.getHost());
+ headers.put("date", requestDate);
+ Signature signature = signer.sign("POST", inbox.getPath(), headers);
+ HttpHeaders requestHeaders = new HttpHeaders();
+ requestHeaders.add("Content-Type", Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE);
+ requestHeaders.add("Date", requestDate);
+ requestHeaders.add("Signature", signature.toString().substring(10));
+ HttpEntity request = new HttpEntity<>(Context.build(data), requestHeaders);
+ //boolean valid = verifySignature(Signature.fromString(requestHeaders.getFirst("Signature")),
+ // keystoreManager.getPublicKey(), "POST", inbox.getPath(), headers);
+ logger.info("Sending context: {}", jsonMapper.writeValueAsString(data));
+ logger.info("Request date: {}", requestDate);
+ ResponseEntity response = apClient.postForEntity(inbox, request, Void.class);
+ logger.info("accepted follower: {}", response.getStatusCodeValue());
+
+ }
+ public boolean verifySignature(String signatureString, URI actor, String method, String path, Map headers) {
+ Optional context = getContext(actor);
+ if (context.isPresent() && context.get() instanceof Person) {
+ Person person = (Person) context.get();
+ Key key = KeystoreManager.publicKeyOf(person);
+ Verifier verifier = new Verifier(key, Signature.fromString(signatureString));
+ try {
+ boolean result = verifier.verify(method, path, headers);
+ logger.info("signature is valid: {}", result);
+ return result;
+ } catch (NoSuchAlgorithmException | SignatureException | IOException e) {
+ logger.info("signature exception", e);
+ return false;
+ }
+ }
+ logger.info("person not found");
+ return false;
+ }
+ public Optional getContext(URI contextUri) {
+ Context context = apClient.getForEntity(contextUri, Context.class).getBody();
+ if (context == null) {
+ logger.warn("Cannot identify {}", contextUri);
+ return Optional.empty();
+ }
+ return Optional.of(context);
+ }
+ public Optional discoverPerson(String acct) {
+ Jid acctId = Jid.of(acct);
+ URI resourceUri = UriComponentsBuilder.fromUriString(
+ String.format("https://%s/.well-known/webfinger?resource=acct:%s", acctId.getDomain(), acct)).build().toUri();
+ Account acctData = apClient.getForEntity(resourceUri, Account.class).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
new file mode 100644
index 00000000..8e8d0104
--- /dev/null
+++ b/src/main/java/com/juick/server/TelegramBotManager.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server;
+
+import com.juick.User;
+import com.juick.service.component.*;
+import com.juick.model.AnonymousUser;
+import com.juick.model.CommandResult;
+import com.juick.server.util.HttpUtils;
+import com.juick.service.MessagesService;
+import com.juick.service.TelegramService;
+import com.juick.service.UserService;
+import com.juick.util.MessageUtils;
+import com.pengrad.telegrambot.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.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(TelegramBotManager.class);
+
+ private TelegramBot bot;
+
+ @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;
+
+ private static final String MSG_LINK = "🔗";
+
+ @PostConstruct
+ public void init() {
+ if (StringUtils.isBlank(telegramToken)) {
+ logger.info("telegram token is not set, exiting");
+ return;
+ }
+ bot = new TelegramBot(telegramToken);
+ if (!telegramDebug) {
+ try {
+ SetWebhook webhook = new SetWebhook().url("https://api.juick.com/tlgmbtwbhk");
+ if (!bot.execute(webhook).isOk()) {
+ logger.error("error setting webhook");
+ }
+ } catch (Exception e) {
+ logger.warn("couldn't initialize telegram bot", e);
+ }
+ } else {
+ bot.setUpdatesListener(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());
+
+ 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.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 = 0;
+ try {
+ mid = Integer.valueOf(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.valueOf(uriComponents.getFragment());
+ prefix = String.format("#%d/%d ", mid, rid);
+ }
+ CommandResult result = null;
+ try {
+ result = commandsManager.processCommand(user_from, prefix + text, attachment);
+ String messageTxt = StringUtils.isNotEmpty(result.getMarkdown()) ? result.getMarkdown()
+ : "Unknown error or unsupported command";
+ telegramNotify(message.from().id().longValue(), messageTxt, new com.juick.Message());
+ } catch (Exception e) {
+ logger.warn("telegram exception", e);
+ }
+ } 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.Message());
+ }
+ } else {
+ telegramNotify(message.from().id().longValue(),
+ "Can not reply to this message", replyMessage.messageId(), new com.juick.Message());
+ }
+ } else {
+ try {
+ CommandResult result = commandsManager.processCommand(user_from, text, attachment);
+ String messageTxt = StringUtils.isNotEmpty(result.getMarkdown()) ? result.getMarkdown()
+ : "Unknown error or unsupported command";
+ telegramNotify(message.from().id().longValue(), messageTxt, new com.juick.Message());
+ } catch (Exception e) {
+ logger.warn("telegram reply exception", e);
+ }
+ }
+ }
+ }
+ } 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));
+ }
+ }
+
+ 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.Message source) {
+ telegramNotify(chatId, msg, 0, source);
+ }
+
+ public void telegramNotify(Long chatId, String msg, Integer replyTo, @Nonnull com.juick.Message source) {
+ String attachment = MessageUtils.attachmentUrl(source);
+ if (StringUtils.isEmpty(attachment)) {
+ 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);
+ }
+ });
+ } else {
+ SendPhoto telegramPhoto = new SendPhoto(chatId, attachment);
+ String trimmedPost = msg.length() > 1024 ? msg.substring(0, 1023) + "..." : msg;
+ telegramPhoto.caption(trimmedPost);
+ if (replyTo > 0) {
+ telegramPhoto.replyToMessageId(replyTo);
+ }
+ telegramPhoto.parseMode(ParseMode.Markdown);
+ 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);
+ }
+ });
+ }
+ }
+
+ private void processTelegramResponse(Long chatId, SendResponse response, com.juick.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 MessageReadEvent(this, 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 processMessageEvent(MessageEvent messageEvent) {
+ com.juick.Message jmsg = messageEvent.getMessage();
+ List subscribedUsers = messageEvent.getUsers();
+ 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)) {
+ com.juick.Message op = messagesService.getMessage(jmsg.getMid());
+ 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));
+ }
+ }
+
+ @Override
+ public void processLikeEvent(LikeEvent likeEvent) {
+ User liker = likeEvent.getUser();
+ com.juick.Message message = likeEvent.getMessage();
+ List subscribers = likeEvent.getSubscribers();
+ 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.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.Message()));
+ }
+
+ @Override
+ public void processPingEvent(PingEvent pingEvent) {
+
+ }
+
+ @Override
+ public void processMessageReadEvent(MessageReadEvent messageReadEvent) {
+
+ }
+
+ @Override
+ public void processTopEvent(TopEvent topEvent) {
+ com.juick.Message message = topEvent.getMessage();
+ telegramService.getTelegramIdentifiers(Collections.singletonList(message.getUser()))
+ .forEach(c -> telegramNotify(c, String.format("Your [post](%s) became popular!",
+ formatUrl(message)), new com.juick.Message()));
+ }
+
+ @Override
+ public void processSubscribeEvent(SubscribeEvent subscribeEvent) {
+ User subscriber = subscribeEvent.getUser();
+ User target = subscribeEvent.getToUser();
+ telegramService.getTelegramIdentifiers(Collections.singletonList(target))
+ .forEach(c -> telegramNotify(c, String.format("%s subscribed to your blog",
+ MessageUtils.getMarkdownUser(subscriber)), new com.juick.Message()));
+ }
+}
diff --git a/src/main/java/com/juick/server/TopManager.java b/src/main/java/com/juick/server/TopManager.java
new file mode 100644
index 00000000..e5c00242
--- /dev/null
+++ b/src/main/java/com/juick/server/TopManager.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server;
+
+import com.juick.Message;
+import com.juick.Tag;
+import com.juick.service.MessagesService;
+import com.juick.service.component.TopEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.inject.Inject;
+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 ApplicationEventPublisher applicationEventPublisher;
+
+ @Scheduled(fixedRate = 3600000)
+ public void updateTop() {
+ messagesService.getPopularCandidates().forEach(m -> {
+ Message jmsg = messagesService.getMessage(m);
+ 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 TopEvent(this, jmsg));
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/juick/server/TwitterManager.java b/src/main/java/com/juick/server/TwitterManager.java
new file mode 100644
index 00000000..613594e6
--- /dev/null
+++ b/src/main/java/com/juick/server/TwitterManager.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.juick.server;
+
+import com.juick.Message;
+import com.juick.User;
+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.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 com.juick.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 processMessageEvent(MessageEvent messageEvent) {
+ Message msg = messageEvent.getMessage();
+ 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);
+ }
+ }
+ }
+
+ @Override
+ public void processSubscribeEvent(SubscribeEvent subscribeEvent) {
+
+ }
+
+ @Override
+ public void processLikeEvent(LikeEvent likeEvent) {
+
+ }
+
+ @Override
+ public void processPingEvent(PingEvent pingEvent) {
+
+ }
+
+ @Override
+ public void processMessageReadEvent(MessageReadEvent messageReadEvent) {
+
+ }
+
+ @Override
+ public void processTopEvent(TopEvent topEvent) {
+ Message jmsg = topEvent.getMessage();
+ 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
new file mode 100644
index 00000000..23768ed2
--- /dev/null
+++ b/src/main/java/com/juick/server/Utils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.juick.server;
+
+import 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/WebsocketManager.java b/src/main/java/com/juick/server/WebsocketManager.java
new file mode 100644
index 00000000..1b62b984
--- /dev/null
+++ b/src/main/java/com/juick/server/WebsocketManager.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server;
+
+import com.juick.User;
+import com.juick.model.AnonymousUser;
+import com.juick.model.CommandResult;
+import com.juick.server.util.HttpForbiddenException;
+import com.juick.server.util.HttpNotFoundException;
+import com.juick.service.MessagesService;
+import com.juick.service.UserService;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.PingMessage;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.annotation.Nonnull;
+import javax.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Created by vitalyster on 28.06.2016.
+ */
+@Component
+public class WebsocketManager extends TextWebSocketHandler {
+ private static final Logger logger = LoggerFactory.getLogger(WebsocketManager.class);
+
+ private final List clients = new CopyOnWriteArrayList<>();
+
+ @Inject
+ private UserService userService;
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private CommandsManager commandsManager;
+
+
+ @Override
+ public void afterConnectionEstablished(WebSocketSession session) {
+
+ UserSession userSession = new UserSession(session);
+ URI hLocation = session.getUri();
+
+ // Auth
+ UriComponents uriComponents = UriComponentsBuilder.fromUri(hLocation).build();
+ List hash = uriComponents.getQueryParams().get("hash");
+ if (hash != null && hash.get(0).length() == 16) {
+ userSession.visitor = userService.getUserByHash(hash.get(0));
+ } else {
+ logger.debug("wrong hash for {} from {}", userSession.visitor.getUid(), userSession);
+ }
+
+ if (hLocation.getPath().equals("/ws/")) {
+ logger.debug("user {} connected", userSession.visitor.getUid());
+ } else if (hLocation.getPath().equals("/ws/_all")) {
+ logger.debug("user {} connected to legacy _all ({})", userSession.visitor.getUid(), hLocation.getPath());
+ userSession.legacy = true;
+ userSession.allMessages = true;
+ } else if (hLocation.getPath().equals("/ws/_replies")) {
+ logger.debug("user {} connected to legacy _replies ({})", userSession.visitor.getUid(), hLocation.getPath());
+ userSession.legacy = true;
+ userSession.allReplies = true;
+ } else if (hLocation.getPath().matches("^/ws/(\\d)+$")) {
+ int MID = NumberUtils.toInt(hLocation.getPath().substring(4), 0);
+ if (MID > 0) {
+ if (messagesService.canViewThread(MID, userSession.visitor.getUid())) {
+ logger.debug("user {} connected to legacy thread ({}) from {}", userSession.visitor.getUid(), MID, userSession);
+ userSession.legacy = true;
+ userSession.MID = MID;
+ } else {
+ throw new HttpForbiddenException();
+ }
+ }
+ } else {
+ throw new HttpNotFoundException();
+ }
+ clients.add(userSession);
+ logger.debug("{} clients connected", clients.size());
+ }
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
+ logger.debug("session closed with status {}: {}", status.getCode(), status.getReason());
+ clients.removeIf(c -> c.getDelegate().getId().equals(session.getId()));
+ logger.debug("{} clients connected", clients.size());
+ }
+
+ @Scheduled(fixedRate = 30000)
+ public void ping() {
+ clients.forEach(c -> {
+ try {
+ if (c.isOpen()) {
+ c.sendMessage(new PingMessage());
+ }
+ } catch (IOException e) {
+ logger.error("WebSocket PING exception", e);
+ }
+ });
+ }
+
+ @Override
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+ UserSession ws = clients.stream().filter(c -> c.getDelegate().equals(session))
+ .findFirst().orElseThrow(IllegalStateException::new);
+ if (!ws.visitor.isAnonymous()) {
+ String command = message.getPayload().trim();
+ if (StringUtils.isNotEmpty(command)) {
+ CommandResult result = commandsManager.processCommand(ws.visitor, command, URI.create(""));
+ ws.sendMessage(new TextMessage(result.getText()));
+ }
+ } else {
+ ws.sendMessage(new TextMessage("Authorization required"));
+ }
+ }
+
+ public List getClients() {
+ return clients;
+ }
+
+ class UserSession extends ConcurrentWebSocketSessionDecorator {
+ User visitor;
+ int MID;
+ boolean allMessages;
+ boolean allReplies;
+ Instant tsConnected;
+ Instant tsLastData;
+ boolean legacy;
+
+ UserSession(WebSocketSession session) {
+ super(session, 60000, 65536);
+ this.visitor = AnonymousUser.INSTANCE;
+ tsConnected = tsLastData = Instant.now();
+ }
+
+ @Nonnull
+ @Override
+ public String toString() {
+ HttpHeaders headers = getHandshakeHeaders();
+ return headers.getOrDefault("X-Real-IP",
+ Collections.singletonList(getRemoteAddress().toString())).get(0);
+ }
+ }
+}
diff --git a/src/main/java/com/juick/server/XMPPConnection.java b/src/main/java/com/juick/server/XMPPConnection.java
new file mode 100644
index 00000000..9c0c09e1
--- /dev/null
+++ b/src/main/java/com/juick/server/XMPPConnection.java
@@ -0,0 +1,693 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server;
+
+import com.juick.User;
+import com.juick.formatters.PlainTextFormatter;
+import com.juick.service.component.*;
+import com.juick.model.CommandResult;
+import com.juick.model.UserInfo;
+import com.juick.server.xmpp.iq.MessageQuery;
+import com.juick.server.xmpp.s2s.BasicXmppSession;
+import com.juick.server.xmpp.s2s.StanzaListener;
+import com.juick.service.*;
+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.context.ApplicationEventPublisher;
+import rocks.xmpp.addr.Jid;
+import rocks.xmpp.core.XmppException;
+import rocks.xmpp.core.session.XmppSession;
+import rocks.xmpp.core.stanza.AbstractIQHandler;
+import rocks.xmpp.core.stanza.model.*;
+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 rocks.xmpp.util.XmppUtils;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.xml.bind.JAXBException;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.net.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.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * @author ugnich
+ */
+public class XMPPConnection implements StanzaListener, NotificationListener {
+
+ private static final Logger logger = LoggerFactory.getLogger("com.juick.server.xmpp");
+
+ private ExternalComponent router;
+ @Inject
+ private XMPPServer xmpp;
+ @Inject
+ private CommandsManager commandsManager;
+ @Value("${xmppbot_jid:juick@localhost}")
+ private Jid jid;
+ @Value("${componentname:localhost}")
+ private String componentName;
+ @Value("${component_port:5347}")
+ private int componentPort;
+ @Value("${xmpp_password:secret}")
+ private String password;
+ @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String tmpDir;
+
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private UserService userService;
+ @Inject
+ private SubscriptionService subscriptionService;
+ @Inject
+ private PMQueriesService pmQueriesService;
+ @Inject
+ private BasicXmppSession session;
+ @Inject
+ private ExecutorService service;
+ @Inject
+ private ApplicationEventPublisher applicationEventPublisher;
+ @Value("${service_user:juick}")
+ private String serviceUsername;
+
+ private User serviceUser;
+
+ @PostConstruct
+ public void init() {
+ logger.info("stream router start connecting to {}", componentPort);
+ xmpp.addStanzaListener(this);
+ router = ExternalComponent.create(componentName, password, session.getConfiguration(), "localhost", componentPort);
+ ServiceDiscoveryManager serviceDiscoveryManager = router.getManager(ServiceDiscoveryManager.class);
+ serviceDiscoveryManager.addIdentity(Identity.clientBot().withName("Juick"));
+ EntityCapabilitiesManager entityCapabilitiesManager = router.getManager(EntityCapabilitiesManager.class);
+ entityCapabilitiesManager.setNode("https://juick.com/caps");
+ MessageDeliveryReceiptsManager messageDeliveryReceiptsManager = router.getManager(MessageDeliveryReceiptsManager.class);
+ messageDeliveryReceiptsManager.setEnabled(true);
+ PingManager pingManager = router.getManager(PingManager.class);
+ pingManager.setEnabled(true);
+ SoftwareVersionManager softwareVersionManager = router.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(
+ getClass().getClassLoader().getResource("juick.png"))));
+ } catch (MalformedURLException e) {
+ logger.error("invalid url", e);
+ } catch (IOException e) {
+ logger.warn("invalid resource", e);
+ }
+ router.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");
+ router.send(warningMessage);
+ return iq.createError(new StanzaError(Condition.BAD_REQUEST, "Please stop this spam"));
+ });
+ router.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()) {
+ UserInfo info = userService.getUserInfo(user);
+ VCard userVCard = new VCard();
+ userVCard.setFormattedName(info.getFullName());
+ userVCard.setNickname(user.getName());
+ try {
+ userVCard.setPhoto(new VCard.Image(new URI("http://i.juick.com/a/" + user.getUid() + ".png")));
+ if (info.getUrl() != null) {
+ userVCard.setUrl(new URL(info.getUrl()));
+ }
+ } catch (MalformedURLException | URISyntaxException e) {
+ logger.warn("url exception", e);
+ }
+ return iq.createResult(userVCard);
+ }
+ return iq.createError(Condition.BAD_REQUEST);
+ }
+ });
+ router.addInboundMessageListener(e -> {
+ ClientMessage result = incomingMessage(e.getMessage());
+ if (result != null) {
+ router.send(result);
+ }
+ });
+ router.addInboundIQListener(e -> {
+ IQ iq = e.getIQ();
+ Jid jid = iq.getTo();
+ if (!jid.getDomain().equals(this.jid.getDomain())) {
+ router.send(iq);
+ }
+ });
+ FileTransferManager fileTransferManager = router.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,
+ e.getDescription(), URI.create(String.format("juick://%s", targetFilename)));
+ if (result != null) {
+ router.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");
+ router.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);
+ }
+ });
+ router.addConnectionListener(event -> {
+ if (event.getType().equals(rocks.xmpp.core.session.ConnectionEvent.Type.RECONNECTION_SUCCEEDED)) {
+ logger.info("component connected");
+ }
+ });
+ router.addSessionStatusListener(event -> {
+ logger.info("event: " + event.getStatus(), event.getThrowable());
+ if (event.getStatus().equals(XmppSession.Status.AUTHENTICATED)) {
+ logger.info("Authenticated, broadcasting...");
+ broadcastPresence(null);
+ }
+ });
+ router.addInboundPresenceListener(event -> {
+ incomingPresence(event.getPresence());
+ });
+ service.submit(() -> {
+ try {
+ router.connect();
+ } catch (XmppException e) {
+ logger.warn("xmpp exception", e);
+ }
+ });
+ serviceUser = userService.getUserByName(serviceUsername);
+ }
+
+ private String stanzaToString(Stanza stanza) throws XMLStreamException, JAXBException {
+ StringWriter stanzaWriter = new StringWriter();
+ XMLStreamWriter xmppStreamWriter = XmppUtils.createXmppStreamWriter(
+ router.getConfiguration().getXmlOutputFactory().createXMLStreamWriter(stanzaWriter));
+ router.createMarshaller().marshal(stanza, xmppStreamWriter);
+ xmppStreamWriter.flush();
+ xmppStreamWriter.close();
+ return stanzaWriter.toString();
+ }
+
+ private void sendJuickMessage(com.juick.Message jmsg, List users) {
+ List jids = new ArrayList<>();
+
+ for (User user : users) {
+ jids.addAll(userService.getJIDsbyUID(user.getUid()));
+ }
+ com.juick.Message fullMsg = messagesService.getMessage(jmsg.getMid());
+ 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));
+ router.send(ClientMessage.from(msg));
+ }
+ }
+
+ public void sendJuickComment(com.juick.Message jmsg, List users) {
+ String replyQuote;
+ String replyTo;
+
+ com.juick.Message replyMessage = jmsg.getReplyto() > 0 ? messagesService.getReply(jmsg.getMid(), jmsg.getReplyto())
+ : messagesService.getMessage(jmsg.getMid());
+ replyTo = replyMessage.getUser().getName();
+ com.juick.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));
+ router.send(ClientMessage.from(msg));
+ }
+ }
+ }
+
+ @Override
+ public void processMessageEvent(MessageEvent event) {
+ com.juick.Message msg = event.getMessage();
+ List subscribers = event.getUsers();
+ 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());
+ }
+ router.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);
+ }
+
+ @Override
+ public void processSubscribeEvent(SubscribeEvent subscribeEvent) {
+
+ }
+
+ @Override
+ public void processLikeEvent(LikeEvent likeEvent) {
+ List users = likeEvent.getSubscribers();
+ com.juick.Message jmsg = likeEvent.getMessage();
+ User liker = likeEvent.getUser();
+
+ 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)));
+ router.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));
+ router.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);
+ router.send(ClientPresence.from(p));
+ });
+ }
+
+ @Override
+ public void processMessageReadEvent(MessageReadEvent messageReadEvent) {
+
+ }
+
+ @Override
+ public void processTopEvent(TopEvent topEvent) {
+ com.juick.Message message = topEvent.getMessage();
+ 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);
+ router.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");
+ }
+ router.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));
+ router.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);
+ router.send(ClientPresence.from(reply));
+
+ reply.setFrom(reply.getFrom().withResource(jid.getResource()));
+ reply.setPriority((byte) 10);
+ reply.setType(null);
+ router.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));
+ router.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);
+ router.send(ClientPresence.from(reply));
+ }
+ }
+
+ public ClientMessage incomingMessage(Message msg) {
+ ClientMessage result = null;
+ 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(router.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.");
+ }
+
+ com.juick.Message jmsg = msg.getExtension(com.juick.Message.class);
+ if (jmsg != null) {
+ if (MessageUtils.isReply(jmsg)) {
+ // to get quote and attachment
+ com.juick.Message original = messagesService.getMessage(jmsg.getMid());
+ com.juick.Message reply = messagesService.getReply(jmsg.getMid(), jmsg.getRid());
+ applicationEventPublisher.publishEvent(new MessageEvent(this, reply,
+ subscriptionService.getUsersSubscribedToComments(original, reply)));
+ } else if (!MessageUtils.isPM(jmsg)) {
+ applicationEventPublisher.publishEvent(new MessageEvent(this,
+ messagesService.getMessage(jmsg.getMid()), subscriptionService.getSubscribedUsers(jmsg.getUser().getUid(), jmsg)));
+ }
+ } else {
+ URI attachment = URI.create(StringUtils.EMPTY);
+ OobX oobX = msg.getExtension(OobX.class);
+ if (oobX != null) {
+ attachment = oobX.getUri();
+ }
+ try {
+ if (msg.getTo().asBareJid().equals(jid.asBareJid())) {
+ return incomingMessageJuick(user_from, msg.getFrom(), StringUtils.defaultString(msg.getBody()), attachment);
+ } else {
+ // PM
+ result = incomingMessageJuick(user_from, msg.getFrom(),
+ String.format("@%s %s", msg.getTo().getLocal(), StringUtils.defaultString(msg.getBody())), attachment);
+ }
+ } catch (Exception e1) {
+ logger.warn("message exception", e1);
+ }
+ }
+ } else if (to.getDomain().endsWith(jid.getDomain()) && (to.getDomain().equals(jid.getDomain())
+ || to.getDomain().endsWith("." + jid.getDomain()))) {
+ if (logger.isInfoEnabled()) {
+ try {
+ logger.info("unhandled message: {}", stanzaToString(msg));
+ } catch (JAXBException | XMLStreamException ex) {
+ logger.error("JAXB exception", ex);
+ }
+ }
+ } else {
+ return ClientMessage.from(msg);
+ }
+ return result;
+ }
+ private ClientMessage incomingMessageJuick(User user_from, Jid from, String command, @Nonnull URI attachment) {
+ if (StringUtils.isBlank(command) && attachment.toString().isEmpty()) {
+ return null;
+ }
+
+ int commandlen = command.length();
+
+ // COMPATIBILITY
+ if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) {
+ command = command.substring(3);
+ }
+
+ try {
+ CommandResult result = commandsManager.processCommand(user_from, command.trim(), 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;
+ }
+
+ @Override
+ public void stanzaReceived(Stanza xmlValue) {
+ router.send(xmlValue);
+ }
+
+ 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));
+ router.send(ClientPresence.from(presence));
+ } catch (IllegalArgumentException ex) {
+ logger.warn("Invalid jid: {}", j, ex);
+ }
+ });
+ }
+
+ @PreDestroy
+ public void close() throws Exception {
+ broadcastPresence(Presence.Type.UNAVAILABLE);
+ if (router != null) {
+ router.close();
+ }
+ }
+
+ public ExternalComponent getRouter() {
+ return router;
+ }
+}
diff --git a/src/main/java/com/juick/server/XMPPServer.java b/src/main/java/com/juick/server/XMPPServer.java
new file mode 100644
index 00000000..86ab6a78
--- /dev/null
+++ b/src/main/java/com/juick/server/XMPPServer.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server;
+
+import com.juick.server.xmpp.router.StreamError;
+import com.juick.server.xmpp.s2s.*;
+import com.juick.service.UserService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.xmlpull.v1.XmlPullParserException;
+import rocks.xmpp.addr.Jid;
+import rocks.xmpp.core.stanza.model.Stanza;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.net.ssl.*;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.SecureRandom;
+import java.security.cert.*;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * @author ugnich
+ */
+public class XMPPServer implements ConnectionListener {
+ private static final Logger logger = LoggerFactory.getLogger("com.juick.server.xmpp");
+
+ private static final int TIMEOUT_MINUTES = 15;
+
+ @Inject
+ public ExecutorService service;
+ @Value("${hostname:localhost}")
+ private Jid jid;
+ @Value("${s2s_port:5269}")
+ private int s2sPort;
+ @Value("${broken_ssl_hosts:}")
+ public String[] brokenSSLhosts;
+ @Value("${banned_hosts:}")
+ public String[] bannedHosts;
+
+ private final List inConnections = new CopyOnWriteArrayList<>();
+ private final Map> outConnections = new ConcurrentHashMap<>();
+ private final List outCache = new CopyOnWriteArrayList<>();
+ private final List stanzaListeners = new CopyOnWriteArrayList<>();
+ private final AtomicBoolean closeFlag = new AtomicBoolean(false);
+
+ SSLContext sc;
+ CertificateFactory cf;
+ CertPathValidator cpv;
+ PKIXParameters params;
+ private TrustManager[] trustAllCerts = new TrustManager[]{
+ new X509TrustManager() {
+ public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
+ }
+
+ public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
+ }
+
+ public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ }
+ };
+ private boolean tlsConfigured = false;
+
+
+ private ServerSocket listener;
+
+ @Inject
+ private BasicXmppSession session;
+ @Inject
+ private UserService userService;
+ @Inject
+ private KeystoreManager keystoreManager;
+
+ @PostConstruct
+ public void init() throws KeyStoreException {
+ closeFlag.set(false);
+ try {
+ sc = SSLContext.getInstance("TLSv1.2");
+ sc.init(keystoreManager.getKeymanagerFactory().getKeyManagers(), trustAllCerts, new SecureRandom());
+ TrustManagerFactory trustManagerFactory =
+ TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ Set ca = new HashSet<>();
+ trustManagerFactory.init((KeyStore)null);
+ Arrays.stream(trustManagerFactory.getTrustManagers()).forEach(t -> Arrays.stream(((X509TrustManager)t).getAcceptedIssuers()).forEach(cert -> ca.add(new TrustAnchor(cert, null))));
+ params = new PKIXParameters(ca);
+ params.setRevocationEnabled(false);
+ cpv = CertPathValidator.getInstance("PKIX");
+ cf = CertificateFactory.getInstance( "X.509" );
+ tlsConfigured = true;
+ } catch (Exception e) {
+ logger.warn("tls unavailable");
+ }
+ service.submit(() -> {
+ try {
+ listener = new ServerSocket(s2sPort);
+ logger.info("s2s listener ready");
+ while (!listener.isClosed()) {
+ if (Thread.currentThread().isInterrupted()) break;
+ Socket socket = listener.accept();
+ ConnectionIn client = new ConnectionIn(this, socket);
+ addConnectionIn(client);
+ service.submit(client);
+ }
+ } catch (SocketException e) {
+ // shutdown
+ } catch (IOException | XmlPullParserException e) {
+ logger.warn("xmpp exception", e);
+ }
+ });
+ }
+
+ public void addConnectionIn(ConnectionIn c) {
+ c.setListener(this);
+ inConnections.add(c);
+ }
+
+ public void addConnectionOut(ConnectionOut c, Optional socket) {
+ c.setListener(this);
+ outConnections.put(c, socket);
+ }
+
+ public void removeConnectionIn(ConnectionIn c) {
+ inConnections.remove(c);
+ }
+
+ public void removeConnectionOut(ConnectionOut c) {
+ outConnections.remove(c);
+ }
+
+ public String getFromCache(Jid to) {
+ final String[] cache = new String[1];
+ outCache.stream().filter(c -> c.hostname != null && c.hostname.equals(to)).findFirst().ifPresent(c -> {
+ cache[0] = c.xml;
+ outCache.remove(c);
+ });
+ return cache[0];
+ }
+
+ public Optional getConnectionOut(Jid hostname, boolean needReady) {
+ return outConnections.keySet().stream().filter(c -> c.to != null &&
+ c.to.equals(hostname) && (!needReady || c.streamReady)).findFirst();
+ }
+
+ public Optional getConnectionIn(String streamID) {
+ return inConnections.stream().filter(c -> c.streamID != null && c.streamID.equals(streamID)).findFirst();
+ }
+
+ public void sendOut(Jid hostname, String xml) {
+ boolean haveAnyConn = false;
+
+ ConnectionOut connOut = null;
+ for (ConnectionOut c : outConnections.keySet()) {
+ if (c.to != null && c.to.equals(hostname)) {
+ if (c.streamReady) {
+ connOut = c;
+ break;
+ } else {
+ haveAnyConn = true;
+ break;
+ }
+ }
+ }
+ if (connOut != null) {
+ connOut.send(xml);
+ return;
+ }
+
+ boolean haveCache = false;
+ for (CacheEntry c : outCache) {
+ if (c.hostname != null && c.hostname.equals(hostname)) {
+ c.xml += xml;
+ c.updated = Instant.now();
+ haveCache = true;
+ break;
+ }
+ }
+ if (!haveCache) {
+ outCache.add(new CacheEntry(hostname, xml));
+ }
+
+ if (!haveAnyConn && !closeFlag.get()) {
+ try {
+ createDialbackConnection(hostname.toEscapedString(), null, null);
+ } catch (Exception e) {
+ logger.warn("dialback error", e);
+ }
+ }
+ }
+
+ void createDialbackConnection(String to, String checkSID, String dbKey) throws Exception {
+ ConnectionOut connectionOut = new ConnectionOut(getJid(), Jid.of(to), null, null, checkSID, dbKey);
+ addConnectionOut(connectionOut, Optional.empty());
+ service.submit(() -> {
+ try {
+ Socket socket = new Socket();
+ socket.connect(DNSQueries.getServerAddress(to));
+ connectionOut.setInputStream(socket.getInputStream());
+ connectionOut.setOutputStream(socket.getOutputStream());
+ addConnectionOut(connectionOut, Optional.of(socket));
+ connectionOut.connect();
+ } catch (IOException e) {
+ logger.info("dialback to " + to + " exception", e);
+ }
+ });
+ }
+
+ public void startDialback(Jid from, String streamId, String dbKey) throws Exception {
+ Optional c = getConnectionOut(from, false);
+ if (c.isPresent()) {
+ c.get().sendDialbackVerify(streamId, dbKey);
+ } else {
+ createDialbackConnection(from.toEscapedString(), streamId, dbKey);
+ }
+ }
+
+ public void addStanzaListener(StanzaListener listener) {
+ stanzaListeners.add(listener);
+ }
+
+ public void onStanzaReceived(String xmlValue) {
+ logger.info("S2S: {}", xmlValue);
+ Stanza stanza = parse(xmlValue);
+ stanzaListeners.forEach(l -> l.stanzaReceived(stanza));
+ }
+
+ public BasicXmppSession getSession() {
+ return session;
+ }
+
+ public List getInConnections() {
+ return inConnections;
+ }
+
+ public Map> getOutConnections() {
+ return outConnections;
+ }
+
+ @Override
+ public boolean isTlsAvailable() {
+ return tlsConfigured;
+ }
+
+ @Override
+ public void starttls(ConnectionIn connection) {
+ logger.debug("stream {} securing", connection.streamID);
+ connection.sendStanza("");
+ try {
+ connection.setSocket(sc.getSocketFactory().createSocket(connection.getSocket(), connection.getSocket().getInetAddress().getHostAddress(),
+ connection.getSocket().getPort(), false));
+ SSLSocket sslSocket = (SSLSocket) connection.getSocket();
+ sslSocket.addHandshakeCompletedListener(handshakeCompletedEvent -> {
+ try {
+ CertPath certPath = cf.generateCertPath(Arrays.asList(handshakeCompletedEvent.getPeerCertificates()));
+ cpv.validate(certPath, params);
+ connection.setTrusted(true);
+ logger.info("connection from {} is trusted", connection.from);
+ } catch (SSLPeerUnverifiedException | CertificateException | CertPathValidatorException | InvalidAlgorithmParameterException e) {
+ logger.info("connection from {} is NOT trusted, falling back to dialback", connection.from);
+ }
+ });
+ sslSocket.setUseClientMode(false);
+ sslSocket.setNeedClientAuth(true);
+ sslSocket.startHandshake();
+ connection.setSecured(true);
+ logger.debug("stream from {} secured", connection.streamID);
+ connection.restartParser();
+ } catch (XmlPullParserException | IOException sex) {
+ logger.warn("stream {} ssl error {}", connection.streamID, sex);
+ connection.sendStanza("");
+ removeConnectionIn(connection);
+ connection.closeConnection();
+ }
+ }
+
+ @Override
+ public void proceed(ConnectionOut connection) {
+ try {
+ Socket socket = outConnections.get(connection).get();
+ socket = sc.getSocketFactory().createSocket(socket, socket.getInetAddress().getHostAddress(),
+ socket.getPort(), false);
+ SSLSocket sslSocket = (SSLSocket) socket;
+ sslSocket.addHandshakeCompletedListener(handshakeCompletedEvent -> {
+ try {
+ CertPath certPath = cf.generateCertPath(Arrays.asList(handshakeCompletedEvent.getPeerCertificates()));
+ cpv.validate(certPath, params);
+ connection.setTrusted(true);
+ logger.info("connection to {} is trusted", connection.to);
+ } catch (SSLPeerUnverifiedException | CertificateException | CertPathValidatorException | InvalidAlgorithmParameterException e) {
+ logger.info("connection to {} is NOT trusted, falling back to dialback", connection.to);
+ }
+ });
+ sslSocket.setNeedClientAuth(true);
+ sslSocket.startHandshake();
+ connection.setSecured(true);
+ logger.debug("stream to {} secured", connection.getStreamID());
+ connection.setInputStream(socket.getInputStream());
+ connection.setOutputStream(socket.getOutputStream());
+ connection.restartStream();
+ connection.sendOpenStream();
+ } catch (NoSuchElementException | XmlPullParserException | IOException sex) {
+ logger.error("s2s ssl error: {} {}, error {}", connection.to, connection.getStreamID(), sex);
+ connection.send("");
+ removeConnectionOut(connection);
+ connection.logoff();
+ }
+ }
+
+ @Override
+ public void verify(ConnectionOut connection, String from, String type, String sid) {
+ if (from != null && from.equals(connection.to.toEscapedString()) && sid != null && !sid.isEmpty() && type != null) {
+ getConnectionIn(sid).ifPresent(c -> c.sendDialbackResult(Jid.of(from), type));
+ }
+ }
+
+ @Override
+ public void dialbackError(ConnectionOut connection, StreamError error) {
+ logger.warn("Stream error from {}: {}", connection.getStreamID(), error.getCondition());
+ removeConnectionOut(connection);
+ connection.logoff();
+ }
+
+ @Override
+ public void finished(ConnectionOut connection, boolean dirty) {
+ logger.warn("stream to {} {} finished, dirty={}", connection.to, connection.getStreamID(), dirty);
+ removeConnectionOut(connection);
+ connection.logoff();
+ }
+
+ @Override
+ public void exception(ConnectionOut connection, Exception ex) {
+ logger.error("s2s out exception: {} {}, exception {}", connection.to, connection.getStreamID(), ex);
+ removeConnectionOut(connection);
+ connection.logoff();
+ }
+
+ @Override
+ public void ready(ConnectionOut connection) {
+ logger.debug("stream to {} {} ready", connection.to, connection.getStreamID());
+ String cache = getFromCache(connection.to);
+ if (cache != null) {
+ logger.debug("stream to {} {} sending cache", connection.to, connection.getStreamID());
+ connection.send(cache);
+ }
+ }
+
+ @Override
+ public boolean securing(ConnectionOut connection) {
+ return tlsConfigured && !Arrays.asList(brokenSSLhosts).contains(connection.to.toEscapedString());
+ }
+
+ public Stanza parse(String xml) {
+ try {
+ Unmarshaller unmarshaller = session.createUnmarshaller();
+ return (Stanza)unmarshaller.unmarshal(new StringReader(xml));
+ } catch (JAXBException e) {
+ logger.error("JAXB exception", e);
+ }
+ return null;
+ }
+
+ public Jid getJid() {
+ return jid;
+ }
+ @Scheduled(fixedDelay = 10000)
+ public void cleanUp() {
+ Instant now = Instant.now();
+ outConnections.keySet().stream().filter(c -> Duration.between(c.getUpdated(), now).toMinutes() > TIMEOUT_MINUTES)
+ .forEach(c -> {
+ logger.info("closing idle outgoing connection to {}", c.to);
+ c.logoff();
+ outConnections.remove(c);
+ });
+
+ inConnections.stream().filter(c -> Duration.between(c.updated, now).toMinutes() > TIMEOUT_MINUTES)
+ .forEach(c -> {
+ logger.info("closing idle incoming connection from {}", c.from);
+ c.closeConnection();
+ inConnections.remove(c);
+ });
+ }
+ @PreDestroy
+ public void preDestroy() throws IOException {
+ closeFlag.set(true);
+ if (listener != null && !listener.isClosed()) {
+ listener.close();
+ }
+ service.shutdown();
+ logger.info("XMPP server destroyed");
+ }
+
+ public int getServerPort() {
+ return s2sPort;
+ }
+}
diff --git a/src/main/java/com/juick/server/api/ApiSocialLogin.java b/src/main/java/com/juick/server/api/ApiSocialLogin.java
new file mode 100644
index 00000000..8d9f9402
--- /dev/null
+++ b/src/main/java/com/juick/server/api/ApiSocialLogin.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.juick.server.api;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.scribejava.apis.FacebookApi;
+import com.github.scribejava.apis.VkontakteApi;
+import com.github.scribejava.core.builder.ServiceBuilder;
+import com.github.scribejava.core.model.OAuth2AccessToken;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Verb;
+import com.github.scribejava.core.oauth.OAuth20Service;
+import com.juick.model.facebook.User;
+import com.juick.server.util.HttpBadRequestException;
+import com.juick.service.CrosspostService;
+import com.juick.service.EmailService;
+import com.juick.service.TelegramService;
+import com.juick.service.UserService;
+import com.juick.model.vk.UsersResponse;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.io.IOException;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+
+/**
+ *
+ * @author Ugnich Anton
+ */
+@Controller
+public class ApiSocialLogin {
+
+ private static final Logger logger = LoggerFactory.getLogger(ApiSocialLogin.class);
+
+ @Value("${facebook_appid:appid}")
+ private String FACEBOOK_APPID;
+ @Value("${facebook_secret:secret}")
+ private String FACEBOOK_SECRET;
+ private static final String FACEBOOK_REDIRECT = "https://api.juick.com/_fblogin";
+ private static final String VK_REDIRECT = "https://api.juick.com/_vklogin";
+ private static final String TWITTER_VERIFY_URL = "https://api.twitter.com/1.1/account/verify_credentials.json";
+ @Inject
+ private ObjectMapper jsonMapper;
+ private ServiceBuilder facebookBuilder, twitterBuilder, vkBuilder;
+
+ @Value("${twitter_consumer_key:appid}")
+ private String twitterConsumerKey;
+ @Value("${twitter_consumer_secret:secret}")
+ private String twitterConsumerSecret;
+ @Value("${vk_appid:appid}")
+ private String VK_APPID;
+ @Value("${vk_secret:secret}")
+ private String VK_SECRET;
+ @Value("${telegram_token:secret}")
+ private String telegramToken;
+
+ @Inject
+ private CrosspostService crosspostService;
+ @Inject
+ private UserService userService;
+ @Inject
+ private EmailService emailService;
+ @Inject
+ private TelegramService telegramService;
+
+ @PostConstruct
+ public void init() {
+ facebookBuilder = new ServiceBuilder(FACEBOOK_APPID);
+ twitterBuilder = new ServiceBuilder(twitterConsumerKey);
+ vkBuilder = new ServiceBuilder(VK_APPID);
+ }
+
+ @GetMapping("/api/_fblogin")
+ protected String doFacebookLogin(@RequestParam(required = false) String code,
+ @RequestParam(required = false) String state) throws IOException, ExecutionException, InterruptedException {
+ if (StringUtils.isBlank(code)) {
+ String fbstate = UUID.randomUUID().toString();
+ crosspostService.addFacebookState(fbstate, state);
+ OAuth20Service facebookAuthService = facebookBuilder
+ .apiSecret(FACEBOOK_SECRET)
+ .callback(FACEBOOK_REDIRECT)
+ .scope("email")
+ .state(fbstate)
+ .build(FacebookApi.instance());
+ return "redirect:" + facebookAuthService.getAuthorizationUrl();
+ }
+
+ String redirectUrl = crosspostService.verifyFacebookState(state);
+
+ if (StringUtils.isEmpty(redirectUrl)) {
+ logger.error("state is missing");
+ throw new HttpBadRequestException();
+ }
+ OAuth20Service facebookService = facebookBuilder
+ .apiKey(FACEBOOK_APPID)
+ .apiSecret(FACEBOOK_SECRET)
+ .callback(FACEBOOK_REDIRECT)
+ .scope("email")
+ .state(state)
+ .build(FacebookApi.instance());
+ OAuth2AccessToken token = facebookService.getAccessToken(code);
+ final OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://graph.facebook.com/v2.10/me?fields=id,name,link,verified,email");
+ facebookService.signRequest(token, meRequest);
+ String graph = facebookService.execute(meRequest).getBody();
+ if (StringUtils.isBlank(graph)) {
+ logger.error("FACEBOOK GRAPH ERROR");
+ throw new HttpBadRequestException();
+ }
+ User fb = jsonMapper.readValue(graph, User.class);
+ long fbID = NumberUtils.toLong(fb.getId(), 0);
+ if (fbID == 0 || StringUtils.isBlank(fb.getName()) || StringUtils.isBlank(fb.getLink())) {
+ logger.error("Missing required fields, id: {}, name: {}, link: {}", fbID, fb.getName(), fb.getLink());
+ throw new HttpBadRequestException();
+ }
+
+ int uid = crosspostService.getUIDbyFBID(fbID);
+ if (uid > 0) {
+ if (!crosspostService.updateFacebookUser(fbID, token.getAccessToken(), fb.getName(), fb.getLink())) {
+ logger.error("error updating facebook user, id: {}, token: {}", fbID, token.getAccessToken());
+ throw new HttpBadRequestException();
+ }
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl);
+ uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid));
+ return "redirect:" + uriComponentsBuilder.build().toUriString();
+ } else if (fb.getVerified()) {
+ if (!crosspostService.createFacebookUser(fbID, state, token.getAccessToken(), fb.getName(), fb.getLink())) {
+ if (StringUtils.isNotEmpty(fb.getEmail())) {
+ logger.info("found {} for facebook user {}", fb.getEmail(), fb.getLink());
+ Integer userId = crosspostService.getUIDbyFBID(fbID);
+ if (!emailService.getEmails(userId, false).contains(fb.getEmail())) {
+ emailService.addEmail(userId, fb.getEmail());
+ }
+ }
+ logger.info("email not found for facebook user {}", fb.getLink());
+ throw new HttpBadRequestException();
+ }
+ return "redirect:/signup?type=fb&hash=" + state;
+ } else {
+ logger.error("Facebook account is not verified, id: {}", fbID);
+ throw new HttpBadRequestException();
+ }
+ }/*
+ @GetMapping("/_twitter")
+ protected void doTwitterLogin(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ExecutionException, InterruptedException {
+ String hash = StringUtils.EMPTY, request_token = StringUtils.EMPTY, request_token_secret = StringUtils.EMPTY;
+ String verifier = request.getParameter("oauth_verifier");
+ Cookie[] cookies = request.getCookies();
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals("hash")) {
+ hash = cookie.getValue();
+ }
+ if (cookie.getName().equals("request_token")) {
+ request_token = cookie.getValue();
+ }
+ if (cookie.getName().equals("request_token_secret")) {
+ request_token_secret = cookie.getValue();
+ }
+ }
+ com.juick.User user = UserUtils.getCurrentUser();
+ OAuth10aService oAuthService = twitterBuilder
+ .apiSecret(twitterConsumerSecret)
+ .callback("http://juick.com/_twitter")
+ .build(TwitterApi.instance());
+
+ if (request_token.isEmpty() && request_token_secret.isEmpty()
+ && (verifier == null || verifier.isEmpty())) {
+ OAuth1RequestToken requestToken = oAuthService.getRequestToken();
+ String authUrl = oAuthService.getAuthorizationUrl(requestToken);
+ response.addCookie(new Cookie("request_token", requestToken.getToken()));
+ response.addCookie(new Cookie("request_token_secret", requestToken.getTokenSecret()));
+ response.setStatus(HttpServletResponse.SC_FOUND);
+ response.setHeader("Location", authUrl);
+ } else {
+ if (verifier != null && verifier.length() > 0) {
+ OAuth1RequestToken requestToken = new OAuth1RequestToken(request_token, request_token_secret);
+ OAuth1AccessToken accessToken = oAuthService.getAccessToken(requestToken, verifier);
+ OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, TWITTER_VERIFY_URL);
+ oAuthService.signRequest(accessToken, oAuthRequest);
+ com.juick.twitter.User twitterUser = jsonMapper.readValue(oAuthService.execute(oAuthRequest).getBody(),
+ com.juick.twitter.User.class);
+ if (userService.linkTwitterAccount(user, accessToken.getToken(), accessToken.getTokenSecret(),
+ twitterUser.getScreenName())) {
+ response.setStatus(HttpServletResponse.SC_FOUND);
+ response.setHeader("Location", "http://juick.com/settings");
+ } else {
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+ }
+ }*/
+ @GetMapping("/api/_vklogin")
+ protected String doVKLogin(@RequestParam(required = false) String code,
+ @RequestParam String state) throws IOException, ExecutionException, InterruptedException {
+ if (StringUtils.isBlank(code)) {
+ String vkstate = UUID.randomUUID().toString();
+ crosspostService.addVKState(vkstate, state);
+ OAuth20Service vkAuthService = vkBuilder
+ .apiSecret(VK_SECRET)
+ .scope("friends,wall,offline")
+ .state(vkstate)
+ .callback(VK_REDIRECT)
+ .build(VkontakteApi.instance());
+ return "redirect:" + vkAuthService.getAuthorizationUrl();
+ }
+
+ String redirectUrl = crosspostService.verifyVKState(state);
+ if (StringUtils.isBlank(redirectUrl)) {
+ logger.error("state is missing");
+ throw new HttpBadRequestException();
+ }
+
+ OAuth20Service vkService = vkBuilder
+ .apiKey(VK_APPID)
+ .apiSecret(VK_SECRET)
+ .build(VkontakteApi.instance());
+ OAuth2AccessToken token = vkService.getAccessToken(code);
+
+ OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.73");
+ vkService.signRequest(token, meRequest);
+ String graph = vkService.execute(meRequest).getBody();
+
+ com.juick.model.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0);
+ String vkName = jsonUser.getFirstName() + " " + jsonUser.getLastName();
+ String vkLink = jsonUser.getScreenName();
+
+ if (vkName.length() == 1 || StringUtils.isBlank(vkLink)) {
+ logger.error("vk user error");
+ throw new HttpBadRequestException();
+ }
+
+ Long vkID = NumberUtils.toLong(jsonUser.getId(), 0);
+ int uid = crosspostService.getUIDbyVKID(vkID);
+ if (uid > 0) {
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl);
+ uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid));
+ return "redirect:" + uriComponentsBuilder.build().toUriString();
+ } else {
+ String loginhash = UUID.randomUUID().toString();
+ if (!crosspostService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) {
+ logger.error("create vk user error");
+ throw new HttpBadRequestException();
+ }
+ return "redirect:/signup?type=vk&hash=" + loginhash;
+ }
+ }
+ /*
+ @GetMapping("/_tglogin")
+ public String doDurovLogin(HttpServletRequest request,
+ @RequestParam Map params,
+ HttpServletResponse response) {
+ String dataCheckString = params.entrySet().stream()
+ .filter(p -> !p.getKey().equals("hash"))
+ .sorted(Map.Entry.comparingByKey())
+ .map(p -> p.getKey() + "=" + p.getValue())
+ .collect(Collectors.joining("\n"));
+ String hash = params.get("hash");
+ byte[] secretKey = DigestUtils.sha256(telegramToken);
+ String resultString = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secretKey).hmacHex(dataCheckString);
+ if (hash.equals(resultString)) {
+ Long tgUser = Long.valueOf(params.get("id"));
+ int uid = telegramService.getUser(tgUser);
+ if (uid > 0) {
+ Cookie c = new Cookie("hash", userService.getHashByUID(uid));
+ c.setMaxAge(50 * 24 * 60 * 60);
+ response.addCookie(c);
+ return Utils.getPreviousPageByRequest(request).orElse("redirect:/");
+ } else {
+ String username = StringUtils.defaultString(params.get("username"), params.get("first_name"));
+ telegramService.createTelegramUser(tgUser, username);
+ return "redirect:/signup?type=durov&hash=" + userService.getSignUpHashByTelegramID(tgUser, username);
+ }
+ } else {
+ logger.warn("invalid tg hash {} for {}", resultString, hash);
+ }
+ throw new HttpBadRequestException();
+ }*/
+}
diff --git a/src/main/java/com/juick/server/api/Index.java b/src/main/java/com/juick/server/api/Index.java
new file mode 100644
index 00000000..56f01370
--- /dev/null
+++ b/src/main/java/com/juick/server/api/Index.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server.api;
+
+import com.juick.Status;
+import com.juick.server.WebsocketManager;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import springfox.documentation.annotations.ApiIgnore;
+
+import javax.inject.Inject;
+import java.net.URI;
+
+/**
+ * Created by vitalyster on 25.07.2016.
+ */
+@RestController
+public class Index {
+ @Inject
+ private WebsocketManager wsHandler;
+
+ @ApiIgnore
+ @RequestMapping(value = { "/api/", "/ws/" }, method = RequestMethod.GET, headers = "Connection!=Upgrade")
+ public ResponseEntity description() {
+ URI redirectUri = ServletUriComponentsBuilder.fromCurrentRequestUri().path("/swagger-ui.html").build().toUri();
+ return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).location(redirectUri).build();
+ }
+ @ApiIgnore
+ @RequestMapping(value = "/api/status", method = RequestMethod.GET,
+ produces = MediaType.APPLICATION_JSON_UTF8_VALUE, headers = "Connection!=Upgrade")
+ public Status status() {
+ return Status.getStatus(String.valueOf(wsHandler.getClients().size()));
+ }
+}
diff --git a/src/main/java/com/juick/server/api/Messages.java b/src/main/java/com/juick/server/api/Messages.java
new file mode 100644
index 00000000..4f0009dd
--- /dev/null
+++ b/src/main/java/com/juick/server/api/Messages.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server.api;
+
+import com.juick.Message;
+import com.juick.Tag;
+import com.juick.User;
+import com.juick.server.Utils;
+import com.juick.service.component.MessageReadEvent;
+import com.juick.model.CommandResult;
+import com.juick.server.util.HttpBadRequestException;
+import com.juick.server.util.HttpNotFoundException;
+import com.juick.server.util.UserUtils;
+import com.juick.service.MessagesService;
+import com.juick.service.TagService;
+import com.juick.service.UserService;
+import org.apache.commons.io.IOUtils;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author ugnich
+ */
+@RestController
+@RequestMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+public class Messages {
+
+ private static final ResponseEntity> NOT_FOUND = ResponseEntity
+ .status(HttpStatus.NOT_FOUND)
+ .body(Collections.emptyList());
+
+ private static final ResponseEntity> FORBIDDEN = ResponseEntity
+ .status(HttpStatus.FORBIDDEN)
+ .body(Collections.emptyList());
+
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private UserService userService;
+ @Inject
+ private TagService tagService;
+ @Inject
+ private ApplicationEventPublisher applicationEventPublisher;
+
+ // TODO: serialize image urls
+
+ @GetMapping("/api/home")
+ public ResponseEntity> getHome(
+ @RequestParam(defaultValue = "0") int before_mid) {
+ User visitor = UserUtils.getCurrentUser();
+ if (!visitor.isAnonymous()) {
+ int vuid = visitor.getUid();
+ List mids = messagesService.getMyFeed(vuid, before_mid, true);
+ return ResponseEntity.ok(messagesService.getMessages(visitor, mids));
+ }
+ return FORBIDDEN;
+ }
+
+ @GetMapping("/api/messages")
+ public ResponseEntity> getMessages(
+ @RequestParam(required = false) String uname,
+ @RequestParam(name = "before_mid", defaultValue = "0") Integer before,
+ @RequestParam(required = false, defaultValue = "0") Integer daysback,
+ @RequestParam(required = false) String withrecommended,
+ @RequestParam(required = false) String popular,
+ @RequestParam(required = false) String search,
+ @RequestParam(required = false, defaultValue = "0") Integer page,
+ @RequestParam(required = false) String media,
+ @RequestParam(required = false) String tag) {
+
+ User visitor = UserUtils.getCurrentUser();
+
+ List mids;
+ if (!StringUtils.isEmpty(uname)) {
+ User user = userService.getUserByName(uname);
+ if (!user.isAnonymous()) {
+ if (!StringUtils.isEmpty(media)) {
+ mids = messagesService.getUserPhotos(user.getUid(), 0, before);
+ } else if (!StringUtils.isEmpty(tag)) {
+ Tag tagObject = tagService.getTag(tag, false);
+ if (tagObject != null) {
+ mids = messagesService.getUserTag(user.getUid(), tagObject.TID, 0, before);
+ } else {
+ return NOT_FOUND;
+ }
+ } else if (!StringUtils.isEmpty(withrecommended)) {
+ mids = messagesService.getUserBlogWithRecommendations(user.getUid(), 0, before);
+ } else if (daysback > 0) {
+ mids = messagesService.getUserBlogAtDay(user.getUid(), 0, daysback);
+ } else if (!StringUtils.isEmpty(search)) {
+ mids = messagesService.getUserSearch(visitor, user.getUid(), Utils.encodeSphinx(search), 0, page);
+ } else {
+ mids = messagesService.getUserBlog(user.getUid(), 0, before);
+ }
+ } else {
+ return NOT_FOUND;
+ }
+ } else {
+ if (!StringUtils.isEmpty(popular)) {
+ mids = messagesService.getPopular(visitor.getUid(), before);
+ } else if (!StringUtils.isEmpty(media)) {
+ mids = messagesService.getPhotos(visitor.getUid(), before);
+ } else if (!StringUtils.isEmpty(tag)) {
+ Tag tagObject = tagService.getTag(tag, false);
+ if (tagObject != null) {
+ mids = messagesService.getTag(tagObject.TID, visitor.getUid(), before, 20);
+ } else {
+ return NOT_FOUND;
+ }
+ } else if (!StringUtils.isEmpty(search)) {
+ mids = messagesService.getSearch(visitor, Utils.encodeSphinx(search), page);
+ } else {
+ mids = messagesService.getAll(visitor.getUid(), before);
+ }
+ }
+ return ResponseEntity.ok(messagesService.getMessages(visitor, mids));
+ }
+ @DeleteMapping("/api/messages")
+ public CommandResult deleteMessage(@RequestParam int mid, @RequestParam(required = false, defaultValue = "0") int rid) {
+ User visitor = UserUtils.getCurrentUser();
+ if (rid > 0) {
+ if (messagesService.deleteReply(visitor.getUid(), mid, rid)) {
+ return CommandResult.fromString("Reply deleted");
+ }
+ }
+ if (messagesService.deleteMessage(visitor.getUid(), mid)) {
+ return CommandResult.fromString("Message deleted");
+ }
+ throw new HttpBadRequestException();
+ }
+ @GetMapping("/api/messages/discussions")
+ public List getDiscussions(
+ @RequestParam(required = false, defaultValue = "0") Long to) {
+ return messagesService.getMessages(UserUtils.getCurrentUser(), messagesService.getDiscussions(UserUtils.getCurrentUser().getUid(), to));
+ }
+ @GetMapping("/api/thread")
+ public ResponseEntity> getThread(
+ @RequestParam(defaultValue = "0") int mid) {
+ User visitor = UserUtils.getCurrentUser();
+ com.juick.Message msg = messagesService.getMessage(mid);
+ if (msg != null) {
+ if (!messagesService.canViewThread(mid, visitor.getUid())) {
+ return FORBIDDEN;
+ } else {
+ if (userService.getUserByName(msg.getUser().getName()).isBanned()) {
+ throw new HttpNotFoundException();
+ }
+ msg.setRecommendations(new HashSet<>(messagesService.getMessageRecommendations(msg.getMid())));
+ List replies = messagesService.getReplies(visitor, mid);
+ if (!visitor.isAnonymous()) {
+ userService.updateLastSeen(visitor);
+ applicationEventPublisher.publishEvent(
+ new MessageReadEvent(this, visitor, msg));
+ }
+ replies.add(0, msg);
+ return ResponseEntity.ok(replies);
+ }
+ }
+ return NOT_FOUND;
+ }
+ @GetMapping(value = "/api/thread/mark_read/{mid}-{rid}.gif", produces = MediaType.IMAGE_GIF_VALUE)
+ public byte[] markThreadRead(@PathVariable int mid, @PathVariable int rid) throws IOException {
+ User visitor = UserUtils.getCurrentUser();
+ if (!visitor.isAnonymous()) {
+ messagesService.setLastReadComment(visitor, mid, rid);
+ Message msg = messagesService.getMessage(mid);
+ userService.updateLastSeen(visitor);
+ applicationEventPublisher.publishEvent(
+ new MessageReadEvent(this, visitor, msg));
+ return IOUtils.toByteArray(
+ Objects.requireNonNull(getClass().getClassLoader().getResource("Transparent.gif")));
+ }
+ throw new HttpBadRequestException();
+ }
+}
diff --git a/src/main/java/com/juick/server/api/Notifications.java b/src/main/java/com/juick/server/api/Notifications.java
new file mode 100644
index 00000000..62275f5a
--- /dev/null
+++ b/src/main/java/com/juick/server/api/Notifications.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server.api;
+
+import com.juick.Message;
+import com.juick.Status;
+import com.juick.ExternalToken;
+import com.juick.User;
+import com.juick.model.AnonymousUser;
+import com.juick.server.util.HttpBadRequestException;
+import com.juick.server.util.HttpForbiddenException;
+import com.juick.service.MessagesService;
+import com.juick.service.PushQueriesService;
+import com.juick.service.SubscriptionService;
+import com.juick.server.util.UserUtils;
+import com.juick.service.UserService;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import springfox.documentation.annotations.ApiIgnore;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Created by vitalyster on 24.10.2016.
+ */
+@RestController
+public class Notifications {
+
+ @Inject
+ private PushQueriesService pushQueriesService;
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private SubscriptionService subscriptionService;
+ @Inject
+ private UserService userService;
+
+
+ private User collectTokens(Integer uid) {
+ User user = userService.getUserByUID(uid).orElse(AnonymousUser.INSTANCE);
+ user.setUnreadCount(messagesService.getUnread(user).size());
+ pushQueriesService.getGCMRegID(uid).forEach(t -> user.getTokens().add(new ExternalToken(null, "gcm", t, null)));
+ pushQueriesService.getAPNSToken(uid).forEach(t -> user.getTokens().add(new ExternalToken(null, "apns", t, null)));
+ pushQueriesService.getMPNSURL(uid).forEach(t -> user.getTokens().add(new ExternalToken(null, "mpns", t, null)));
+ return user;
+ }
+
+ @ApiIgnore
+ @RequestMapping(value = "/api/notifications", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public ResponseEntity> doGet(
+ @RequestParam(required = false, defaultValue = "0") int uid,
+ @RequestParam(required = false, defaultValue = "0") int mid,
+ @RequestParam(required = false, defaultValue = "0") int rid) {
+ User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous() || !(visitor.getName().equals("juick"))) {
+ throw new HttpForbiddenException();
+ }
+ if (uid > 0 && mid == 0) {
+ // PM
+ return ResponseEntity.ok(Collections.singletonList(collectTokens(uid)));
+ } else {
+ if (mid > 0) {
+ // reply
+ Message msg = messagesService.getMessage(mid);
+ if (msg != null) {
+ List users;
+ if (rid > 0) {
+ Message op = messagesService.getMessage(mid);
+ Message reply = messagesService.getReply(mid, rid);
+ users = subscriptionService.getUsersSubscribedToComments(op, reply);
+ } else {
+ users = subscriptionService.getSubscribedUsers(msg.getUser().getUid(), msg);
+ }
+
+ return ResponseEntity.ok(users.stream().map(User::getUid)
+ .map(this::collectTokens).collect(Collectors.toList()));
+ }
+ } else {
+ // read
+ return ResponseEntity.ok(Collections.singletonList(collectTokens(uid)));
+ }
+ }
+ throw new HttpBadRequestException();
+ }
+
+ @ApiIgnore
+ @RequestMapping(value = "/api/notifications", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public Status doDelete(
+ @RequestBody List list) {
+ User visitor = UserUtils.getCurrentUser();
+ if ((visitor.isAnonymous()) || !(visitor.getName().equals("juick"))) {
+ throw new HttpForbiddenException();
+ }
+ list.forEach(t -> {
+ switch (t.getType()) {
+ case "gcm":
+ pushQueriesService.deleteGCMToken(t.getToken());
+ break;
+ case "apns":
+ pushQueriesService.deleteAPNSToken(t.getToken());
+ break;
+ case "mpns":
+ pushQueriesService.deleteMPNSToken(t.getToken());
+ break;
+ default:
+ throw new HttpBadRequestException();
+ }
+ });
+
+ return Status.OK;
+ }
+ @ApiIgnore
+ @RequestMapping(value = "/api/notifications/delete", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public Status doDeleteTokens(
+ @RequestBody List list) {
+ User visitor = UserUtils.getCurrentUser();
+ if ((visitor.isAnonymous()) || !(visitor.getName().equals("juick"))) {
+ throw new HttpForbiddenException();
+ }
+ list.forEach(t -> {
+ switch (t.getType()) {
+ case "gcm":
+ pushQueriesService.deleteGCMToken(t.getToken());
+ break;
+ case "apns":
+ pushQueriesService.deleteAPNSToken(t.getToken());
+ break;
+ case "mpns":
+ pushQueriesService.deleteMPNSToken(t.getToken());
+ break;
+ default:
+ throw new HttpBadRequestException();
+ }
+ });
+
+ return Status.OK;
+ }
+
+ @ApiIgnore
+ @RequestMapping(value = "/api/notifications", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public Status doPut(
+ @RequestBody List list) throws IOException {
+ User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ throw new HttpForbiddenException();
+ }
+ list.forEach(t -> {
+ switch (t.getType()) {
+ case "gcm":
+ pushQueriesService.addGCMToken(visitor.getUid(), t.getToken());
+ break;
+ case "apns":
+ pushQueriesService.addAPNSToken(visitor.getUid(), t.getToken());
+ break;
+ case "mpns":
+ pushQueriesService.addMPNSToken(visitor.getUid(), t.getToken());
+ break;
+ default:
+ throw new HttpBadRequestException();
+ }
+ });
+ return Status.OK;
+ }
+
+ @Deprecated
+ @RequestMapping(value = "/api/android/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public Status doAndroidRegister(
+ @RequestParam(name = "regid") String regId) {
+ User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ throw new HttpForbiddenException();
+ }
+ pushQueriesService.addGCMToken(visitor.getUid(), regId);
+ return Status.OK;
+ }
+
+ @Deprecated
+ @RequestMapping(value = "/api/android/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public Status doAndroidUnRegister(@RequestParam(name = "regid") String regId) {
+ pushQueriesService.deleteGCMToken(regId);
+ return Status.OK;
+ }
+
+ @Deprecated
+ @RequestMapping(value = "/api/winphone/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public Status doWinphoneRegister(
+ Principal principal,
+ @RequestParam(name = "url") String regId) {
+ User visitor = UserUtils.getCurrentUser();
+ pushQueriesService.addMPNSToken(visitor.getUid(), regId);
+ return Status.OK;
+ }
+
+ @Deprecated
+ @RequestMapping(value = "/api/winphone/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public Status doWinphoneUnRegister(@RequestParam(name = "url") String regId) {
+ pushQueriesService.deleteMPNSToken(regId);
+ return Status.OK;
+ }
+}
diff --git a/src/main/java/com/juick/server/api/PM.java b/src/main/java/com/juick/server/api/PM.java
new file mode 100644
index 00000000..0c36fe00
--- /dev/null
+++ b/src/main/java/com/juick/server/api/PM.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server.api;
+
+import com.juick.Chat;
+import com.juick.User;
+import com.juick.service.component.MessageEvent;
+import com.juick.model.AnonymousUser;
+import com.juick.model.PrivateChats;
+import com.juick.server.util.*;
+import com.juick.service.PMQueriesService;
+import com.juick.service.UserService;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.inject.Inject;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author ugnich
+ */
+@RestController
+public class PM {
+ @Inject
+ private UserService userService;
+ @Inject
+ private PMQueriesService pmQueriesService;
+ @Inject
+ private ApplicationEventPublisher applicationEventPublisher;
+
+ @RequestMapping(value = "/api/pm", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public List doGetPM(
+ @RequestParam(required = false) String uname) {
+ User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ throw new HttpForbiddenException();
+ }
+ int uid = 0;
+ if (uname != null && uname.matches("^[a-zA-Z0-9\\-]{2,16}$")) {
+ uid = userService.getUIDbyName(uname);
+ }
+
+ if (uid == 0) {
+ throw new HttpBadRequestException();
+ }
+
+ return pmQueriesService.getPMMessages(visitor.getUid(), uid);
+ }
+
+ @RequestMapping(value = "/api/pm", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public com.juick.Message doPostPM(
+ @RequestParam String uname,
+ @RequestParam String body) {
+ User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ throw new HttpForbiddenException();
+ }
+ User userTo = AnonymousUser.INSTANCE;
+ if (WebUtils.isUserName(uname)) {
+ userTo = userService.getUserByName(uname);
+ }
+
+ if (userTo.getUid() == 0 || body == null || body.length() < 1 || body.length() > 10240) {
+ throw new HttpBadRequestException();
+ }
+
+ if (userService.isInBLAny(userTo.getUid(), visitor.getUid())) {
+ throw new HttpForbiddenException();
+ }
+
+ if (pmQueriesService.createPM(visitor.getUid(), userTo.getUid(), body)) {
+ com.juick.Message jmsg = new com.juick.Message();
+ jmsg.setUser(visitor);
+ jmsg.setText(body);
+ jmsg.setTo(userTo);
+ applicationEventPublisher.publishEvent(new MessageEvent(this, jmsg, Collections.singletonList(jmsg.getTo())));
+ return jmsg;
+
+ }
+ throw new HttpBadRequestException();
+ }
+ @RequestMapping(value = "/api/groups_pms", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public PrivateChats doGetGroupsPMs(
+ @RequestParam(defaultValue = "5") int cnt) {
+ User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ throw new HttpForbiddenException();
+ }
+ // TODO: ignore cnt param for now but make sure paging param will not be cnt
+
+ List lastconv = pmQueriesService.getLastChats(visitor);
+ PrivateChats pms = new PrivateChats();
+ pms.setUsers(lastconv);
+ return pms;
+ }
+}
diff --git a/src/main/java/com/juick/server/api/Post.java b/src/main/java/com/juick/server/api/Post.java
new file mode 100644
index 00000000..303ff109
--- /dev/null
+++ b/src/main/java/com/juick/server/api/Post.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server.api;
+
+import com.juick.Message;
+import com.juick.Reaction;
+import com.juick.Status;
+import com.juick.User;
+import com.juick.server.CommandsManager;
+import com.juick.model.CommandResult;
+import com.juick.server.util.*;
+import com.juick.service.MessagesService;
+import com.juick.service.SubscriptionService;
+import com.juick.service.UserService;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.inject.Inject;
+import javax.validation.constraints.NotNull;
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+
+/**
+ * Created by vt on 24/11/2016.
+ */
+@RestController
+public class Post {
+ private static Logger logger = LoggerFactory.getLogger(Post.class);
+
+ @Inject
+ private UserService userService;
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private SubscriptionService subscriptionService;
+ @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String tmpDir;
+ @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String imgDir;
+ @Inject
+ CommandsManager commandsManager;
+
+ @RequestMapping(value = "/api/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public CommandResult doPostMessage(
+ @RequestParam(required = false, defaultValue = StringUtils.EMPTY) String body,
+ @RequestParam(required = false) String img,
+ @RequestParam(required = false) MultipartFile attach) throws Exception {
+ User visitor = UserUtils.getCurrentUser();
+
+ if (visitor.isAnonymous())
+ throw new HttpForbiddenException();
+
+ if (body.length() > 4096) {
+ throw new HttpBadRequestException();
+ }
+ body = body.replace("\r", StringUtils.EMPTY);
+
+ URI attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir);
+
+ if (StringUtils.isBlank(attachmentFName.toString()) && img != null && img.length() > 10) {
+ URI juickUri = URI.create(img);
+ if (juickUri.getScheme().equals("juick")) {
+ attachmentFName = juickUri;
+ } else {
+ try {
+ URL imgUrl = new URL(img);
+ attachmentFName = HttpUtils.downloadImage(imgUrl, tmpDir);
+ } catch (Exception e) {
+ logger.error("DOWNLOAD ERROR", e);
+ throw new HttpBadRequestException();
+ }
+ }
+ }
+ if (StringUtils.isBlank(body) && StringUtils.isBlank(attachmentFName.toString())) {
+ // Should be there for compatibility
+ throw new HttpBadRequestException();
+ }
+ return commandsManager.processCommand(visitor, body, attachmentFName);
+ }
+
+ @RequestMapping(value = "/api/comment", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public CommandResult doPostComment(
+ @RequestParam(defaultValue = "0") int mid,
+ @RequestParam(defaultValue = "0") int rid,
+ @RequestParam(required = false, defaultValue = StringUtils.EMPTY) String body,
+ @RequestParam(required = false) String img,
+ @RequestParam(required = false) MultipartFile attach)
+ throws Exception {
+ User visitor = UserUtils.getCurrentUser();
+ int vuid = visitor.getUid();
+ if (vuid == 0) {
+ throw new HttpForbiddenException();
+ }
+ if (mid == 0) {
+ throw new HttpBadRequestException();
+ }
+ com.juick.Message msg = messagesService.getMessage(mid);
+ if (msg == null) {
+ throw new HttpNotFoundException();
+ }
+
+ com.juick.Message reply = null;
+ if (rid > 0) {
+ reply = messagesService.getReply(mid, rid);
+ if (reply == null) {
+ throw new HttpNotFoundException();
+ }
+ }
+
+ if (body.length() > 4096) {
+ throw new HttpBadRequestException();
+ }
+ body = body.replace("\r", StringUtils.EMPTY);
+
+ if ((msg.ReadOnly && msg.getUser().getUid() != vuid) || userService.isInBLAny(msg.getUser().getUid(), vuid)
+ || (reply != null && userService.isInBLAny(reply.getUser().getUid(), vuid))) {
+ throw new HttpForbiddenException();
+ }
+
+ URI attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir);
+
+ if (StringUtils.isBlank(attachmentFName.toString()) && img != null && img.length() > 10) {
+ try {
+ attachmentFName = HttpUtils.downloadImage(new URL(img), tmpDir);
+ } catch (Exception e) {
+ logger.error("DOWNLOAD ERROR", e);
+ throw new HttpBadRequestException();
+ }
+ }
+ if (StringUtils.isBlank(body) && StringUtils.isBlank(attachmentFName.toString())) {
+ // Should be there for compatibility
+ throw new HttpBadRequestException();
+ }
+ return commandsManager.processCommand(visitor, String.format("#%d/%d %s", mid, rid, body),
+ attachmentFName);
+ }
+
+ @PostMapping("/api/like")
+ @ResponseStatus(value = HttpStatus.OK)
+ public Status doPostRecomm(@RequestParam Integer mid) throws Exception {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ throw new HttpForbiddenException();
+ }
+ com.juick.Message msg = messagesService.getMessage(mid);
+ if (msg == null) {
+ throw new HttpNotFoundException();
+ }
+ if (msg.getUser().getUid() == visitor.getUid()) {
+ throw new HttpForbiddenException();
+ }
+ CommandResult status = commandsManager.processCommand(visitor, String.format("! #%d", mid),
+ URI.create(StringUtils.EMPTY));
+ return Status.getStatus(status.getText());
+ }
+
+ @PostMapping("/api/subscribe")
+ @ResponseStatus(value = HttpStatus.OK)
+ public Status doPostSubscribe(@RequestParam Integer mid) throws Exception {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ throw new HttpForbiddenException();
+ }
+ com.juick.Message msg = messagesService.getMessage(mid);
+ if (msg == null) {
+ throw new HttpNotFoundException();
+ }
+ if (msg.getUser().getUid() == visitor.getUid()) {
+ throw new HttpForbiddenException();
+ }
+ CommandResult status = commandsManager.processCommand(visitor, String.format("S #%d", mid),
+ URI.create(StringUtils.EMPTY));
+ return Status.getStatus(status.getText());
+ }
+
+ @GetMapping("/api/reactions")
+ @ResponseStatus(value = HttpStatus.OK)
+ public List reactionsList() {
+ return messagesService.listReactions();
+ }
+
+ @PostMapping("/api/react")
+ @ResponseStatus(value = HttpStatus.OK)
+ public Status doPostReact(@RequestParam Integer mid,@RequestParam @NotNull int reactionId,
+ @RequestParam (required = false, defaultValue = "1") int count) {
+
+ logger.info("got reaction with type: {}", reactionId);
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ throw new HttpForbiddenException();
+ }
+ com.juick.Message msg = messagesService.getMessage(mid);
+ if (msg == null) {
+ throw new HttpNotFoundException();
+ }
+ if (msg.getUser().getUid() == visitor.getUid()) {
+ throw new HttpForbiddenException();
+ }
+ MessagesService.RecommendStatus recommendStatus = MessagesService.RecommendStatus.Error;
+ for (int i = 0; i < count; i++)
+ recommendStatus = messagesService.likeMessage(mid, visitor.getUid(),
+ reactionId);
+
+ return recommendStatus == MessagesService.RecommendStatus.Error ? Status.ERROR :Status.OK;
+ }
+
+ @PostMapping("/api/update")
+ public CommandResult updateMessage(@RequestParam Integer mid,
+ @RequestParam(required = false, defaultValue = "0") Integer rid,
+ @RequestParam String body) {
+ User visitor = UserUtils.getCurrentUser();
+ User author = rid == 0 ? messagesService.getMessageAuthor(mid) : messagesService.getReply(mid, rid).getUser();
+ if (visitor.equals(author)) {
+ if (messagesService.updateMessage(mid, rid, body)) {
+ Message result = rid == 0 ? messagesService.getMessage(mid) : messagesService.getReply(mid, rid);
+ return CommandResult.build(result, "Message updated", StringUtils.EMPTY);
+ }
+ throw new HttpBadRequestException();
+ }
+ throw new HttpForbiddenException();
+ }
+}
diff --git a/src/main/java/com/juick/server/api/Service.java b/src/main/java/com/juick/server/api/Service.java
new file mode 100644
index 00000000..ed62886f
--- /dev/null
+++ b/src/main/java/com/juick/server/api/Service.java
@@ -0,0 +1,166 @@
+package com.juick.server.api;
+
+import com.juick.Message;
+import com.juick.User;
+import com.juick.server.CommandsManager;
+import com.juick.server.EmailManager;
+import com.juick.server.ServerManager;
+import com.juick.server.util.HttpForbiddenException;
+import com.juick.server.util.UserUtils;
+import com.juick.service.EmailService;
+import com.juick.service.UserService;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.mail.util.MimeMessageParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import springfox.documentation.annotations.ApiIgnore;
+
+import javax.inject.Inject;
+import javax.mail.Session;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Paths;
+import java.util.*;
+
+@Controller
+public class Service {
+ private static Logger logger = LoggerFactory.getLogger(Service.class);
+ @Inject
+ private UserService userService;
+ @Inject
+ private EmailService emailService;
+ @Inject
+ private CommandsManager commandsManager;
+ @Inject
+ private EmailManager emailManager;
+ @Value("${api_user:juick}")
+ private String serviceUser;
+ @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String tmpDir;
+ @Value("${banned_emails:}")
+ private String[] ignoredEmails;
+ @Inject
+ private ServerManager serverManager;
+
+ private Session session = Session.getDefaultInstance(new Properties());
+
+ @ApiIgnore
+ @PostMapping("/api/mail")
+ @ResponseStatus(value = HttpStatus.OK)
+ public void processMail(InputStream data) throws Exception {
+ if (UserUtils.getCurrentUser().getName().equals(serviceUser)) {
+ MimeMessage msg = new MimeMessage(session, data);
+ String[] returnPaths = msg.getHeader("Return-Path");
+ if (returnPaths != null) {
+ logger.info("got msg with return path {}", returnPaths[0]);
+ if (returnPaths[0].equals("<>")) {
+ return;
+ }
+ }
+ String from = msg.getFrom() == null || msg.getFrom().length > 1 ? ((InternetAddress) msg.getSender()).getAddress()
+ : ((InternetAddress) msg.getFrom()[0]).getAddress();
+
+ User visitor = userService.getUserByEmail(from);
+ if (!visitor.isAnonymous()) {
+ MimeMessageParser parser = new MimeMessageParser(msg);
+ parser.parse();
+ final String[] body = {parser.getPlainContent()};
+ if (body[0] == null) {
+ parser.getAttachmentList().stream()
+ .filter(a -> a.getContentType().equals("text/plain")).findFirst()
+ .ifPresent(a -> {
+ try {
+ body[0] = IOUtils.toString(a.getInputStream(), StandardCharsets.UTF_8);
+ logger.info("got text: {}", body[0]);
+ } catch (IOException e) {
+ logger.info("attachment error: {}", e);
+ }
+ });
+ }
+ final String[] attachmentFName = new String[1];
+ parser.getAttachmentList().stream().filter(a ->
+ a.getContentType().equals("image/jpeg") || a.getContentType().equals("image/png"))
+ .findFirst().ifPresent(a -> {
+ logger.info("got attachment: {}", a.getContentType());
+ String attachmentType;
+ if (a.getContentType().equals("image/jpeg")) {
+ attachmentType = "jpg";
+ } else {
+ attachmentType = "png";
+ }
+ attachmentFName[0] = DigestUtils.md5Hex(UUID.randomUUID().toString()) + "." + attachmentType;
+ try {
+ logger.info("got inputstream: {}", a.getInputStream());
+ FileOutputStream fos = new FileOutputStream(Paths.get(tmpDir, attachmentFName[0]).toString());
+ IOUtils.copy(a.getInputStream(), fos);
+ fos.close();
+ } catch (IOException e) {
+ logger.info("attachment error: {}", e);
+ }
+ });
+ String[] inReplyToHeaders = msg.getHeader("In-Reply-To");
+ if (inReplyToHeaders != null && inReplyToHeaders.length > 0) {
+ Scanner inReplyToScanner = new Scanner(inReplyToHeaders[0].trim()).useDelimiter(EmailManager.MSGID_PATTERN);
+ int mid = Integer.parseInt(inReplyToScanner.next());
+ int rid = Integer.parseInt(inReplyToScanner.next());
+ logger.info("Message is reply to #{}/{}", mid, rid);
+ body[0] = rid > 0 ? String.format("#%d/%d %s", mid, rid, body[0])
+ : String.format("#%d %s", mid, body[0]);
+ }
+ URI attachmentUri = StringUtils.isNotEmpty(attachmentFName[0]) ? URI.create(String.format("juick://%s", attachmentFName[0]))
+ : URI.create(StringUtils.EMPTY);
+ commandsManager.processCommand(visitor, body[0], attachmentUri);
+ } else {
+ if (!Arrays.asList(ignoredEmails).contains(from)) {
+ String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase();
+ emailService.addVerificationCode(null, from, verificationCode);
+ String signupUrl = String.format("Follow this link to create Juick account: https://juick.com/signup?type=email&hash=%s", verificationCode);
+ emailManager.sendEmail(from, "Juick registration", signupUrl, StringUtils.EMPTY, Collections.emptyMap());
+ }
+ }
+ } else {
+ throw new HttpForbiddenException();
+ }
+ }
+ private void endSession(SseEmitter emitter) {
+ serverManager.getSessions().stream()
+ .filter(s -> s.getEmitter().equals(emitter))
+ .forEach(session -> serverManager.getSessions().remove(session));
+ }
+ @GetMapping("/api/events")
+ public SseEmitter handle() throws IOException {
+ User visitor = UserUtils.getCurrentUser();
+ logger.info("{} connected", visitor.getName());
+ if (!visitor.isAnonymous()) {
+ userService.updateLastSeen(visitor);
+ }
+ SseEmitter emitter = new SseEmitter(600000L);
+ serverManager.getSessions().add(new ServerManager.EventSession(visitor, emitter));
+
+ emitter.onCompletion(() -> endSession(emitter));
+ emitter.onTimeout(() -> endSession(emitter));
+
+ return emitter;
+ }
+ @ExceptionHandler(AsyncRequestTimeoutException.class)
+ public void eventErrorHandler(Exception ex) {
+ logger.debug("SSE timeout", ex);
+ }
+}
diff --git a/src/main/java/com/juick/server/api/Tags.java b/src/main/java/com/juick/server/api/Tags.java
new file mode 100644
index 00000000..7a8e572a
--- /dev/null
+++ b/src/main/java/com/juick/server/api/Tags.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server.api;
+
+import com.juick.User;
+import com.juick.model.TagStats;
+import com.juick.server.util.UserUtils;
+import com.juick.service.TagService;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.inject.Inject;
+import java.util.List;
+
+/**
+ * Created by vitalyster on 29.11.2016.
+ */
+@RestController
+public class Tags {
+ @Inject
+ private TagService tagService;
+
+ @RequestMapping(value = "/api/tags", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public List tags(
+ @RequestParam(required = false, defaultValue = "0") int user_id
+ ) {
+ User visitor = UserUtils.getCurrentUser();
+ if (user_id == 0) {
+ user_id = visitor.getUid();
+ }
+ if (user_id > 0) {
+ return tagService.getUserTagStats(user_id);
+ }
+ return tagService.getTagStats();
+ }
+}
diff --git a/src/main/java/com/juick/server/api/Users.java b/src/main/java/com/juick/server/api/Users.java
new file mode 100644
index 00000000..7686d722
--- /dev/null
+++ b/src/main/java/com/juick/server/api/Users.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.juick.server.api;
+
+import com.juick.User;
+import com.juick.model.ApplicationStatus;
+import com.juick.model.UserInfo;
+import com.juick.server.util.HttpForbiddenException;
+import com.juick.server.util.HttpNotFoundException;
+import com.juick.service.CrosspostService;
+import com.juick.service.EmailService;
+import com.juick.service.MessagesService;
+import com.juick.service.UserService;
+import com.juick.server.util.UserUtils;
+import com.juick.server.util.WebUtils;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author ugnich
+ */
+@RestController
+public class Users {
+ @Inject
+ private UserService userService;
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private CrosspostService crosspostService;
+ @Inject
+ private EmailService emailService;
+
+ @RequestMapping(value = "/api/auth", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public String getAuthToken() {
+ return userService.getHashByUID(UserUtils.getCurrentUser().getUid());
+ }
+
+ @RequestMapping(value = "/api/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public List doGetUsers(
+ @RequestParam(value = "uname", required = false) List unames) {
+ List users = new ArrayList<>();
+
+ if (unames != null) {
+ unames.removeIf(WebUtils::isNotUserName);
+
+ if (!unames.isEmpty() && unames.size() < 20)
+ users.addAll(userService.getUsersByName(unames));
+ }
+
+ if (!users.isEmpty())
+ return users;
+ if (!UserUtils.getCurrentUser().isAnonymous()) {
+ return Collections.singletonList(UserUtils.getCurrentUser());
+ }
+
+ throw new HttpNotFoundException();
+ }
+
+ @GetMapping("/api/me")
+ public SecureUser getMe() {
+ User visitor = UserUtils.getCurrentUser();
+ SecureUser me = new SecureUser();
+ me.setUid(visitor.getUid());
+ me.setName(visitor.getName());
+ me.setAuthHash(getAuthToken());
+ List unread = messagesService.getUnread(visitor);
+ me.setUnread(unread);
+ me.setUnreadCount(unread.size());
+ me.setRead(userService.getUserFriends(visitor.getUid()));
+ me.setReaders(userService.getUserReaders(visitor.getUid()));
+ return me;
+ }
+
+ @RequestMapping(value = "/api/users/read", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public List doGetUserRead(
+ @RequestParam String uname) {
+ User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ throw new HttpForbiddenException();
+ }
+ int uid = 0;
+ if (uname == null) {
+ uid = visitor.getUid();
+ } else {
+ if (WebUtils.isUserName(uname)) {
+ com.juick.User u = userService.getUserByName(uname);
+ if (!u.isAnonymous()) {
+ uid = u.getUid();
+ }
+ }
+ }
+
+ if (uid > 0) {
+ return userService.getUserFriends(uid);
+ }
+ throw new HttpNotFoundException();
+ }
+
+ @RequestMapping(value = "/api/users/readers", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public List doGetUserReaders(
+ @RequestParam String uname) {
+ User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ throw new HttpForbiddenException();
+ }
+ int uid = 0;
+ if (uname == null) {
+ uid = visitor.getUid();
+ } else {
+ if (WebUtils.isUserName(uname)) {
+ com.juick.User u = userService.getUserByName(uname);
+ if (!u.isAnonymous()) {
+ uid = u.getUid();
+ }
+ }
+ }
+
+ if (uid > 0) {
+ return userService.getUserReaders(uid);
+ }
+ throw new HttpNotFoundException();
+ }
+
+ @GetMapping("/api/info/{uname}")
+ public UserInfo getUserInfo(@PathVariable String uname) {
+ User user = userService.getUserByName(uname);
+ if (!user.isBanned()) {
+ return userService.getUserInfo(user);
+ }
+ throw new HttpNotFoundException();
+ }
+
+ class SecureUser extends User {
+ public String getHash() {
+ return getAuthHash();
+ }
+ public UserInfo getUserInfo() {
+ return userService.getUserInfo(this);
+ }
+ public List getJIDs() {
+ return userService.getAllJIDs(this);
+ }
+ public List getEmails() {
+ return userService.getEmails(this);
+ }
+ public String getActiveEmail() {
+ return emailService.getNotificationsEmail(this.getUid());
+ }
+ public String getTwitterName() {
+ return crosspostService.getTwitterName(this.getUid());
+ }
+ public String getTelegramName() {
+ return crosspostService.getTelegramName(this.getUid());
+ }
+ public ApplicationStatus getFacebookStatus() {
+ return crosspostService.getFbCrossPostStatus(this.getUid());
+ }
+ }
+}
diff --git a/src/main/java/com/juick/server/api/activity/Profile.java b/src/main/java/com/juick/server/api/activity/Profile.java
new file mode 100644
index 00000000..10390ea1
--- /dev/null
+++ b/src/main/java/com/juick/server/api/activity/Profile.java
@@ -0,0 +1,379 @@
+package com.juick.server.api.activity;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.juick.Message;
+import com.juick.User;
+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.server.api.activity.model.Activity;
+import com.juick.server.api.activity.model.Context;
+import com.juick.server.api.activity.model.activities.Announce;
+import com.juick.server.api.activity.model.activities.Create;
+import com.juick.server.api.activity.model.activities.Delete;
+import com.juick.server.api.activity.model.activities.Follow;
+import com.juick.server.api.activity.model.activities.Undo;
+import com.juick.server.api.activity.model.objects.Image;
+import com.juick.server.api.activity.model.objects.Key;
+import com.juick.server.api.activity.model.objects.Note;
+import com.juick.server.api.activity.model.objects.OrderedCollection;
+import com.juick.server.api.activity.model.objects.OrderedCollectionPage;
+import com.juick.server.api.activity.model.objects.Person;
+import com.juick.server.util.HttpBadRequestException;
+import com.juick.server.util.HttpNotFoundException;
+import com.juick.server.util.UserUtils;
+import com.juick.service.MessagesService;
+import com.juick.service.UserService;
+import com.juick.service.activities.DeleteUserEvent;
+import com.juick.service.activities.FollowEvent;
+import com.juick.service.activities.UndoFollowEvent;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.inject.Inject;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@RestController
+public class Profile {
+ private static final Logger logger = LoggerFactory.getLogger(Profile.class);
+ @Inject
+ private UserService userService;
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private KeystoreManager keystoreManager;
+ @Inject
+ private SignatureManager signatureManager;
+ @Inject
+ private ActivityPubManager activityPubManager;
+ @Inject
+ private ApplicationEventPublisher applicationEventPublisher;
+ @Inject
+ private CommandsManager commandsManager;
+ @Value("${web_domain:localhost}")
+ private String domain;
+ @Value("${ap_base_uri:http://localhost:8080/}")
+ private String baseUri;
+ @Value("${img_url:http://localhost:8080/i/}")
+ private String baseImagesUri;
+ @Inject
+ private ObjectMapper jsonMapper;
+
+ @GetMapping(value = "/u/{userName}", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE})
+ public Person getUser(@PathVariable String userName) {
+ User user = userService.getUserByName(userName);
+ if (!user.isAnonymous()) {
+ Person person = new Person();
+ person.setId(activityPubManager.personUri(user));
+ person.setUrl(activityPubManager.personWebUri(user));
+ person.setName(userName);
+ person.setPreferredUsername(userName);
+ Key publicKey = new Key();
+ publicKey.setId(person.getId() + "#main-key");
+ publicKey.setOwner(person.getId());
+ publicKey.setPublicKeyPem(keystoreManager.getPublicKeyPem());
+ person.setPublicKey(publicKey);
+ person.setInbox(activityPubManager.inboxUri());
+ person.setOutbox(activityPubManager.outboxUri(user));
+ person.setFollowers(activityPubManager.followersUri(user));
+ person.setFollowing(activityPubManager.followingUri(user));
+ UriComponentsBuilder image = UriComponentsBuilder.fromUriString(baseImagesUri);
+ image.path(String.format("/a/%d.png", user.getUid()));
+ Image avatar = new Image();
+ avatar.setUrl(image.toUriString());
+ avatar.setMediaType("image/png");
+ person.setIcon(avatar);
+ return (Person) Context.build(person);
+ }
+ throw new HttpNotFoundException();
+ }
+
+ @GetMapping(value = "/u/{userName}/blog/toc", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE})
+ public OrderedCollection getOutbox(@PathVariable String userName) {
+ User user = userService.getUserByName(userName);
+ if (!user.isAnonymous()) {
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri);
+ OrderedCollection blog = new OrderedCollection();
+ blog.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString());
+ blog.setTotalItems(userService.getStatsMessages(user.getUid()));
+ blog.setFirst(uriComponentsBuilder.path(String.format("/u/%s/blog", userName)).toUriString());
+ return (OrderedCollection) Context.build(blog);
+ }
+ throw new HttpNotFoundException();
+ }
+
+ @GetMapping(value = "/u/{userName}/blog", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE})
+ public OrderedCollectionPage getOutboxPage(@PathVariable String userName,
+ @RequestParam(required = false, defaultValue = "0") int before) {
+ User visitor = UserUtils.getCurrentUser();
+ User user = userService.getUserByName(userName);
+ if (!user.isAnonymous()) {
+ UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri);
+ String personUri = uri.path(String.format("/u/%s", userName)).toUriString();
+ List mids = messagesService.getUserBlog(user.getUid(), 0, before);
+ List notes = messagesService.getMessages(visitor, mids).stream().map(activityPubManager::makeNote).collect(Collectors.toList());
+ OrderedCollectionPage page = new OrderedCollectionPage();
+ page.setPartOf(uri.replacePath(String.format("/u/%s/blog/toc", userName)).toUriString());
+ page.setFirst(uri.replacePath(String.format("/u/%s/blog", userName)).toUriString());
+ page.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString());
+ page.setOrderedItems(notes.stream().map(a -> {
+ Create create = new Create();
+ create.setId(a.getId() + "#Create");
+ create.setTo(a.getTo());
+ create.setActor(personUri);
+ create.setObject(a);
+ create.setPublished(a.getPublished());
+ return create;
+ }).collect(Collectors.toList()));
+ int beforeNext = mids.stream().reduce((fst, second) -> second).orElse(0);
+ if (beforeNext > 0) {
+ page.setNext(uri.queryParam("before", beforeNext).toUriString());
+ }
+ page.setLast(uri.replaceQueryParam("before", "1").toUriString());
+ return (OrderedCollectionPage) Context.build(page);
+ }
+ throw new HttpNotFoundException();
+ }
+
+ @GetMapping(value = "/u/{userName}/followers/toc", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE})
+ public OrderedCollection getFollowers(@PathVariable String userName) {
+ User user = userService.getUserByName(userName);
+ if (!user.isAnonymous()) {
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri);
+ OrderedCollection followers = new OrderedCollection();
+ followers.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString());
+ followers.setTotalItems(userService.getStatsMyReaders(user.getUid()));
+ followers.setFirst(uriComponentsBuilder.path(String.format("/u/%s/followers", userName)).toUriString());
+ return (OrderedCollection) Context.build(followers);
+ }
+ throw new HttpNotFoundException();
+ }
+
+ @GetMapping(value = "/u/{userName}/followers", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE})
+ public OrderedCollectionPage getFollowersPage(@PathVariable String userName,
+ @RequestParam(required = false, defaultValue = "0") int page) {
+ User user = userService.getUserByName(userName);
+ if (!user.isAnonymous()) {
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri);
+ uriComponentsBuilder.path(String.format("/u/%s/followers", userName));
+ List followers = userService.getUserReaders(user.getUid());
+ Stream followersPage = followers.stream().skip(20 * page).limit(20);
+
+ OrderedCollectionPage result = new OrderedCollectionPage();
+ result.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString());
+ result.setOrderedItems(followersPage.map(a -> {
+ Person follower = new Person();
+ follower.setName(a.getName());
+ follower.setPreferredUsername(a.getName());
+ follower.setUrl(activityPubManager.personWebUri(a));
+ return follower;
+ }).collect(Collectors.toList()));
+ boolean hasNext = followers.size() <= 20 * page;
+ if (hasNext) {
+ result.setNext(uriComponentsBuilder.queryParam("page", page + 1).toUriString());
+ }
+ return (OrderedCollectionPage) Context.build(result);
+ }
+ throw new HttpNotFoundException();
+ }
+
+ @GetMapping(value = "/u/{userName}/following/toc", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE})
+ public OrderedCollection getFollowing(@PathVariable String userName) {
+ User user = userService.getUserByName(userName);
+ if (!user.isAnonymous()) {
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri);
+ OrderedCollection following = new OrderedCollection();
+ following.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString());
+ following.setTotalItems(userService.getUserFriends(user.getUid()).size());
+ following.setFirst(uriComponentsBuilder.path(String.format("/u/%s/followers", userName)).toUriString());
+ return (OrderedCollection) Context.build(following);
+ }
+ throw new HttpNotFoundException();
+ }
+
+ @GetMapping(value = "/u/{userName}/following", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE})
+ public OrderedCollectionPage getFollowingPage(@PathVariable String userName,
+ @RequestParam(required = false, defaultValue = "0") int page) {
+ User user = userService.getUserByName(userName);
+ if (!user.isAnonymous()) {
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri);
+ uriComponentsBuilder.path(String.format("/u/%s/following", userName));
+ List following = userService.getUserFriends(user.getUid());
+ Stream followingPage = following.stream().skip(20 * page).limit(20);
+
+ OrderedCollectionPage result = new OrderedCollectionPage();
+ result.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString());
+ result.setOrderedItems(followingPage.map(a -> {
+ Person follower = new Person();
+ follower.setName(a.getName());
+ follower.setPreferredUsername(a.getName());
+ follower.setUrl(activityPubManager.personWebUri(a));
+ return follower;
+ }).collect(Collectors.toList()));
+ boolean hasNext = following.size() <= 20 * page;
+ if (hasNext) {
+ result.setNext(uriComponentsBuilder.queryParam("page", page + 1).toUriString());
+ }
+ return (OrderedCollectionPage) Context.build(result);
+ }
+ throw new HttpNotFoundException();
+ }
+
+ @GetMapping(value = "/n/{mid}-{rid}", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE})
+ public Context showNote(@PathVariable int mid, @PathVariable int rid) {
+ if (rid > 0) {
+ // reply
+ return Context.build(activityPubManager.makeNote(
+ messagesService.getReply(mid, rid)));
+ }
+ return Context.build(activityPubManager.makeNote(
+ messagesService.getMessage(mid)));
+ }
+
+ @PostMapping(value = "/api/inbox", consumes = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE})
+ public ResponseEntity processInbox(@RequestBody Activity activity,
+ @RequestHeader(name = "Host") String host,
+ @RequestHeader(name = "Date") String date,
+ @RequestHeader(name = "Digest", required = false) String digest,
+ @RequestHeader(name = "Content-Type") String contentType,
+ @RequestHeader(name = "User-Agent", required = false) String userAgent,
+ @RequestHeader(name = "Accept-Encoding", required = false) String acceptEncoding,
+ @RequestHeader(name = "Signature", required = false) String signature) throws Exception {
+ UriComponents componentsBuilder = ServletUriComponentsBuilder.fromCurrentRequestUri().build();
+ Map headers = new HashMap<>();
+ headers.put("host", host.split(":", 2)[0]);
+ headers.put("date", date);
+ headers.put("digest", digest);
+ headers.put("content-type", contentType);
+ headers.put("user-agent", userAgent);
+ headers.put("accept-encoding", acceptEncoding);
+ boolean valid = signatureManager.verifySignature(signature, URI.create(activity.getActor()), "POST",
+ componentsBuilder.getPath(), headers);
+ if (valid) {
+ if (activity instanceof Follow) {
+ Follow followRequest = (Follow) activity;
+ String actor = followRequest.getActor();
+ Person follower = (Person) signatureManager.getContext(URI.create(actor)).orElseThrow(HttpBadRequestException::new);
+ applicationEventPublisher.publishEvent(
+ new FollowEvent(this, followRequest));
+ return new ResponseEntity<>(HttpStatus.ACCEPTED);
+
+ }
+ if (activity instanceof Undo) {
+ String follower = (String) ((Map) activity.getObject()).get("object");
+ applicationEventPublisher.publishEvent(new UndoFollowEvent(this, activity.getActor(), follower));
+ return new ResponseEntity<>(HttpStatus.OK);
+ }
+ if (activity instanceof Delete) {
+ if (activity.getObject() instanceof String) {
+ // Delete user
+ applicationEventPublisher.publishEvent(new DeleteUserEvent(this, (String)activity.getObject()));
+ return new ResponseEntity<>(HttpStatus.OK);
+ }
+ }
+ if (activity instanceof Create) {
+ if (activity.getObject() instanceof Map) {
+ Map note = (Map) activity.getObject();
+ if (note.get("type").equals("Note")) {
+ URI noteId = URI.create((String) note.get("id"));
+ if (messagesService.replyExists(noteId)) {
+ return new ResponseEntity<>(HttpStatus.OK);
+ } else {
+ String inReplyTo = (String) note.get("inReplyTo");
+ if (StringUtils.isNotBlank(inReplyTo)) {
+ if (inReplyTo.startsWith(baseUri)) {
+ UriComponents uri = UriComponentsBuilder.fromUriString(inReplyTo).build();
+ String postId = uri.getPath().substring(uri.getPath().lastIndexOf('/') + 1).replace("-", "/");
+ User user = new User();
+ user.setUri(URI.create(activity.getActor()));
+ String attachment = StringUtils.EMPTY;
+ if (note.get("attachment") != null && ((List) note.get("attachment")).size() > 0) {
+ Map attachmentObj = (Map) ((List