From 7a2f89266c8f6337e4e81a2fd8488e0f80f4f9bd Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Fri, 3 Apr 2020 23:53:23 +0300 Subject: Reorganize layout and code cleanup --- src/main/java/com/juick/www/HelpService.java | 69 --- src/main/java/com/juick/www/ad/SapeService.java | 4 +- .../java/com/juick/www/api/ApiSocialLogin.java | 322 +++++++++++ src/main/java/com/juick/www/api/Index.java | 42 ++ src/main/java/com/juick/www/api/Messages.java | 217 ++++++++ src/main/java/com/juick/www/api/Notifications.java | 213 ++++++++ src/main/java/com/juick/www/api/PM.java | 120 +++++ src/main/java/com/juick/www/api/Post.java | 238 +++++++++ src/main/java/com/juick/www/api/Service.java | 223 ++++++++ .../java/com/juick/www/api/SystemActivity.java | 108 ++++ src/main/java/com/juick/www/api/Tags.java | 54 ++ src/main/java/com/juick/www/api/Users.java | 279 ++++++++++ .../java/com/juick/www/api/activity/Profile.java | 410 ++++++++++++++ .../activity/helpers/ActivityIdDeserializer.java | 38 ++ .../activity/helpers/LinkValueDeserializer.java | 38 ++ .../com/juick/www/api/activity/model/Activity.java | 50 ++ .../com/juick/www/api/activity/model/Context.java | 144 +++++ .../www/api/activity/model/activities/Accept.java | 23 + .../api/activity/model/activities/Announce.java | 23 + .../www/api/activity/model/activities/Block.java | 23 + .../www/api/activity/model/activities/Create.java | 23 + .../www/api/activity/model/activities/Delete.java | 23 + .../www/api/activity/model/activities/Follow.java | 23 + .../www/api/activity/model/activities/Like.java | 23 + .../www/api/activity/model/activities/Undo.java | 23 + .../www/api/activity/model/activities/Update.java | 23 + .../www/api/activity/model/objects/Emoji.java | 23 + .../www/api/activity/model/objects/Hashtag.java | 28 + .../www/api/activity/model/objects/Image.java | 32 ++ .../juick/www/api/activity/model/objects/Key.java | 41 ++ .../juick/www/api/activity/model/objects/Link.java | 32 ++ .../www/api/activity/model/objects/Mention.java | 29 + .../juick/www/api/activity/model/objects/Note.java | 81 +++ .../activity/model/objects/OrderedCollection.java | 42 ++ .../model/objects/OrderedCollectionPage.java | 75 +++ .../www/api/activity/model/objects/Person.java | 104 ++++ .../juick/www/api/apple/AppSiteAssociation.java | 66 +++ .../java/com/juick/www/api/hostmeta/HostMeta.java | 42 ++ .../java/com/juick/www/api/webfinger/Resource.java | 68 +++ .../com/juick/www/api/webfinger/model/Account.java | 41 ++ .../com/juick/www/api/webfinger/model/Link.java | 48 ++ .../juick/www/api/webhooks/TelegramWebhook.java | 58 ++ .../java/com/juick/www/api/xnodeinfo2/Info.java | 103 ++++ .../juick/www/api/xnodeinfo2/model/NodeInfo.java | 104 ++++ .../com/juick/www/api/xnodeinfo2/model/Server.java | 63 +++ .../www/api/xnodeinfo2/model/ServiceInfo.java | 44 ++ .../com/juick/www/api/xnodeinfo2/model/Usage.java | 51 ++ .../juick/www/api/xnodeinfo2/model/UserStats.java | 51 ++ src/main/java/com/juick/www/controllers/Help.java | 7 +- .../com/juick/www/controllers/MessagesWWW.java | 590 --------------------- src/main/java/com/juick/www/controllers/Site.java | 590 +++++++++++++++++++++ src/main/java/com/juick/www/rss/Feeds.java | 76 +++ src/main/java/com/juick/www/rss/MessagesView.java | 158 ++++++ src/main/java/com/juick/www/rss/RepliesView.java | 111 ++++ .../com/juick/www/rss/extension/JuickModule.java | 33 ++ .../www/rss/extension/JuickModuleGenerator.java | 66 +++ .../juick/www/rss/extension/JuickModuleImpl.java | 54 ++ .../juick/www/rss/extension/JuickModuleParser.java | 42 ++ 58 files changed, 5063 insertions(+), 666 deletions(-) delete mode 100644 src/main/java/com/juick/www/HelpService.java create mode 100644 src/main/java/com/juick/www/api/ApiSocialLogin.java create mode 100644 src/main/java/com/juick/www/api/Index.java create mode 100644 src/main/java/com/juick/www/api/Messages.java create mode 100644 src/main/java/com/juick/www/api/Notifications.java create mode 100644 src/main/java/com/juick/www/api/PM.java create mode 100644 src/main/java/com/juick/www/api/Post.java create mode 100644 src/main/java/com/juick/www/api/Service.java create mode 100644 src/main/java/com/juick/www/api/SystemActivity.java create mode 100644 src/main/java/com/juick/www/api/Tags.java create mode 100644 src/main/java/com/juick/www/api/Users.java create mode 100644 src/main/java/com/juick/www/api/activity/Profile.java create mode 100644 src/main/java/com/juick/www/api/activity/helpers/ActivityIdDeserializer.java create mode 100644 src/main/java/com/juick/www/api/activity/helpers/LinkValueDeserializer.java create mode 100644 src/main/java/com/juick/www/api/activity/model/Activity.java create mode 100644 src/main/java/com/juick/www/api/activity/model/Context.java create mode 100644 src/main/java/com/juick/www/api/activity/model/activities/Accept.java create mode 100644 src/main/java/com/juick/www/api/activity/model/activities/Announce.java create mode 100644 src/main/java/com/juick/www/api/activity/model/activities/Block.java create mode 100644 src/main/java/com/juick/www/api/activity/model/activities/Create.java create mode 100644 src/main/java/com/juick/www/api/activity/model/activities/Delete.java create mode 100644 src/main/java/com/juick/www/api/activity/model/activities/Follow.java create mode 100644 src/main/java/com/juick/www/api/activity/model/activities/Like.java create mode 100644 src/main/java/com/juick/www/api/activity/model/activities/Undo.java create mode 100644 src/main/java/com/juick/www/api/activity/model/activities/Update.java create mode 100644 src/main/java/com/juick/www/api/activity/model/objects/Emoji.java create mode 100644 src/main/java/com/juick/www/api/activity/model/objects/Hashtag.java create mode 100644 src/main/java/com/juick/www/api/activity/model/objects/Image.java create mode 100644 src/main/java/com/juick/www/api/activity/model/objects/Key.java create mode 100644 src/main/java/com/juick/www/api/activity/model/objects/Link.java create mode 100644 src/main/java/com/juick/www/api/activity/model/objects/Mention.java create mode 100644 src/main/java/com/juick/www/api/activity/model/objects/Note.java create mode 100644 src/main/java/com/juick/www/api/activity/model/objects/OrderedCollection.java create mode 100644 src/main/java/com/juick/www/api/activity/model/objects/OrderedCollectionPage.java create mode 100644 src/main/java/com/juick/www/api/activity/model/objects/Person.java create mode 100644 src/main/java/com/juick/www/api/apple/AppSiteAssociation.java create mode 100644 src/main/java/com/juick/www/api/hostmeta/HostMeta.java create mode 100644 src/main/java/com/juick/www/api/webfinger/Resource.java create mode 100644 src/main/java/com/juick/www/api/webfinger/model/Account.java create mode 100644 src/main/java/com/juick/www/api/webfinger/model/Link.java create mode 100644 src/main/java/com/juick/www/api/webhooks/TelegramWebhook.java create mode 100644 src/main/java/com/juick/www/api/xnodeinfo2/Info.java create mode 100644 src/main/java/com/juick/www/api/xnodeinfo2/model/NodeInfo.java create mode 100644 src/main/java/com/juick/www/api/xnodeinfo2/model/Server.java create mode 100644 src/main/java/com/juick/www/api/xnodeinfo2/model/ServiceInfo.java create mode 100644 src/main/java/com/juick/www/api/xnodeinfo2/model/Usage.java create mode 100644 src/main/java/com/juick/www/api/xnodeinfo2/model/UserStats.java delete mode 100644 src/main/java/com/juick/www/controllers/MessagesWWW.java create mode 100644 src/main/java/com/juick/www/controllers/Site.java create mode 100644 src/main/java/com/juick/www/rss/Feeds.java create mode 100644 src/main/java/com/juick/www/rss/MessagesView.java create mode 100644 src/main/java/com/juick/www/rss/RepliesView.java create mode 100644 src/main/java/com/juick/www/rss/extension/JuickModule.java create mode 100644 src/main/java/com/juick/www/rss/extension/JuickModuleGenerator.java create mode 100644 src/main/java/com/juick/www/rss/extension/JuickModuleImpl.java create mode 100644 src/main/java/com/juick/www/rss/extension/JuickModuleParser.java (limited to 'src/main/java/com/juick/www') diff --git a/src/main/java/com/juick/www/HelpService.java b/src/main/java/com/juick/www/HelpService.java deleted file mode 100644 index 9d3c3f16..00000000 --- a/src/main/java/com/juick/www/HelpService.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.www; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.springframework.cache.annotation.Cacheable; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.regex.Pattern; - -/** - * Created by aalexeev on 12/11/16. - */ -public class HelpService { - private static final Pattern LANG_PATTERN = Pattern.compile("[a-z]{2}"); - - private static final Pattern PAGE_PATTERN = Pattern.compile("[a-zA-Z0-9\\-_]+"); - - private final String helpPath; - - - public HelpService(String helpPath) { - this.helpPath = helpPath; - } - - @Cacheable("help") - public String getHelp(final String page, final String lang) { - if (canBePage(page) && canBeLang(lang)) { - String path = StringUtils.joinWith("/", helpPath, lang, page + ".md"); - - try (InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) { - if (is != null) - return IOUtils.toString(is, StandardCharsets.UTF_8); - } catch (IOException e) { - } - } - return null; - } - - public boolean canBePage(final String anything) { - return anything != null && PAGE_PATTERN.matcher(anything).matches(); - } - - public boolean canBeLang(final String anything) { - return anything != null && LANG_PATTERN.matcher(anything).matches(); - } - - public String getHelpPath() { - return helpPath; - } -} diff --git a/src/main/java/com/juick/www/ad/SapeService.java b/src/main/java/com/juick/www/ad/SapeService.java index 9d58f8b4..4ef4a213 100644 --- a/src/main/java/com/juick/www/ad/SapeService.java +++ b/src/main/java/com/juick/www/ad/SapeService.java @@ -19,7 +19,7 @@ package com.juick.www.ad; import com.juick.model.User; import com.juick.service.security.annotation.Visitor; -import com.juick.www.controllers.MessagesWWW; +import com.juick.www.controllers.Site; import org.apache.commons.lang3.StringUtils; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.ui.ModelMap; @@ -34,7 +34,7 @@ import ru.sape.Sape; import javax.inject.Inject; import java.net.URI; -@ControllerAdvice(assignableTypes = MessagesWWW.class) +@ControllerAdvice(assignableTypes = Site.class) @ConditionalOnProperty("sape_user") public class SapeService { diff --git a/src/main/java/com/juick/www/api/ApiSocialLogin.java b/src/main/java/com/juick/www/api/ApiSocialLogin.java new file mode 100644 index 00000000..6499b507 --- /dev/null +++ b/src/main/java/com/juick/www/api/ApiSocialLogin.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.www.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.scribejava.apis.AppleClientSecretGenerator; +import com.github.scribejava.apis.AppleSignInApi; +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.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.juick.model.AuthResponse; +import com.juick.model.ext.facebook.User; +import com.juick.server.util.HttpBadRequestException; +import com.juick.service.CrosspostService; +import com.juick.service.EmailService; +import com.juick.service.UserService; +import com.juick.model.ext.vk.UsersResponse; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.proc.BadJOSEException; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.text.ParseException; +import java.util.Collections; +import java.util.Map; +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 OAuth20Service facebookAuthService, vkAuthService, appleSignInService; + + @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("${google_client_id:}") + private String googleClientId; + @Value("${apple_app_id:appid}") + private String appleApplicationId; + @Value("${ap_base_uri:http://localhost:8080/}") + private String baseUri; + + @Inject + private CrosspostService crosspostService; + @Inject + private UserService userService; + @Inject + private EmailService emailService; + @Inject + private AppleClientSecretGenerator clientSecretGenerator; + @Inject + private Users users; + + private final HttpTransport transport = new NetHttpTransport(); + private final JsonFactory jsonFactory = new JacksonFactory(); + private GoogleIdTokenVerifier verifier; + + @PostConstruct + public void init() { + ServiceBuilder facebookBuilder = new ServiceBuilder(FACEBOOK_APPID); + ServiceBuilder twitterBuilder = new ServiceBuilder(twitterConsumerKey); + ServiceBuilder vkBuilder = new ServiceBuilder(VK_APPID); + verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory) + .setAudience(Collections.singletonList(googleClientId)) + .build(); + facebookAuthService = facebookBuilder + .apiSecret(FACEBOOK_SECRET) + .callback(FACEBOOK_REDIRECT) + .defaultScope("email") + .build(FacebookApi.instance()); + vkAuthService = vkBuilder + .apiSecret(VK_SECRET) + .defaultScope("friends,wall,offline") + .callback(VK_REDIRECT) + .build(VkontakteApi.instance()); + ServiceBuilder appleSignInBuilder = new ServiceBuilder(appleApplicationId); + UriComponentsBuilder redirectBuilder = UriComponentsBuilder.fromUriString(baseUri); + String appleSignInRedirectUri = redirectBuilder.replacePath("/api/_applelogin").build().toUriString(); + appleSignInService = appleSignInBuilder + .callback(appleSignInRedirectUri) + .defaultScope("email") + .build(new AppleSignInApi(clientSecretGenerator)); + } + + @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); + return "redirect:" + facebookAuthService.getAuthorizationUrl(fbstate); + } + + String redirectUrl = crosspostService.verifyFacebookState(state); + + if (StringUtils.isEmpty(redirectUrl)) { + logger.error("state is missing"); + throw new HttpBadRequestException(); + } + OAuth2AccessToken token = facebookAuthService.getAccessToken(code); + final OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://graph.facebook.com/v3.2/me?fields=id,name,email"); + facebookAuthService.signRequest(token, meRequest); + String graph = facebookAuthService.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())) { + logger.error("Missing required fields, id: {}, name: {}", fbID, fb.getName()); + throw new HttpBadRequestException(); + } + + int uid = crosspostService.getUIDbyFBID(fbID); + if (uid > 0) { + if (!crosspostService.updateFacebookUser(fbID, token.getAccessToken(), fb.getName())) { + 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)); + uriComponentsBuilder.queryParam("retpath", redirectUrl); + return "redirect:" + uriComponentsBuilder.build().toUriString(); + } else { + if (!crosspostService.createFacebookUser(fbID, state, token.getAccessToken(), fb.getName())) { + if (StringUtils.isNotEmpty(fb.getEmail())) { + logger.info("found {} for facebook user {}", fb.getEmail(), fb.getName()); + 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.getName()); + throw new HttpBadRequestException(); + } + return "redirect:/signup?type=fb&hash=" + state; + } + } + @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); + return "redirect:" + vkAuthService.getAuthorizationUrl(vkstate); + } + + String redirectUrl = crosspostService.verifyVKState(state); + if (StringUtils.isBlank(redirectUrl)) { + logger.error("state is missing"); + throw new HttpBadRequestException(); + } + OAuth2AccessToken token = vkAuthService.getAccessToken(code); + + OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.73"); + vkAuthService.signRequest(token, meRequest); + String graph = vkAuthService.execute(meRequest).getBody(); + + com.juick.model.ext.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)); + uriComponentsBuilder.queryParam("retpath", redirectUrl); + 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; + } + } + @ResponseBody + @PostMapping("/api/_google") + public ResponseEntity googleSignIn(@RequestParam(name = "idToken") String idTokenString) + throws GeneralSecurityException, IOException { + logger.info("Token: {}", idTokenString); + logger.info("Client: {}", googleClientId); + GoogleIdToken idToken = verifier.verify(idTokenString); + if (idToken != null) { + String email = idToken.getPayload().getEmail(); + com.juick.model.User visitor = userService.getUserByEmail(email); + if (visitor.isAnonymous()) { + String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); + emailService.addVerificationCode(null, email, verificationCode); + return ResponseEntity.ok(new AuthResponse(null, email, verificationCode)); + } else { + return ResponseEntity.ok(new AuthResponse(users.getMe(visitor), null, null)); + } + } + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null); + } + @ResponseBody + @PostMapping("/api/signup") + public ResponseEntity signupWithEmail(String username, String password, String verificationCode) { + if (username.length() < 2 || username.length() > 16 || !username.matches("^[a-zA-Z0-9\\-]+$") + || password.length() < 6 || password.length() > 32) { + throw new HttpBadRequestException(); + } + + String verifiedEmail = emailService.getEmailByAuthCode(verificationCode); + if (StringUtils.isNotEmpty(verifiedEmail)) { + com.juick.model.User newUser = userService.createUser(username, password).orElseThrow(HttpBadRequestException::new); + emailService.addEmail(newUser.getUid(), verifiedEmail); + emailService.deleteAuthCode(verificationCode); + return ResponseEntity.ok(newUser); + } else { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null); + } + } + @GetMapping("/api/_applelogin") + public String doAppleLogin(@RequestParam(required = false) String code, @RequestParam String state) { + if (StringUtils.isBlank(code)) { + String astate = UUID.randomUUID().toString(); + crosspostService.addVKState(astate, state); + return "redirect:" + appleSignInService.getAuthorizationUrl(astate); + } + throw new HttpBadRequestException(); + } + @PostMapping("/api/_applelogin") + public String doVerifyAppleResponse(@RequestParam Map body) throws InterruptedException, ExecutionException, IOException, ParseException, JOSEException, BadJOSEException { + OAuth2AccessToken token = appleSignInService.getAccessToken(body.get("code")); + var jsonNode = jsonMapper.readTree(token.getRawResponse()); + var idToken = jsonNode.get("id_token").textValue(); + + AppleSignInApi api = (AppleSignInApi) appleSignInService.getApi(); + var email = api.validateToken(idToken); + + if (email.isPresent()) { + com.juick.model.User user = userService.getUserByEmail(email.get()); + if (!user.isAnonymous()) { + String redirectUrl = crosspostService.verifyVKState(body.get("state")); + if (StringUtils.isBlank(redirectUrl)) { + logger.error("state is missing"); + throw new HttpBadRequestException(); + } + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl); + uriComponentsBuilder.queryParam("hash", userService.getHashByUID(user.getUid())); + uriComponentsBuilder.queryParam("retpath", redirectUrl); + return "redirect:" + uriComponentsBuilder.build().toUriString(); + } else { + String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); + emailService.addVerificationCode(null, email.get(), verificationCode); + return "redirect:/signup?type=email&hash=" + verificationCode; + } + } + throw new HttpBadRequestException(); + } +} diff --git a/src/main/java/com/juick/www/api/Index.java b/src/main/java/com/juick/www/api/Index.java new file mode 100644 index 00000000..5b771efd --- /dev/null +++ b/src/main/java/com/juick/www/api/Index.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api; + +import org.springframework.http.HttpStatus; +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 java.net.URI; + +/** + * Created by vitalyster on 25.07.2016. + */ +@RestController +public class Index { + + @ApiIgnore + @RequestMapping(value = { "/api/", "/ws/" }, method = RequestMethod.GET) + public ResponseEntity description() { + URI redirectUri = ServletUriComponentsBuilder.fromCurrentRequestUri().path("/swagger-ui.html").build().toUri(); + return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).location(redirectUri).build(); + } +} diff --git a/src/main/java/com/juick/www/api/Messages.java b/src/main/java/com/juick/www/api/Messages.java new file mode 100644 index 00000000..59ed7c8f --- /dev/null +++ b/src/main/java/com/juick/www/api/Messages.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api; + +import com.juick.model.Message; +import com.juick.model.Tag; +import com.juick.model.User; +import com.juick.server.Utils; +import com.juick.www.WebApp; +import com.juick.model.CommandResult; +import com.juick.server.util.HttpBadRequestException; +import com.juick.service.MessagesService; +import com.juick.service.TagService; +import com.juick.service.UserService; +import com.juick.service.component.SystemEvent; +import com.juick.service.security.annotation.Visitor; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.io.Resource; +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.*; +import java.util.stream.Collectors; + +/** + * @author ugnich + */ +@RestController +@RequestMapping(produces = MediaType.APPLICATION_JSON_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; + @Inject + private WebApp webApp; + @Value("classpath:Transparent.gif") + private Resource invisiblePixel; + + // TODO: serialize image urls + + @GetMapping("/api/home") + public ResponseEntity> getHome( + @Visitor User visitor, + @RequestParam(defaultValue = "0") int before_mid) { + if (!visitor.isAnonymous()) { + int vuid = visitor.getUid(); + List mids = messagesService.getMyFeed(vuid, before_mid, true); + List msgs = messagesService.getMessages(visitor, mids); + msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarUrl(m.getUser()))); + return ResponseEntity.ok(msgs); + } + return FORBIDDEN; + } + + @GetMapping("/api/messages") + public ResponseEntity> getMessages( + @Visitor User visitor, + @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) { + List mids; + if (!StringUtils.isEmpty(uname)) { + User user = userService.getUserByName(uname); + if (!user.isAnonymous() && !user.isBanned()) { + 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); + } + } + List msgs = messagesService.getMessages(visitor, mids); + msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarUrl(m.getUser()))); + return ResponseEntity.ok(msgs); + } + @DeleteMapping("/api/messages") + public CommandResult deleteMessage( + @Visitor User visitor, + @RequestParam int mid, @RequestParam(required = false, defaultValue = "0") int rid) { + 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( + @Visitor User visitor, + @RequestParam(required = false, defaultValue = "0") Long to) { + List msgs = messagesService.getMessages(visitor, + messagesService.getDiscussions(visitor.getUid(), to)); + msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarUrl(m.getUser()))); + return msgs; + } + @GetMapping("/api/thread") + public ResponseEntity> getThread( + @Visitor User visitor, + @RequestParam(defaultValue = "0") int mid) { + Optional message = messagesService.getMessage(mid); + if (message.isPresent()) { + Message msg = message.get(); + if (!messagesService.canViewThread(mid, visitor.getUid())) { + return FORBIDDEN; + } else { + msg.getUser().setAvatar(webApp.getAvatarUrl(msg.getUser())); + msg.setRecommendations(new HashSet<>(messagesService.getMessagesRecommendations( + Collections.singletonList(msg.getMid())) + .stream().map(Pair::getRight).collect(Collectors.toList()))); + msg.getRecommendations().forEach(r -> r.setAvatar(webApp.getAvatarUrl(r))); + List replies = messagesService.getReplies(visitor, mid); + replies.forEach(m -> m.getUser().setAvatar(webApp.getAvatarUrl(m.getUser()))); + if (!visitor.isAnonymous()) { + userService.updateLastSeen(visitor); + applicationEventPublisher.publishEvent( + new SystemEvent(this, SystemActivity.read(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( + @Visitor User visitor, + @PathVariable int mid, @PathVariable int rid) throws IOException { + if (!visitor.isAnonymous()) { + messagesService.setLastReadComment(visitor, mid, rid); + Message msg = messagesService.getMessage(mid).orElseThrow(IllegalStateException::new); + userService.updateLastSeen(visitor); + applicationEventPublisher.publishEvent( + new SystemEvent(this, SystemActivity.read(visitor, msg))); + return IOUtils.toByteArray(invisiblePixel.getInputStream()); + } + throw new HttpBadRequestException(); + } +} diff --git a/src/main/java/com/juick/www/api/Notifications.java b/src/main/java/com/juick/www/api/Notifications.java new file mode 100644 index 00000000..ca382246 --- /dev/null +++ b/src/main/java/com/juick/www/api/Notifications.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api; + +import com.juick.model.ExternalToken; +import com.juick.model.Message; +import com.juick.model.Status; +import com.juick.model.User; +import com.juick.model.AnonymousUser; +import com.juick.server.util.HttpBadRequestException; +import com.juick.service.MessagesService; +import com.juick.service.PushQueriesService; +import com.juick.service.SubscriptionService; +import com.juick.service.TelegramService; +import com.juick.service.UserService; +import com.juick.service.security.annotation.Visitor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.annotations.ApiIgnore; + +import javax.inject.Inject; +import java.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; + @Inject + private TelegramService telegramService; + @Value("${api_user:juick}") + private String serviceUser; + + + 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))); + List xmppJids = userService.getJIDsbyUID(uid).stream() + .map(jid -> new ExternalToken(null, "xmpp", jid, null)) + .collect(Collectors.toList()); + user.getTokens().addAll(xmppJids); + List tgIds = telegramService.getTelegramIdentifiers(Collections.singletonList(user)).stream() + .map(tgId -> new ExternalToken(null, "durov", String.valueOf(tgId), null)) + .collect(Collectors.toList()); + user.getTokens().addAll(tgIds); + return user; + } + + @ApiIgnore + @RequestMapping(value = "/api/notifications", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> doGet( + @Visitor User visitor, + @RequestParam(required = false, defaultValue = "0") int uid, + @RequestParam(required = false, defaultValue = "0") int mid, + @RequestParam(required = false, defaultValue = "0") int rid) { + if (!(visitor.getName().equals(serviceUser))) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null); + } + if (uid > 0 && mid == 0) { + // PM + return ResponseEntity.ok(Collections.singletonList(collectTokens(uid))); + } else { + if (mid > 0) { + // reply + Message msg = messagesService.getMessage(mid).orElseThrow(IllegalStateException::new); + List users; + if (rid > 0) { + Message op = messagesService.getMessage(mid).orElseThrow(IllegalStateException::new); + 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))); + } + } + } + + @ApiIgnore + @RequestMapping(value = "/api/notifications", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity doDelete( + @Visitor User visitor, + @RequestBody List list) { + if (!visitor.getName().equals(serviceUser)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null); + } + 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 ResponseEntity.ok(Status.OK); + } + @ApiIgnore + @RequestMapping(value = "/api/notifications/delete", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity doDeleteTokens( + @Visitor User visitor, + @RequestBody List list) { + if (!visitor.getName().equals(serviceUser)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null); + } + 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 ResponseEntity.ok(Status.OK); + } + + @ApiIgnore + @RequestMapping(value = "/api/notifications", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) + public Status doPut( + @Visitor User visitor, + @RequestBody List list) { + 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_VALUE) + public Status doAndroidRegister( + @Visitor User visitor, + @RequestParam(name = "regid") String regId) { + pushQueriesService.addGCMToken(visitor.getUid(), regId); + return Status.OK; + } + + @Deprecated + @RequestMapping(value = "/api/winphone/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public Status doWinphoneRegister( + @Visitor User visitor, + @RequestParam(name = "url") String regId) { + pushQueriesService.addMPNSToken(visitor.getUid(), regId); + return Status.OK; + } +} diff --git a/src/main/java/com/juick/www/api/PM.java b/src/main/java/com/juick/www/api/PM.java new file mode 100644 index 00000000..b81dcc78 --- /dev/null +++ b/src/main/java/com/juick/www/api/PM.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api; + +import com.juick.model.Chat; +import com.juick.model.Message; +import com.juick.model.User; +import com.juick.model.AnonymousUser; +import com.juick.model.PrivateChats; +import com.juick.server.util.HttpBadRequestException; +import com.juick.server.util.HttpForbiddenException; +import com.juick.server.util.WebUtils; +import com.juick.www.WebApp; +import com.juick.service.PMQueriesService; +import com.juick.service.UserService; +import com.juick.service.component.SystemEvent; +import com.juick.service.security.annotation.Visitor; +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; + @Inject + private WebApp webApp; + + @RequestMapping(value = "/api/pm", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public List doGetPM( + @Visitor User visitor, + @RequestParam(required = false) String uname) { + int uid = 0; + if (uname != null && uname.matches("^[a-zA-Z0-9\\-]{2,16}$")) { + uid = userService.getUIDbyName(uname); + } + + if (uid == 0) { + throw new HttpBadRequestException(); + } + + List msgs = pmQueriesService.getPMMessages(visitor.getUid(), uid); + msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarUrl(m.getUser()))); + return msgs; + } + + @RequestMapping(value = "/api/pm", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) + public Message doPostPM( + @Visitor User visitor, + @RequestParam String uname, + @RequestParam String body) { + 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)) { + Message jmsg = new Message(); + jmsg.setUser(visitor); + jmsg.setText(body); + jmsg.setTo(userTo); + jmsg.getUser().setAvatar(webApp.getAvatarUrl(jmsg.getUser())); + applicationEventPublisher.publishEvent( + new SystemEvent(this, + SystemActivity.message(jmsg.getUser(), jmsg, Collections.singletonList(jmsg.getTo())))); + return jmsg; + + } + throw new HttpBadRequestException(); + } + @RequestMapping(value = "/api/groups_pms", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public PrivateChats doGetGroupsPMs( + @Visitor User visitor, + @RequestParam(defaultValue = "5") int cnt) { + // TODO: ignore cnt param for now but make sure paging param will not be cnt + + List lastconv = pmQueriesService.getLastChats(visitor); + lastconv.forEach(c -> c.setAvatar(webApp.getAvatarUrl(c))); + PrivateChats pms = new PrivateChats(); + pms.setUsers(lastconv); + return pms; + } +} diff --git a/src/main/java/com/juick/www/api/Post.java b/src/main/java/com/juick/www/api/Post.java new file mode 100644 index 00000000..3c1fbf6b --- /dev/null +++ b/src/main/java/com/juick/www/api/Post.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api; + +import com.juick.model.Message; +import com.juick.model.Reaction; +import com.juick.model.Status; +import com.juick.model.User; +import com.juick.model.CommandResult; +import com.juick.server.ActivityPubManager; +import com.juick.server.CommandsManager; +import com.juick.server.util.HttpBadRequestException; +import com.juick.server.util.HttpForbiddenException; +import com.juick.server.util.HttpNotFoundException; +import com.juick.server.util.HttpUtils; +import com.juick.service.MessagesService; +import com.juick.service.UserService; +import com.juick.service.activities.UpdateEvent; +import com.juick.service.security.annotation.Visitor; +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.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; +import java.util.Optional; + +/** + * Created by vt on 24/11/2016. + */ +@RestController +public class Post { + private final static Logger logger = LoggerFactory.getLogger("API"); + + @Inject + private UserService userService; + @Inject + private MessagesService messagesService; + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + @Inject + CommandsManager commandsManager; + @Inject + ApplicationEventPublisher applicationEventPublisher; + @Inject + ActivityPubManager activityPubManager; + + @RequestMapping(value = "/api/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(value = HttpStatus.OK) + public CommandResult doPostMessage( + @Visitor User visitor, + @RequestParam(required = false, defaultValue = StringUtils.EMPTY) String body, + @RequestParam(required = false) String img, + @RequestParam(required = false) MultipartFile attach) throws Exception { + 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_VALUE) + public CommandResult doPostComment( + @Visitor User visitor, + @RequestParam(defaultValue = "0") int mid, + @RequestParam(defaultValue = "0") int rid, + @RequestParam(required = false, defaultValue = StringUtils.EMPTY) final String body, + @RequestParam(required = false) String img, + @RequestParam(required = false) MultipartFile attach) + throws Exception { + if (mid == 0) { + throw new HttpBadRequestException(); + } + Optional message = messagesService.getMessage(mid); + if (message.isEmpty()) { + throw new HttpNotFoundException(); + } + + Message msg = message.get(); + + Message reply = null; + if (rid > 0) { + reply = messagesService.getReply(mid, rid); + if (reply == null) { + throw new HttpNotFoundException(); + } + } + + if ((msg.ReadOnly && msg.getUser().getUid() != visitor.getUid()) + || userService.isInBLAny(msg.getUser().getUid(), visitor.getUid()) + || (reply != null && userService.isInBLAny(reply.getUser().getUid(), visitor.getUid()))) { + // TODO: validator + 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(@Visitor User visitor, @RequestParam Integer mid) throws Exception { + Optional message = messagesService.getMessage(mid); + if (message.isEmpty()) { + throw new HttpNotFoundException(); + } + Message msg = message.get(); + 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(@Visitor User visitor, + @RequestParam Integer mid) throws Exception { + Optional message = messagesService.getMessage(mid); + if (message.isEmpty()) { + throw new HttpNotFoundException(); + } + Message msg = message.get(); + 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( + @Visitor User visitor, + @RequestParam Integer mid, @RequestParam @NotNull int reactionId, + @RequestParam(required = false, defaultValue = "1") int count) { + + logger.info("got reaction with type: {}", reactionId); + Optional message = messagesService.getMessage(mid); + if (message.isEmpty()) { + throw new HttpNotFoundException(); + } + Message msg = message.get(); + 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(@Visitor User visitor, + @RequestParam Integer mid, + @RequestParam(required = false, defaultValue = "0") Integer rid, + @RequestParam String body) { + 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).orElseThrow(IllegalStateException::new) + : messagesService.getReply(mid, rid); + applicationEventPublisher.publishEvent( + new UpdateEvent(this, author, activityPubManager.messageUri(mid, rid))); + return CommandResult.build(result, "Message updated", StringUtils.EMPTY); + } + throw new HttpBadRequestException(); + } + throw new HttpForbiddenException(); + } +} diff --git a/src/main/java/com/juick/www/api/Service.java b/src/main/java/com/juick/www/api/Service.java new file mode 100644 index 00000000..cb918682 --- /dev/null +++ b/src/main/java/com/juick/www/api/Service.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api; + +import com.juick.model.Message; +import com.juick.model.User; +import com.juick.model.CommandResult; +import com.juick.server.CommandsManager; +import com.juick.server.EmailManager; +import com.juick.server.ServerManager; +import com.juick.server.util.HttpBadRequestException; +import com.juick.server.util.HttpForbiddenException; +import com.juick.service.EmailService; +import com.juick.service.MessagesService; +import com.juick.service.UserService; +import com.juick.service.component.AccountVerificationEvent; +import com.juick.service.security.annotation.Visitor; +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.context.ApplicationEventPublisher; +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("Session"); + @Inject + private UserService userService; + @Inject + private EmailService emailService; + @Inject + private MessagesService messagesService; + @Inject + private CommandsManager commandsManager; + @Inject + private ApplicationEventPublisher applicationEventPublisher; + @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(@Visitor User current, InputStream data) throws Exception { + if (current.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) { + int mid, rid; + var originalMessage = messagesService.findMessageByProperty("messageId", inReplyToHeaders[0]); + if (originalMessage.isPresent()) { + mid = originalMessage.get().getLeft(); + rid = originalMessage.get().getRight(); + } else { + Scanner inReplyToScanner = new Scanner(inReplyToHeaders[0].trim()).useDelimiter(EmailManager.MSGID_PATTERN); + mid = Integer.parseInt(inReplyToScanner.next()); + 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); + CommandResult result = commandsManager.processCommand(visitor, body[0], attachmentUri); + if (result.getNewMessage().isPresent()) { + String[] messageIds = msg.getHeader("Message-Id"); + if (messageIds.length == 1) { + Message message = result.getNewMessage().get(); + messagesService.setMessageProperty(message.getMid(), message.getRid(), "messageId", messageIds[0]); + } else { + logger.warn("Wrong number of Message-Id headers"); + } + } + } else { + if (!Arrays.asList(ignoredEmails).contains(from)) { + String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); + emailService.addVerificationCode(null, from, verificationCode); + applicationEventPublisher.publishEvent(new AccountVerificationEvent(this, from, verificationCode)); + } + } + } else { + throw new HttpForbiddenException(); + } + } + @ApiIgnore + @PostMapping("/api/mail/unsubscribe") + @ResponseStatus(value = HttpStatus.OK) + public void processMailUnsubscribe(@Visitor User current, InputStream data) throws Exception { + if (current.getName().equals(serviceUser)) { + MimeMessage msg = new MimeMessage(session, data); + 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()) { + if (!emailService.disableEmail(visitor, from)) { + throw new HttpBadRequestException(); + } + } + } 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(@Visitor User visitor) throws IOException { + 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/www/api/SystemActivity.java b/src/main/java/com/juick/www/api/SystemActivity.java new file mode 100644 index 00000000..0827537a --- /dev/null +++ b/src/main/java/com/juick/www/api/SystemActivity.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api; + +import com.juick.model.Message; +import com.juick.model.User; + +import java.util.Collections; +import java.util.List; + +public class SystemActivity { + public static SystemActivity like(User from, Message message, List subscribers) { + var like = new SystemActivity(); + like.setType(ActivityType.like); + like.setFrom(from); + like.setMessage(message); + like.setTo(subscribers); + return like; + } + + public static SystemActivity message(User from, Message message, List subscribers) { + var msg = new SystemActivity(); + msg.setType(ActivityType.message); + msg.setFrom(from); + msg.setMessage(message); + msg.setTo(subscribers); + return msg; + } + + public static SystemActivity read(User from, Message message) { + var read = new SystemActivity(); + read.setType(ActivityType.message); + read.setFrom(from); + read.setTo(Collections.emptyList()); + var msg = new Message(); + msg.setMid(message.getMid()); + msg.setUser(from); + msg.setService(true); + msg.setUnread(false); + read.setMessage(msg); + return read; + } + + public static SystemActivity follow(User from, List to) { + var follow = new SystemActivity(); + follow.setFrom(from); + follow.setTo(to); + return follow; + } + + public enum ActivityType { + message, + like, + follow + } + + private ActivityType type; + private User from; + private List to; + private Message message; + + public ActivityType getType() { + return type; + } + + public void setType(ActivityType type) { + this.type = type; + } + + public User getFrom() { + return from; + } + + public void setFrom(User from) { + this.from = from; + } + + public List getTo() { + return to; + } + + public void setTo(List to) { + this.to = to; + } + + public Message getMessage() { + return message; + } + + public void setMessage(Message message) { + this.message = message; + } +} diff --git a/src/main/java/com/juick/www/api/Tags.java b/src/main/java/com/juick/www/api/Tags.java new file mode 100644 index 00000000..b50e9bd5 --- /dev/null +++ b/src/main/java/com/juick/www/api/Tags.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api; + +import com.juick.model.User; +import com.juick.model.TagStats; +import com.juick.service.TagService; +import com.juick.service.security.annotation.Visitor; +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_VALUE) + public List tags( + @Visitor User visitor, + @RequestParam(required = false, defaultValue = "0") int user_id + ) { + 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/www/api/Users.java b/src/main/java/com/juick/www/api/Users.java new file mode 100644 index 00000000..06467b7d --- /dev/null +++ b/src/main/java/com/juick/www/api/Users.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api; + +import com.juick.model.User; +import com.juick.model.AnonymousUser; +import com.juick.model.ApplicationStatus; +import com.juick.server.util.HttpBadRequestException; +import com.juick.server.util.HttpNotFoundException; +import com.juick.server.util.HttpUtils; +import com.juick.server.util.WebUtils; +import com.juick.www.WebApp; +import com.juick.service.*; +import com.juick.service.component.MailVerificationEvent; +import com.juick.service.security.annotation.Visitor; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.inject.Inject; +import java.io.IOException; +import java.net.URI; +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 TelegramService telegramService; + @Inject + private EmailService emailService; + @Inject + private TagService tagService; + @Inject + private WebApp webApp; + @Inject + private ImagesService imagesService; + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + @Inject + private ApplicationEventPublisher applicationEventPublisher; + + @RequestMapping(value = "/api/auth", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public String getAuthToken(@Visitor User visitor) { + return userService.getHashByUID(visitor.getUid()); + } + + @RequestMapping(value = "/api/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public List doGetUsers( + @Visitor User visitor, + @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)); + } + users.forEach(u -> u.setAvatar(webApp.getAvatarUrl(u))); + if (!users.isEmpty()) + return users; + if (!visitor.isAnonymous()) { + visitor.setAvatar(webApp.getAvatarUrl(visitor)); + return Collections.singletonList(visitor); + } + + throw new HttpNotFoundException(); + } + + @GetMapping("/api/me") + public SecureUser getMe(@Visitor User visitor) { + SecureUser me = new SecureUser(); + me.setUid(visitor.getUid()); + me.setName(visitor.getName()); + me.setAuthHash(getAuthToken(visitor)); + List unread = messagesService.getUnread(visitor); + me.setUnread(unread); + me.setUnreadCount(unread.size()); + var friends = userService.getUserFriends(visitor.getUid()); + friends.forEach(r -> r.setAvatar(webApp.getAvatarUrl(r))); + me.setRead(friends); + var readers = userService.getUserReaders(visitor.getUid()); + readers.forEach(r -> r.setAvatar(webApp.getAvatarUrl(r))); + me.setReaders(readers); + me.setAvatar(webApp.getAvatarUrl(visitor)); + me.getTagStats().addAll(tagService.getUserTagStats(me.getUid())); + return (SecureUser)userService.getUserInfo(me); + } + @PostMapping("/api/me") + public ResponseEntity updateMe(@Visitor User visitor, + @RequestParam(required = false) String password, + @RequestParam(value = "jid-del", required = false) String jidForDeletion, + @RequestParam(value = "email-add", required = false) String newEmail, + @RequestParam(value = "email-del", required = false) String emailForDeletion, + @RequestParam(value = "account-del", required = false) String accountToDelete) { + if (StringUtils.isNotEmpty(password)) { + if (!userService.updatePassword(visitor, password)) { + throw new HttpBadRequestException(); + } + } + if (StringUtils.isNotEmpty(jidForDeletion)) { + if (!userService.deleteJID(visitor.getUid(), jidForDeletion)) { + throw new HttpBadRequestException(); + } + } + if (StringUtils.isNotEmpty(newEmail)) { + if (!emailService.verifyAddressByCode(visitor.getUid(), newEmail)) { + String authCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); + if (emailService.addVerificationCode(visitor.getUid(), newEmail, authCode)) { + applicationEventPublisher.publishEvent(new MailVerificationEvent(this, newEmail , authCode)); + } + } + } + if (StringUtils.isNotEmpty(emailForDeletion)) { + if (!emailService.deleteEmail(visitor.getUid(), emailForDeletion)) { + throw new HttpBadRequestException(); + } + } + if (StringUtils.isNotEmpty(accountToDelete)) { + switch (accountToDelete) { + case "twitter": + crosspostService.deleteTwitterToken(visitor.getUid()); + break; + case "vk": + crosspostService.deleteVKUser(visitor.getUid()); + break; + case "durov": + telegramService.deleteTelegramUser(visitor.getUid()); + break; + } + } + return ResponseEntity.ok().build(); + } + @PostMapping("/api/me/subscribe") + public ResponseEntity subscribeMe(@Visitor User visitor, String email) { + // TODO: check status + emailService.setNotificationsEmail(visitor.getUid(), email); + return ResponseEntity.ok().build(); + } + @PostMapping("/api/me/upload") + public void updateInfo(@Visitor User visitor, + @RequestParam MultipartFile avatar) throws IOException { + String avatarTmpPath = HttpUtils.receiveMultiPartFile(avatar, tmpDir).getHost(); + if (StringUtils.isNotEmpty(avatarTmpPath)) { + imagesService.saveAvatar(avatarTmpPath, visitor.getUid()); + } + } + + @RequestMapping(value = "/api/users/read", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public List doGetUserRead( + @Visitor User visitor, + @RequestParam String uname) { + int uid = 0; + if (uname == null) { + uid = visitor.getUid(); + } else { + if (WebUtils.isUserName(uname)) { + User u = userService.getUserByName(uname); + if (!u.isAnonymous()) { + uid = u.getUid(); + } + } + } + + if (uid > 0) { + List friends = userService.getUserFriends(uid); + friends.forEach(f -> f.setAvatar(webApp.getAvatarUrl(f))); + return friends; + } + throw new HttpNotFoundException(); + } + + @RequestMapping(value = "/api/users/readers", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public List doGetUserReaders( + @Visitor User visitor, + @RequestParam String uname) { + int uid = 0; + if (uname == null) { + uid = visitor.getUid(); + } else { + if (WebUtils.isUserName(uname)) { + User u = userService.getUserByName(uname); + if (!u.isAnonymous()) { + uid = u.getUid(); + } + } + } + + if (uid > 0) { + List readers = userService.getUserReaders(uid); + readers.forEach(r -> r.setAvatar(webApp.getAvatarUrl(r))); + return readers; + } + throw new HttpNotFoundException(); + } + + @GetMapping("/api/info/{uname}") + public User getUserInfo(@Visitor User visitor, @PathVariable String uname) { + User user = userService.getUserByName(uname); + if (!user.isBanned()) { + user.setRead(doGetUserRead(visitor, uname)); + user.setReaders(doGetUserReaders(visitor, uname)); + user.setAvatar(webApp.getAvatarUrl(user)); + return userService.getUserInfo(user); + } + throw new HttpNotFoundException(); + } + + @Deprecated + @GetMapping(value = "/api/avatar", produces = MediaType.IMAGE_PNG_VALUE) + public byte[] getAvatarUrl( + @RequestParam(required = false) String uname, + @RequestParam(required = false) String jid) + throws IOException { + User user = AnonymousUser.INSTANCE; + if (StringUtils.isNotEmpty(uname)) { + user = userService.getUserByName(uname); + } + if (user.isAnonymous() && StringUtils.isNotEmpty(jid)) { + user = userService.getUserByJID(jid); + } + return IOUtils.toByteArray(URI.create(webApp.getAvatarUrl(user))); + } + public class SecureUser extends User { + public String getHash() { + return getAuthHash(); + } + 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/www/api/activity/Profile.java b/src/main/java/com/juick/www/api/activity/Profile.java new file mode 100644 index 00000000..bdd7cab2 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/Profile.java @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.model.Message; +import com.juick.model.User; +import com.juick.formatters.PlainTextFormatter; +import com.juick.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.www.api.activity.model.Activity; +import com.juick.www.api.activity.model.Context; +import com.juick.www.api.activity.model.activities.Announce; +import com.juick.www.api.activity.model.activities.Create; +import com.juick.www.api.activity.model.activities.Delete; +import com.juick.www.api.activity.model.activities.Follow; +import com.juick.www.api.activity.model.activities.Like; +import com.juick.www.api.activity.model.activities.Undo; +import com.juick.www.api.activity.model.objects.Image; +import com.juick.www.api.activity.model.objects.Key; +import com.juick.www.api.activity.model.objects.Note; +import com.juick.www.api.activity.model.objects.OrderedCollection; +import com.juick.www.api.activity.model.objects.OrderedCollectionPage; +import com.juick.www.api.activity.model.objects.Person; +import com.juick.server.util.HttpNotFoundException; +import com.juick.www.WebApp; +import com.juick.service.MessagesService; +import com.juick.service.UserService; +import com.juick.service.activities.AnnounceEvent; +import com.juick.service.activities.FollowEvent; +import com.juick.service.activities.UndoAnnounceEvent; +import com.juick.service.activities.UndoFollowEvent; +import com.juick.service.security.annotation.Visitor; +import com.overzealous.remark.Remark; +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 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.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.inject.Inject; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +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("ActivityPub"); + @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; + @Inject + private ObjectMapper jsonMapper; + @Inject + private WebApp webApp; + @Inject + private Remark remarkConverter; + + @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)); + Image avatar = new Image(); + avatar.setUrl(webApp.getAvatarUrl(user)); + 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(@Visitor User visitor, @PathVariable String userName, + @RequestParam(required = false, defaultValue = "0") int before) { + User user = userService.getUserByName(userName); + if (!user.isAnonymous() && !user.isBanned()) { + 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).orElseThrow(IllegalStateException::new))); + } + + @PostMapping(value = "/api/inbox", consumes = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) + public ResponseEntity processInbox( + @Visitor User visitor, + InputStream inboxData) throws Exception { + String inbox = IOUtils.toString(inboxData, StandardCharsets.UTF_8); + logger.info("Inbox: {}", inbox); + Activity activity = jsonMapper.readValue(inbox, Activity.class); + if ((StringUtils.isNotEmpty(visitor.getUri().toString()) + && visitor.getUri().equals(URI.create(activity.getActor()))) + || !visitor.isAnonymous()) { + if (activity instanceof Follow) { + Follow followRequest = (Follow) activity; + applicationEventPublisher.publishEvent( + new FollowEvent(this, followRequest)); + return new ResponseEntity<>(CommandResult.fromString("Follow request accepted"), HttpStatus.ACCEPTED); + + } + if (activity instanceof Undo) { + Map object = (Map) activity.getObject(); + String objectType = (String) object.get("type"); + String objectObject = (String) object.get("object"); + if (objectType.equals("Follow")) { + applicationEventPublisher.publishEvent(new UndoFollowEvent(this, activity.getActor(), objectObject)); + return new ResponseEntity<>(CommandResult.fromString("Undo follow request accepted"), HttpStatus.OK); + } else if (objectType.equals("Like") || objectType.equals("Announce")) { + applicationEventPublisher.publishEvent(new UndoAnnounceEvent(this, activity.getActor(), objectObject)); + return new ResponseEntity<>(CommandResult.fromString("Undo like/announce request accepted"), 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<>(CommandResult.fromString("Reply already exists"), HttpStatus.OK); + } else { + String inReplyTo = (String) note.get("inReplyTo"); + if (StringUtils.isNotBlank(inReplyTo)) { + if (inReplyTo.startsWith(baseUri)) { + String postId = activityPubManager.postId(inReplyTo); + User user = new User(); + user.setUri(URI.create(activity.getActor())); + String markdown = remarkConverter.convertFragment((String) note.get("content")); + String commandBody = note.get("attachment") == null ? markdown : + ((List) note.get("attachment")).stream().map(attachmentObj -> { + Map attachment = (Map) attachmentObj; + String attachmentUrl = attachment.get("url"); + String attachmentName = attachment.get("name"); + return PlainTextFormatter.markdownUrl(attachmentUrl, attachmentName); + }).reduce(markdown, (currentUrl, nextUrl) -> String.format("%s\n%s", currentUrl, nextUrl)); + + CommandResult result = commandsManager.processCommand( + user, String.format("#%s %s", postId, commandBody), + URI.create(StringUtils.EMPTY)); + logger.info(jsonMapper.writeValueAsString(result)); + if (result.getNewMessage().isPresent()) { + messagesService.updateReplyUri(result.getNewMessage().get(), noteId); + return new ResponseEntity<>(result, HttpStatus.OK); + } else { + return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); + } + } else { + Message reply = messagesService.getReplyByUri(inReplyTo); + if (reply != null) { + User user = new User(); + user.setUri(URI.create(activity.getActor())); + String markdown = remarkConverter.convertFragment((String)note.get("content")); + // combine note text with attachment urls + String commandBody = note.get("attachment") == null ? markdown : + ((List) note.get("attachment")).stream().map(attachmentObj -> { + Map attachment = (Map) attachmentObj; + String attachmentUrl = attachment.get("url"); + String attachmentName = attachment.get("name"); + return PlainTextFormatter.markdownUrl(attachmentUrl, attachmentName); + }).reduce(markdown, (currentUrl, nextUrl) -> String.format("%s\n%s", currentUrl, nextUrl)); + CommandResult result = commandsManager.processCommand( + user, + String.format("#%d/%d %s", reply.getMid(), reply.getRid(), commandBody), + URI.create(StringUtils.EMPTY)); + logger.info(jsonMapper.writeValueAsString(result)); + if (result.getNewMessage().isPresent()) { + messagesService.updateReplyUri(result.getNewMessage().get(), noteId); + return new ResponseEntity<>(result, HttpStatus.OK); + } else { + return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); + } + } + } + } + } + } + } + } + if (activity instanceof Delete) { + if (activity.getObject() instanceof String) { + // Delete gone user + // TODO: check if it is really deleted and remove copy-paste + if (activity.getActor().equals(activity.getObject())) { + return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), HttpStatus.ACCEPTED); + } + } + Map tombstone = (Map) activity.getObject(); + if (tombstone.get("type").equals("Tombstone")) { + URI actor = URI.create(activity.getActor()); + URI reply = URI.create((String)tombstone.get("id")); + messagesService.deleteReply(actor, reply); + return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), HttpStatus.OK); + } + } + if (activity instanceof Like || activity instanceof Announce) { + String messageUri = activity.getObject() instanceof String ? (String) activity.getObject() + : activity.getObject() instanceof Context ? ((Context) activity.getObject()).getId() + : (String) ((Map)activity.getObject()).get("id"); + applicationEventPublisher.publishEvent(new AnnounceEvent(this, activity.getActor(), messageUri)); + return new ResponseEntity<>(CommandResult.fromString("Like/announce request accepted"), HttpStatus.OK); + } + logger.warn("Unknown activity: {}", jsonMapper.writeValueAsString(activity)); + return new ResponseEntity<>(CommandResult.fromString("Unknown activity"), HttpStatus.NOT_IMPLEMENTED); + } + if (activity instanceof Delete) { + if (activity.getObject() instanceof String) { + // Delete gone user + if (activity.getActor().equals(activity.getObject())) { + return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), HttpStatus.ACCEPTED); + } + } + } + return new ResponseEntity<>(CommandResult.fromString("Can not authenticate"), HttpStatus.UNAUTHORIZED); + } + @PostMapping(value = "/u/", produces = MediaType.APPLICATION_JSON_VALUE) + public User fetchUser(@RequestParam URI uri) { + return activityPubManager.personToUser(uri); + } +} diff --git a/src/main/java/com/juick/www/api/activity/helpers/ActivityIdDeserializer.java b/src/main/java/com/juick/www/api/activity/helpers/ActivityIdDeserializer.java new file mode 100644 index 00000000..e349bff7 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/helpers/ActivityIdDeserializer.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.helpers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.IOException; + +public class ActivityIdDeserializer extends JsonDeserializer { + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonToken jsonToken = p.getCurrentToken(); + if (jsonToken == JsonToken.START_OBJECT) { + JsonNode node = p.getCodec().readTree(p); + return node.get("id").textValue(); + } + return p.getValueAsString(); + } +} diff --git a/src/main/java/com/juick/www/api/activity/helpers/LinkValueDeserializer.java b/src/main/java/com/juick/www/api/activity/helpers/LinkValueDeserializer.java new file mode 100644 index 00000000..ab797f6c --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/helpers/LinkValueDeserializer.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.helpers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.IOException; + +public class LinkValueDeserializer extends JsonDeserializer { + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonToken jsonToken = p.getCurrentToken(); + if (jsonToken == JsonToken.VALUE_STRING) { + return p.getValueAsString(); + } + JsonNode node = p.getCodec().readTree(p); + return node.get("href").textValue(); + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/Activity.java b/src/main/java/com/juick/www/api/activity/model/Activity.java new file mode 100644 index 00000000..2ac22349 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/Activity.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.juick.www.api.activity.helpers.ActivityIdDeserializer; +import org.apache.commons.lang3.StringUtils; + +import java.util.UUID; + +public abstract class Activity extends Context { + + @JsonDeserialize(using = ActivityIdDeserializer.class) + private String actor; + private Object object; + + public String getActor() { + return actor; + } + + public void setActor(String actor) { + this.actor = actor; + if (StringUtils.isEmpty(getId())) { + setId(String.format("%s#%s", this.actor, UUID.randomUUID().toString())); + } + } + + public Object getObject() { + return object; + } + + public void setObject(Object object) { + this.object = object; + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/Context.java b/src/main/java/com/juick/www/api/activity/model/Context.java new file mode 100644 index 00000000..57e29e0f --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/Context.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.juick.www.api.activity.helpers.LinkValueDeserializer; +import com.juick.www.api.activity.model.activities.*; +import com.juick.www.api.activity.model.objects.*; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property="type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = Create.class, name = "Create"), + @JsonSubTypes.Type(value = Update.class, name = "Update"), + @JsonSubTypes.Type(value = Delete.class, name = "Delete"), + @JsonSubTypes.Type(value = Follow.class, name = "Follow"), + @JsonSubTypes.Type(value = Accept.class, name = "Accept"), + @JsonSubTypes.Type(value = Undo.class, name = "Undo"), + @JsonSubTypes.Type(value = Like.class, name = "Like"), + @JsonSubTypes.Type(value = Block.class, name = "Block"), + @JsonSubTypes.Type(value = Announce.class, name = "Announce"), + @JsonSubTypes.Type(value = Activity.class, name = "Activity"), + @JsonSubTypes.Type(value = Image.class, name = "Image"), + @JsonSubTypes.Type(value = Key.class, name = "Key"), + @JsonSubTypes.Type(value = Link.class, name = "Link"), + @JsonSubTypes.Type(value = Hashtag.class, name = "Hashtag"), + @JsonSubTypes.Type(value = Mention.class, name = "Mention"), + @JsonSubTypes.Type(value = Emoji.class, name = "Emoji"), + @JsonSubTypes.Type(value = Note.class, name = "Note"), + @JsonSubTypes.Type(value = OrderedCollection.class, name = "OrderedCollection"), + @JsonSubTypes.Type(value = OrderedCollectionPage.class, name = "OrderedCollectionPage"), + @JsonSubTypes.Type(value = Person.class, name = "Person") +}) +public abstract class Context { + + private List context; + + private String id; + + private String name; + + private Instant published; + + @JsonDeserialize(using = LinkValueDeserializer.class) + private String url; + + private List to; + + private List tags; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getType() { + return getClass().getSimpleName(); + } + + @JsonProperty("@context") + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + public List getContext() { + return context; + } + + public final static String ACTIVITY_STREAMS_URI = "https://www.w3.org/ns/activitystreams"; + public final static String SECURITY_URI = "https://w3id.org/security/v1"; + public final static String LD_JSON_MEDIA_TYPE = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""; + public final static String ACTIVITY_MEDIA_TYPE = "application/activity+json"; + public final static String ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE = ACTIVITY_MEDIA_TYPE + "; profile=\"https://www.w3.org/ns/activitystreams\""; + public final static String ACTIVITYSTREAMS_PUBLIC = "https://www.w3.org/ns/activitystreams#Public"; + + public Instant getPublished() { + return published; + } + + public void setPublished(Instant published) { + this.published = published; + } + + public List getTo() { + return to; + } + + public void setTo(List to) { + this.to = to; + } + + public static Context build(Context response) { + response.context = new ArrayList<>(Arrays.asList(ACTIVITY_STREAMS_URI, SECURITY_URI)); + response.context.add(Collections.singletonMap("Hashtag", "as:Hashtag")); + return response; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + @JsonProperty("tag") + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/activities/Accept.java b/src/main/java/com/juick/www/api/activity/model/activities/Accept.java new file mode 100644 index 00000000..30a55839 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/activities/Accept.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.activities; + +import com.juick.www.api.activity.model.Activity; + +public class Accept extends Activity { +} diff --git a/src/main/java/com/juick/www/api/activity/model/activities/Announce.java b/src/main/java/com/juick/www/api/activity/model/activities/Announce.java new file mode 100644 index 00000000..4561b10e --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/activities/Announce.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.activities; + +import com.juick.www.api.activity.model.Activity; + +public class Announce extends Activity { +} diff --git a/src/main/java/com/juick/www/api/activity/model/activities/Block.java b/src/main/java/com/juick/www/api/activity/model/activities/Block.java new file mode 100644 index 00000000..66919235 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/activities/Block.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.activities; + +import com.juick.www.api.activity.model.Activity; + +public class Block extends Activity { +} diff --git a/src/main/java/com/juick/www/api/activity/model/activities/Create.java b/src/main/java/com/juick/www/api/activity/model/activities/Create.java new file mode 100644 index 00000000..5da46545 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/activities/Create.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.activities; + +import com.juick.www.api.activity.model.Activity; + +public class Create extends Activity { +} diff --git a/src/main/java/com/juick/www/api/activity/model/activities/Delete.java b/src/main/java/com/juick/www/api/activity/model/activities/Delete.java new file mode 100644 index 00000000..0214f869 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/activities/Delete.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.activities; + +import com.juick.www.api.activity.model.Activity; + +public class Delete extends Activity { +} diff --git a/src/main/java/com/juick/www/api/activity/model/activities/Follow.java b/src/main/java/com/juick/www/api/activity/model/activities/Follow.java new file mode 100644 index 00000000..b7653d23 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/activities/Follow.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.activities; + +import com.juick.www.api.activity.model.Activity; + +public class Follow extends Activity { +} diff --git a/src/main/java/com/juick/www/api/activity/model/activities/Like.java b/src/main/java/com/juick/www/api/activity/model/activities/Like.java new file mode 100644 index 00000000..76a35a3c --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/activities/Like.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.activities; + +import com.juick.www.api.activity.model.Activity; + +public class Like extends Activity { +} diff --git a/src/main/java/com/juick/www/api/activity/model/activities/Undo.java b/src/main/java/com/juick/www/api/activity/model/activities/Undo.java new file mode 100644 index 00000000..0e1c7e8a --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/activities/Undo.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.activities; + +import com.juick.www.api.activity.model.Activity; + +public class Undo extends Activity { +} diff --git a/src/main/java/com/juick/www/api/activity/model/activities/Update.java b/src/main/java/com/juick/www/api/activity/model/activities/Update.java new file mode 100644 index 00000000..d6ca9c1b --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/activities/Update.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.activities; + +import com.juick.www.api.activity.model.Activity; + +public class Update extends Activity { +} diff --git a/src/main/java/com/juick/www/api/activity/model/objects/Emoji.java b/src/main/java/com/juick/www/api/activity/model/objects/Emoji.java new file mode 100644 index 00000000..bb556292 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/objects/Emoji.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.objects; + +import com.juick.www.api.activity.model.Context; + +public class Emoji extends Context { +} diff --git a/src/main/java/com/juick/www/api/activity/model/objects/Hashtag.java b/src/main/java/com/juick/www/api/activity/model/objects/Hashtag.java new file mode 100644 index 00000000..175cc305 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/objects/Hashtag.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Hashtag extends Mention { + @JsonCreator + public Hashtag(@JsonProperty("href") String href, @JsonProperty("name") String name) { + super(href, name); + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/objects/Image.java b/src/main/java/com/juick/www/api/activity/model/objects/Image.java new file mode 100644 index 00000000..0dfa6271 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/objects/Image.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.objects; + +import com.juick.www.api.activity.model.Context; + +public class Image extends Context { + private String mediaType; + + public String getMediaType() { + return mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/objects/Key.java b/src/main/java/com/juick/www/api/activity/model/objects/Key.java new file mode 100644 index 00000000..26d60213 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/objects/Key.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.objects; + +import com.juick.www.api.activity.model.Context; + +public class Key extends Context { + private String owner; + private String publicKeyPem; + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getPublicKeyPem() { + return publicKeyPem; + } + + public void setPublicKeyPem(String publicKeyPem) { + this.publicKeyPem = publicKeyPem; + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/objects/Link.java b/src/main/java/com/juick/www/api/activity/model/objects/Link.java new file mode 100644 index 00000000..58bf2304 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/objects/Link.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.objects; + +import com.juick.www.api.activity.model.Context; + +public class Link extends Context { + private String href; + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/objects/Mention.java b/src/main/java/com/juick/www/api/activity/model/objects/Mention.java new file mode 100644 index 00000000..a6099b53 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/objects/Mention.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Mention extends Link { + @JsonCreator + public Mention(@JsonProperty("href") String href, @JsonProperty("name") String name) { + this.setHref(href); + this.setName(name); + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/objects/Note.java b/src/main/java/com/juick/www/api/activity/model/objects/Note.java new file mode 100644 index 00000000..b04b1ec9 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/objects/Note.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.objects; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.juick.www.api.activity.model.Context; + +import java.util.List; + +public class Note extends Context { + private String content; + private String attributedTo; + private String inReplyTo; + private List attachment; + private List to; + private List cc; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getAttributedTo() { + return attributedTo; + } + + public void setAttributedTo(String attributedTo) { + this.attributedTo = attributedTo; + } + + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + public List getAttachment() { + return attachment; + } + + public void setAttachment(List attachment) { + this.attachment = attachment; + } + + public List getTo() { + return to; + } + + public void setTo(List to) { + this.to = to; + } + + public List getCc() { + return cc; + } + + public void setCc(List cc) { + this.cc = cc; + } + + public String getInReplyTo() { + return inReplyTo; + } + + public void setInReplyTo(String inReplyTo) { + this.inReplyTo = inReplyTo; + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/objects/OrderedCollection.java b/src/main/java/com/juick/www/api/activity/model/objects/OrderedCollection.java new file mode 100644 index 00000000..d7d43a26 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/objects/OrderedCollection.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.objects; + +import com.juick.www.api.activity.model.Context; + +public class OrderedCollection extends Context { + + private int totalItems; + + public int getTotalItems() { + return totalItems; + } + + public void setTotalItems(int totalItems) { + this.totalItems = totalItems; + } + private String first; + + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/objects/OrderedCollectionPage.java b/src/main/java/com/juick/www/api/activity/model/objects/OrderedCollectionPage.java new file mode 100644 index 00000000..340e24cf --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/objects/OrderedCollectionPage.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.objects; + +import com.juick.www.api.activity.model.Context; + +import java.util.List; + +public class OrderedCollectionPage extends Context { + + private String partOf; + + private String first; + + private String next; + + private String last; + + private List orderedItems; + + public String getNext() { + return next; + } + + public void setNext(String next) { + this.next = next; + } + + public List getOrderedItems() { + return orderedItems; + } + + public void setOrderedItems(List orderedItems) { + this.orderedItems = orderedItems; + } + + public String getPartOf() { + return partOf; + } + + public void setPartOf(String partOf) { + this.partOf = partOf; + } + + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } + + public String getLast() { + return last; + } + + public void setLast(String last) { + this.last = last; + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/objects/Person.java b/src/main/java/com/juick/www/api/activity/model/objects/Person.java new file mode 100644 index 00000000..c653bee1 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/objects/Person.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.activity.model.objects; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.juick.www.api.activity.model.Context; + +public class Person extends Context { + + private String name; + private String preferredUsername; + private Image icon; + private String inbox; + private String outbox; + private String following; + private String followers; + private Key publicKey; + + @Override + public String getType() { + return "Person"; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) + public Image getIcon() { + return icon; + } + + public void setIcon(Image icon) { + this.icon = icon; + } + + public String getOutbox() { + return outbox; + } + + public void setOutbox(String outbox) { + this.outbox = outbox; + } + + public String getInbox() { + return inbox; + } + + public void setInbox(String inbox) { + this.inbox = inbox; + } + + public String getFollowing() { + return following; + } + + public void setFollowing(String following) { + this.following = following; + } + + public String getFollowers() { + return followers; + } + + public void setFollowers(String followers) { + this.followers = followers; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) + public Key getPublicKey() { + return publicKey; + } + + public void setPublicKey(Key publicKey) { + this.publicKey = publicKey; + } + + public String getPreferredUsername() { + return preferredUsername; + } + + public void setPreferredUsername(String preferredUsername) { + this.preferredUsername = preferredUsername; + } +} diff --git a/src/main/java/com/juick/www/api/apple/AppSiteAssociation.java b/src/main/java/com/juick/www/api/apple/AppSiteAssociation.java new file mode 100644 index 00000000..c4afb9fb --- /dev/null +++ b/src/main/java/com/juick/www/api/apple/AppSiteAssociation.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.apple; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; + +@RestController +public class AppSiteAssociation { + @Value("${ios_app_id:}") + private String appId; + + @GetMapping("/.well-known/apple-app-site-association") + @ResponseBody + public SiteAssociations appSiteAssociations() { + WebCredentials webCredentials = new WebCredentials(); + webCredentials.setApps(Collections.singletonList(appId)); + SiteAssociations siteAssociations = new SiteAssociations(); + siteAssociations.setWebcredentials(webCredentials); + return siteAssociations; + } + + private class SiteAssociations { + private WebCredentials webcredentials; + + public WebCredentials getWebcredentials() { + return webcredentials; + } + + public void setWebcredentials(WebCredentials webcredentials) { + this.webcredentials = webcredentials; + } + } + + private class WebCredentials { + private List apps; + + public List getApps() { + return apps; + } + + public void setApps(List apps) { + this.apps = apps; + } + } +} diff --git a/src/main/java/com/juick/www/api/hostmeta/HostMeta.java b/src/main/java/com/juick/www/api/hostmeta/HostMeta.java new file mode 100644 index 00000000..5c500d6d --- /dev/null +++ b/src/main/java/com/juick/www/api/hostmeta/HostMeta.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.hostmeta; + +import com.cliqset.xrd.Link; +import com.cliqset.xrd.XRD; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; + +import static com.cliqset.xrd.XRDConstants.XRD_MEDIA_TYPE; + +@RestController +public class HostMeta { + @Value("${ap_base_uri:http://localhost:8080/}") + private String baseUri; + @GetMapping(value = "/.well-known/host-meta", produces = XRD_MEDIA_TYPE) + public XRD hostMetaResponse() { + Link webfinger = new Link(); + webfinger.setTemplate(String.format("%swebfinger?resource={uri}", baseUri)); + XRD xrd = new XRD(); + xrd.setLinks(Collections.singletonList(webfinger)); + return xrd; + } +} diff --git a/src/main/java/com/juick/www/api/webfinger/Resource.java b/src/main/java/com/juick/www/api/webfinger/Resource.java new file mode 100644 index 00000000..1529ea09 --- /dev/null +++ b/src/main/java/com/juick/www/api/webfinger/Resource.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.webfinger; + +import com.juick.model.User; +import com.juick.www.api.webfinger.model.Account; +import com.juick.www.api.webfinger.model.Link; +import com.juick.server.util.HttpNotFoundException; +import com.juick.service.UserService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; +import rocks.xmpp.addr.Jid; + +import javax.inject.Inject; +import java.util.Collections; + +import static com.juick.www.api.activity.model.Context.ACTIVITY_MEDIA_TYPE; + +@RestController +public class Resource { + @Inject + private UserService userService; + @Value("${web_domain:localhost}") + private String domain; + @Value("${ap_base_uri:http://localhost:8080/}") + private String baseUri; + + @GetMapping(value = "/.well-known/webfinger", produces = "application/jrd+json;charset=utf-8") + public Account getWebResource(@RequestParam String resource) { + if (resource.startsWith("acct:")) { + Jid account = Jid.of(resource.substring(5)); + if (account.getDomain().equals(domain)) { + User user = userService.getUserByName(account.getLocal()); + if (!user.isAnonymous()) { + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseUri); + builder.path(String.format("/u/%s", user.getName())); + Link blog = new Link(); + blog.setRel("self"); + blog.setType(ACTIVITY_MEDIA_TYPE); + blog.setHref(builder.toUriString()); + Account result = new Account(); + result.setSubject(resource); + result.setLinks(Collections.singletonList(blog)); + return result; + } + } + } + throw new HttpNotFoundException(); + } +} diff --git a/src/main/java/com/juick/www/api/webfinger/model/Account.java b/src/main/java/com/juick/www/api/webfinger/model/Account.java new file mode 100644 index 00000000..92008604 --- /dev/null +++ b/src/main/java/com/juick/www/api/webfinger/model/Account.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.webfinger.model; + +import java.util.List; + +public class Account { + private String subject; + private List links; + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public List getLinks() { + return links; + } + + public void setLinks(List links) { + this.links = links; + } +} diff --git a/src/main/java/com/juick/www/api/webfinger/model/Link.java b/src/main/java/com/juick/www/api/webfinger/model/Link.java new file mode 100644 index 00000000..437b53f7 --- /dev/null +++ b/src/main/java/com/juick/www/api/webfinger/model/Link.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.webfinger.model; + +public class Link { + private String rel; + private String type; + private String href; + + public String getRel() { + return rel; + } + + public void setRel(String rel) { + this.rel = rel; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } +} diff --git a/src/main/java/com/juick/www/api/webhooks/TelegramWebhook.java b/src/main/java/com/juick/www/api/webhooks/TelegramWebhook.java new file mode 100644 index 00000000..32218b3a --- /dev/null +++ b/src/main/java/com/juick/www/api/webhooks/TelegramWebhook.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.webhooks; + +import com.juick.server.TelegramBotManager; +import com.pengrad.telegrambot.BotUtils; +import com.pengrad.telegrambot.model.Update; +import org.apache.commons.io.IOUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.annotations.ApiIgnore; + +import javax.inject.Inject; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** + * Created by vt on 24/11/2016. + */ +@ApiIgnore +@RestController +@ConditionalOnProperty({"telegram_token"}) +public class TelegramWebhook { + private static final Logger logger = LoggerFactory.getLogger("Telegram"); + @Inject + private TelegramBotManager telegramBotManager; + + @RequestMapping(value = "/api/tlgmbtwbhk", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public void processUpdate(InputStream body) throws Exception { + String data = IOUtils.toString(body, StandardCharsets.UTF_8); + logger.debug("Telegram update: {}", StringEscapeUtils.unescapeJava(data)); + Update update = BotUtils.parseUpdate(data); + telegramBotManager.processUpdate(update); + } +} diff --git a/src/main/java/com/juick/www/api/xnodeinfo2/Info.java b/src/main/java/com/juick/www/api/xnodeinfo2/Info.java new file mode 100644 index 00000000..36be1a16 --- /dev/null +++ b/src/main/java/com/juick/www/api/xnodeinfo2/Info.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.xnodeinfo2; + +import com.cliqset.xrd.Link; +import com.cliqset.xrd.XRD; +import com.fasterxml.jackson.annotation.JsonView; +import com.juick.www.api.xnodeinfo2.model.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.inject.Inject; +import java.net.URI; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class Info { + @Value("${ap_base_uri:http://localhost:8080/}") + private String baseUri; + @Inject + private JdbcTemplate jdbcTemplate; + + private NodeInfo getCurrentNodeInfo(String version) { + NodeInfo nodeInfo = new NodeInfo(); + nodeInfo.setVersion(version); + Server server = new Server(); + server.setBaseUrl(baseUri); + server.setName("Juick"); + server.setSoftware("Juick"); + server.setVersion("2.x"); + nodeInfo.setServer(server); + nodeInfo.setProtocols(Arrays.asList("xmpp", "activitypub", "smtp")); + Map metadata = new HashMap<>(); + metadata.put("email", "support@juick.com"); + nodeInfo.setMetadata(metadata); + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.setInbound(Arrays.asList("jabber", "mastodon", "email", "telegram")); + serviceInfo.setOutbound(Arrays.asList("jabber", "mastodon", "telegram", "twitter", "email", "rss")); + nodeInfo.setServices(serviceInfo); + UserStats userStats = new UserStats(); + userStats.setTotal(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users WHERE banned=0", Integer.class)); + userStats.setActiveMonth(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users WHERE banned=0 AND last_seen > ?", + Integer.class, ZonedDateTime.now().minus(1, ChronoUnit.MONTHS).toLocalDateTime())); + userStats.setActiveHalfyear(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users WHERE banned=0 AND last_seen > ?", + Integer.class, ZonedDateTime.now().minus(6, ChronoUnit.MONTHS).toLocalDateTime())); + Usage usage = new Usage(); + usage.setUsers(userStats); + usage.setLocalPosts(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM messages", + Integer.class)); + usage.setLocalComments(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM replies", + Integer.class)); + nodeInfo.setUsage(usage); + return nodeInfo; + } + + @GetMapping(value = "/.well-known/x-nodeinfo2", produces = MediaType.APPLICATION_JSON_VALUE) + @JsonView(NodeInfo.XNodeInfoView.class) + public NodeInfo showXNodeInfo() { + return getCurrentNodeInfo("1.0"); + } + + @GetMapping(value = "/.well-known/nodeinfo", produces = MediaType.APPLICATION_JSON_VALUE) + public XRD getNodeInfoLinks() { + Link nodeinfo = new Link(); + nodeinfo.setRel(URI.create("http://nodeinfo.diaspora.software/ns/schema/2.0")); + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri); + uriComponentsBuilder.replacePath("/api/nodeinfo/2.0"); + nodeinfo.setHref(uriComponentsBuilder.build().toUri()); + XRD xrd = new XRD(); + xrd.setLinks(Collections.singletonList(nodeinfo)); + return xrd; + } + + @GetMapping(value = "/api/nodeinfo/2.0", produces = MediaType.APPLICATION_JSON_VALUE) + @JsonView(NodeInfo.NodeInfoView.class) + public NodeInfo showNodeInfo() { + return getCurrentNodeInfo("2.0"); + } +} diff --git a/src/main/java/com/juick/www/api/xnodeinfo2/model/NodeInfo.java b/src/main/java/com/juick/www/api/xnodeinfo2/model/NodeInfo.java new file mode 100644 index 00000000..04526961 --- /dev/null +++ b/src/main/java/com/juick/www/api/xnodeinfo2/model/NodeInfo.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.xnodeinfo2.model; + +import com.fasterxml.jackson.annotation.JsonView; + +import java.util.List; +import java.util.Map; + +public class NodeInfo { + + private String version; + + private Server server; + + private List protocols; + + private ServiceInfo services; + + private Map metadata; + + @JsonView({XNodeInfoView.class, NodeInfoView.class}) + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @JsonView(XNodeInfoView.class) + public Server getServer() { + return server; + } + @JsonView(NodeInfoView.class) + public Server getSoftware() { + return server; + } + + public void setServer(Server server) { + this.server = server; + } + + @JsonView({XNodeInfoView.class, NodeInfoView.class}) + public List getProtocols() { + return protocols; + } + + public void setProtocols(List protocols) { + this.protocols = protocols; + } + + @JsonView({XNodeInfoView.class, NodeInfoView.class}) + public ServiceInfo getServices() { + return services; + } + + public void setServices(ServiceInfo services) { + this.services = services; + } + + @JsonView({XNodeInfoView.class, NodeInfoView.class}) + public boolean getOpenRegistrations() { + return true; + } + + private Usage usage; + + @JsonView({XNodeInfoView.class, NodeInfoView.class}) + public Usage getUsage() { + return usage; + } + + public void setUsage(Usage usage) { + this.usage = usage; + } + + @JsonView(NodeInfoView.class) + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public interface NodeInfoView {} + public interface XNodeInfoView {} +} diff --git a/src/main/java/com/juick/www/api/xnodeinfo2/model/Server.java b/src/main/java/com/juick/www/api/xnodeinfo2/model/Server.java new file mode 100644 index 00000000..a03695f6 --- /dev/null +++ b/src/main/java/com/juick/www/api/xnodeinfo2/model/Server.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.xnodeinfo2.model; + +import com.fasterxml.jackson.annotation.JsonView; + +public class Server { + private String baseUrl; + private String name; + private String software; + private String version; + + @JsonView(NodeInfo.XNodeInfoView.class) + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + @JsonView({NodeInfo.NodeInfoView.class, NodeInfo.XNodeInfoView.class}) + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @JsonView(NodeInfo.XNodeInfoView.class) + public String getSoftware() { + return software; + } + + public void setSoftware(String software) { + this.software = software; + } + + @JsonView({NodeInfo.NodeInfoView.class, NodeInfo.XNodeInfoView.class}) + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } +} diff --git a/src/main/java/com/juick/www/api/xnodeinfo2/model/ServiceInfo.java b/src/main/java/com/juick/www/api/xnodeinfo2/model/ServiceInfo.java new file mode 100644 index 00000000..a9482731 --- /dev/null +++ b/src/main/java/com/juick/www/api/xnodeinfo2/model/ServiceInfo.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.xnodeinfo2.model; + +import com.fasterxml.jackson.annotation.JsonView; + +import java.util.List; + +@JsonView({NodeInfo.NodeInfoView.class, NodeInfo.XNodeInfoView.class}) +public class ServiceInfo { + private List inbound; + private List outbound; + + public List getInbound() { + return inbound; + } + + public void setInbound(List inbound) { + this.inbound = inbound; + } + + public List getOutbound() { + return outbound; + } + + public void setOutbound(List outbound) { + this.outbound = outbound; + } +} diff --git a/src/main/java/com/juick/www/api/xnodeinfo2/model/Usage.java b/src/main/java/com/juick/www/api/xnodeinfo2/model/Usage.java new file mode 100644 index 00000000..436b2565 --- /dev/null +++ b/src/main/java/com/juick/www/api/xnodeinfo2/model/Usage.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.xnodeinfo2.model; + +import com.fasterxml.jackson.annotation.JsonView; + +@JsonView({NodeInfo.NodeInfoView.class, NodeInfo.XNodeInfoView.class}) +public class Usage { + private UserStats users; + private int localPosts; + private int localComments; + + public UserStats getUsers() { + return users; + } + + public void setUsers(UserStats users) { + this.users = users; + } + + public int getLocalPosts() { + return localPosts; + } + + public void setLocalPosts(int localPosts) { + this.localPosts = localPosts; + } + + public int getLocalComments() { + return localComments; + } + + public void setLocalComments(int localComments) { + this.localComments = localComments; + } +} diff --git a/src/main/java/com/juick/www/api/xnodeinfo2/model/UserStats.java b/src/main/java/com/juick/www/api/xnodeinfo2/model/UserStats.java new file mode 100644 index 00000000..15c42fc9 --- /dev/null +++ b/src/main/java/com/juick/www/api/xnodeinfo2/model/UserStats.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.xnodeinfo2.model; + +import com.fasterxml.jackson.annotation.JsonView; + +@JsonView({NodeInfo.NodeInfoView.class, NodeInfo.XNodeInfoView.class}) +public class UserStats { + private int total; + private int activeHalfyear; + private int activeMonth; + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public int getActiveHalfyear() { + return activeHalfyear; + } + + public void setActiveHalfyear(int activeHalfyear) { + this.activeHalfyear = activeHalfyear; + } + + public int getActiveMonth() { + return activeMonth; + } + + public void setActiveMonth(int activeMonth) { + this.activeMonth = activeMonth; + } +} diff --git a/src/main/java/com/juick/www/controllers/Help.java b/src/main/java/com/juick/www/controllers/Help.java index d2796def..7fc84060 100644 --- a/src/main/java/com/juick/www/controllers/Help.java +++ b/src/main/java/com/juick/www/controllers/Help.java @@ -19,10 +19,9 @@ package com.juick.www.controllers; import com.juick.model.User; import com.juick.server.util.HttpNotFoundException; -import com.juick.www.HelpService; -import com.juick.www.WebApp; -import com.juick.service.MessagesService; +import com.juick.service.HelpService; import com.juick.service.security.annotation.Visitor; +import com.juick.www.WebApp; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.springframework.stereotype.Controller; @@ -42,8 +41,6 @@ public class Help { @Inject private HelpService helpService; @Inject - private MessagesService messagesService; - @Inject private Parser cmParser; @Inject private HtmlRenderer helpRenderer; diff --git a/src/main/java/com/juick/www/controllers/MessagesWWW.java b/src/main/java/com/juick/www/controllers/MessagesWWW.java deleted file mode 100644 index 4b0bb17f..00000000 --- a/src/main/java/com/juick/www/controllers/MessagesWWW.java +++ /dev/null @@ -1,590 +0,0 @@ -/* - * Copyright (C) 2008-2019, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.juick.www.controllers; - -import com.juick.model.Message; -import com.juick.model.Tag; -import com.juick.model.User; -import com.juick.formatters.PlainTextFormatter; -import com.juick.server.Utils; -import com.juick.server.util.HttpForbiddenException; -import com.juick.server.util.HttpNotFoundException; -import com.juick.server.util.WebUtils; -import com.juick.www.WebApp; -import com.juick.service.*; -import com.juick.service.security.annotation.Visitor; -import com.juick.util.MessageUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.text.StringEscapeUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.*; - -import javax.inject.Inject; -import javax.servlet.http.HttpServletRequest; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -/** - * - * @author Ugnich Anton - */ -@Controller -public class MessagesWWW { - @Inject - private UserService userService; - @Inject - private TagService tagService; - @Inject - private MessagesService messagesService; - @Inject - private PMQueriesService pmQueriesService; - @Inject - private CrosspostService crosspostService; - @Inject - private WebApp webApp; - - private void fillUserModel(ModelMap model, User user, User visitor) { - user.setAvatar(webApp.getAvatarWebPath(user)); - model.addAttribute("user", user); - model.addAttribute("isSubscribed", userService.isSubscribed(visitor.getUid(), user.getUid())); - model.addAttribute("isInBL", userService.isInBL(visitor.getUid(), user.getUid())); - model.addAttribute("isInBLAny", userService.isInBLAny(user.getUid(), visitor.getUid())); - model.addAttribute("statsIRead", userService.getUserFriends(user.getUid()).size()); - model.addAttribute("statsMyReaders", userService.getStatsMyReaders(user.getUid())); - model.addAttribute("statsMyBL", userService.getUserBLUsers(user.getUid()).size()); - model.addAttribute("statsMessages", userService.getStatsMessages(user.getUid())); - model.addAttribute("statsReplies", userService.getStatsReplies(user.getUid())); - model.addAttribute("iread", userService.getUserReadLeastPopular(user.getUid(), 8)); - model.addAttribute("tagStats", tagService.getUserTagStats(user.getUid()) - .stream().sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).limit(20).map(t -> t.getTag().getName()).collect(Collectors.toList())); - } - - @GetMapping("/") - protected String doGet( - @Visitor User visitor, - @RequestParam(required = false) String tag, - @RequestParam(name = "show", required = false) String paramShow, - @RequestParam(name = "search", required = false) String paramSearch, - @RequestParam(name = "before", required = false, defaultValue = "0") Integer paramBefore, - @RequestParam(name = "to", required = false, defaultValue = "0") Long paramTo, - @RequestParam(name = "page", required = false, defaultValue = "0") Integer page, - ModelMap model) { - if (tag != null) { - return "redirect:/tag/" + URLEncoder.encode(tag, StandardCharsets.UTF_8); - } - visitor.setAvatar(webApp.getAvatarWebPath(visitor)); - - if (paramSearch != null && paramSearch.length() > 64) { - paramSearch = null; - } - - model.addAttribute("discover", false); - - String title; - List mids; - - if (paramSearch != null) { - title = "Поиск: " + StringEscapeUtils.escapeHtml4(paramSearch); - mids = messagesService.getSearch(visitor, Utils.encodeSphinx(paramSearch), page); - } else if (paramShow == null) { - title = "Обсуждения"; - mids = messagesService.getDiscussions(visitor.getUid(), paramTo); - } else if (paramShow.equals("top")) { - title = "Популярные"; - mids = messagesService.getPopular(visitor.getUid(), paramBefore); - model.addAttribute("discover", true); - } else if (paramShow.equals("my") && !visitor.isAnonymous()) { - title = "Моя лента"; - mids = messagesService.getMyFeed(visitor.getUid(), paramBefore, true); - } else if (paramShow.equals("private") && !visitor.isAnonymous()) { - title = "Приватные"; - mids = messagesService.getPrivate(visitor.getUid(), paramBefore); - } else if (paramShow.equals("discuss")) { - return "redirect:/"; - } else if (paramShow.equals("recommended") && !visitor.isAnonymous()) { - title = "Рекомендации"; - mids = messagesService.getRecommended(visitor.getUid(), paramBefore); - } else if (paramShow.equals("photos")) { - title = "Фотографии"; - mids = messagesService.getPhotos(visitor.getUid(), paramBefore); - model.addAttribute("discover", true); - } else if (paramShow.equals("all")) { - title = "Все сообщения"; - mids = messagesService.getAll(visitor.getUid(), paramBefore); - model.addAttribute("discover", true); - } else { - throw new HttpNotFoundException(); - } - - String head = "\n"; - - if (paramBefore > 0 || paramShow != null) { - head = ""; - } - model.addAttribute("title", title); - model.addAttribute("headers", head); - model.addAttribute("visitor", visitor); - model.addAttribute("noindex", !(paramShow == null && paramBefore == 0)); - List msgs = messagesService.getMessages(visitor, mids); - msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); - if (!visitor.isAnonymous()) { - fillUserModel(model, visitor, visitor); - List unread = messagesService.getUnread(visitor); - visitor.setUnreadCount(unread.size()); - List blUIDs = userService.checkBL(visitor.getUid(), - msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList())); - msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); - } - model.addAttribute("msgs", msgs); - model.addAttribute("tags", tagService.getPopularTags()); - model.addAttribute("headers", head); - if (mids.size() >= 20) { - String nextpage = paramSearch != null ? String.format("?page=%d", page + 1) - : (paramShow == null) ? "?to=" + msgs.get(msgs.size() - 1).getUpdated().toEpochMilli() - : "?before=" + mids.get(mids.size() - 1); - if (paramShow != null) { - nextpage += "&show=" + paramShow; - } - if (paramSearch != null) { - nextpage += "&search=" + URLEncoder.encode(paramSearch, StandardCharsets.UTF_8); - } - model.addAttribute("nextpage", nextpage); - } - return "views/index"; - } - - @GetMapping(path = "/{uname}/", headers = "Connection!=Upgrade") - protected String doGetBlog( - @Visitor User visitor, - @RequestParam(required = false, name = "show") String paramShow, - @RequestParam(required = false, name = "tag") String paramTagStr, - @RequestParam(required = false, name = "search") String paramSearch, - @RequestParam(required = false, name = "page", defaultValue = "0") Integer page, - @PathVariable String uname, - @RequestParam(required = false, defaultValue = "0") Integer before, - @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie, - ModelMap model) { - User user = userService.getUserByName(uname); - if (user.isBanned() || user.isAnonymous()) { - throw new HttpNotFoundException(); - } - visitor.setAvatar(webApp.getAvatarWebPath(visitor)); - - List mids; - - Tag paramTag = null; - if (paramTagStr != null) { - if (paramTagStr.length() < 64) { - paramTag = tagService.getTag(paramTagStr, false); - } - if (paramTag == null) { - throw new HttpNotFoundException(); - } else if (!paramTag.getName().equals(paramTagStr)) { - String url = user.getName() + "/?tag=" + URLEncoder.encode(paramTag.getName(), StandardCharsets.UTF_8); - return "redirect:/" + url; - } - } - if (paramSearch != null && paramSearch.length() > 64) { - paramSearch = null; - } - - int privacy = 0; - if (!visitor.isAnonymous()) { - if (user.getUid() == visitor.getUid() || visitor.getUid() == 1) { - privacy = -3; - } else if (userService.isInWL(user.getUid(), visitor.getUid())) { - privacy = -2; - } - } - - String title; - if (paramShow == null) { - if (paramTag != null) { - title = "Блог " + user.getName() + ": *" + StringEscapeUtils.escapeHtml4(paramTag.getName()); - mids = messagesService.getUserTag(user.getUid(), paramTag.TID, privacy, before); - } else if (paramSearch != null) { - title = "Блог " + user.getName() + ": " + StringEscapeUtils.escapeHtml4(paramSearch); - mids = messagesService.getUserSearch(visitor, user.getUid(), Utils.encodeSphinx(paramSearch), privacy, page); - } else { - title = "Блог " + user.getName(); - mids = messagesService.getUserBlog(user.getUid(), privacy, before); - } - } else if (paramShow.equals("recomm")) { - title = "Рекомендации " + user.getName(); - mids = messagesService.getUserRecommendations(user.getUid(), before); - } else if (paramShow.equals("photos")) { - title = "Фотографии " + user.getName(); - mids = messagesService.getUserPhotos(user.getUid(), privacy, before); - } else { - throw new HttpNotFoundException(); - } - - String head = ""; - head += "\n"; - if (paramTag != null && tagService.getTagNoIndex(paramTag.TID)) { - head += ""; - } else if (before > 0 || paramShow != null) { - head += ""; - } - model.addAttribute("pageUrl", "http://juick.com/" + user.getName()); - model.addAttribute("title", title); - model.addAttribute("headers", head); - model.addAttribute("visitor", visitor); - model.addAttribute("noindex", paramShow == null && before == 0); - fillUserModel(model, user, visitor); - model.addAttribute("paramTag", paramTag); - List msgs = messagesService.getMessages(visitor, mids); - msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); - if (!visitor.isAnonymous()) { - List unread = messagesService.getUnread(visitor); - visitor.setUnreadCount(unread.size()); - List blUIDs = userService.checkBL(visitor.getUid(), - msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList())); - msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); - } - model.addAttribute("msgs", msgs); - model.addAttribute("headers", head); - if (mids.size() >= 20) { - String nextpage = paramSearch != null ? String.format("?page=%d", page + 1) : "?before=" + mids.get(mids.size() - 1); - if (paramShow != null) { - nextpage += "&show=" + paramShow; - } - if (paramSearch != null) { - nextpage += "&search=" + URLEncoder.encode(paramSearch, StandardCharsets.UTF_8); - } - if (paramTag != null) { - nextpage += "&tag=" + URLEncoder.encode(paramTag.getName(), StandardCharsets.UTF_8); - } - model.addAttribute("nextpage", nextpage); - } - return "views/blog"; - } - - @GetMapping("/{uname}/tags") - protected String doGetTags( - @Visitor User visitor, - @PathVariable String uname, ModelMap model) { - User user = userService.getUserByName(uname); - if (visitor.isBanned()) { - throw new HttpNotFoundException(); - } - visitor.setAvatar(webApp.getAvatarWebPath(visitor)); - - model.addAttribute("title", "Теги " + user.getName()); - model.addAttribute("headers", ""); - model.addAttribute("visitor", visitor); - fillUserModel(model, user, visitor); - model.addAttribute("tags", tagService.getUserTagStats(user.getUid()).stream() - .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).map(t -> t.getTag().getName()).collect(Collectors.toList())); - - return "views/blog_tags"; - } - - @GetMapping("/{uname}/friends") - protected String doGetFriends( - @Visitor User visitor, - @PathVariable String uname, ModelMap model) { - User user = userService.getUserByName(uname); - if (visitor.isBanned()) { - throw new HttpNotFoundException(); - } - visitor.setAvatar(webApp.getAvatarWebPath(visitor)); - model.addAttribute("title", "Подписки " + user.getName()); - model.addAttribute("headers", ""); - model.addAttribute("visitor", visitor); - fillUserModel(model, user, visitor); - model.addAttribute("users", userService.getUserFriends(user.getUid())); - - return "views/users"; - } - - @GetMapping("/{uname}/readers") - protected String doGetReaders( - @Visitor User visitor, - @PathVariable String uname, ModelMap model) { - User user = userService.getUserByName(uname); - visitor.setAvatar(webApp.getAvatarWebPath(visitor)); - model.addAttribute("title", "Читатели " + user.getName()); - model.addAttribute("headers", ""); - model.addAttribute("visitor", visitor); - fillUserModel(model, user, visitor); - model.addAttribute("users", userService.getUserReaders(user.getUid())); - - return "views/users"; - } - - @GetMapping("/{uname}/bl") - protected String doGetBL( - @Visitor User visitor, - @PathVariable String uname, ModelMap model) { - User user = userService.getUserByName(uname); - if (visitor.getUid() != user.getUid()) { - throw new HttpForbiddenException(); - } - visitor.setAvatar(webApp.getAvatarWebPath(visitor)); - model.addAttribute("title", "Черный список " + user.getName()); - model.addAttribute("headers", ""); - model.addAttribute("visitor", visitor); - fillUserModel(model, user, visitor); - model.addAttribute("users", userService.getUserBLUsers(user.getUid())); - - return "views/users"; - } - @GetMapping("/tag/{tagName}") - protected String tagAction( - @Visitor User visitor, - HttpServletRequest request, - @PathVariable String tagName, - @RequestParam(required = false, defaultValue = "0") int before, - ModelMap model) { - visitor.setAvatar(webApp.getAvatarWebPath(visitor)); - String paramTagStr = StringEscapeUtils.unescapeHtml4(tagName); - Tag paramTag = tagService.getTag(paramTagStr, false); - if (paramTag == null) { - throw new HttpNotFoundException(); - } else if (paramTag.SynonymID > 0 && paramTag.TID != paramTag.SynonymID) { - Tag synTag = tagService.getTag(paramTag.SynonymID); - String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(synTag.getName()), StandardCharsets.UTF_8); - if (request.getQueryString() != null) { - url += "?" + request.getQueryString(); - } - return "redirect:" + url; - } else if (!paramTag.getName().equals(paramTagStr)) { - String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(paramTag.getName()), StandardCharsets.UTF_8); - if (request.getQueryString() != null) { - url += "?" + request.getQueryString(); - } - return "redirect:" + url; - } - - String title = "*" + StringEscapeUtils.escapeHtml4(paramTag.getName()); - model.addAttribute("title", title); - List mids = messagesService.getTag(paramTag.TID, visitor.getUid(), before, (visitor.isAnonymous()) ? 40 : 20); - List msgs = messagesService.getMessages(visitor, mids); - msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); - if (!visitor.isAnonymous()) { - List unread = messagesService.getUnread(visitor); - visitor.setUnreadCount(unread.size()); - List blUIDs = userService.checkBL( - visitor.getUid(), - msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList()) - ); - msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); - fillUserModel(model, visitor, visitor); - } - - String head = StringUtils.EMPTY; - if (tagService.getTagNoIndex(paramTag.TID)) { - head = ""; - } else if (before > 0 || mids.size() < 5) { - head = ""; - } - model.addAttribute("headers", head); - model.addAttribute("visitor", visitor); - model.addAttribute("tag", paramTag); - model.addAttribute("title", title); - model.addAttribute("msgs", msgs); - model.addAttribute("tags", tagService.getPopularTags()); - model.addAttribute("noindex", before > 0); - model.addAttribute("isSubscribed", tagService.isSubscribed(visitor, paramTag)); - model.addAttribute("isInBL", tagService.isInBL(visitor, paramTag)); - if (mids.size() >= 20) { - String nextpage = "/tag/" + URLEncoder.encode(paramTag.getName(), StandardCharsets.UTF_8) + "?before=" + mids.get(mids.size() - 1); - model.addAttribute("nextpage", nextpage); - } - return "views/index"; - } - @GetMapping("/pm/inbox") - protected String doGetInbox(@Visitor User visitor, ModelMap model) { - if (visitor.isAnonymous()) { - return "redirect:/login"; - } - visitor.setAvatar(webApp.getAvatarWebPath(visitor)); - String title = "PM: Inbox"; - List msgs = pmQueriesService.getLastPMInbox(visitor.getUid()); - msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); - fillUserModel(model, visitor, visitor); - model.addAttribute("title", title); - model.addAttribute("visitor", visitor); - model.addAttribute("msgs", msgs); - model.addAttribute("tags", tagService.getPopularTags()); - return "views/pm_inbox"; - } - - @GetMapping("/pm/sent") - protected String doGetSent( - @Visitor User visitor, - @RequestParam(required = false) String uname, - ModelMap model) { - if (visitor.isAnonymous()) { - return "redirect:/login"; - } - visitor.setAvatar(webApp.getAvatarWebPath(visitor)); - String title = "PM: Sent"; - List msgs = pmQueriesService.getLastPMSent(visitor.getUid()); - msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); - if (WebUtils.isNotUserName(uname)) { - uname = StringUtils.EMPTY; - } - fillUserModel(model, visitor, visitor); - model.addAttribute("title", title); - model.addAttribute("visitor", visitor); - model.addAttribute("msgs", msgs); - model.addAttribute("tags", tagService.getPopularTags()); - model.addAttribute("uname", uname); - return "views/pm_sent"; - } - @GetMapping(value = "/{uname}/{mid}", produces = MediaType.TEXT_HTML_VALUE) - protected String threadAction( - @Visitor User visitor, - ModelMap model, - @PathVariable String uname, - @PathVariable int mid, - @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie) { - if (!messagesService.canViewThread(mid, visitor.getUid())) { - throw new HttpForbiddenException(); - } - visitor.setAvatar(webApp.getAvatarWebPath(visitor)); - Optional message = messagesService.getMessage(mid); - - if (message.isEmpty()) { - throw new HttpNotFoundException(); - } - - Message msg = message.get(); - - User user = userService.getUserByName(uname); - if (user.isAnonymous() || !msg.getUser().equals(user)) { - return String.format("redirect:/%s/%d", msg.getUser().getName(), mid); - } - msg.VisitorCanComment = !visitor.isAnonymous(); - msg.getUser().setAvatar(webApp.getAvatarWebPath(msg.getUser())); - List replies = messagesService.getReplies(visitor, msg.getMid()); - // this should be after getReplies to mark thread as read - fillUserModel(model, user, visitor); - if (!visitor.isAnonymous()) { - List unread = messagesService.getUnread(visitor); - visitor.setUnreadCount(unread.size()); - boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid(); - boolean isInBL = userService.isInBLAny(msg.getUser().getUid(), visitor.getUid()); - msg.VisitorCanComment = isMsgAuthor || !(msg.ReadOnly || isInBL); - } - model.addAttribute("msg", msg); - - String title = msg.getUser().getName() + ": " + MessageUtils.getTagsString(msg); - - model.addAttribute("title", title); - model.addAttribute("visitor", visitor); - String headers = ""; - String pageUrl = "https://juick.com/" + msg.getUser().getName() + "/" + msg.getMid(); - if (msg.Hidden) { - headers += ""; - } - String cardType = StringUtils.isNotEmpty(msg.getAttachmentType()) ? "summary_large_image" : "summary"; - if (StringUtils.isNotEmpty(msg.getAttachmentType())) { - // additional check in case of broken images - if (msg.getAttachment() != null) { - String msgImage = msg.getAttachment().getMedium().getUrl(); - headers += ""; - } - } else { - String msgImage = webApp.getAvatarWebPath(msg.getUser()); - headers += ""; - } - model.addAttribute("ogtype", "article"); - String cardDescription = StringEscapeUtils.escapeHtml4(PlainTextFormatter.formatTwitterCard(msg)); - headers += "\n" + - "\n" + - "\n" + - "\n" + - "\n" + - "\n"; - String twitterName = crosspostService.getTwitterName(msg.getUser().getUid()); - if (StringUtils.isNotEmpty(twitterName)) { - headers += "\n"; - } - if (msg.getTags().size() > 0) { - headers += "\n"; - } - model.addAttribute("headers", headers); - model.addAttribute("visitorSubscribed", messagesService.isSubscribed(visitor.getUid(), msg.getMid())); - model.addAttribute("visitorInBL", userService.isInBL(msg.getUser().getUid(), visitor.getUid())); - model.addAttribute("recomm", messagesService.getMessagesRecommendations( - Collections.singletonList(msg.getMid())).stream() - .map(Pair::getRight).collect(Collectors.toList())); - List blUIDs = new ArrayList<>(); - for (Message reply : replies) { - if (reply.getUser().getUid() != msg.getUser().getUid() - && !blUIDs.contains(reply.getUser().getUid())) { - blUIDs.add(reply.getUser().getUid()); - } - reply.VisitorCanComment = !visitor.isAnonymous(); - reply.getUser().setAvatar(webApp.getAvatarWebPath(reply.getUser())); - if (!visitor.isAnonymous()) { - boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid(); - boolean isReplyAuthor = visitor.getUid() == reply.getUser().getUid(); - reply.VisitorCanComment = isMsgAuthor || (!msg.ReadOnly - && msg.VisitorCanComment && (isReplyAuthor || !userService.isInBLAny(visitor.getUid(), reply.getUser().getUid()))); - } - } - model.addAttribute("replies", replies); - return "views/thread"; - } - - @GetMapping("/post") - protected String postAction( - @Visitor User visitor, - @RequestParam(required = false) String body, ModelMap model) { - fillUserModel(model, visitor, visitor); - visitor.setAvatar(webApp.getAvatarWebPath(visitor)); - model.addAttribute("title", "Написать"); - model.addAttribute("headers", ""); - model.addAttribute("visitor", visitor); - if (body == null) { - body = StringUtils.EMPTY; - } else { - if (body.length() > 4096) { - body = body.substring(0, 4096); - } - body = StringEscapeUtils.escapeHtml4(body); - } - model.addAttribute("body", body); - model.addAttribute("visitor", visitor); - model.addAttribute("user", visitor); - model.addAttribute("tags", tagService.getUserTagStats(visitor.getUid()).stream() - .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).map(t -> t.getTag().getName()).collect(Collectors.toList())); - return "views/post"; - } - - // when message id is not fit to int - @ExceptionHandler(NumberFormatException.class) - public ResponseEntity notFoundAction() { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } -} diff --git a/src/main/java/com/juick/www/controllers/Site.java b/src/main/java/com/juick/www/controllers/Site.java new file mode 100644 index 00000000..61b84f71 --- /dev/null +++ b/src/main/java/com/juick/www/controllers/Site.java @@ -0,0 +1,590 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.www.controllers; + +import com.juick.model.Message; +import com.juick.model.Tag; +import com.juick.model.User; +import com.juick.formatters.PlainTextFormatter; +import com.juick.server.Utils; +import com.juick.server.util.HttpForbiddenException; +import com.juick.server.util.HttpNotFoundException; +import com.juick.server.util.WebUtils; +import com.juick.www.WebApp; +import com.juick.service.*; +import com.juick.service.security.annotation.Visitor; +import com.juick.util.MessageUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.text.StringEscapeUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.*; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * + * @author Ugnich Anton + */ +@Controller +public class Site { + @Inject + private UserService userService; + @Inject + private TagService tagService; + @Inject + private MessagesService messagesService; + @Inject + private PMQueriesService pmQueriesService; + @Inject + private CrosspostService crosspostService; + @Inject + private WebApp webApp; + + private void fillUserModel(ModelMap model, User user, User visitor) { + user.setAvatar(webApp.getAvatarWebPath(user)); + model.addAttribute("user", user); + model.addAttribute("isSubscribed", userService.isSubscribed(visitor.getUid(), user.getUid())); + model.addAttribute("isInBL", userService.isInBL(visitor.getUid(), user.getUid())); + model.addAttribute("isInBLAny", userService.isInBLAny(user.getUid(), visitor.getUid())); + model.addAttribute("statsIRead", userService.getUserFriends(user.getUid()).size()); + model.addAttribute("statsMyReaders", userService.getStatsMyReaders(user.getUid())); + model.addAttribute("statsMyBL", userService.getUserBLUsers(user.getUid()).size()); + model.addAttribute("statsMessages", userService.getStatsMessages(user.getUid())); + model.addAttribute("statsReplies", userService.getStatsReplies(user.getUid())); + model.addAttribute("iread", userService.getUserReadLeastPopular(user.getUid(), 8)); + model.addAttribute("tagStats", tagService.getUserTagStats(user.getUid()) + .stream().sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).limit(20).map(t -> t.getTag().getName()).collect(Collectors.toList())); + } + + @GetMapping("/") + protected String doGet( + @Visitor User visitor, + @RequestParam(required = false) String tag, + @RequestParam(name = "show", required = false) String paramShow, + @RequestParam(name = "search", required = false) String paramSearch, + @RequestParam(name = "before", required = false, defaultValue = "0") Integer paramBefore, + @RequestParam(name = "to", required = false, defaultValue = "0") Long paramTo, + @RequestParam(name = "page", required = false, defaultValue = "0") Integer page, + ModelMap model) { + if (tag != null) { + return "redirect:/tag/" + URLEncoder.encode(tag, StandardCharsets.UTF_8); + } + visitor.setAvatar(webApp.getAvatarWebPath(visitor)); + + if (paramSearch != null && paramSearch.length() > 64) { + paramSearch = null; + } + + model.addAttribute("discover", false); + + String title; + List mids; + + if (paramSearch != null) { + title = "Поиск: " + StringEscapeUtils.escapeHtml4(paramSearch); + mids = messagesService.getSearch(visitor, Utils.encodeSphinx(paramSearch), page); + } else if (paramShow == null) { + title = "Обсуждения"; + mids = messagesService.getDiscussions(visitor.getUid(), paramTo); + } else if (paramShow.equals("top")) { + title = "Популярные"; + mids = messagesService.getPopular(visitor.getUid(), paramBefore); + model.addAttribute("discover", true); + } else if (paramShow.equals("my") && !visitor.isAnonymous()) { + title = "Моя лента"; + mids = messagesService.getMyFeed(visitor.getUid(), paramBefore, true); + } else if (paramShow.equals("private") && !visitor.isAnonymous()) { + title = "Приватные"; + mids = messagesService.getPrivate(visitor.getUid(), paramBefore); + } else if (paramShow.equals("discuss")) { + return "redirect:/"; + } else if (paramShow.equals("recommended") && !visitor.isAnonymous()) { + title = "Рекомендации"; + mids = messagesService.getRecommended(visitor.getUid(), paramBefore); + } else if (paramShow.equals("photos")) { + title = "Фотографии"; + mids = messagesService.getPhotos(visitor.getUid(), paramBefore); + model.addAttribute("discover", true); + } else if (paramShow.equals("all")) { + title = "Все сообщения"; + mids = messagesService.getAll(visitor.getUid(), paramBefore); + model.addAttribute("discover", true); + } else { + throw new HttpNotFoundException(); + } + + String head = "\n"; + + if (paramBefore > 0 || paramShow != null) { + head = ""; + } + model.addAttribute("title", title); + model.addAttribute("headers", head); + model.addAttribute("visitor", visitor); + model.addAttribute("noindex", !(paramShow == null && paramBefore == 0)); + List msgs = messagesService.getMessages(visitor, mids); + msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); + if (!visitor.isAnonymous()) { + fillUserModel(model, visitor, visitor); + List unread = messagesService.getUnread(visitor); + visitor.setUnreadCount(unread.size()); + List blUIDs = userService.checkBL(visitor.getUid(), + msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList())); + msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); + } + model.addAttribute("msgs", msgs); + model.addAttribute("tags", tagService.getPopularTags()); + model.addAttribute("headers", head); + if (mids.size() >= 20) { + String nextpage = paramSearch != null ? String.format("?page=%d", page + 1) + : (paramShow == null) ? "?to=" + msgs.get(msgs.size() - 1).getUpdated().toEpochMilli() + : "?before=" + mids.get(mids.size() - 1); + if (paramShow != null) { + nextpage += "&show=" + paramShow; + } + if (paramSearch != null) { + nextpage += "&search=" + URLEncoder.encode(paramSearch, StandardCharsets.UTF_8); + } + model.addAttribute("nextpage", nextpage); + } + return "views/index"; + } + + @GetMapping(path = "/{uname}/", headers = "Connection!=Upgrade") + protected String doGetBlog( + @Visitor User visitor, + @RequestParam(required = false, name = "show") String paramShow, + @RequestParam(required = false, name = "tag") String paramTagStr, + @RequestParam(required = false, name = "search") String paramSearch, + @RequestParam(required = false, name = "page", defaultValue = "0") Integer page, + @PathVariable String uname, + @RequestParam(required = false, defaultValue = "0") Integer before, + @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie, + ModelMap model) { + User user = userService.getUserByName(uname); + if (user.isBanned() || user.isAnonymous()) { + throw new HttpNotFoundException(); + } + visitor.setAvatar(webApp.getAvatarWebPath(visitor)); + + List mids; + + Tag paramTag = null; + if (paramTagStr != null) { + if (paramTagStr.length() < 64) { + paramTag = tagService.getTag(paramTagStr, false); + } + if (paramTag == null) { + throw new HttpNotFoundException(); + } else if (!paramTag.getName().equals(paramTagStr)) { + String url = user.getName() + "/?tag=" + URLEncoder.encode(paramTag.getName(), StandardCharsets.UTF_8); + return "redirect:/" + url; + } + } + if (paramSearch != null && paramSearch.length() > 64) { + paramSearch = null; + } + + int privacy = 0; + if (!visitor.isAnonymous()) { + if (user.getUid() == visitor.getUid() || visitor.getUid() == 1) { + privacy = -3; + } else if (userService.isInWL(user.getUid(), visitor.getUid())) { + privacy = -2; + } + } + + String title; + if (paramShow == null) { + if (paramTag != null) { + title = "Блог " + user.getName() + ": *" + StringEscapeUtils.escapeHtml4(paramTag.getName()); + mids = messagesService.getUserTag(user.getUid(), paramTag.TID, privacy, before); + } else if (paramSearch != null) { + title = "Блог " + user.getName() + ": " + StringEscapeUtils.escapeHtml4(paramSearch); + mids = messagesService.getUserSearch(visitor, user.getUid(), Utils.encodeSphinx(paramSearch), privacy, page); + } else { + title = "Блог " + user.getName(); + mids = messagesService.getUserBlog(user.getUid(), privacy, before); + } + } else if (paramShow.equals("recomm")) { + title = "Рекомендации " + user.getName(); + mids = messagesService.getUserRecommendations(user.getUid(), before); + } else if (paramShow.equals("photos")) { + title = "Фотографии " + user.getName(); + mids = messagesService.getUserPhotos(user.getUid(), privacy, before); + } else { + throw new HttpNotFoundException(); + } + + String head = ""; + head += "\n"; + if (paramTag != null && tagService.getTagNoIndex(paramTag.TID)) { + head += ""; + } else if (before > 0 || paramShow != null) { + head += ""; + } + model.addAttribute("pageUrl", "http://juick.com/" + user.getName()); + model.addAttribute("title", title); + model.addAttribute("headers", head); + model.addAttribute("visitor", visitor); + model.addAttribute("noindex", paramShow == null && before == 0); + fillUserModel(model, user, visitor); + model.addAttribute("paramTag", paramTag); + List msgs = messagesService.getMessages(visitor, mids); + msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); + if (!visitor.isAnonymous()) { + List unread = messagesService.getUnread(visitor); + visitor.setUnreadCount(unread.size()); + List blUIDs = userService.checkBL(visitor.getUid(), + msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList())); + msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); + } + model.addAttribute("msgs", msgs); + model.addAttribute("headers", head); + if (mids.size() >= 20) { + String nextpage = paramSearch != null ? String.format("?page=%d", page + 1) : "?before=" + mids.get(mids.size() - 1); + if (paramShow != null) { + nextpage += "&show=" + paramShow; + } + if (paramSearch != null) { + nextpage += "&search=" + URLEncoder.encode(paramSearch, StandardCharsets.UTF_8); + } + if (paramTag != null) { + nextpage += "&tag=" + URLEncoder.encode(paramTag.getName(), StandardCharsets.UTF_8); + } + model.addAttribute("nextpage", nextpage); + } + return "views/blog"; + } + + @GetMapping("/{uname}/tags") + protected String doGetTags( + @Visitor User visitor, + @PathVariable String uname, ModelMap model) { + User user = userService.getUserByName(uname); + if (visitor.isBanned()) { + throw new HttpNotFoundException(); + } + visitor.setAvatar(webApp.getAvatarWebPath(visitor)); + + model.addAttribute("title", "Теги " + user.getName()); + model.addAttribute("headers", ""); + model.addAttribute("visitor", visitor); + fillUserModel(model, user, visitor); + model.addAttribute("tags", tagService.getUserTagStats(user.getUid()).stream() + .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).map(t -> t.getTag().getName()).collect(Collectors.toList())); + + return "views/blog_tags"; + } + + @GetMapping("/{uname}/friends") + protected String doGetFriends( + @Visitor User visitor, + @PathVariable String uname, ModelMap model) { + User user = userService.getUserByName(uname); + if (visitor.isBanned()) { + throw new HttpNotFoundException(); + } + visitor.setAvatar(webApp.getAvatarWebPath(visitor)); + model.addAttribute("title", "Подписки " + user.getName()); + model.addAttribute("headers", ""); + model.addAttribute("visitor", visitor); + fillUserModel(model, user, visitor); + model.addAttribute("users", userService.getUserFriends(user.getUid())); + + return "views/users"; + } + + @GetMapping("/{uname}/readers") + protected String doGetReaders( + @Visitor User visitor, + @PathVariable String uname, ModelMap model) { + User user = userService.getUserByName(uname); + visitor.setAvatar(webApp.getAvatarWebPath(visitor)); + model.addAttribute("title", "Читатели " + user.getName()); + model.addAttribute("headers", ""); + model.addAttribute("visitor", visitor); + fillUserModel(model, user, visitor); + model.addAttribute("users", userService.getUserReaders(user.getUid())); + + return "views/users"; + } + + @GetMapping("/{uname}/bl") + protected String doGetBL( + @Visitor User visitor, + @PathVariable String uname, ModelMap model) { + User user = userService.getUserByName(uname); + if (visitor.getUid() != user.getUid()) { + throw new HttpForbiddenException(); + } + visitor.setAvatar(webApp.getAvatarWebPath(visitor)); + model.addAttribute("title", "Черный список " + user.getName()); + model.addAttribute("headers", ""); + model.addAttribute("visitor", visitor); + fillUserModel(model, user, visitor); + model.addAttribute("users", userService.getUserBLUsers(user.getUid())); + + return "views/users"; + } + @GetMapping("/tag/{tagName}") + protected String tagAction( + @Visitor User visitor, + HttpServletRequest request, + @PathVariable String tagName, + @RequestParam(required = false, defaultValue = "0") int before, + ModelMap model) { + visitor.setAvatar(webApp.getAvatarWebPath(visitor)); + String paramTagStr = StringEscapeUtils.unescapeHtml4(tagName); + Tag paramTag = tagService.getTag(paramTagStr, false); + if (paramTag == null) { + throw new HttpNotFoundException(); + } else if (paramTag.SynonymID > 0 && paramTag.TID != paramTag.SynonymID) { + Tag synTag = tagService.getTag(paramTag.SynonymID); + String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(synTag.getName()), StandardCharsets.UTF_8); + if (request.getQueryString() != null) { + url += "?" + request.getQueryString(); + } + return "redirect:" + url; + } else if (!paramTag.getName().equals(paramTagStr)) { + String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(paramTag.getName()), StandardCharsets.UTF_8); + if (request.getQueryString() != null) { + url += "?" + request.getQueryString(); + } + return "redirect:" + url; + } + + String title = "*" + StringEscapeUtils.escapeHtml4(paramTag.getName()); + model.addAttribute("title", title); + List mids = messagesService.getTag(paramTag.TID, visitor.getUid(), before, (visitor.isAnonymous()) ? 40 : 20); + List msgs = messagesService.getMessages(visitor, mids); + msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); + if (!visitor.isAnonymous()) { + List unread = messagesService.getUnread(visitor); + visitor.setUnreadCount(unread.size()); + List blUIDs = userService.checkBL( + visitor.getUid(), + msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList()) + ); + msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); + fillUserModel(model, visitor, visitor); + } + + String head = StringUtils.EMPTY; + if (tagService.getTagNoIndex(paramTag.TID)) { + head = ""; + } else if (before > 0 || mids.size() < 5) { + head = ""; + } + model.addAttribute("headers", head); + model.addAttribute("visitor", visitor); + model.addAttribute("tag", paramTag); + model.addAttribute("title", title); + model.addAttribute("msgs", msgs); + model.addAttribute("tags", tagService.getPopularTags()); + model.addAttribute("noindex", before > 0); + model.addAttribute("isSubscribed", tagService.isSubscribed(visitor, paramTag)); + model.addAttribute("isInBL", tagService.isInBL(visitor, paramTag)); + if (mids.size() >= 20) { + String nextpage = "/tag/" + URLEncoder.encode(paramTag.getName(), StandardCharsets.UTF_8) + "?before=" + mids.get(mids.size() - 1); + model.addAttribute("nextpage", nextpage); + } + return "views/index"; + } + @GetMapping("/pm/inbox") + protected String doGetInbox(@Visitor User visitor, ModelMap model) { + if (visitor.isAnonymous()) { + return "redirect:/login"; + } + visitor.setAvatar(webApp.getAvatarWebPath(visitor)); + String title = "PM: Inbox"; + List msgs = pmQueriesService.getLastPMInbox(visitor.getUid()); + msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); + fillUserModel(model, visitor, visitor); + model.addAttribute("title", title); + model.addAttribute("visitor", visitor); + model.addAttribute("msgs", msgs); + model.addAttribute("tags", tagService.getPopularTags()); + return "views/pm_inbox"; + } + + @GetMapping("/pm/sent") + protected String doGetSent( + @Visitor User visitor, + @RequestParam(required = false) String uname, + ModelMap model) { + if (visitor.isAnonymous()) { + return "redirect:/login"; + } + visitor.setAvatar(webApp.getAvatarWebPath(visitor)); + String title = "PM: Sent"; + List msgs = pmQueriesService.getLastPMSent(visitor.getUid()); + msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); + if (WebUtils.isNotUserName(uname)) { + uname = StringUtils.EMPTY; + } + fillUserModel(model, visitor, visitor); + model.addAttribute("title", title); + model.addAttribute("visitor", visitor); + model.addAttribute("msgs", msgs); + model.addAttribute("tags", tagService.getPopularTags()); + model.addAttribute("uname", uname); + return "views/pm_sent"; + } + @GetMapping(value = "/{uname}/{mid}", produces = MediaType.TEXT_HTML_VALUE) + protected String threadAction( + @Visitor User visitor, + ModelMap model, + @PathVariable String uname, + @PathVariable int mid, + @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie) { + if (!messagesService.canViewThread(mid, visitor.getUid())) { + throw new HttpForbiddenException(); + } + visitor.setAvatar(webApp.getAvatarWebPath(visitor)); + Optional message = messagesService.getMessage(mid); + + if (message.isEmpty()) { + throw new HttpNotFoundException(); + } + + Message msg = message.get(); + + User user = userService.getUserByName(uname); + if (user.isAnonymous() || !msg.getUser().equals(user)) { + return String.format("redirect:/%s/%d", msg.getUser().getName(), mid); + } + msg.VisitorCanComment = !visitor.isAnonymous(); + msg.getUser().setAvatar(webApp.getAvatarWebPath(msg.getUser())); + List replies = messagesService.getReplies(visitor, msg.getMid()); + // this should be after getReplies to mark thread as read + fillUserModel(model, user, visitor); + if (!visitor.isAnonymous()) { + List unread = messagesService.getUnread(visitor); + visitor.setUnreadCount(unread.size()); + boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid(); + boolean isInBL = userService.isInBLAny(msg.getUser().getUid(), visitor.getUid()); + msg.VisitorCanComment = isMsgAuthor || !(msg.ReadOnly || isInBL); + } + model.addAttribute("msg", msg); + + String title = msg.getUser().getName() + ": " + MessageUtils.getTagsString(msg); + + model.addAttribute("title", title); + model.addAttribute("visitor", visitor); + String headers = ""; + String pageUrl = "https://juick.com/" + msg.getUser().getName() + "/" + msg.getMid(); + if (msg.Hidden) { + headers += ""; + } + String cardType = StringUtils.isNotEmpty(msg.getAttachmentType()) ? "summary_large_image" : "summary"; + if (StringUtils.isNotEmpty(msg.getAttachmentType())) { + // additional check in case of broken images + if (msg.getAttachment() != null) { + String msgImage = msg.getAttachment().getMedium().getUrl(); + headers += ""; + } + } else { + String msgImage = webApp.getAvatarWebPath(msg.getUser()); + headers += ""; + } + model.addAttribute("ogtype", "article"); + String cardDescription = StringEscapeUtils.escapeHtml4(PlainTextFormatter.formatTwitterCard(msg)); + headers += "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n"; + String twitterName = crosspostService.getTwitterName(msg.getUser().getUid()); + if (StringUtils.isNotEmpty(twitterName)) { + headers += "\n"; + } + if (msg.getTags().size() > 0) { + headers += "\n"; + } + model.addAttribute("headers", headers); + model.addAttribute("visitorSubscribed", messagesService.isSubscribed(visitor.getUid(), msg.getMid())); + model.addAttribute("visitorInBL", userService.isInBL(msg.getUser().getUid(), visitor.getUid())); + model.addAttribute("recomm", messagesService.getMessagesRecommendations( + Collections.singletonList(msg.getMid())).stream() + .map(Pair::getRight).collect(Collectors.toList())); + List blUIDs = new ArrayList<>(); + for (Message reply : replies) { + if (reply.getUser().getUid() != msg.getUser().getUid() + && !blUIDs.contains(reply.getUser().getUid())) { + blUIDs.add(reply.getUser().getUid()); + } + reply.VisitorCanComment = !visitor.isAnonymous(); + reply.getUser().setAvatar(webApp.getAvatarWebPath(reply.getUser())); + if (!visitor.isAnonymous()) { + boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid(); + boolean isReplyAuthor = visitor.getUid() == reply.getUser().getUid(); + reply.VisitorCanComment = isMsgAuthor || (!msg.ReadOnly + && msg.VisitorCanComment && (isReplyAuthor || !userService.isInBLAny(visitor.getUid(), reply.getUser().getUid()))); + } + } + model.addAttribute("replies", replies); + return "views/thread"; + } + + @GetMapping("/post") + protected String postAction( + @Visitor User visitor, + @RequestParam(required = false) String body, ModelMap model) { + fillUserModel(model, visitor, visitor); + visitor.setAvatar(webApp.getAvatarWebPath(visitor)); + model.addAttribute("title", "Написать"); + model.addAttribute("headers", ""); + model.addAttribute("visitor", visitor); + if (body == null) { + body = StringUtils.EMPTY; + } else { + if (body.length() > 4096) { + body = body.substring(0, 4096); + } + body = StringEscapeUtils.escapeHtml4(body); + } + model.addAttribute("body", body); + model.addAttribute("visitor", visitor); + model.addAttribute("user", visitor); + model.addAttribute("tags", tagService.getUserTagStats(visitor.getUid()).stream() + .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).map(t -> t.getTag().getName()).collect(Collectors.toList())); + return "views/post"; + } + + // when message id is not fit to int + @ExceptionHandler(NumberFormatException.class) + public ResponseEntity notFoundAction() { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/juick/www/rss/Feeds.java b/src/main/java/com/juick/www/rss/Feeds.java new file mode 100644 index 00000000..42cadc5d --- /dev/null +++ b/src/main/java/com/juick/www/rss/Feeds.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.rss; + +import com.juick.model.User; +import com.juick.server.util.HttpNotFoundException; +import com.juick.service.MessagesService; +import com.juick.service.UserService; +import com.juick.service.security.annotation.Visitor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.ModelAndView; + +import javax.inject.Inject; +import java.util.List; + +/** + * Created by vitalyster on 13.12.2016. + */ +@Controller +public class Feeds { + + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + + @GetMapping("/rss/{userName}/blog") + public ModelAndView getBlog(@Visitor User visitor, @PathVariable String userName) { + User user = userService.getUserByName(userName); + if (!user.isAnonymous() && !user.isBanned()) { + List mids = messagesService.getUserBlog(user.getUid(), 0, 0); + ModelAndView modelAndView = new ModelAndView(); + modelAndView.setViewName("messagesView"); + modelAndView.addObject("user", user); + modelAndView.addObject("messages", messagesService.getMessages(visitor, mids)); + return modelAndView; + } + throw new HttpNotFoundException(); + } + + @GetMapping("/rss/") + public ModelAndView getLast( + @Visitor User visitor, + @RequestParam(value = "hours", required = false, defaultValue = "0") Integer hours) { + List mids = messagesService.getLastMessages(hours); + ModelAndView modelAndView = new ModelAndView(); + modelAndView.setViewName("messagesView"); + modelAndView.addObject("messages", messagesService.getMessages(visitor, mids)); + return modelAndView; + } + @GetMapping("/rss/comments") + public ModelAndView getLastReplies(@RequestParam(value = "hours", required = false, defaultValue = "0") Integer hours) { + ModelAndView modelAndView = new ModelAndView(); + modelAndView.setViewName("repliesView"); + modelAndView.addObject("messages", messagesService.getLastReplies(hours)); + return modelAndView; + } +} diff --git a/src/main/java/com/juick/www/rss/MessagesView.java b/src/main/java/com/juick/www/rss/MessagesView.java new file mode 100644 index 00000000..d9c5a6b7 --- /dev/null +++ b/src/main/java/com/juick/www/rss/MessagesView.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.rss; + +import com.juick.model.Message; +import com.juick.model.User; +import com.juick.www.rss.extension.JuickModule; +import com.juick.www.rss.extension.JuickModuleImpl; +import com.juick.www.WebApp; +import com.juick.util.MessageUtils; +import com.rometools.modules.atom.modules.AtomLinkModule; +import com.rometools.modules.atom.modules.AtomLinkModuleImpl; +import com.rometools.modules.mediarss.MediaEntryModuleImpl; +import com.rometools.modules.mediarss.MediaModule; +import com.rometools.modules.mediarss.MediaModuleImpl; +import com.rometools.modules.mediarss.types.MediaContent; +import com.rometools.modules.mediarss.types.Metadata; +import com.rometools.modules.mediarss.types.Thumbnail; +import com.rometools.modules.mediarss.types.UrlReference; +import com.rometools.rome.feed.atom.Link; +import com.rometools.rome.feed.rss.*; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.servlet.view.feed.AbstractRssFeedView; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Created by vitalyster on 13.12.2016. + */ +public class MessagesView extends AbstractRssFeedView { + + private static final Logger logger = LoggerFactory.getLogger(MessagesView.class); + + @Inject + private WebApp webApp; + + @PostConstruct + public void init() { + setContentType("application/rss+xml;charset=UTF-8"); + } + + @SuppressWarnings("unchecked") + @Override + protected List buildFeedItems(@Nonnull Map model, + @Nonnull HttpServletRequest request, + @Nonnull HttpServletResponse response) { + List msgs = (List)model.get("messages"); + return msgs.stream().map(this::createRssItem).collect(Collectors.toList()); + } + + @Override + protected void buildFeedMetadata(Map model, Channel feed, HttpServletRequest request) { + Object userObj = model.get("user"); + if (userObj != null) { + User user = (User) userObj; + feed.setDescription(String.format("The latest messages by @%s at Juick", user.getName())); + String title = String.format("%s - Juick", user.getName()); + feed.setTitle(title); + String link = String.format("http://juick.com/%s/", user.getName()); + feed.setLink(link); + Image rssImage = new Image(); + rssImage.setUrl(webApp.getAvatarUrl(user)); + rssImage.setTitle(title); + rssImage.setLink(link); + feed.setImage(rssImage); + String href = String.format("http://rss.juick.com/%s/blog", user.getName()); + AtomLinkModule atomLinkModule = new AtomLinkModuleImpl(); + Link atomLink = new Link(); + atomLink.setHref(href); + atomLink.setType("application/rss+xml"); + atomLink.setRel("self"); + atomLinkModule.setLinks(Collections.singletonList(atomLink)); + + feed.getModules().add(atomLinkModule); + } else { + feed.setDescription("The latest messages at Juick"); + feed.setLink("http://juick.com/"); + feed.setTitle("Juick"); + } + + MediaModule mediaModule = new MediaModuleImpl(); + feed.getModules().add(mediaModule); + + + } + + private Item createRssItem(Message msg) { + Item item = new Item(); + String messageUrl = String.format("http://juick.com/%s/%d", msg.getUser().getName(), msg.getMid()); + String messageTitle = String.format("@%s: %s", msg.getUser().getName(), MessageUtils.getTagsString(msg)); + boolean isCode = msg.getTags().stream().anyMatch(t -> t.getName().equals("code")); + String messageDescription = isCode ? MessageUtils.formatMessageCode(StringUtils.defaultString(msg.getText())) + : MessageUtils.formatMessage(StringUtils.defaultString(msg.getText())); + item.setLink(messageUrl); + //item.setGuid(messageUrl); + item.setTitle(messageTitle); + Description description = new Description(); + description.setType("text/html"); + description.setValue(messageDescription); + item.setDescription(description); + item.setPubDate(Date.from(msg.getCreated())); + item.setComments(messageUrl); + msg.getTags().stream().map(t -> { + Category category = new Category(); + category.setValue(t.getName()); + return category; + }).forEach(c -> item.getCategories().add(c)); + JuickModule juickModule = new JuickModuleImpl(); + juickModule.setUid(msg.getUser().getUid()); + item.getModules().add(juickModule); + if (StringUtils.isNotEmpty(msg.getAttachmentType())) { + String type = msg.getAttachmentType().equals("jpg") ? "image/jpeg" : "image/png"; + MediaEntryModuleImpl module = new MediaEntryModuleImpl(); + try { + UrlReference reference = new UrlReference(MessageUtils.attachmentUrl(msg)); + MediaContent mediaContent = new MediaContent(reference); + mediaContent.setType(type); + Metadata metadata = new Metadata(); + metadata.setThumbnail(new Thumbnail[]{new Thumbnail(new URI(msg.getPhoto().getThumbnail()))}); + module.setMetadata(metadata); + module.setMediaContents(new MediaContent[]{mediaContent}); + item.getModules().add(module); + } catch (URISyntaxException e) { + logger.error("Invalid URI", e); + } + + } + return item; + } +} diff --git a/src/main/java/com/juick/www/rss/RepliesView.java b/src/main/java/com/juick/www/rss/RepliesView.java new file mode 100644 index 00000000..2e5b9f67 --- /dev/null +++ b/src/main/java/com/juick/www/rss/RepliesView.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.rss; + +import com.juick.model.ResponseReply; +import com.juick.util.MessageUtils; +import com.rometools.modules.mediarss.MediaEntryModuleImpl; +import com.rometools.modules.mediarss.MediaModule; +import com.rometools.modules.mediarss.MediaModuleImpl; +import com.rometools.modules.mediarss.types.MediaContent; +import com.rometools.modules.mediarss.types.Metadata; +import com.rometools.modules.mediarss.types.Thumbnail; +import com.rometools.modules.mediarss.types.UrlReference; +import com.rometools.rome.feed.rss.Channel; +import com.rometools.rome.feed.rss.Description; +import com.rometools.rome.feed.rss.Item; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.servlet.view.feed.AbstractRssFeedView; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Created by vitalyster on 13.12.2016. + */ +public class RepliesView extends AbstractRssFeedView { + + private static final Logger logger = LoggerFactory.getLogger(RepliesView.class); + + @PostConstruct + public void init() { + setContentType("application/rss+xml;charset=UTF-8"); + } + + @SuppressWarnings("unchecked") + @Override + protected @Nonnull List buildFeedItems(@Nonnull Map model, + @Nonnull HttpServletRequest request, + @Nonnull HttpServletResponse response) { + List msgs = (List)model.get("messages"); + return msgs.stream().map(this::createRssItem).collect(Collectors.toList()); + } + + @Override + protected void buildFeedMetadata(Map model, Channel feed, HttpServletRequest request) { + feed.setTitle("Juick"); + feed.setLink("http://juick.com/"); + feed.setDescription("The latest comments at Juick"); + MediaModule mediaModule = new MediaModuleImpl(); + feed.getModules().add(mediaModule); + } + + private Item createRssItem(ResponseReply msg) { + Item item = new Item(); + String messageUrl = String.format("http://juick.com/m/%d#%d", msg.getMid(), msg.getRid()); + String messageTitle = String.format("@%s:", msg.getUname()); + String messageDescription = msg.isHtml() ? msg.getDescription() : MessageUtils.formatMessage(msg.getDescription()); + item.setLink(messageUrl); + //item.setGuid(messageUrl); + item.setTitle(messageTitle); + Description description = new Description(); + description.setType("text/html"); + description.setValue(messageDescription); + item.setDescription(description); + item.setPubDate(msg.getPubDate()); + if (StringUtils.isNotEmpty(msg.getAttachmentType())) { + String type = msg.getAttachmentType().equals("jpg") ? "image/jpeg" : "image/png"; + MediaEntryModuleImpl module = new MediaEntryModuleImpl(); + try { + UrlReference reference = new UrlReference( + String.format("http://i.juick.com/photos-1024/%d-%d.%s", msg.getMid(), msg.getRid(), type)); + MediaContent mediaContent = new MediaContent(reference); + mediaContent.setType(type); + Metadata metadata = new Metadata(); + metadata.setThumbnail(new Thumbnail[]{new Thumbnail( + new URI(String.format("http://i.juick.com/ps/%d-%d.%s", msg.getMid(), msg.getRid(), type)))}); + module.setMetadata(metadata); + module.setMediaContents(new MediaContent[]{mediaContent}); + item.getModules().add(module); + } catch (URISyntaxException e) { + logger.error("Invalid URI", e); + } + + } + return item; + } +} diff --git a/src/main/java/com/juick/www/rss/extension/JuickModule.java b/src/main/java/com/juick/www/rss/extension/JuickModule.java new file mode 100644 index 00000000..6b24390a --- /dev/null +++ b/src/main/java/com/juick/www/rss/extension/JuickModule.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.rss.extension; + +import com.rometools.rome.feed.module.Module; + +/** + * Created by vitalyster on 13.12.2016. + */ +public interface JuickModule extends Module { + + String URI = "http://juick.com/"; + + Integer getUid(); + + void setUid(Integer uid); + +} diff --git a/src/main/java/com/juick/www/rss/extension/JuickModuleGenerator.java b/src/main/java/com/juick/www/rss/extension/JuickModuleGenerator.java new file mode 100644 index 00000000..61368785 --- /dev/null +++ b/src/main/java/com/juick/www/rss/extension/JuickModuleGenerator.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.rss.extension; + +import com.rometools.rome.feed.module.Module; +import com.rometools.rome.io.ModuleGenerator; +import org.jdom2.Element; +import org.jdom2.Namespace; + +import java.util.Set; + +/** + * Created by vt on 13/12/2016. + */ +public class JuickModuleGenerator implements ModuleGenerator { + + private static final Namespace JUICK_NS = Namespace.getNamespace("juick", JuickModule.URI); + + @Override + public String getNamespaceUri() { + return JuickModule.URI; + } + + private static final Set NAMESPACES; + + static { + NAMESPACES = Set.of(JUICK_NS); + } + + @Override + public Set getNamespaces() { + return NAMESPACES; + } + + @Override + public void generate(Module module, Element element) { + // this is not necessary, it is done to avoid the namespace definition in every item. + Element root = element; + while (root.getParent()!=null && root.getParent() instanceof Element) { + root = element.getParentElement(); + } + root.addNamespaceDeclaration(JUICK_NS); + + JuickModule juickModule = (JuickModule) module; + if (juickModule.getUid() > 0) { + Element user = new Element("user", JUICK_NS); + user.setAttribute("uid", String.valueOf(juickModule.getUid()), JUICK_NS); + element.addContent(user); + } + } +} diff --git a/src/main/java/com/juick/www/rss/extension/JuickModuleImpl.java b/src/main/java/com/juick/www/rss/extension/JuickModuleImpl.java new file mode 100644 index 00000000..ea6c4a66 --- /dev/null +++ b/src/main/java/com/juick/www/rss/extension/JuickModuleImpl.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.rss.extension; + +import com.rometools.rome.feed.CopyFrom; +import com.rometools.rome.feed.module.ModuleImpl; + +/** + * Created by vitalyster on 13.12.2016. + */ +public class JuickModuleImpl extends ModuleImpl implements JuickModule { + + private Integer uid; + + public JuickModuleImpl() { + super(JuickModule.class, JuickModule.URI); + } + + @Override + public Integer getUid() { + return uid; + } + + @Override + public void setUid(Integer uid) { + this.uid = uid; + } + + @Override + public Class getInterface() { + return JuickModule.class; + } + + @Override + public void copyFrom(CopyFrom obj) { + JuickModule juickModule = (JuickModule) obj; + setUid(juickModule.getUid()); + } +} diff --git a/src/main/java/com/juick/www/rss/extension/JuickModuleParser.java b/src/main/java/com/juick/www/rss/extension/JuickModuleParser.java new file mode 100644 index 00000000..fbf8c8bb --- /dev/null +++ b/src/main/java/com/juick/www/rss/extension/JuickModuleParser.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.rss.extension; + +import com.rometools.rome.feed.module.Module; +import com.rometools.rome.io.ModuleParser; +import org.apache.commons.lang3.math.NumberUtils; +import org.jdom2.Element; + +import java.util.Locale; + +/** + * Created by vitalyster on 13.12.2016. + */ +public class JuickModuleParser implements ModuleParser { + @Override + public String getNamespaceUri() { + return JuickModule.URI; + } + + @Override + public Module parse(Element element, Locale locale) { + JuickModuleImpl juickModule = new JuickModuleImpl(); + juickModule.setUid(NumberUtils.toInt(element.getAttributeValue("uid", JuickModule.URI), 0)); + return juickModule; + } +} -- cgit v1.2.3