From 7aaa3f9a29c280f01c677c918932620be45cdbd7 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Thu, 8 Nov 2018 21:38:27 +0300 Subject: Merge everything into single Spring Boot application --- .../com/juick/server/api/activity/Profile.java | 379 +++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 src/main/java/com/juick/server/api/activity/Profile.java (limited to 'src/main/java/com/juick/server/api/activity/Profile.java') diff --git a/src/main/java/com/juick/server/api/activity/Profile.java b/src/main/java/com/juick/server/api/activity/Profile.java new file mode 100644 index 00000000..10390ea1 --- /dev/null +++ b/src/main/java/com/juick/server/api/activity/Profile.java @@ -0,0 +1,379 @@ +package com.juick.server.api.activity; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.Message; +import com.juick.User; +import com.juick.model.CommandResult; +import com.juick.server.ActivityPubManager; +import com.juick.server.CommandsManager; +import com.juick.server.KeystoreManager; +import com.juick.server.SignatureManager; +import com.juick.server.api.activity.model.Activity; +import com.juick.server.api.activity.model.Context; +import com.juick.server.api.activity.model.activities.Announce; +import com.juick.server.api.activity.model.activities.Create; +import com.juick.server.api.activity.model.activities.Delete; +import com.juick.server.api.activity.model.activities.Follow; +import com.juick.server.api.activity.model.activities.Undo; +import com.juick.server.api.activity.model.objects.Image; +import com.juick.server.api.activity.model.objects.Key; +import com.juick.server.api.activity.model.objects.Note; +import com.juick.server.api.activity.model.objects.OrderedCollection; +import com.juick.server.api.activity.model.objects.OrderedCollectionPage; +import com.juick.server.api.activity.model.objects.Person; +import com.juick.server.util.HttpBadRequestException; +import com.juick.server.util.HttpNotFoundException; +import com.juick.server.util.UserUtils; +import com.juick.service.MessagesService; +import com.juick.service.UserService; +import com.juick.service.activities.DeleteUserEvent; +import com.juick.service.activities.FollowEvent; +import com.juick.service.activities.UndoFollowEvent; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.inject.Inject; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RestController +public class Profile { + private static final Logger logger = LoggerFactory.getLogger(Profile.class); + @Inject + private UserService userService; + @Inject + private MessagesService messagesService; + @Inject + private KeystoreManager keystoreManager; + @Inject + private SignatureManager signatureManager; + @Inject + private ActivityPubManager activityPubManager; + @Inject + private ApplicationEventPublisher applicationEventPublisher; + @Inject + private CommandsManager commandsManager; + @Value("${web_domain:localhost}") + private String domain; + @Value("${ap_base_uri:http://localhost:8080/}") + private String baseUri; + @Value("${img_url:http://localhost:8080/i/}") + private String baseImagesUri; + @Inject + private ObjectMapper jsonMapper; + + @GetMapping(value = "/u/{userName}", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) + public Person getUser(@PathVariable String userName) { + User user = userService.getUserByName(userName); + if (!user.isAnonymous()) { + Person person = new Person(); + person.setId(activityPubManager.personUri(user)); + person.setUrl(activityPubManager.personWebUri(user)); + person.setName(userName); + person.setPreferredUsername(userName); + Key publicKey = new Key(); + publicKey.setId(person.getId() + "#main-key"); + publicKey.setOwner(person.getId()); + publicKey.setPublicKeyPem(keystoreManager.getPublicKeyPem()); + person.setPublicKey(publicKey); + person.setInbox(activityPubManager.inboxUri()); + person.setOutbox(activityPubManager.outboxUri(user)); + person.setFollowers(activityPubManager.followersUri(user)); + person.setFollowing(activityPubManager.followingUri(user)); + UriComponentsBuilder image = UriComponentsBuilder.fromUriString(baseImagesUri); + image.path(String.format("/a/%d.png", user.getUid())); + Image avatar = new Image(); + avatar.setUrl(image.toUriString()); + avatar.setMediaType("image/png"); + person.setIcon(avatar); + return (Person) Context.build(person); + } + throw new HttpNotFoundException(); + } + + @GetMapping(value = "/u/{userName}/blog/toc", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) + public OrderedCollection getOutbox(@PathVariable String userName) { + User user = userService.getUserByName(userName); + if (!user.isAnonymous()) { + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri); + OrderedCollection blog = new OrderedCollection(); + blog.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + blog.setTotalItems(userService.getStatsMessages(user.getUid())); + blog.setFirst(uriComponentsBuilder.path(String.format("/u/%s/blog", userName)).toUriString()); + return (OrderedCollection) Context.build(blog); + } + throw new HttpNotFoundException(); + } + + @GetMapping(value = "/u/{userName}/blog", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) + public OrderedCollectionPage getOutboxPage(@PathVariable String userName, + @RequestParam(required = false, defaultValue = "0") int before) { + User visitor = UserUtils.getCurrentUser(); + User user = userService.getUserByName(userName); + if (!user.isAnonymous()) { + UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); + String personUri = uri.path(String.format("/u/%s", userName)).toUriString(); + List mids = messagesService.getUserBlog(user.getUid(), 0, before); + List notes = messagesService.getMessages(visitor, mids).stream().map(activityPubManager::makeNote).collect(Collectors.toList()); + OrderedCollectionPage page = new OrderedCollectionPage(); + page.setPartOf(uri.replacePath(String.format("/u/%s/blog/toc", userName)).toUriString()); + page.setFirst(uri.replacePath(String.format("/u/%s/blog", userName)).toUriString()); + page.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + page.setOrderedItems(notes.stream().map(a -> { + Create create = new Create(); + create.setId(a.getId() + "#Create"); + create.setTo(a.getTo()); + create.setActor(personUri); + create.setObject(a); + create.setPublished(a.getPublished()); + return create; + }).collect(Collectors.toList())); + int beforeNext = mids.stream().reduce((fst, second) -> second).orElse(0); + if (beforeNext > 0) { + page.setNext(uri.queryParam("before", beforeNext).toUriString()); + } + page.setLast(uri.replaceQueryParam("before", "1").toUriString()); + return (OrderedCollectionPage) Context.build(page); + } + throw new HttpNotFoundException(); + } + + @GetMapping(value = "/u/{userName}/followers/toc", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) + public OrderedCollection getFollowers(@PathVariable String userName) { + User user = userService.getUserByName(userName); + if (!user.isAnonymous()) { + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri); + OrderedCollection followers = new OrderedCollection(); + followers.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + followers.setTotalItems(userService.getStatsMyReaders(user.getUid())); + followers.setFirst(uriComponentsBuilder.path(String.format("/u/%s/followers", userName)).toUriString()); + return (OrderedCollection) Context.build(followers); + } + throw new HttpNotFoundException(); + } + + @GetMapping(value = "/u/{userName}/followers", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) + public OrderedCollectionPage getFollowersPage(@PathVariable String userName, + @RequestParam(required = false, defaultValue = "0") int page) { + User user = userService.getUserByName(userName); + if (!user.isAnonymous()) { + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri); + uriComponentsBuilder.path(String.format("/u/%s/followers", userName)); + List followers = userService.getUserReaders(user.getUid()); + Stream followersPage = followers.stream().skip(20 * page).limit(20); + + OrderedCollectionPage result = new OrderedCollectionPage(); + result.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + result.setOrderedItems(followersPage.map(a -> { + Person follower = new Person(); + follower.setName(a.getName()); + follower.setPreferredUsername(a.getName()); + follower.setUrl(activityPubManager.personWebUri(a)); + return follower; + }).collect(Collectors.toList())); + boolean hasNext = followers.size() <= 20 * page; + if (hasNext) { + result.setNext(uriComponentsBuilder.queryParam("page", page + 1).toUriString()); + } + return (OrderedCollectionPage) Context.build(result); + } + throw new HttpNotFoundException(); + } + + @GetMapping(value = "/u/{userName}/following/toc", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) + public OrderedCollection getFollowing(@PathVariable String userName) { + User user = userService.getUserByName(userName); + if (!user.isAnonymous()) { + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri); + OrderedCollection following = new OrderedCollection(); + following.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + following.setTotalItems(userService.getUserFriends(user.getUid()).size()); + following.setFirst(uriComponentsBuilder.path(String.format("/u/%s/followers", userName)).toUriString()); + return (OrderedCollection) Context.build(following); + } + throw new HttpNotFoundException(); + } + + @GetMapping(value = "/u/{userName}/following", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) + public OrderedCollectionPage getFollowingPage(@PathVariable String userName, + @RequestParam(required = false, defaultValue = "0") int page) { + User user = userService.getUserByName(userName); + if (!user.isAnonymous()) { + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri); + uriComponentsBuilder.path(String.format("/u/%s/following", userName)); + List following = userService.getUserFriends(user.getUid()); + Stream followingPage = following.stream().skip(20 * page).limit(20); + + OrderedCollectionPage result = new OrderedCollectionPage(); + result.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + result.setOrderedItems(followingPage.map(a -> { + Person follower = new Person(); + follower.setName(a.getName()); + follower.setPreferredUsername(a.getName()); + follower.setUrl(activityPubManager.personWebUri(a)); + return follower; + }).collect(Collectors.toList())); + boolean hasNext = following.size() <= 20 * page; + if (hasNext) { + result.setNext(uriComponentsBuilder.queryParam("page", page + 1).toUriString()); + } + return (OrderedCollectionPage) Context.build(result); + } + throw new HttpNotFoundException(); + } + + @GetMapping(value = "/n/{mid}-{rid}", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) + public Context showNote(@PathVariable int mid, @PathVariable int rid) { + if (rid > 0) { + // reply + return Context.build(activityPubManager.makeNote( + messagesService.getReply(mid, rid))); + } + return Context.build(activityPubManager.makeNote( + messagesService.getMessage(mid))); + } + + @PostMapping(value = "/api/inbox", consumes = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) + public ResponseEntity processInbox(@RequestBody Activity activity, + @RequestHeader(name = "Host") String host, + @RequestHeader(name = "Date") String date, + @RequestHeader(name = "Digest", required = false) String digest, + @RequestHeader(name = "Content-Type") String contentType, + @RequestHeader(name = "User-Agent", required = false) String userAgent, + @RequestHeader(name = "Accept-Encoding", required = false) String acceptEncoding, + @RequestHeader(name = "Signature", required = false) String signature) throws Exception { + UriComponents componentsBuilder = ServletUriComponentsBuilder.fromCurrentRequestUri().build(); + Map headers = new HashMap<>(); + headers.put("host", host.split(":", 2)[0]); + headers.put("date", date); + headers.put("digest", digest); + headers.put("content-type", contentType); + headers.put("user-agent", userAgent); + headers.put("accept-encoding", acceptEncoding); + boolean valid = signatureManager.verifySignature(signature, URI.create(activity.getActor()), "POST", + componentsBuilder.getPath(), headers); + if (valid) { + if (activity instanceof Follow) { + Follow followRequest = (Follow) activity; + String actor = followRequest.getActor(); + Person follower = (Person) signatureManager.getContext(URI.create(actor)).orElseThrow(HttpBadRequestException::new); + applicationEventPublisher.publishEvent( + new FollowEvent(this, followRequest)); + return new ResponseEntity<>(HttpStatus.ACCEPTED); + + } + if (activity instanceof Undo) { + String follower = (String) ((Map) activity.getObject()).get("object"); + applicationEventPublisher.publishEvent(new UndoFollowEvent(this, activity.getActor(), follower)); + return new ResponseEntity<>(HttpStatus.OK); + } + if (activity instanceof Delete) { + if (activity.getObject() instanceof String) { + // Delete user + applicationEventPublisher.publishEvent(new DeleteUserEvent(this, (String)activity.getObject())); + return new ResponseEntity<>(HttpStatus.OK); + } + } + if (activity instanceof Create) { + if (activity.getObject() instanceof Map) { + Map note = (Map) activity.getObject(); + if (note.get("type").equals("Note")) { + URI noteId = URI.create((String) note.get("id")); + if (messagesService.replyExists(noteId)) { + return new ResponseEntity<>(HttpStatus.OK); + } else { + String inReplyTo = (String) note.get("inReplyTo"); + if (StringUtils.isNotBlank(inReplyTo)) { + if (inReplyTo.startsWith(baseUri)) { + UriComponents uri = UriComponentsBuilder.fromUriString(inReplyTo).build(); + String postId = uri.getPath().substring(uri.getPath().lastIndexOf('/') + 1).replace("-", "/"); + User user = new User(); + user.setUri(URI.create(activity.getActor())); + String attachment = StringUtils.EMPTY; + if (note.get("attachment") != null && ((List) note.get("attachment")).size() > 0) { + Map attachmentObj = (Map) ((List) note.get("attachment")).get(0); + attachment = (String) attachmentObj.get("url"); + } + CommandResult result = commandsManager.processCommand(user, String.format("#%s %s", postId, note.get("content")), URI.create(attachment)); + logger.info(jsonMapper.writeValueAsString(result)); + if (result.getNewMessage().isPresent()) { + messagesService.updateReplyUri(result.getNewMessage().get(), noteId); + return new ResponseEntity<>(HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } else { + Message reply = messagesService.getReplyByUri(inReplyTo); + if (reply != null) { + User user = new User(); + user.setUri(URI.create(activity.getActor())); + String attachment = StringUtils.EMPTY; + if (note.get("attachment") != null && ((List) note.get("attachment")).size() > 0) { + Map attachmentObj = (Map) ((List) note.get("attachment")).get(0); + attachment = (String) attachmentObj.get("url"); + } + CommandResult result = commandsManager.processCommand(user, String.format("#%d/%d %s", reply.getMid(), reply.getRid(), note.get("content")), URI.create(attachment)); + logger.info(jsonMapper.writeValueAsString(result)); + if (result.getNewMessage().isPresent()) { + messagesService.updateReplyUri(result.getNewMessage().get(), noteId); + return new ResponseEntity<>(HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + } + } + } + } + } + } + if (activity instanceof Delete) { + 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<>(HttpStatus.OK); + } + } + if (activity instanceof Announce) { + logger.info("Announce: {}", jsonMapper.writeValueAsString(activity)); + return new ResponseEntity<>(HttpStatus.OK); + } + logger.warn("Unknown activity: {}", jsonMapper.writeValueAsString(activity)); + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + @PostMapping(value = "/u/", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public User fetchUser(@RequestParam URI uri) { + Person person = (Person) signatureManager.getContext(uri).orElseThrow(HttpBadRequestException::new); + User user = new User(); + user.setUri(URI.create(person.getUrl())); + user.setName(person.getPreferredUsername()); + if (person.getIcon() != null) { + user.setAvatar(person.getIcon().getUrl()); + } + return user; + } +} -- cgit v1.2.3