From e4def788788927b4bb2a8a233fad09b24e5d3168 Mon Sep 17 00:00:00 2001 From: Yvan Date: Tue, 4 Feb 2025 01:45:41 +0000 Subject: [PATCH] Right, we're returning valid JSON again and are once again searchable from real Fedi servers! I'm not at all happy with either approach to generating the JSON here. Need some balance between the built in automation/jackson stuff and the custom/generated things we need to do...) --- .../dev/activitypub/activitypubbot/Bot.java | 2 + .../BotAPActorJsonSerializer.java | 72 +++++++++++++++++++ .../activitypubbot/RestHandler.java | 33 ++++++--- 3 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotAPActorJsonSerializer.java diff --git a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/Bot.java b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/Bot.java index 7e00d3b..d4056fc 100644 --- a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/Bot.java +++ b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/Bot.java @@ -11,11 +11,13 @@ import org.bouncycastle.openssl.PEMWriter; import org.bouncycastle.util.io.pem.PemObject; import java.io.StringWriter; import java.io.IOException; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; /** * POJO for our Bot... */ @Data +@JsonSerialize(using = BotAPActorJsonSerializer.class) public class Bot { @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) diff --git a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotAPActorJsonSerializer.java b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotAPActorJsonSerializer.java new file mode 100644 index 0000000..b65f114 --- /dev/null +++ b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotAPActorJsonSerializer.java @@ -0,0 +1,72 @@ +package dev.activitypub.activitypubbot; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +/** + * There is so much to be derived and generated when serialising a Bot + * instanced for ActivityPub purposes it seems neater to just put all that in + * one place than try to override each individual thing. FIXME: But I'm not + * convinced this is the right approach, there's far too much manual specifying + * and building of strings. So I need to look deeper into better options for + * JSON generation. Part of the problem also is the need to mix in + * configuration/property values to generate some fields. So there's a larger + * design question to be resolved here. + * + * As per: https://www.baeldung.com/jackson-custom-serialization + */ +public class BotAPActorJsonSerializer extends StdSerializer { + + public BotAPActorJsonSerializer() { + this(null); + } + + public BotAPActorJsonSerializer(final Class t) { + super(t); + } + + @Override + public final void serialize(final Bot bot, final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonProcessingException { + // FIXME: these are basically server settings and should come from properties? + // or should they be in the database... as general settings, or as part of the user? + // what are the implications of the domain, can it change and things still be valid? + String domain = "springbot.seth.id.au"; + String scheme = "https"; + + jgen.writeStartObject(); + + // FIXME: got a whole bunch of string values here that should be refactored up or something + jgen.writeArrayFieldStart("@context"); + jgen.writeString("https://www.w3.org/ns/activitystreams"); + jgen.writeString("https://w3id.org/security/v1"); + jgen.writeEndArray(); + + // FIXME: we at least need some sort of "URI generator" + String id = scheme + "://" + domain + "/users/" + bot.getUsername(); + jgen.writeStringField("id", id ); + jgen.writeStringField("inbox", id + "/inbox" ); + jgen.writeStringField("outbox", id + "/outbox" ); + + jgen.writeStringField("preferredUsername", bot.getUsername()); + jgen.writeStringField("name", bot.getName()); + jgen.writeStringField("type", bot.getType()); + jgen.writeStringField("summary", bot.getSummary()); + + jgen.writeBooleanField("manuallyApproveFollowers", bot.isManuallyApproveFollowers()); + jgen.writeBooleanField("indexable", bot.isIndexable()); + + jgen.writeFieldName("publicKey"); + jgen.writeStartObject(); + jgen.writeStringField("id", id + "#main-key" ); + jgen.writeStringField("owner", id ); + jgen.writeStringField("publicKeyPem", bot.getPublicKeyPEMString()); + jgen.writeEndObject(); + jgen.writeEndObject(); + } +} + + diff --git a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/RestHandler.java b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/RestHandler.java index 84d07e6..c688877 100644 --- a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/RestHandler.java +++ b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/RestHandler.java @@ -11,6 +11,10 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.thymeleaf.context.Context; import org.thymeleaf.spring5.SpringTemplateEngine; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.List; +import java.util.Map; /** * Here we handle any JSON/REST requests, which is how ActivityPub instances talk to each other. @@ -25,7 +29,7 @@ public class RestHandler { * Really just an alias to /user/ */ @GetMapping(value = "/@{username}", produces = "application/activity+json") // content type based on Masto request - public ResponseEntity atactor(@PathVariable String username) { + public Bot atactor(@PathVariable String username) { return this.actor( username ); } @@ -33,33 +37,46 @@ public class RestHandler { * Get the bot/user 'actor' data response */ @GetMapping(value = "/users/{username}", produces = "application/activity+json") // content type based on Masto request - public ResponseEntity actor(@PathVariable String username) { + public Bot actor(@PathVariable String username) { Bot bot = botServ.getBotByUsername( username ); if( bot == null ) { - return new ResponseEntity<>("These are not the droids you are looking for.", HttpStatus.NOT_FOUND); + //return new ResponseEntity<>("These are not the droids you are looking for.", HttpStatus.NOT_FOUND); } - return ResponseEntity.ok(bot.toString()); + return bot; } /** * Webfinger the user... */ @GetMapping(value = "/.well-known/webfinger", produces = "application/jrd+json") // content type based on Masto request - public ResponseEntity webfinger(@RequestParam("resource") String resource) { + public String webfinger(@RequestParam("resource") String resource) throws JsonProcessingException { // resource should be of the form: acct:@ // so this should be robustly checked, but for now just yoink out the username int colonPos = resource.indexOf(":"); int atPos = resource.indexOf("@"); if ( colonPos < 0 || atPos < 0 || atPos < colonPos ) { - return new ResponseEntity<>("Incorrect query format",HttpStatus.BAD_REQUEST); + //return new ResponseEntity<>("Incorrect query format",HttpStatus.BAD_REQUEST); } String username = resource.substring(colonPos + 1, atPos); Bot bot = botServ.getBotByUsername( username ); if( bot == null ) { - return new ResponseEntity<>("These are not the droids you are looking for.", HttpStatus.NOT_FOUND); + //return new ResponseEntity<>("These are not the droids you are looking for.", HttpStatus.NOT_FOUND); } - return ResponseEntity.ok(bot.toString()); + + // TODO: So here's a whole other way of generating custom JSON as compared to the BotAPActorJSONSerializer approach + Map response = Map.of( + "subject", bot.getUsername() + "@springbot.seth.id.au", + "links", List.of( + Map.of( + "rel", "self", + "type", "application/activity+json", + "href", "https://springbot.seth.id.au/users/" + bot.getUsername() + ) + ) + ); + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString( response ); } }