Skip to content

Commit

Permalink
Refactor ActivityPub inbox handling to be more extensible
Browse files Browse the repository at this point in the history
  • Loading branch information
grishka committed Jun 27, 2020
1 parent 9ddf2ac commit 1ead73f
Show file tree
Hide file tree
Showing 28 changed files with 882 additions and 577 deletions.
2 changes: 2 additions & 0 deletions src/main/java/smithereen/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public static void main(String[] args){
throw new RuntimeException(x);
}

ActivityPubRoutes.registerActivityHandlers();

ipAddress(Config.serverIP);
port(Config.serverPort);
if(Config.staticFilesPath!=null)
Expand Down
68 changes: 68 additions & 0 deletions src/main/java/smithereen/ObjectLinkResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package smithereen;

import org.jetbrains.annotations.NotNull;

import java.net.URI;
import java.sql.SQLException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import smithereen.activitypub.objects.ActivityPubObject;
import smithereen.data.ForeignUser;
import smithereen.data.Post;
import smithereen.data.User;
import smithereen.storage.PostStorage;
import smithereen.storage.UserStorage;

import static smithereen.Utils.parseIntOrDefault;

public class ObjectLinkResolver{

private static final Pattern POSTS=Pattern.compile("^/posts/(\\d+)$");
private static final Pattern USERS=Pattern.compile("^/users/(\\d+)$");

private static Post getPost(String _id) throws SQLException{
int id=parseIntOrDefault(_id, 0);
if(id==0)
throw new ObjectNotFoundException("Invalid local post ID");
Post post=PostStorage.getPostByID(id);
if(post==null)
throw new ObjectNotFoundException("Post with ID "+id+" not found");
return post;
}

private static User getUser(String _id) throws SQLException{
int id=parseIntOrDefault(_id, 0);
if(id==0)
throw new ObjectNotFoundException();
User user=UserStorage.getById(id);
if(user==null || user instanceof ForeignUser)
throw new ObjectNotFoundException();
return user;
}

@NotNull
public static ActivityPubObject resolve(URI link) throws SQLException{
if(!Config.isLocal(link)){
User user=UserStorage.getUserByActivityPubID(link);
if(user!=null)
return user;
Post post=PostStorage.getPostByID(link);
if(post!=null)
return post;
throw new ObjectNotFoundException("Can't resolve remote object: "+link);
}

Matcher matcher=POSTS.matcher(link.getPath());
if(matcher.find()){
return getPost(matcher.group(1));
}

matcher=USERS.matcher(link.getPath());
if(matcher.find()){
return getUser(matcher.group(1));
}

throw new ObjectNotFoundException("Invalid local URI");
}
}
15 changes: 15 additions & 0 deletions src/main/java/smithereen/activitypub/ActivityHandlerContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package smithereen.activitypub;

import smithereen.data.ForeignUser;

public class ActivityHandlerContext{
private String origRequestBody;
public final ForeignUser ldSignatureOwner;
public final ForeignUser httpSignatureOwner;

public ActivityHandlerContext(String origRequestBody, ForeignUser ldSignatureOwner, ForeignUser httpSignatureOwner){
this.origRequestBody=origRequestBody;
this.ldSignatureOwner=ldSignatureOwner;
this.httpSignatureOwner=httpSignatureOwner;
}
}
9 changes: 9 additions & 0 deletions src/main/java/smithereen/activitypub/ActivityPubWorker.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import smithereen.activitypub.objects.LinkOrObject;
import smithereen.activitypub.objects.Mention;
import smithereen.activitypub.objects.Tombstone;
import smithereen.activitypub.objects.activities.Accept;
import smithereen.activitypub.objects.activities.Create;
import smithereen.activitypub.objects.activities.Delete;
import smithereen.activitypub.objects.activities.Follow;
Expand Down Expand Up @@ -157,6 +158,14 @@ public void sendFollowActivity(User self, ForeignUser target){
executor.submit(new SendOneActivityRunnable(follow, target.inbox, self));
}

public void sendAcceptFollowActivity(ForeignUser actor, User self, Follow follow){
Accept accept=new Accept();
accept.actor=new LinkOrObject(self.activityPubID);
accept.object=new LinkOrObject(follow);
accept.activityPubID=Config.localURI("/"+self.username+"#acceptFollow"+actor.id);
executor.submit(new SendOneActivityRunnable(accept, actor.inbox, self));
}

