From 5be22ee8963cf45190b13b41dc2dac7b7b0bfb01 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Thu, 4 Oct 2018 12:18:42 +0300 Subject: ActivityPub: persist followers and forward message event to them --- .../java/com/juick/server/ActivityPubManager.java | 135 +++++++++++++++++++-- .../java/com/juick/server/SignatureManager.java | 2 +- .../com/juick/server/api/activity/Profile.java | 36 ++---- .../com/juick/server/api/activity/model/Note.java | 5 + .../java/com/juick/service/ActivityPubService.java | 53 ++++++++ .../db/migration/V1.4__ActivityPub followers.sql | 7 ++ 6 files changed, 200 insertions(+), 38 deletions(-) create mode 100644 juick-server/src/main/java/com/juick/service/ActivityPubService.java create mode 100644 juick-server/src/main/resources/db/migration/V1.4__ActivityPub followers.sql (limited to 'juick-server') diff --git a/juick-server/src/main/java/com/juick/server/ActivityPubManager.java b/juick-server/src/main/java/com/juick/server/ActivityPubManager.java index 362754fd..ded04313 100644 --- a/juick-server/src/main/java/com/juick/server/ActivityPubManager.java +++ b/juick-server/src/main/java/com/juick/server/ActivityPubManager.java @@ -1,37 +1,148 @@ package com.juick.server; +import com.juick.Message; +import com.juick.User; +import com.juick.server.api.activity.model.Link; +import com.juick.server.api.activity.model.Note; import com.juick.server.api.activity.model.Person; import com.juick.server.api.activity.model.activities.Accept; +import com.juick.server.api.activity.model.activities.Create; +import com.juick.service.SocialService; import com.juick.service.activities.FollowEvent; +import com.juick.service.component.*; +import com.juick.util.MessageUtils; +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.ApplicationListener; import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; import javax.inject.Inject; import java.io.IOException; import java.net.URI; +import java.util.Collections; @Component -public class ActivityPubManager implements ApplicationListener { +public class ActivityPubManager implements ApplicationListener, NotificationListener { private static final Logger logger = LoggerFactory.getLogger(ActivityPubManager.class); @Inject SignatureManager signatureManager; + @Inject + ActivityPubManager activityPubManager; + @Inject + SocialService socialService; + @Value("${ap_base_uri:http://localhost:8080/}") + private String baseUri; + + private UriComponentsBuilder uri; + + @PostConstruct + public void init() { + uri = UriComponentsBuilder.fromUriString(baseUri); + } + @Override public void onApplicationEvent(@Nonnull FollowEvent followEvent) { - logger.info("received follower request"); - // automatically accept follower requests - Person me = (Person) signatureManager.getContext(URI.create((String)followEvent.getRequest().getObject())); - Person follower = (Person) signatureManager.getContext(URI.create(followEvent.getRequest().getActor())); - Accept accept = new Accept(); - accept.setActor(me.getId()); - accept.setObject(followEvent.getRequest()); - try { - signatureManager.post(me, follower, accept); - } catch (IOException e) { - logger.info("activitypub exception", e); + 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)); + Person follower = (Person) signatureManager.getContext(URI.create(followEvent.getRequest().getActor())); + 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 processMessageEvent(MessageEvent messageEvent) { + Message msg = messageEvent.getMessage(); + User user = msg.getUser(); + String userUri = personUri(user); + Note note = makeNote(msg); + Person me = (Person) signatureManager.getContext(URI.create(userUri)); + socialService.getFollowers(user).forEach(acct -> { + Person follower = (Person) signatureManager.getContext(URI.create(acct)); + Create create = new Create(); + create.setId(note.getId()); + create.setActor(me.getId()); + create.setPublished(note.getPublished()); + create.setObject(note); + try { + logger.info("Posting to follower {}", follower.getId()); + signatureManager.post(me, follower, create); + } catch (IOException e) { + logger.warn("activitypub exception", e); + } + }); + } + + public String inboxUri() { + return uri.replacePath("/api/inbox").toUriString(); + } + + public String outboxUri(User user) { + return uri.replacePath(String.format("/u/%s/blog/toc", user.getName())).toUriString(); + } + + public String personUri(User user) { + return uri.replacePath(String.format("/u/%s", user.getName())).toUriString(); + } + + public String followersUri(User user) { + return uri.replacePath(String.format("/u/%s/followers/toc", user.getName())).toUriString(); + } + + public String followingUri(User user) { + return uri.replacePath(String.format("/u/%s/following/toc", user.getName())).toUriString(); + } + + public Note makeNote(Message msg) { + Note note = new Note(); + note.setId(uri.replacePath(String.format("/m/%d", msg.getMid())).toUriString()); + note.setAttributedTo(personUri(msg.getUser())); + note.setTo(Collections.singletonList("https://www.w3.org/ns/activitystreams#Public")); + note.setCc(Collections.singletonList(followersUri(msg.getUser()))); + note.setPublished(msg.getTimestamp()); + note.setContent(MessageUtils.formatMessage(msg.getText())); + if (StringUtils.isNotBlank(msg.getAttachmentType())) { + Link attachment = new Link(); + attachment.setHref(msg.getAttachment().getMedium().getUrl()); + note.setAttachment(attachment); + } + 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) { } } diff --git a/juick-server/src/main/java/com/juick/server/SignatureManager.java b/juick-server/src/main/java/com/juick/server/SignatureManager.java index d89919f0..268b6543 100644 --- a/juick-server/src/main/java/com/juick/server/SignatureManager.java +++ b/juick-server/src/main/java/com/juick/server/SignatureManager.java @@ -52,7 +52,7 @@ public class SignatureManager { //boolean valid = verifySignature(Signature.fromString(requestHeaders.getFirst("Signature")), // keystoreManager.getPublicKey(), "POST", inbox.getPath(), headers); ResponseEntity response = new RestTemplate().postForEntity(inbox, request, Void.class); - logger.info("accepted follower: {}", response.getStatusCode().is2xxSuccessful()); + logger.info("accepted follower: {}", response.getStatusCodeValue()); } public boolean verifySignature(String signatureString, URI actor, String method, String path, Map headers) { Context context = getContext(actor); diff --git a/juick-server/src/main/java/com/juick/server/api/activity/Profile.java b/juick-server/src/main/java/com/juick/server/api/activity/Profile.java index 656d85dd..7b854d52 100644 --- a/juick-server/src/main/java/com/juick/server/api/activity/Profile.java +++ b/juick-server/src/main/java/com/juick/server/api/activity/Profile.java @@ -1,6 +1,7 @@ package com.juick.server.api.activity; import com.juick.User; +import com.juick.server.ActivityPubManager; import com.juick.server.KeystoreManager; import com.juick.server.SignatureManager; import com.juick.server.api.activity.model.*; @@ -43,6 +44,8 @@ public class Profile { @Inject private SignatureManager signatureManager; @Inject + private ActivityPubManager activityPubManager; + @Inject private ApplicationEventPublisher applicationEventPublisher; @Value("${web_domain:localhost}") private String domain; @@ -55,11 +58,9 @@ public class Profile { 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.setId(activityPubManager.personUri(user)); + person.setUrl(activityPubManager.personUri(user)); person.setName(userName); person.setPreferredUsername(userName); Key publicKey = new Key(); @@ -67,11 +68,10 @@ public class Profile { 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()); + person.setInbox(activityPubManager.inboxUri()); + person.setOutbox(activityPubManager.outboxUri(user)); + person.setFollowers(activityPubManager.followersUri(user)); + person.setFollowing(activityPubManager.followingUri(user)); UriComponentsBuilder image = UriComponentsBuilder.fromUriString(baseImagesUri); image.path(String.format("/a/%d.png", user.getUid())); Image avatar = new Image(); @@ -103,23 +103,9 @@ public class Profile { 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(); + String followersUri = activityPubManager.followersUri(user); 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()); + List notes = messagesService.getMessages(visitor, mids).stream().map(activityPubManager::makeNote).collect(Collectors.toList()); Person person = new Person(); person.setName(user.getName()); OrderedCollectionPage page = new OrderedCollectionPage(); diff --git a/juick-server/src/main/java/com/juick/server/api/activity/model/Note.java b/juick-server/src/main/java/com/juick/server/api/activity/model/Note.java index ff64c4b9..c15cd8d3 100644 --- a/juick-server/src/main/java/com/juick/server/api/activity/model/Note.java +++ b/juick-server/src/main/java/com/juick/server/api/activity/model/Note.java @@ -1,5 +1,10 @@ package com.juick.server.api.activity.model; +import com.juick.Message; +import com.juick.util.MessageUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collections; import java.util.List; public class Note extends Context { diff --git a/juick-server/src/main/java/com/juick/service/ActivityPubService.java b/juick-server/src/main/java/com/juick/service/ActivityPubService.java new file mode 100644 index 00000000..6200a75e --- /dev/null +++ b/juick-server/src/main/java/com/juick/service/ActivityPubService.java @@ -0,0 +1,53 @@ +package com.juick.service; + +import com.juick.User; +import com.juick.model.AnonymousUser; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import java.util.List; + +@Repository +public class ActivityPubService extends BaseJdbcService implements SocialService { + @Value("${ap_base_uri:http://localhost:8080/}") + private String baseUri; + @Inject + private UserService userService; + + @Transactional(readOnly = true) + @Override + public @Nonnull User getUserByAccountUri(String acct) { + UriComponents baseUriComponents = UriComponentsBuilder.fromUriString(baseUri).build(); + UriComponents acctComponents = UriComponentsBuilder.fromUriString(acct).build(); + if (acctComponents.getHost().equals(baseUriComponents.getHost())) { + // /u/ugnich -> ugnich + String userName = acctComponents.getPath().substring(3); + return userService.getUserByName(userName); + } + return AnonymousUser.INSTANCE; + } + + @Transactional(readOnly = true) + @Override + public @Nonnull List getFollowers(User user) { + return getJdbcTemplate().queryForList("SELECT acct FROM followers WHERE user_id=?", String.class, user.getUid()); + } + + @Transactional + @Override + public void addFollower(User user, String acct) { + getJdbcTemplate().update("INSERT INTO followers(user_id, acct) " + + "VALUES(?, ?)", user.getUid(), acct); + } + + @Transactional + @Override + public void removeFollower(User user, String acct) { + getJdbcTemplate().update("DELETE FROM followers WHERE user_id=? AND acct=?", user.getUid(), acct); + } +} diff --git a/juick-server/src/main/resources/db/migration/V1.4__ActivityPub followers.sql b/juick-server/src/main/resources/db/migration/V1.4__ActivityPub followers.sql new file mode 100644 index 00000000..16b39f62 --- /dev/null +++ b/juick-server/src/main/resources/db/migration/V1.4__ActivityPub followers.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS `followers` ( + `user_id` int(10) unsigned DEFAULT NULL, + `acct` char(64) NOT NULL, + `ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY `acct` (`acct`), + foreign key (user_id) references users(id) +); \ No newline at end of file -- cgit v1.2.3