diff options
author | Vitaly Takmazov | 2020-04-04 01:15:01 +0300 |
---|---|---|
committer | Vitaly Takmazov | 2020-04-04 01:15:01 +0300 |
commit | a608baeed738894433aacfa041e2617f60ce959f (patch) | |
tree | 1e0de7056417ff0833ae3d4600de9fec6eb81631 /src/main/java/com/juick/ActivityPubManager.java | |
parent | 7a2f89266c8f6337e4e81a2fd8488e0f80f4f9bd (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.java | 405 |
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; + } +} |