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 0267522..69a6f6a 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 @@ -2,86 +2,53 @@ package dev.activitypub.activitypubbot; import lombok.Getter; import lombok.Setter; -import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import jakarta.persistence.Id; -import jakarta.persistence.Column; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import org.springframework.data.annotation.CreatedDate; +import lombok.AccessLevel; +import lombok.Data; import java.time.Instant; /** - * Our core Bot (aka user) data as stored persistently in the database. - * This is all the key non-derived data as required by the ActivityPub - * specification. NOTE: This is not a comprehensive implementation! + * POJO for our Bot... */ -@Entity -@EntityListeners(AuditingEntityListener.class) +@Data public class Bot { - @Id - @GeneratedValue(strategy=GenerationType.AUTO) - private Long id; + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) + Long id; // does this even need to be exposed here? + String username; // "preferredUsername: springbot", + String name; // "Spring Bot", + String summary; // "

A bot written using Java/Spring

", - /////////////////////////////////////////////////////////////////////////// - // The next values are user-supplied + @Setter(AccessLevel.NONE) + Instant published; // "2025-01-24T00:00:00Z", - /** - * Username of the bot, i.e. the value after the @: @<username>@<domain> - * Note that in the Activity Pub spec this is encoded as 'preferredUsername', - * we shorten just to username here for clarity and brevity in the code. - * - * @param username the username value - * @return the username value - */ - @Column(nullable=false,unique=true) - @Getter @Setter private String username; // "preferredUsername: springbot", - - /** - * The "friendly" formatted name of the bot, can have spaces, etc. - * - * @param name a presentational name for the bot - * @return the presentational name for the bot - */ - @Column(nullable=true,unique=false) - @Getter @Setter private String name; // "Spring Bot", - - /** - * A bit of text to describe the bot/account, an "about". - * - * @param summary Text describing the bot/account - * @return the summary string - */ - @Column(nullable=true,unique=false) - @Getter @Setter private String summary; // "

A bot written using Java/Spring

", - - /////////////////////////////////////////////////////////////////////////// - // The following values will be auto-generated on bot-creation - - @CreatedDate // TODO: is this really the right approach to auto-timestamp, not sure about all this "auditing" stuff, feels like a misuse - @Getter private Instant published; // "2025-01-24T00:00:00Z", - - // TODO how and where do we generate this beastie?!?! - @Column(nullable=true,unique=false) // FIXME: this isn't true, just easy for now - @Getter private String publicKeyPem; // "-----BEGIN PUBLIC KEY-----\\nMI [...] AB\\n-----END PUBLIC KEY-----" + @Setter(AccessLevel.NONE) + String publicKeyPem; // "-----BEGIN PUBLIC KEY-----\\nMI [...] AB\\n-----END PUBLIC KEY-----" - /////////////////////////////////////////////////////////////////////////// - // These values can just be constants for the timebeing - - @Column(nullable=false,unique=false) - @Getter private static final String type = "Person"; + @Setter(AccessLevel.NONE) + String type = "Person"; - @Column(nullable=false,unique=false) - @Getter private static final boolean manuallyApproveFollowers = false; + @Setter(AccessLevel.NONE) + boolean manuallyApproveFollowers = false; - @Column(nullable=false,unique=false) - @Getter private static final boolean indexable = false; + @Setter(AccessLevel.NONE) + boolean indexable = false; @Override public String toString() { return "Bot: " + this.username; } + + Bot() { + } + Bot( String username, String name, String summary, String type, boolean manuallyApproveFollowers, boolean indexable, String pubicKeyPem ) { + this.username = username; + this.name = name; + this.summary = summary; + this.type = type; + this.manuallyApproveFollowers = manuallyApproveFollowers; + this.indexable = indexable; + this.publicKeyPem = publicKeyPem; + } } + diff --git a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotController.java b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotController.java index b0c7fc2..ec0d629 100644 --- a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotController.java +++ b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotController.java @@ -18,16 +18,13 @@ import lombok.extern.slf4j.Slf4j; public class BotController { @Autowired - private BotRepo botRepo; - - @Autowired - private BotService botService; + private BotService botServ; @GetMapping("/viewbot") public String listAll(Model model) { log.info("WebHandler::viewbot"); - List botlist = botService.findAll(); + List botlist = botServ.findAll(); model.addAttribute("bots", botlist); return "viewbot"; @@ -47,7 +44,7 @@ public class BotController { public String makebotpost(@ModelAttribute("bot") Bot bot) { //log.info(bot); - botRepo.save(bot); + botServ.save(bot); return "makebot_submitted"; } diff --git a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotJdbcRepo.java b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotJdbcRepo.java new file mode 100644 index 0000000..a50380d --- /dev/null +++ b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotJdbcRepo.java @@ -0,0 +1,12 @@ +package dev.activitypub.activitypubbot; + +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; +import java.util.List; + +public interface BotJdbcRepo extends CrudRepository { + Optional findByUsername(String username); + List findAll(); +} + diff --git a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotJpaRepo.java b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotJpaRepo.java new file mode 100644 index 0000000..6bceb43 --- /dev/null +++ b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotJpaRepo.java @@ -0,0 +1,41 @@ +package dev.activitypub.activitypubbot; + +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * CRUD layer? TODO: Does "JPA" make sense in the name here? + */ +@Repository +public class BotJpaRepo implements BotRepo { + + // underlying BotModel storage + private final BotJdbcRepo botJdbcRepo; + + public BotJpaRepo(BotJdbcRepo botJdbcRepo) { + this.botJdbcRepo = botJdbcRepo; + } + + @Override + public Bot save(Bot bot) { + BotModel botModel = BotModel.from(bot); + BotModel saved = botJdbcRepo.save(botModel); + return saved.asBot(); + // this pattern comes from the 'ensembler' MemberRepositoryJdbcAdapter impl + // TODO: in our case I'm not sure we need to be returning a Bot, could be void? + } + + public Optional findByUsername(String username) { + // username is a unique key so technically should only be one of these...? + Optional botModels = botJdbcRepo.findByUsername(username); + return botModels.map(BotModel::asBot); + } + + public List findAll() { + List botModels = botJdbcRepo.findAll(); + return botModels.stream().map(BotModel::asBot).collect(Collectors.toList()); + } +} diff --git a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotModel.java b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotModel.java new file mode 100644 index 0000000..49c7b99 --- /dev/null +++ b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotModel.java @@ -0,0 +1,103 @@ +package dev.activitypub.activitypubbot; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.Instant; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Our core Bot (aka user) data as stored persistently in the database. + * This is all the key non-derived data as required by the ActivityPub + * specification. NOTE: This is not a comprehensive implementation! + */ +@Entity +@Table(name = "bot") +@EntityListeners(AuditingEntityListener.class) +public class BotModel { + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + private Long id; + + + /////////////////////////////////////////////////////////////////////////// + // The next values are user-supplied + + /** + * Username of the bot, i.e. the value after the @: @<username>@<domain> + * Note that in the Activity Pub spec this is encoded as 'preferredUsername', + * we shorten just to username here for clarity and brevity in the code. + * + * @param username the username value + * @return the username value + */ + @Column(nullable=false,unique=true) + @Getter @Setter private String username; // "preferredUsername: springbot", + + /** + * The "friendly" formatted name of the bot, can have spaces, etc. + * + * @param name a presentational name for the bot + * @return the presentational name for the bot + */ + @Column(nullable=true,unique=false) + @Getter @Setter private String name; // "Spring Bot", + + /** + * A bit of text to describe the bot/account, an "about". + * + * @param summary Text describing the bot/account + * @return the summary string + */ + @Column(nullable=true,unique=false) + @Getter @Setter private String summary; // "

