From 9100b5bda037fcd1b051b98585744077132320bc Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Sun, 22 Aug 2021 00:13:02 +0300 Subject: Type-safe ActivityStreams deserialization --- src/main/java/com/juick/ActivityPubManager.java | 4 +- .../java/com/juick/www/api/activity/Profile.java | 121 +++++++++------------ .../api/activity/helpers/ContextDeserializer.java | 38 +++++++ .../com/juick/www/api/activity/model/Activity.java | 9 +- .../com/juick/www/api/activity/model/Context.java | 10 +- .../juick/www/api/activity/model/objects/Note.java | 6 +- .../www/api/activity/model/objects/Tombstone.java | 24 ++++ 7 files changed, 135 insertions(+), 77 deletions(-) create mode 100644 src/main/java/com/juick/www/api/activity/helpers/ContextDeserializer.java create mode 100644 src/main/java/com/juick/www/api/activity/model/objects/Tombstone.java (limited to 'src/main') diff --git a/src/main/java/com/juick/ActivityPubManager.java b/src/main/java/com/juick/ActivityPubManager.java index 8aa2ef78..cb4d0b54 100644 --- a/src/main/java/com/juick/ActivityPubManager.java +++ b/src/main/java/com/juick/ActivityPubManager.java @@ -92,7 +92,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene @Override public void processFollowEvent(@Nonnull FollowEvent followEvent) { - String acct = (String)followEvent.getRequest().getObject(); + String acct = followEvent.getRequest().getObject().getId(); logger.info("received follower request to {}", acct); User followedUser = socialService.getUserByAccountUri(acct); if (!followedUser.isAnonymous()) { @@ -195,7 +195,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene Update update = new Update(); update.setId(objectUri + "#update"); update.setActor(me.getId()); - update.setObject(objectUri); + update.setObject(new Context(objectUri)); logger.info("Update to follower {}", follower.getId()); signatureManager.post(me, follower, update); } catch (NoSuchElementException e) { diff --git a/src/main/java/com/juick/www/api/activity/Profile.java b/src/main/java/com/juick/www/api/activity/Profile.java index 72cc849e..751d0cff 100644 --- a/src/main/java/com/juick/www/api/activity/Profile.java +++ b/src/main/java/com/juick/www/api/activity/Profile.java @@ -40,6 +40,7 @@ import com.juick.www.api.activity.model.objects.Note; import com.juick.www.api.activity.model.objects.OrderedCollection; import com.juick.www.api.activity.model.objects.OrderedCollectionPage; import com.juick.www.api.activity.model.objects.Person; +import com.juick.www.api.activity.model.objects.Tombstone; import com.juick.util.HttpNotFoundException; import com.juick.www.WebApp; import com.juick.service.MessagesService; @@ -310,32 +311,52 @@ public class Profile { } } if (activity instanceof Create) { - if (activity.getObject() instanceof Map) { - Map note = (Map) activity.getObject(); - if (note.get("type").equals("Note")) { - URI noteId = URI.create((String) note.get("id")); - if (messagesService.replyExists(noteId)) { - return new ResponseEntity<>(CommandResult.fromString("Reply already exists"), - HttpStatus.OK); - } else { - String inReplyTo = (String) note.get("inReplyTo"); - if (StringUtils.isNotBlank(inReplyTo)) { - if (inReplyTo.startsWith(baseUri)) { - String postId = activityPubManager.postId(inReplyTo); + if (activity.getObject() instanceof Note) { + Note note = (Note) activity.getObject(); + URI noteId = URI.create((String) note.getId()); + if (messagesService.replyExists(noteId)) { + return new ResponseEntity<>(CommandResult.fromString("Reply already exists"), HttpStatus.OK); + } else { + String inReplyTo = (String) note.getInReplyTo(); + if (StringUtils.isNotBlank(inReplyTo)) { + if (inReplyTo.startsWith(baseUri)) { + String postId = activityPubManager.postId(inReplyTo); + User user = new User(); + user.setUri(URI.create(activity.getActor())); + String markdown = remarkConverter.convertFragment((String) note.getContent()); + String commandBody = note.getAttachment() == null ? markdown + : note.getAttachment().stream().map(attachment -> { + String attachmentUrl = attachment.getUrl(); + String attachmentName = attachment.getName(); + return PlainTextFormatter.markdownUrl(attachmentUrl, attachmentName); + }).reduce(markdown, + (currentUrl, nextUrl) -> String.format("%s\n%s", currentUrl, nextUrl)); + + CommandResult result = commandsManager.processCommand(user, + String.format("#%s %s", postId, commandBody), URI.create(StringUtils.EMPTY)); + logger.info(jsonMapper.writeValueAsString(result)); + if (result.getNewMessage().isPresent()) { + messagesService.updateReplyUri(result.getNewMessage().get(), noteId); + return new ResponseEntity<>(result, HttpStatus.OK); + } else { + return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); + } + } else { + Message reply = messagesService.getReplyByUri(inReplyTo); + if (reply != null) { User user = new User(); user.setUri(URI.create(activity.getActor())); - String markdown = remarkConverter.convertFragment((String) note.get("content")); - String commandBody = note.get("attachment") == null ? markdown - : ((List) note.get("attachment")).stream().map(attachmentObj -> { - Map attachment = (Map) attachmentObj; - String attachmentUrl = attachment.get("url"); - String attachmentName = attachment.get("name"); + String markdown = remarkConverter.convertFragment((String) note.getContent()); + // combine note text with attachment urls + String commandBody = note.getAttachment() == null ? markdown + : note.getAttachment().stream().map(attachment -> { + String attachmentUrl = attachment.getUrl(); + String attachmentName = attachment.getName(); return PlainTextFormatter.markdownUrl(attachmentUrl, attachmentName); }).reduce(markdown, (currentUrl, nextUrl) -> String.format("%s\n%s", currentUrl, nextUrl)); - CommandResult result = commandsManager.processCommand(user, - String.format("#%s %s", postId, commandBody), + String.format("#%d/%d %s", reply.getMid(), reply.getRid(), commandBody), URI.create(StringUtils.EMPTY)); logger.info(jsonMapper.writeValueAsString(result)); if (result.getNewMessage().isPresent()) { @@ -344,34 +365,6 @@ public class Profile { } else { return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); } - } else { - Message reply = messagesService.getReplyByUri(inReplyTo); - if (reply != null) { - User user = new User(); - user.setUri(URI.create(activity.getActor())); - String markdown = remarkConverter.convertFragment((String) note.get("content")); - // combine note text with attachment urls - String commandBody = note.get("attachment") == null ? markdown - : ((List) note.get("attachment")).stream() - .map(attachmentObj -> { - Map attachment = (Map) attachmentObj; - String attachmentUrl = attachment.get("url"); - String attachmentName = attachment.get("name"); - return PlainTextFormatter.markdownUrl(attachmentUrl, - attachmentName); - }).reduce(markdown, (currentUrl, nextUrl) -> String - .format("%s\n%s", currentUrl, nextUrl)); - CommandResult result = commandsManager.processCommand(user, - String.format("#%d/%d %s", reply.getMid(), reply.getRid(), commandBody), - URI.create(StringUtils.EMPTY)); - logger.info(jsonMapper.writeValueAsString(result)); - if (result.getNewMessage().isPresent()) { - messagesService.updateReplyUri(result.getNewMessage().get(), noteId); - return new ResponseEntity<>(result, HttpStatus.OK); - } else { - return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); - } - } } } } @@ -379,26 +372,23 @@ public class Profile { } } if (activity instanceof Delete) { - if (activity.getObject() instanceof String) { - // Delete gone user - // TODO: check if it is really deleted and remove copy-paste - if (activity.getActor().equals(activity.getObject())) { - return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), - HttpStatus.ACCEPTED); - } + // Delete gone user + // TODO: check if it is really deleted and remove copy-paste + if (activity.getActor().equals(activity.getObject().getUrl())) { + return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), + HttpStatus.ACCEPTED); } - Map tombstone = (Map) activity.getObject(); - if (tombstone.get("type").equals("Tombstone")) { + + if (activity.getObject() instanceof Tombstone) { + Tombstone tombstone = (Tombstone) activity.getObject(); URI actor = URI.create(activity.getActor()); - URI reply = URI.create((String) tombstone.get("id")); + URI reply = URI.create((String) tombstone.getId()); messagesService.deleteReply(actor, reply); return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), HttpStatus.OK); } } if (activity instanceof Like || activity instanceof Announce) { - String messageUri = activity.getObject() instanceof String ? (String) activity.getObject() - : activity.getObject() instanceof Context ? ((Context) activity.getObject()).getId() - : (String) ((Map) activity.getObject()).get("id"); + String messageUri = activity.getObject().getId(); applicationEventPublisher.publishEvent(new AnnounceEvent(this, activity.getActor(), messageUri)); return new ResponseEntity<>(CommandResult.fromString("Like/announce request accepted"), HttpStatus.OK); } @@ -411,12 +401,9 @@ public class Profile { return new ResponseEntity<>(CommandResult.fromString("Unknown activity"), HttpStatus.NOT_IMPLEMENTED); } if (activity instanceof Delete) { - if (activity.getObject() instanceof String) { - // Delete gone user - if (activity.getActor().equals(activity.getObject())) { - return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), - HttpStatus.ACCEPTED); - } + // Delete gone user + if (activity.getActor().equals(activity.getObject().getId())) { + return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), HttpStatus.ACCEPTED); } } return new ResponseEntity<>(CommandResult.fromString("Can not authenticate"), HttpStatus.UNAUTHORIZED); diff --git a/src/main/java/com/juick/www/api/activity/helpers/ContextDeserializer.java b/src/main/java/com/juick/www/api/activity/helpers/ContextDeserializer.java new file mode 100644 index 00000000..367269ef --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/helpers/ContextDeserializer.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2008-2021, 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 . + */ + +package com.juick.www.api.activity.helpers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.juick.www.api.activity.model.Context; + +import java.io.IOException; + +public class ContextDeserializer extends JsonDeserializer { + @Override + public Context deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonToken jsonToken = p.getCurrentToken(); + if (jsonToken == JsonToken.VALUE_STRING) { + String id = p.getText(); + return new Context(id); + } + return p.readValueAs(Context.class); + } +} diff --git a/src/main/java/com/juick/www/api/activity/model/Activity.java b/src/main/java/com/juick/www/api/activity/model/Activity.java index d2f905af..7cc0b13f 100644 --- a/src/main/java/com/juick/www/api/activity/model/Activity.java +++ b/src/main/java/com/juick/www/api/activity/model/Activity.java @@ -19,6 +19,8 @@ package com.juick.www.api.activity.model; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.juick.www.api.activity.helpers.ActivityIdDeserializer; +import com.juick.www.api.activity.helpers.ContextDeserializer; + import org.apache.commons.lang3.StringUtils; import java.util.UUID; @@ -27,7 +29,7 @@ public abstract class Activity extends Context { @JsonDeserialize(using = ActivityIdDeserializer.class) private String actor; - private Object object; + private Context object; public String getActor() { return actor; @@ -40,11 +42,12 @@ public abstract class Activity extends Context { } } - public Object getObject() { + @JsonDeserialize(using = ContextDeserializer.class) + public Context getObject() { return object; } - public void setObject(Object object) { + public void setObject(Context object) { this.object = object; } } diff --git a/src/main/java/com/juick/www/api/activity/model/Context.java b/src/main/java/com/juick/www/api/activity/model/Context.java index d7b23539..edfa89b1 100644 --- a/src/main/java/com/juick/www/api/activity/model/Context.java +++ b/src/main/java/com/juick/www/api/activity/model/Context.java @@ -30,7 +30,7 @@ import java.util.List; import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property="type") +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property="type", defaultImpl = Context.class) @JsonSubTypes({ @JsonSubTypes.Type(value = Create.class, name = "Create"), @JsonSubTypes.Type(value = Update.class, name = "Update"), @@ -55,7 +55,7 @@ import java.util.Map; @JsonSubTypes.Type(value = Person.class, name = "Person"), @JsonSubTypes.Type(value = Application.class, name = "Application") }) -public abstract class Context { +public class Context { private List context; @@ -120,6 +120,12 @@ public abstract class Context { return response; } + public Context() {} + + public Context(String id) { + setId(id); + } + public String getUrl() { return url; } diff --git a/src/main/java/com/juick/www/api/activity/model/objects/Note.java b/src/main/java/com/juick/www/api/activity/model/objects/Note.java index 66470b5c..7889b172 100644 --- a/src/main/java/com/juick/www/api/activity/model/objects/Note.java +++ b/src/main/java/com/juick/www/api/activity/model/objects/Note.java @@ -26,7 +26,7 @@ public class Note extends Context { private String content; private String attributedTo; private String inReplyTo; - private List attachment; + private List attachment; private List to; private List cc; private boolean sensitive; @@ -48,11 +48,11 @@ public class Note extends Context { } @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - public List getAttachment() { + public List getAttachment() { return attachment; } - public void setAttachment(List attachment) { + public void setAttachment(List attachment) { this.attachment = attachment; } diff --git a/src/main/java/com/juick/www/api/activity/model/objects/Tombstone.java b/src/main/java/com/juick/www/api/activity/model/objects/Tombstone.java new file mode 100644 index 00000000..1a229299 --- /dev/null +++ b/src/main/java/com/juick/www/api/activity/model/objects/Tombstone.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2008-2021, 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 . + */ + +package com.juick.www.api.activity.model.objects; + +import com.juick.www.api.activity.model.Context; + +public class Tombstone extends Context { + +} \ No newline at end of file -- cgit v1.2.3