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.Like; 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.server.www.WebApp; import com.juick.service.MessagesService; import com.juick.service.UserService; import com.juick.service.activities.*; 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; @Inject private ObjectMapper jsonMapper; @Inject private WebApp webApp; @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(@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).orElseThrow(IllegalStateException::new))); } @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) { 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<>(HttpStatus.OK); } else if (objectType.equals("Like") || objectType.equals("Announce")) { applicationEventPublisher.publishEvent(new UndoAnnounceEvent(this, activity.getActor(), objectObject)); 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)) { String postId = activityPubManager.postId(inReplyTo); 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 Like || activity instanceof Announce) { applicationEventPublisher.publishEvent(new AnnounceEvent(this, activity.getActor(), (String)((Map)activity.getObject()).get("object"))); 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; } }