aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/juick/ActivityPubManager.java
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2020-04-04 01:15:01 +0300
committerGravatar Vitaly Takmazov2020-04-04 01:15:01 +0300
commita608baeed738894433aacfa041e2617f60ce959f (patch)
tree1e0de7056417ff0833ae3d4600de9fec6eb81631 /src/main/java/com/juick/ActivityPubManager.java
parent7a2f89266c8f6337e4e81a2fd8488e0f80f4f9bd (diff)
Initialize all components from configuration
Diffstat (limited to 'src/main/java/com/juick/ActivityPubManager.java')
-rw-r--r--src/main/java/com/juick/ActivityPubManager.java405
1 files changed, 405 insertions, 0 deletions
diff --git a/src/main/java/com/juick/ActivityPubManager.java b/src/main/java/com/juick/ActivityPubManager.java
new file mode 100644
index 00000000..e3b1ac8e
--- /dev/null
+++ b/src/main/java/com/juick/ActivityPubManager.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2008-2020, 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick;
+
+import com.juick.model.Message;
+import com.juick.model.Reaction;
+import com.juick.model.User;
+import com.juick.util.formatters.PlainTextFormatter;
+import com.juick.model.Tag;
+import com.juick.www.api.SystemActivity.ActivityType;
+import com.juick.www.api.activity.model.Context;
+import com.juick.www.api.activity.model.activities.*;
+import com.juick.www.api.activity.model.objects.Hashtag;
+import com.juick.www.api.activity.model.objects.Image;
+import com.juick.www.api.activity.model.objects.Mention;
+import com.juick.www.api.activity.model.objects.Note;
+import com.juick.www.api.activity.model.objects.Person;
+import com.juick.util.HttpBadRequestException;
+import com.juick.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.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;
+
+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<String> segments = uriComponents.getPathSegments();
+ if (segments.get(0).equals("n")) {
+ String[] ids = segments.get(1).split("-", 2);
+ if (ids.length == 2 && Integer.parseInt(ids[1]) == 0) {
+ // only messages
+ logger.info("{} recommends {}", event.getActorUri(), Integer.valueOf(ids[0]));
+ messagesService.likeMessage(Integer.parseInt(ids[0]), 0, Reaction.LIKE, event.getActorUri());
+ }
+ }
+ }
+
+ @Override
+ public void undoAnnounceEvent(UndoAnnounceEvent event) {
+ UriComponents uriComponents = UriComponentsBuilder.fromUriString(event.getMessageUri()).build();
+ List<String> segments = uriComponents.getPathSegments();
+ if (segments.get(0).equals("n")) {
+ String[] ids = segments.get(1).split("-", 2);
+ if (ids.length == 2 && Integer.parseInt(ids[1]) == 0) {
+ // only messages
+ logger.info("{} stop recommending {}", event.getActorUri(), Integer.valueOf(ids[0]));
+ messagesService.likeMessage(Integer.parseInt(ids[0]), 0, null, event.getActorUri());
+ }
+ }
+ }
+
+ @Override
+ public void processUpdateEvent(UpdateEvent event) {
+ String objectUri = event.getMessageUri();
+ User user = event.getUser();
+ String userUri = personUri(user);
+ Person me = (Person) signatureManager.getContext(URI.create(userUri)).get();
+ socialService.getFollowers(user).forEach(acct -> {
+ Person follower = (Person) signatureManager.getContext(URI.create(acct)).get();
+ Update update = new Update();
+ update.setId(objectUri + "#update");
+ update.setActor(me.getId());
+ update.setObject(objectUri);
+ try {
+ logger.info("Update to follower {}", follower.getId());
+ signatureManager.post(me, follower, update);
+ } catch (IOException e) {
+ logger.warn("activitypub exception", e);
+ }
+ });
+ }
+
+ @Override
+ public void processSystemEvent(SystemEvent systemEvent) {
+ ActivityType type = systemEvent.getActivity().getType();
+ if (type.equals(ActivityType.message)) {
+ processMessage(systemEvent.getActivity().getMessage());
+ } else if (type.equals(ActivityType.like)) {
+ if (systemEvent.getActivity().getFrom().equals(serviceUser)) {
+ processTop(systemEvent.getActivity().getMessage());
+ }
+ }
+ }
+ private void processMessage(Message msg) {
+ if (MessageUtils.isPM(msg) || msg.isService()) {
+ return;
+ }
+ User user = msg.getUser();
+ String userUri = personUri(user);
+ Note note = makeNote(msg);
+ Person me = (Person) signatureManager.getContext(URI.create(userUri)).get();
+ Set<String> 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<String> cc = new ArrayList<>(note.getCc());
+ cc.add(replier);
+ note.setCc(cc);
+ }
+ subscribers.addAll(note.getCc());
+ subscribers.forEach(acct -> {
+ Optional<Context> 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(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<Context> noteContext = signatureManager.getContext(msg.getReplyToUri());
+ if (noteContext.isPresent()) {
+ Note activity = (Note) noteContext.get();
+ Optional<Context> 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<Context> personContext = signatureManager.discoverPerson(m.substring(1));
+ if (personContext.isPresent()) {
+ Person person = (Person) personContext.get();
+ note.getTags().add(new Mention(person.getUrl(), person.getPreferredUsername()));
+ List<String> 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<String, Object> 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 processPingEvent(PingEvent pingEvent) {
+
+ }
+
+ private void processTop(Message message) {
+ 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 -> {
+ var follower = signatureManager.getContext(URI.create(acct));
+ follower.ifPresentOrElse((person) -> {
+ try {
+ logger.info("Announcing top: {}", message.getMid());
+ signatureManager.post(me, (Person)person, announce);
+ } catch (IOException e) {
+ logger.warn("activitypub exception", e);
+ }
+ }, () -> logger.warn("Follower not found: {}", acct));
+ });
+ }
+ public User personToUser(URI uri) throws HttpBadRequestException {
+ Person person = (Person) signatureManager.getContext(uri).orElseThrow(HttpBadRequestException::new);
+ User user = new User();
+ user.setUri(URI.create(person.getId()));
+ user.setName(person.getPreferredUsername());
+ if (person.getIcon() != null) {
+ user.setAvatar(person.getIcon().getUrl());
+ }
+ return user;
+ }
+}