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...)

This commit is contained in:
Yvan 2025-02-04 01:45:41 +00:00
parent 2d026711c1
commit e4def78878
3 changed files with 99 additions and 8 deletions

View file

@ -11,11 +11,13 @@ import org.bouncycastle.openssl.PEMWriter;
import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemObject;
import java.io.StringWriter; import java.io.StringWriter;
import java.io.IOException; import java.io.IOException;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/** /**
* POJO for our Bot... * POJO for our Bot...
*/ */
@Data @Data
@JsonSerialize(using = BotAPActorJsonSerializer.class)
public class Bot { public class Bot {
@Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE)

View file

@ -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<Bot> {
public BotAPActorJsonSerializer() {
this(null);
}
public BotAPActorJsonSerializer(final Class<Bot> 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();
}
}

View file

@ -11,6 +11,10 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.thymeleaf.context.Context; import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.SpringTemplateEngine; 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. * 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/<username> * Really just an alias to /user/<username>
*/ */
@GetMapping(value = "/@{username}", produces = "application/activity+json") // content type based on Masto request @GetMapping(value = "/@{username}", produces = "application/activity+json") // content type based on Masto request
public ResponseEntity<String> atactor(@PathVariable String username) { public Bot atactor(@PathVariable String username) {
return this.actor( username ); return this.actor( username );
} }
@ -33,33 +37,46 @@ public class RestHandler {
* Get the bot/user 'actor' data response * Get the bot/user 'actor' data response
*/ */
@GetMapping(value = "/users/{username}", produces = "application/activity+json") // content type based on Masto request @GetMapping(value = "/users/{username}", produces = "application/activity+json") // content type based on Masto request
public ResponseEntity<String> actor(@PathVariable String username) { public Bot actor(@PathVariable String username) {
Bot bot = botServ.getBotByUsername( username ); Bot bot = botServ.getBotByUsername( username );
if( bot == null ) { 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... * Webfinger the user...
*/ */
@GetMapping(value = "/.well-known/webfinger", produces = "application/jrd+json") // content type based on Masto request @GetMapping(value = "/.well-known/webfinger", produces = "application/jrd+json") // content type based on Masto request
public ResponseEntity<String> webfinger(@RequestParam("resource") String resource) { public String webfinger(@RequestParam("resource") String resource) throws JsonProcessingException {
// resource should be of the form: acct:<username>@<domain> // resource should be of the form: acct:<username>@<domain>
// so this should be robustly checked, but for now just yoink out the username // so this should be robustly checked, but for now just yoink out the username
int colonPos = resource.indexOf(":"); int colonPos = resource.indexOf(":");
int atPos = resource.indexOf("@"); int atPos = resource.indexOf("@");
if ( colonPos < 0 || atPos < 0 || atPos < colonPos ) { 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); String username = resource.substring(colonPos + 1, atPos);
Bot bot = botServ.getBotByUsername( username ); Bot bot = botServ.getBotByUsername( username );
if( bot == null ) { 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<String, Object> 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 );
} }
} }