public void sendRejectFriendRequestActivity(User self, ForeignUser target){
Follow follow=new Follow();
follow.actor=new LinkOrObject(self.activityPubID);
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/smithereen/activitypub/ActivityTypeHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package smithereen.activitypub;

import java.sql.SQLException;

import smithereen.activitypub.objects.Activity;
import smithereen.activitypub.objects.ActivityPubObject;
import smithereen.activitypub.objects.Actor;

public abstract class ActivityTypeHandler<A extends Actor, T extends Activity, O extends ActivityPubObject>{
public abstract void handle(ActivityHandlerContext context, A actor, T activity, O object) throws SQLException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package smithereen.activitypub;

import java.sql.SQLException;

import smithereen.BadRequestException;
import smithereen.activitypub.objects.Activity;
import smithereen.activitypub.objects.ActivityPubObject;
import smithereen.activitypub.objects.Actor;

public abstract class DoublyNestedActivityTypeHandler<A extends Actor, T extends Activity, N extends Activity, NN extends Activity, O extends ActivityPubObject> extends NestedActivityTypeHandler<A, T, N, O>{
public abstract void handle(ActivityHandlerContext context, A actor, T activity, N nested, NN innerNested, O object) throws SQLException;

@Override
public final void handle(ActivityHandlerContext context, A actor, T activity, N nested, O object) throws SQLException{
if(nested.object.object==null)
throw new BadRequestException("Nested activity must not be a link");
if(!(nested.object.object instanceof Activity))
throw new BadRequestException("Nested activity must be an Activity subtype");
Activity a=(Activity) nested.object.object;
handle(context, actor, activity, nested, (NN)a, object);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package smithereen.activitypub;

import java.sql.SQLException;

import smithereen.BadRequestException;
import smithereen.activitypub.objects.Activity;
import smithereen.activitypub.objects.ActivityPubObject;
import smithereen.activitypub.objects.Actor;

public abstract class NestedActivityTypeHandler<A extends Actor, T extends Activity, N extends Activity, O extends ActivityPubObject> extends ActivityTypeHandler<A, T, O>{
public abstract void handle(ActivityHandlerContext context, A actor, T activity, N nested, O object) throws SQLException;

@Override
public final void handle(ActivityHandlerContext context, A actor, T activity, O object) throws SQLException{
if(activity.object.object==null)
throw new BadRequestException("Nested activity must not be a link");
if(!(activity.object.object instanceof Activity))
throw new BadRequestException("Nested activity must be an Activity subtype");
Activity a=(Activity) activity.object.object;
handle(context, actor, activity, (N)a, object);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package smithereen.activitypub.handlers;

import java.sql.SQLException;

import smithereen.ObjectNotFoundException;
import smithereen.activitypub.ActivityHandlerContext;
import smithereen.activitypub.NestedActivityTypeHandler;
import smithereen.activitypub.objects.activities.Accept;
import smithereen.activitypub.objects.activities.Follow;
import smithereen.data.ForeignUser;
import smithereen.data.User;
import smithereen.storage.UserStorage;

public class AcceptFollowPersonHandler extends NestedActivityTypeHandler<ForeignUser, Accept, Follow, User>{
@Override
public void handle(ActivityHandlerContext context, ForeignUser actor, Accept activity, Follow nested, User object) throws SQLException{
User follower=UserStorage.getUserByActivityPubID(nested.actor.link);
if(follower==null)
throw new ObjectNotFoundException("Follower not found");
UserStorage.setFollowAccepted(follower.id, actor.id, true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package smithereen.activitypub.handlers;

import java.io.IOException;
import java.sql.SQLException;
import java.sql.Timestamp;

import smithereen.BadRequestException;
import smithereen.Config;
import smithereen.ObjectNotFoundException;
import smithereen.activitypub.ActivityHandlerContext;
import smithereen.activitypub.ActivityPub;
import smithereen.activitypub.ActivityTypeHandler;
import smithereen.activitypub.objects.ActivityPubObject;
import smithereen.activitypub.objects.activities.Announce;
import smithereen.data.ForeignUser;
import smithereen.data.Post;
import smithereen.data.notifications.Notification;
import smithereen.storage.NewsfeedStorage;
import smithereen.storage.NotificationsStorage;
import smithereen.storage.PostStorage;
import smithereen.storage.UserStorage;

public class AnnounceNoteHandler extends ActivityTypeHandler<ForeignUser, Announce, Post>{
@Override
public void handle(ActivityHandlerContext context, ForeignUser actor, Announce activity, Post post) throws SQLException{
if(post.user==null){
ActivityPubObject _author;
try{
_author=ActivityPub.fetchRemoteObject(post.attributedTo.toString());
}catch(IOException x){
throw new BadRequestException(x.toString());
}
if(!(_author instanceof ForeignUser)){
throw new IllegalArgumentException("Post author isn't a user");
}
ForeignUser author=(ForeignUser) _author;
UserStorage.putOrUpdateForeignUser(author);
post.owner=post.user=author;
}
PostStorage.putForeignWallPost(post);
long time=activity.published==null ? System.currentTimeMillis() : activity.published.getTime();
NewsfeedStorage.putRetoot(actor.id, post.id, new Timestamp(time));

if(!(post.user instanceof ForeignUser)){
Notification n=new Notification();
n.type=Notification.Type.RETOOT;
n.actorID=actor.id;
n.objectID=post.id;
n.objectType=Notification.ObjectType.POST;
NotificationsStorage.putNotification(post.user.id, n);
}
}
}
126 changes: 126 additions & 0 deletions src/main/java/smithereen/activitypub/handlers/CreateNoteHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package smithereen.activitypub.handlers;

import java.io.IOException;
import java.net.URI;
import java.sql.SQLException;
import java.util.ArrayList;

import smithereen.BadRequestException;
import smithereen.activitypub.ActivityHandlerContext;
import smithereen.activitypub.ActivityPub;
import smithereen.activitypub.ActivityPubWorker;
import smithereen.activitypub.ActivityTypeHandler;
import smithereen.activitypub.objects.ActivityPubObject;
import smithereen.activitypub.objects.LinkOrObject;
import smithereen.activitypub.objects.Mention;
import smithereen.activitypub.objects.activities.Create;
import smithereen.data.ForeignUser;
import smithereen.data.Post;
import smithereen.data.User;
import smithereen.data.notifications.NotificationUtils;
import smithereen.storage.PostStorage;
import smithereen.storage.UserStorage;

public class CreateNoteHandler extends ActivityTypeHandler<ForeignUser, Create, Post>{
@Override
public void handle(ActivityHandlerContext context, ForeignUser actor, Create activity, Post post) throws SQLException{
if(!post.attributedTo.equals(actor.activityPubID))
throw new BadRequestException("object.attributedTo and actor.id must match");
if(PostStorage.getPostByID(post.activityPubID)!=null){
// Already exists. Ignore and return 200 OK.
return;
}
if(post.user==null || post.user.id!=actor.id)
throw new BadRequestException("Can only create posts for self");
if(post.owner==null)
throw new BadRequestException("Unknown wall owner (from partOf, which must be an outbox URI if present)");
boolean isPublic=false;
if(post.to==null || post.to.isEmpty()){
if(post.cc==null || post.cc.isEmpty()){
throw new BadRequestException("to or cc are both empty");
}else{
for(LinkOrObject cc:post.cc){
if(cc.link==null)
throw new BadRequestException("post.cc must only contain links");
if(ActivityPub.isPublic(cc.link)){
isPublic=true;
break;
}
}
}
}else{
for(LinkOrObject to:post.to){
if(to.link==null)
throw new BadRequestException("post.to must only contain links");
if(ActivityPub.isPublic(to.link)){
isPublic=true;
break;
}
}
}
if(!isPublic)
throw new BadRequestException("Only public posts are supported");
if(post.user==post.owner && post.inReplyTo==null){
URI followers=actor.getFollowersURL();
boolean addressesAnyFollowers=false;
for(LinkOrObject l:post.to){
if(followers.equals(l.link)){
addressesAnyFollowers=true;
break;
}
}
if(!addressesAnyFollowers){
for(LinkOrObject l:post.cc){
if(followers.equals(l.link)){
addressesAnyFollowers=true;
break;
}
}
}
if(!addressesAnyFollowers){
System.out.println("Dropping this post because it's public but doesn't address any followers");
return;
}
}
if(post.tag!=null){
for(ActivityPubObject tag:post.tag){
if(tag instanceof Mention){
URI uri=((Mention) tag).href;
User mentionedUser=UserStorage.getUserByActivityPubID(uri);
if(mentionedUser==null){
try{
ActivityPubObject _user=ActivityPub.fetchRemoteObject(uri.toString());
if(_user instanceof ForeignUser){
ForeignUser u=(ForeignUser) _user;
UserStorage.putOrUpdateForeignUser(u);
mentionedUser=u;
}
}catch(IOException ignore){}
}
if(mentionedUser!=null){
if(post.mentionedUsers.isEmpty())
post.mentionedUsers=new ArrayList<>();
if(!post.mentionedUsers.contains(mentionedUser))
post.mentionedUsers.add(mentionedUser);
}
}
}
}
if(post.inReplyTo!=null){
if(post.inReplyTo.equals(post.activityPubID))
throw new BadRequestException("Post can't be a reply to itself. This makes no sense.");
Post parent=PostStorage.getPostByID(post.inReplyTo);
if(parent!=null){
post.setParent(parent);
PostStorage.putForeignWallPost(post);
NotificationUtils.putNotificationsForPost(post, parent);
}else{
System.out.println("Don't have parent post "+post.inReplyTo+" for "+post.activityPubID);
ActivityPubWorker.getInstance().fetchReplyThread(post);
}
}else{
PostStorage.putForeignWallPost(post);
NotificationUtils.putNotificationsForPost(post, null);
}
}
}

0 comments on commit 1ead73f

Please sign in to comment.