A bot written using Java/Spring

", + + /////////////////////////////////////////////////////////////////////////// + // The following values will be auto-generated on bot-creation + + @CreatedDate // TODO: is this really the right approach to auto-timestamp, not sure about all this "auditing" stuff, feels like a misuse + @Getter private Instant published; // "2025-01-24T00:00:00Z", + + // TODO how and where do we generate this beastie?!?! + @Column(nullable=true,unique=false) // FIXME: this isn't true, just easy for now + @Getter private String publicKeyPem; // "-----BEGIN PUBLIC KEY-----\\nMI [...] AB\\n-----END PUBLIC KEY-----" + + @Column(nullable=false,unique=false) + @Getter private String type; + + @Column(nullable=false,unique=false) + @Getter private boolean manuallyApproveFollowers; + + @Column(nullable=false,unique=false) + @Getter private boolean indexable; + + static BotModel from(Bot bot) { + BotModel bm = new BotModel(); + bm.username = bot.getUsername(); + bm.name = bot.getName(); + bm.summary = bot.getSummary(); + bm.type = bot.getType(); + bm.manuallyApproveFollowers = bot.isManuallyApproveFollowers(); + bm.indexable = bot.isIndexable(); + bm.publicKeyPem = bot.getPublicKeyPem(); + return bm; + } + + Bot asBot() { + return new Bot( username, name, summary, type, manuallyApproveFollowers, indexable, publicKeyPem ); + } + + @Override + public String toString() { + return "Bot: " + this.username; + } + +} diff --git a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotRepo.java b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotRepo.java index 999ba05..aef8d5a 100644 --- a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotRepo.java +++ b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotRepo.java @@ -1,10 +1,20 @@ package dev.activitypub.activitypubbot; -import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; +import java.util.List; /** - * Database shenanigans... + * Interface for a Bot respository. + * + * Working to the @Repository documentation and some other examples + * for this, a pattern noted to be "close to the DAO pattern". But I + * am not yet clear on the need/justification for this approach. */ -public interface BotRepo extends JpaRepository { - Bot findByUsername(String username); +public interface BotRepo { + + Bot save(Bot bot); + + Optional findByUsername(String username); + + List findAll(); } diff --git a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotService.java b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotService.java index 1f493fb..5900b59 100644 --- a/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotService.java +++ b/Java/Spring/activitypubbot/src/main/java/dev/activitypub/activitypubbot/BotService.java @@ -31,7 +31,7 @@ public class BotService { } public Bot getBotByUsername( String username ) { - return repo.findByUsername( username ); + return repo.findByUsername( username ).get(); } @@ -66,5 +66,9 @@ public class BotService { public String getOutbox(Bot bot) { return getUsersURI() + bot.getUsername() + "/outbox"; } + + public Bot save(Bot bot) { + return repo.save(bot); + } } 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 0bf8c04..27f2fa2 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 @@ -17,7 +17,6 @@ import org.thymeleaf.spring5.SpringTemplateEngine; * Here we handle any JSON/REST requests, which is how ActivityPub instances talk to each other. */ @RestController -@RequestMapping( headers = "accept=application/json" ) public class RestHandler { @Autowired @@ -32,7 +31,7 @@ public class RestHandler { } /** - * Access the bot/user + * 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) {