Now generating templated JSON actor and webfinger responses, which work against a Mastodon user search... the borrowed Thymeleaf config stuff to enable JSON templating has broken the HTML templating though. Sigh.
This commit is contained in:
parent
614b3a48dd
commit
5f9f024904
10 changed files with 260 additions and 57 deletions
|
|
@ -33,6 +33,7 @@ dependencies {
|
|||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'org.springframework.shell:spring-shell-starter'
|
||||
implementation 'org.postgresql:postgresql'
|
||||
implementation 'org.thymeleaf:thymeleaf-spring5:3.1.2.RELEASE'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
testImplementation 'org.springframework.shell:spring-shell-starter-test'
|
||||
|
|
|
|||
|
|
@ -20,12 +20,8 @@ public class BotController {
|
|||
@Autowired
|
||||
private BotRepo botRepo;
|
||||
|
||||
private BotService botService;
|
||||
|
||||
@Autowired
|
||||
public BotController(BotService botService) {
|
||||
this.botService = botService;
|
||||
}
|
||||
private BotService botService;
|
||||
|
||||
@GetMapping("/viewbot")
|
||||
public String listAll(Model model) {
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||
* Database shenanigans...
|
||||
*/
|
||||
public interface BotRepo extends JpaRepository<Bot, Long> {
|
||||
|
||||
Bot findByUsername(String username);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,10 +25,15 @@ public class BotService {
|
|||
return repo.save(bot);
|
||||
}
|
||||
|
||||
// so far these methods just being calls to repo make me wonder why we need a service in the first place...
|
||||
public List<Bot> findAll() {
|
||||
return repo.findAll();
|
||||
}
|
||||
|
||||
public Bot getBotByUsername( String username ) {
|
||||
return repo.findByUsername( username );
|
||||
}
|
||||
|
||||
|
||||
// whilst the URI structure of the below are up to the implementor we're using Mastodon as a reference
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@ package dev.activitypub.activitypubbot;
|
|||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.spring5.SpringTemplateEngine;
|
||||
|
||||
/**
|
||||
* Here we handle any JSON/REST requests, which is how ActivityPub instances talk to each other.
|
||||
|
|
@ -11,63 +19,58 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
@RequestMapping( headers = "accept=application/json" )
|
||||
public class RestHandler {
|
||||
|
||||
@Autowired
|
||||
private BotService botServ;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("messageTemplateEngine")
|
||||
protected SpringTemplateEngine jsonTemplater;
|
||||
|
||||
/**
|
||||
* Really just an alias to /user/springbot
|
||||
* TODO: how to auto-map @<user> to /users/<user>
|
||||
* Really just an alias to /user/<username>
|
||||
*/
|
||||
@RequestMapping(value = "/@springbot", headers = "accept=application/json")
|
||||
public String atactor() {
|
||||
return this.actor();
|
||||
@RequestMapping(value = "/@{username}", produces = "application/activity+json") // content type based on Masto request
|
||||
public String atactor(@PathVariable String username) {
|
||||
return this.actor( username );
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the bot/user - ultimately this should check the database for
|
||||
* /user/<user> to pull out the relevant data.
|
||||
* Access the bot/user
|
||||
*/
|
||||
@RequestMapping(value = "/users/springbot", headers = "accept=application/json")
|
||||
public String actor() {
|
||||
// TODO: this needs some sort of object wrapper or template, and data in database
|
||||
return """
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1"
|
||||
],
|
||||
@RequestMapping(value = "/users/{username}", produces = "application/activity+json") // content type based on Masto request
|
||||
public String actor(@PathVariable String username) {
|
||||
|
||||
"id": "https://springbot.seth.id.au/users/springbot",
|
||||
"url": "https://springbot.seth.id.au/@springbot",
|
||||
"inbox": "https://springbot.seth.id.au/users/springbot/inbox",
|
||||
"type": "Person",
|
||||
"preferredUsername": "springbot",
|
||||
"name": "Spring Bot",
|
||||
"manuallyApproveFollowers": false,
|
||||
"indexable": false,
|
||||
"published": "2025-01-24T00:00:00Z",
|
||||
"summary": "<p>A bot written using Java/Spring</p>",
|
||||
"publicKey": {
|
||||
"id": "https://springbot.seth.id.au/users/springbot#main-key",
|
||||
"owner": "https://springap.seth.id.au/users/springbot",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0A5W6M6b+3meJAU0/Fki\\nkMSrEZ6EEThAv4NmyCeDlRbmFQWbh5rWtb69TxkGkkiSFNM3sgg+RSW44Ehn10mL\\nTptfk6oSWFnFHw9MPxmwlWm1Xw8zmp2OMUlI82w11PECFdITJw/1HW73JSVQYfFq\\nWo9rD6nI9G3LPpAB16015NJ9hyeMvz5RA9p9UE540q0l5iJD/l7bxCjHglOQInQX\\neCiR2ErzQSVq3AMhBehoP7HuhKjs8swi8dOgjO3sawqxUyv2+lkesFD2rvxCcXRO\\nBkg/Y7nmJSEcqtcmKYQdObPCIt/wCZNAihJz7dwnGKLE2+JJqPZMer9fAj077OkQ\\neQIDAQAB\\n-----END PUBLIC KEY-----"
|
||||
}
|
||||
}""";
|
||||
Bot bot = botServ.getBotByUsername( username );
|
||||
if( bot == null ) {
|
||||
throw new ResponseStatusException( HttpStatus.NOT_FOUND, "These are not the droids you are looking for" );
|
||||
}
|
||||
// TODO: kludgetown... we should generate JSON programmatically rather than templates
|
||||
final Context ctx = new Context();
|
||||
ctx.setVariable("bot", bot);
|
||||
final String json = jsonTemplater.process("json/actor", ctx);
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Webfinger the user...
|
||||
*/
|
||||
@GetMapping("/.well-known/webfinger")
|
||||
public String webfinger() {
|
||||
return """
|
||||
{
|
||||
"subject": "acct:springbot@springbot.seth.id.au",
|
||||
@GetMapping(value = "/.well-known/webfinger", produces = "application/jrd+json") // content type based on Masto request
|
||||
public String webfinger(@RequestParam("resource") String resource) {
|
||||
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": "https://springbot.seth.id.au/users/springbot"
|
||||
}
|
||||
]
|
||||
}""";
|
||||
}
|
||||
// resource should be of the form: acct:<username>@<domain>
|
||||
// so this should be robustly checked, but for now just yoink out the username
|
||||
String username = resource.substring(resource.indexOf(":") + 1, resource.indexOf("@"));
|
||||
if ( username == null) {
|
||||
throw new ResponseStatusException( HttpStatus.NOT_FOUND, "Malformed" );
|
||||
}
|
||||
Bot bot = botServ.getBotByUsername( username );
|
||||
if( bot == null ) {
|
||||
throw new ResponseStatusException( HttpStatus.NOT_FOUND, "These are not the droids you are looking for" );
|
||||
}
|
||||
// TODO: kludgetown... we should generate JSON programmatically rather than templates
|
||||
final Context ctx = new Context();
|
||||
ctx.setVariable("bot", bot);
|
||||
final String json = jsonTemplater.process("json/webfinger", ctx);
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
package dev.activitypub.activitypubbot;
|
||||
// Derived from: https://github.com/RAJNISH3/ThymeLeafApp/blob/master/src/main/java/com/sample/thymeleaf/ThymeleafConfiguration.java
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.thymeleaf.spring5.SpringTemplateEngine;
|
||||
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
public class ThymeleafConf {
|
||||
|
||||
/**
|
||||
* Path to root package/directory in which the different types of message templates are found.
|
||||
*/
|
||||
public static final String TEMPLATES_BASE = "classpath:/templates/";
|
||||
/** Pattern relative to templates base used to match JSON templates. */
|
||||
public static final String JSON_TEMPLATES_RESOLVE_PATTERN = "json/*";
|
||||
public static final String HTML_TEMPLATES_RESOLVE_PATTERN = "html/*";
|
||||
|
||||
/**
|
||||
* Creates the template resolver that retrieves text message payloads.
|
||||
*
|
||||
* @return Template resolver.
|
||||
*/
|
||||
@Autowired
|
||||
ThymeleafRemoteResourceResolver thymeRemoteConfig;
|
||||
|
||||
|
||||
/**
|
||||
* Creates the template resolver that retrieves JSON message payloads.
|
||||
*
|
||||
* @return Template resolver.
|
||||
*/
|
||||
@Bean
|
||||
public SpringResourceTemplateResolver jsonMessageTemplateResolver() {
|
||||
SpringResourceTemplateResolver theResourceTemplateResolver = new SpringResourceTemplateResolver();
|
||||
theResourceTemplateResolver.setPrefix(TEMPLATES_BASE);
|
||||
theResourceTemplateResolver.setResolvablePatterns(Collections.singleton(JSON_TEMPLATES_RESOLVE_PATTERN));
|
||||
theResourceTemplateResolver.setSuffix(".json");
|
||||
/*
|
||||
* There is no json template mode so the next line has been commented out. Thymeleaf will recognize the ".json"
|
||||
* template resource suffix so there is no need to set a template mode.
|
||||
*/
|
||||
// theResourceTemplateResolver.setTemplateMode("json");
|
||||
theResourceTemplateResolver.setCharacterEncoding("UTF-8");
|
||||
theResourceTemplateResolver.setCacheable(false);
|
||||
theResourceTemplateResolver.setOrder(2);
|
||||
return theResourceTemplateResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the template resolver that retrieves JSON message payloads.
|
||||
*
|
||||
* @return Template resolver.
|
||||
*/
|
||||
@Bean
|
||||
public SpringResourceTemplateResolver htmlMessageTemplateResolver() {
|
||||
SpringResourceTemplateResolver theResourceTemplateResolver = new SpringResourceTemplateResolver();
|
||||
theResourceTemplateResolver.setPrefix(TEMPLATES_BASE);
|
||||
theResourceTemplateResolver.setResolvablePatterns(Collections.singleton(HTML_TEMPLATES_RESOLVE_PATTERN));
|
||||
theResourceTemplateResolver.setSuffix(".html");
|
||||
/*
|
||||
* There is no json template mode so the next line has been commented out. Thymeleaf will recognize the ".json"
|
||||
* template resource suffix so there is no need to set a template mode.
|
||||
*/
|
||||
// theResourceTemplateResolver.setTemplateMode("json");
|
||||
theResourceTemplateResolver.setCharacterEncoding("UTF-8");
|
||||
theResourceTemplateResolver.setCacheable(false);
|
||||
theResourceTemplateResolver.setOrder(2);
|
||||
return theResourceTemplateResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the template engine for all message templates.
|
||||
*
|
||||
* @param inTemplateResolvers
|
||||
* Template resolver for different types of messages etc. Note that any template resolvers defined
|
||||
* elsewhere will also be included in this collection.
|
||||
* @return Template engine.
|
||||
*/
|
||||
@Bean
|
||||
public SpringTemplateEngine messageTemplateEngine(
|
||||
final Collection<SpringResourceTemplateResolver> inTemplateResolvers) {
|
||||
final SpringTemplateEngine theTemplateEngine = new SpringTemplateEngine();
|
||||
for (SpringResourceTemplateResolver theTemplateResolver : inTemplateResolvers) {
|
||||
theTemplateEngine.addTemplateResolver(theTemplateResolver);
|
||||
theTemplateEngine.addTemplateResolver(thymeRemoteConfig);
|
||||
}
|
||||
return theTemplateEngine;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package dev.activitypub.activitypubbot;
|
||||
// From: https://github.com/RAJNISH3/ThymeLeafApp/blob/master/src/main/java/com/sample/thymeleaf/ThymeleafRemoteResourceResolver.java
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.templateresolver.StringTemplateResolver;
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
|
||||
@Service
|
||||
public class ThymeleafRemoteResourceResolver extends StringTemplateResolver {
|
||||
|
||||
private final static String PREFIX = "";
|
||||
|
||||
public ThymeleafRemoteResourceResolver() {
|
||||
setResolvablePatterns(Collections.singleton("*"));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration, String ownerTemplate,
|
||||
String template, Map<String, Object> templateResolutionAttributes) {
|
||||
|
||||
// ThymeleafTemplate is our internal object that contains the content.
|
||||
// You should change this to match you're set up.
|
||||
//InputStream inp = this.getClass().getResourceAsStream("/templates/text/personalDetails.txt");//text
|
||||
InputStream inp = this.getClass().getResourceAsStream("/templates/text/personalDtlsLoop.txt");
|
||||
//Thread.currentThread().getContextClassLoader().getResourceAsStream("/templates/text/personalDetails.txt");
|
||||
String s1 = null;
|
||||
try {
|
||||
s1 = convertInputStreamToString(inp);
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (inp != null) {
|
||||
return super.computeTemplateResource(configuration, "txt", s1,
|
||||
templateResolutionAttributes);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static String convertInputStreamToString(InputStream inputStream)
|
||||
throws IOException {
|
||||
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) != -1) {
|
||||
result.write(buffer, 0, length);
|
||||
}
|
||||
|
||||
return result.toString(StandardCharsets.UTF_8.name());
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package dev.activitypub.activitypubbot;
|
|||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
|
@ -19,13 +20,13 @@ public class WebHandler {
|
|||
* handle requests for our "actor" - this presents the web/html view of
|
||||
* the bot
|
||||
*/
|
||||
@RequestMapping("/@springbot")
|
||||
public String atactor() {
|
||||
@RequestMapping("/@{username}")
|
||||
public String atactor(@PathVariable String username) {
|
||||
log.info("WebHandler::atactor");
|
||||
return this.actor();
|
||||
return this.actor( username );
|
||||
}
|
||||
@RequestMapping("/users/springbot")
|
||||
public String actor() {
|
||||
@RequestMapping("/users/{username}")
|
||||
public String actor(@PathVariable String username) {
|
||||
log.info("WebHandler::actor");
|
||||
return "index"; // just flinging out the index page for now
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1"
|
||||
],
|
||||
|
||||
"id": "https://springbot.seth.id.au/users/[(${bot.username})]",
|
||||
"url": "https://springbot.seth.id.au/@[(${bot.username})]",
|
||||
"inbox": "https://springbot.seth.id.au/users/[(${bot.username})]/inbox",
|
||||
"type": "[(${bot.type})]",
|
||||
"preferredUsername": "[(${bot.username})]",
|
||||
"name": "[(${bot.name})]",
|
||||
"manuallyApproveFollowers": false,
|
||||
"indexable": false,
|
||||
"published": "[(${bot.published})]",
|
||||
"summary": "<p>[(${bot.summary})]</p>",
|
||||
"publicKey": {
|
||||
"id": "https://springbot.seth.id.au/users/[(${bot.username})]#main-key",
|
||||
"owner": "https://springbot.seth.id.au/users/[(${bot.username})]",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0A5W6M6b+3meJAU0/Fki\\nkMSrEZ6EEThAv4NmyCeDlRbmFQWbh5rWtb69TxkGkkiSFNM3sgg+RSW44Ehn10mL\\nTptfk6oSWFnFHw9MPxmwlWm1Xw8zmp2OMUlI82w11PECFdITJw/1HW73JSVQYfFq\\nWo9rD6nI9G3LPpAB16015NJ9hyeMvz5RA9p9UE540q0l5iJD/l7bxCjHglOQInQX\\neCiR2ErzQSVq3AMhBehoP7HuhKjs8swi8dOgjO3sawqxUyv2+lkesFD2rvxCcXRO\\nBkg/Y7nmJSEcqtcmKYQdObPCIt/wCZNAihJz7dwnGKLE2+JJqPZMer9fAj077OkQ\\neQIDAQAB\\n-----END PUBLIC KEY-----"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"subject": "acct:[(${bot.username})]@springbot.seth.id.au",
|
||||
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": "https://springbot.seth.id.au/users/[(${bot.username})]"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue