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 ); } }