package com.juick.server; import com.juick.Message; import com.juick.Reaction; import com.juick.User; import com.juick.formatters.PlainTextFormatter; import com.juick.server.api.activity.model.Context; import com.juick.server.api.activity.model.activities.Accept; import com.juick.server.api.activity.model.activities.Announce; import com.juick.server.api.activity.model.activities.Create; import com.juick.server.api.activity.model.activities.Delete; import com.juick.server.api.activity.model.objects.Hashtag; import com.juick.server.api.activity.model.objects.Image; import com.juick.server.api.activity.model.objects.Mention; import com.juick.server.api.activity.model.objects.Note; import com.juick.server.api.activity.model.objects.Person; import com.juick.server.util.HttpUtils; import com.juick.service.MessagesService; import com.juick.service.SocialService; import com.juick.service.UserService; import com.juick.service.activities.*; import com.juick.service.component.*; import com.juick.util.MessageUtils; import com.mitchellbosecke.pebble.PebbleEngine; import com.mitchellbosecke.pebble.template.PebbleTemplate; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import javax.annotation.Nonnull; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @Component public class ActivityPubManager implements ActivityListener, NotificationListener { private static final Logger logger = LoggerFactory.getLogger("ActivityPub"); @Inject private SignatureManager signatureManager; @Inject private SocialService socialService; @Inject private UserService userService; @Inject private MessagesService messagesService; @Inject private PebbleEngine pebbleEngine; @Value("${ap_base_uri:http://localhost:8080/}") private String baseUri; @Value("${service_user:juick}") private String serviceUsername; private User serviceUser; @PostConstruct public void init() { serviceUser = userService.getUserByName(serviceUsername); } @Override public void processFollowEvent(@Nonnull FollowEvent followEvent) { String acct = (String)followEvent.getRequest().getObject(); logger.info("received follower request to {}", acct); User followedUser = socialService.getUserByAccountUri(acct); if (!followedUser.isAnonymous()) { // automatically accept follower requests Person me = (Person) signatureManager.getContext(URI.create(acct)).get(); Person follower = (Person) signatureManager.getContext(URI.create(followEvent.getRequest().getActor())).get(); Accept accept = new Accept(); accept.setActor(me.getId()); accept.setObject(followEvent.getRequest()); try { signatureManager.post(me, follower, accept); socialService.addFollower(followedUser, follower.getId()); logger.info("Follower added for {}", followedUser.getName()); } catch (IOException e) { logger.info("activitypub exception", e); } } } @Override public void undoFollowEvent(UndoFollowEvent event) { String actor = event.getActor(); String me = event.getObject(); logger.info("{} stopping to follow {}", actor, me); User followedUser = socialService.getUserByAccountUri(me); if (!followedUser.isAnonymous()) { socialService.removeFollower(followedUser, actor); } } @Override public void deleteUserEvent(DeleteUserEvent event) { String acct = event.getUserUri(); logger.info("Deleting {} from followers", acct); socialService.removeAccount(acct); } @Override public void deleteMessageEvent(DeleteMessageEvent event) { Message msg = event.getMessage(); User user = msg.getUser(); String userUri = personUri(user); Note note = makeNote(msg); Person me = (Person) signatureManager.getContext(URI.create(userUri)).get(); socialService.getFollowers(user).forEach(acct -> { Person follower = (Person) signatureManager.getContext(URI.create(acct)).get(); Delete delete = new Delete(); delete.setId(note.getId()); delete.setActor(me.getId()); delete.setPublished(note.getPublished()); delete.setObject(note); try { logger.info("Deletion to follower {}", follower.getId()); signatureManager.post(me, follower, delete); } catch (IOException e) { logger.warn("activitypub exception", e); } }); } @Override public void processAnnounceEvent(AnnounceEvent event) { UriComponents uriComponents = UriComponentsBuilder.fromUriString(event.getMessageUri()).build(); List segments = uriComponents.getPathSegments(); if (segments.get(0).equals("n")) { String[] ids = segments.get(1).split("-", 2); if (ids.length == 2 && Integer.valueOf(ids[1]) == 0) { // only messages logger.info("{} recommends {}", event.getActorUri(), Integer.valueOf(ids[0])); messagesService.likeMessage(Integer.valueOf(ids[0]), 0, Reaction.LIKE, event.getActorUri()); } } } @Override public void undoAnnounceEvent(UndoAnnounceEvent event) { UriComponents uriComponents = UriComponentsBuilder.fromUriString(event.getMessageUri()).build(); List segments = uriComponents.getPathSegments(); if (segments.get(0).equals("n")) { String[] ids = segments.get(1).split("-", 2); if (ids.length == 2 && Integer.valueOf(ids[1]) == 0) { // only messages logger.info("{} stop recommending {}", event.getActorUri(), Integer.valueOf(ids[0])); messagesService.likeMessage(Integer.valueOf(ids[0]), 0, null, event.getActorUri()); } } } @Override public void processMessageEvent(MessageEvent messageEvent) { Message msg = messageEvent.getMessage(); if (MessageUtils.isPM(msg)) { return; } User user = msg.getUser(); String userUri = personUri(user); Note note = makeNote(msg); Person me = (Person) signatureManager.getContext(URI.create(userUri)).get(); Set subscribers = new HashSet<>(socialService.getFollowers(user)); if (MessageUtils.isReply(msg) && msg.getTo().getUri().toASCIIString().length() > 0) { String replier = msg.getTo().getUri().toASCIIString(); subscribers.add(replier); List cc = new ArrayList<>(note.getCc()); cc.add(replier); note.setCc(cc); } subscribers.addAll(note.getCc()); subscribers.forEach(acct -> { Optional context = signatureManager.getContext(URI.create(acct)); if (context.isPresent() && context.get() instanceof Person) { Person follower = (Person)context.get(); Create create = new Create(); create.setId(note.getId()); create.setActor(me.getId()); create.setPublished(note.getPublished()); create.setObject(note); try { signatureManager.post(me, follower, create); } catch (IOException e) { logger.warn("activitypub exception", e); } } }); } public String inboxUri() { UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); return uri.replacePath("/api/inbox").toUriString(); } public String outboxUri(User user) { UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); return uri.replacePath(String.format("/u/%s/blog/toc", user.getName())).toUriString(); } public String personUri(User user) { if (user.getUri().toString().length() > 0) { return user.getUri().toASCIIString(); } UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); return uri.replacePath(String.format("/u/%s", user.getName())).toUriString(); } public String personWebUri(User user) { UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); return uri.replacePath(String.format("/%s/", user.getName())).toUriString(); } public String followersUri(User user) { UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); return uri.replacePath(String.format("/u/%s/followers/toc", user.getName())).toUriString(); } public String followingUri(User user) { UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); return uri.replacePath(String.format("/u/%s/following/toc", user.getName())).toUriString(); } public String messageUri(Message msg) { return messageUri(msg.getMid(), msg.getRid()); } public String messageUri(int mid, int rid) { UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); uri.replacePath(String.format("/n/%d-%d", mid, rid)); return uri.toUriString(); } public String tagUri(com.juick.Tag tag) { UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); return uri.replacePath(String.format("/t/%s", tag.getName())).toUriString(); } public String postId(String messageUri) { UriComponents uri = UriComponentsBuilder.fromUriString(messageUri).build(); return uri.getPath().substring(uri.getPath().lastIndexOf('/') + 1).replace("-", "/"); } public Note makeNote(Message msg) { Note note = new Note(); note.setId(messageUri(msg)); note.setUrl(PlainTextFormatter.formatUrl(msg)); note.setAttributedTo(personUri(msg.getUser())); if (MessageUtils.isReply(msg)) { if (msg.getReplyToUri().toASCIIString().length() > 0) { note.setInReplyTo(msg.getReplyToUri().toASCIIString()); } else { note.setInReplyTo(messageUri(msg.getMid(), msg.getReplyto())); } } if (MessageUtils.isPM(msg)) { note.setTo(Collections.singletonList(personUri(msg.getTo()))); } else { note.setTo(Collections.singletonList(Context.ACTIVITYSTREAMS_PUBLIC)); note.setCc(Collections.singletonList(followersUri(msg.getUser()))); } note.setPublished(msg.getCreated()); if (StringUtils.isNotBlank(msg.getAttachmentType())) { Image attachment = new Image(); attachment.setId(msg.getAttachment().getMedium().getUrl()); attachment.setUrl(msg.getAttachment().getMedium().getUrl()); attachment.setMediaType(HttpUtils.mediaType(msg.getAttachmentType())); note.setAttachment(Collections.singletonList(attachment)); } note.setTags(msg.getTags().stream().map(t -> new Hashtag(tagUri(t), t.getName())).collect(Collectors.toList())); if (msg.getReplyToUri() != null && msg.getReplyToUri().toASCIIString().length() > 0) { Optional noteContext = signatureManager.getContext(msg.getReplyToUri()); if (noteContext.isPresent()) { Note activity = (Note) noteContext.get(); Optional personContext = signatureManager.getContext(URI.create(activity.getAttributedTo())); if (personContext.isPresent()) { Person person = (Person) personContext.get(); note.getTags().add(new Mention(person.getUrl(), person.getPreferredUsername())); msg.getTo().setName(person.getPreferredUsername()); note.setInReplyTo(activity.getInReplyTo()); } } } else if (MessageUtils.isReply(msg)) { note.getTags().add(new Mention(personWebUri(msg.getTo()), msg.getTo().getName())); } MessageUtils.getGlobalMentions(msg).forEach(m -> { // @user@server.tld -> user@server.tld Optional personContext = signatureManager.discoverPerson(m.substring(1)); if (personContext.isPresent()) { Person person = (Person) personContext.get(); note.getTags().add(new Mention(person.getUrl(), person.getPreferredUsername())); List cc = new ArrayList<>(note.getCc()); cc.add(person.getId()); note.setCc(cc); } }); if (msg.isHtml()) { note.setContent(msg.getText()); } else { PebbleTemplate noteTemplate = pebbleEngine.getTemplate("layouts/note"); Map context = new HashMap<>(); context.put("msg", msg); context.put("baseUri", baseUri); try { Writer writer = new StringWriter(); noteTemplate.evaluate(writer, context); note.setContent(writer.toString()); } catch (IOException e) { logger.warn("template not rendered, falling back"); note.setContent(MessageUtils.formatMessage(StringUtils.defaultString(msg.getText()))); } } return note; } @Override public void processSubscribeEvent(SubscribeEvent subscribeEvent) { } @Override public void processLikeEvent(LikeEvent likeEvent) { } @Override public void processPingEvent(PingEvent pingEvent) { } @Override public void processMessageReadEvent(MessageReadEvent messageReadEvent) { } @Override public void processTopEvent(TopEvent topEvent) { Message message = topEvent.getMessage(); Note note = makeNote(message); Announce announce = new Announce(); announce.setId(note.getId() + "#top"); announce.setActor(personUri(serviceUser)); announce.setTo(Collections.singletonList(Context.ACTIVITYSTREAMS_PUBLIC)); announce.setObject(note); Person me = (Person) signatureManager.getContext(URI.create(announce.getActor())).get(); socialService.getFollowers(serviceUser).forEach(acct -> { Person follower = (Person) signatureManager.getContext(URI.create(acct)).get(); try { logger.info("Announcing top: {}", message.getMid()); signatureManager.post(me, follower, announce); } catch (IOException e) { logger.warn("activitypub exception", e); } }); } }