package com.juick.server.api.activity; import com.juick.User; import com.juick.server.KeystoreManager; import com.juick.server.SignatureManager; import com.juick.server.api.activity.model.*; import com.juick.server.api.activity.model.activities.Create; import com.juick.server.api.activity.model.activities.Follow; import com.juick.server.api.activity.model.activities.Undo; 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.FollowEvent; import com.juick.util.MessageUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; 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.Collections; 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 { @Inject private UserService userService; @Inject private MessagesService messagesService; @Inject private KeystoreManager keystoreManager; @Inject private SignatureManager signatureManager; @Inject private ApplicationEventPublisher applicationEventPublisher; @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; @GetMapping(value = "/u/{userName}", produces = { Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITY_JSON_MEDIA_TYPE }) public Person getUser(@PathVariable String userName) { User user = userService.getUserByName(userName); if (!user.isAnonymous()) { UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); Person person = new Person(); uri.replacePath(String.format("/u/%s", userName)); person.setId(uri.toUriString()); person.setUrl(uri.toUriString()); 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); uri.replacePath("/api/inbox"); person.setInbox(uri.toUriString()); person.setOutbox(uri.replacePath(String.format("/u/%s/blog/toc", userName)).toUriString()); person.setFollowers(uri.replacePath(String.format("/u/%s/followers/toc", userName)).toUriString()); person.setFollowing(uri.replacePath(String.format("/u/%s/following/toc", userName)).toUriString()); 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.ACTIVITY_JSON_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.ACTIVITY_JSON_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(); String followersUri = uri.replacePath(String.format("/u/%s/followers/toc", userName)).toUriString(); List mids = messagesService.getUserBlog(user.getUid(), 0, before); List notes = messagesService.getMessages(visitor, mids).stream().map(m -> { Note note = new Note(); note.setId(uri.replacePath(String.format("/m/%d", m.getMid())).toUriString()); note.setAttributedTo(personUri); note.setTo(Collections.singletonList("https://www.w3.org/ns/activitystreams#Public")); note.setCc(Collections.singletonList(followersUri)); note.setPublished(m.getTimestamp()); note.setContent(MessageUtils.formatMessage(m.getText())); if (StringUtils.isNotBlank(m.getAttachmentType())) { Link attachment = new Link(); attachment.setHref(m.getAttachment().getMedium().getUrl()); note.setAttachment(attachment); } return note; }).collect(Collectors.toList()); Person person = new Person(); person.setName(user.getName()); 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.ACTIVITY_JSON_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.ACTIVITY_JSON_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); Person person = new Person(); person.setName(user.getName()); OrderedCollectionPage result = new OrderedCollectionPage(); result.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); result.setOrderedItems(followersPage.map(a -> { Person follower = new Person(); follower.setName(a.getName()); person.setPreferredUsername(a.getName()); follower.setUrl(uriComponentsBuilder.replacePath(String.format("/u/%s", a.getName())).toUriString()); 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.ACTIVITY_JSON_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.ACTIVITY_JSON_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); Person person = new Person(); person.setName(user.getName()); person.setPreferredUsername(user.getName()); OrderedCollectionPage result = new OrderedCollectionPage(); result.setId(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); result.setOrderedItems(followingPage.map(a -> { Person follower = new Person(); follower.setName(a.getName()); follower.setUrl(uriComponentsBuilder.replacePath(String.format("/u/%s", a.getName())).toUriString()); 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(); } @PostMapping(value = "/api/inbox", consumes = { Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITY_JSON_MEDIA_TYPE }) public ResponseEntity processInbox(@RequestBody Context activity, @RequestHeader(name = "Host") String host, @RequestHeader(name = "Date") String date, @RequestHeader(name = "Digest") String digest, @RequestHeader(name = "Content-Type") String contentType, @RequestHeader(name = "Signature") String signature) { if (activity instanceof Follow) { Follow followRequest = (Follow) activity; UriComponents componentsBuilder = ServletUriComponentsBuilder.fromCurrentRequestUri().build(); Map headers = new HashMap<>(); headers.put("host", host); headers.put("date", date); headers.put("digest", digest); headers.put("content-type", contentType); boolean valid = signatureManager.verifySignature(signature, URI.create(followRequest.getActor()), "POST", componentsBuilder.getPath(), headers); if (valid) { applicationEventPublisher.publishEvent( new FollowEvent(this, followRequest)); return new ResponseEntity<>(HttpStatus.ACCEPTED); } return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); } if (activity instanceof Undo) { return new ResponseEntity<>(HttpStatus.OK); } return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } }