From 7a2fe26ddf6351f3813a6bfefa15ec576c7fd128 Mon Sep 17 00:00:00 2001 From: Andrew Pietila Date: Sat, 11 Jan 2025 09:42:29 -0600 Subject: [PATCH] Initial commit. --- .gitattributes | 2 + .gitignore | 33 ++++ pom.xml | 95 ++++++++++++ .../relay_server/BrainzRelayApplication.java | 13 ++ .../controllers/ActorController.java | 62 ++++++++ .../controllers/InboxController.java | 141 ++++++++++++++++++ .../controllers/IndexController.java | 16 ++ src/main/resources/application.properties | 1 + 8 files changed, 363 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/social/brainz/relay_server/BrainzRelayApplication.java create mode 100644 src/main/java/social/brainz/relay_server/controllers/ActorController.java create mode 100644 src/main/java/social/brainz/relay_server/controllers/InboxController.java create mode 100644 src/main/java/social/brainz/relay_server/controllers/IndexController.java create mode 100644 src/main/resources/application.properties diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..caf59ac --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5eac309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..faed788 --- /dev/null +++ b/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.1 + + + social.brainz + relay-server + 0.0.1-SNAPSHOT + Brainz Relay + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + org.eclipse.parsson + jakarta.json + 1.1.7 + + + + jakarta.json + jakarta.json-api + 2.1.3 + + + + + com.apicatalog + titanium-json-ld + 1.4.1 + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + 3.4.1 + + + + + org.springframework.boot + spring-boot-devtools + 3.4.1 + + + + + org.springframework.boot + spring-boot-starter-web + 3.4.1 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/src/main/java/social/brainz/relay_server/BrainzRelayApplication.java b/src/main/java/social/brainz/relay_server/BrainzRelayApplication.java new file mode 100644 index 0000000..b0066df --- /dev/null +++ b/src/main/java/social/brainz/relay_server/BrainzRelayApplication.java @@ -0,0 +1,13 @@ +package social.brainz.relay_server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BrainzRelayApplication { + + public static void main(String[] args) { + SpringApplication.run(BrainzRelayApplication.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/social/brainz/relay_server/controllers/ActorController.java b/src/main/java/social/brainz/relay_server/controllers/ActorController.java new file mode 100644 index 0000000..2c492b1 --- /dev/null +++ b/src/main/java/social/brainz/relay_server/controllers/ActorController.java @@ -0,0 +1,62 @@ +package social.brainz.relay_server.controllers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; + + +@SuppressWarnings({ "unchecked", "rawtypes", "unused" }) +@RestController +public class ActorController { + private static Map actorMap = new HashMap(); + + static { + // TODO: Config. + Map publicKeyMap = new HashMap<>(); + publicKeyMap.put("id", "https://relay.brainz.social/actor#main-key"); + publicKeyMap.put("owner", "https://relay.brainz.social/actor"); + // TODO: Real public key + publicKeyMap.put("publicKeyPem", ""); + actorMap.put("publicKey", publicKeyMap); + actorMap.put("inbox", "https://relay.brainz.social/inbox"); + actorMap.put("outbox", "https://relay.brainz.social/outbox"); + actorMap.put("following", "https://relay.brainz.social/following"); + actorMap.put("followers", "https://relay.brainz.social/followers"); + actorMap.put("preferredUsername", "relay"); + Map endpointsMap = new HashMap<>(); + endpointsMap.put("sharedInbox", "https://relay.brainz.social/inbox"); + actorMap.put("endpoints", endpointsMap); + actorMap.put("summary", "BrainzRelay bot"); + actorMap.put("url", "https://relay.brainz.relay/actor"); + List contextList = new ArrayList(); + contextList.add("https://www.w3.org/ns/activitystreams"); + contextList.add("https://w3id.org/security/v1"); + actorMap.put("id", "https://relay.brainz.social/actor"); + actorMap.put("type", "Application"); + actorMap.put("name", "BrainzRelay"); + } + + @GetMapping("/actor") + public ResponseEntity getActor(@RequestHeader(HttpHeaders.ACCEPT) String accept) { + HttpHeaders headers = new HttpHeaders(); + if ( accept.matches("application/activity\\+json")) { + headers.add("Content-Type", "application/activity+json"); + } else if ( accept.matches("application/ld\\+json")) { + headers.add("Content-Type", "application/ld+json"); + } else { + headers.add("Content-Type", "application/activity+json"); + } + return new ResponseEntity<>(actorMap, headers, HttpStatus.OK); + } + +} diff --git a/src/main/java/social/brainz/relay_server/controllers/InboxController.java b/src/main/java/social/brainz/relay_server/controllers/InboxController.java new file mode 100644 index 0000000..67456e5 --- /dev/null +++ b/src/main/java/social/brainz/relay_server/controllers/InboxController.java @@ -0,0 +1,141 @@ +package social.brainz.relay_server.controllers; + +import java.io.StringReader; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.util.UriComponentsBuilder; + +import com.apicatalog.jsonld.JsonLd; +import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.document.JsonDocument; +import com.apicatalog.jsonld.http.media.MediaType; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; + +@Controller +public class InboxController { + @SuppressWarnings("rawtypes") + @PostMapping("/inbox") + public ResponseEntity postInbox(@RequestBody String entity) throws JsonLdError { + System.out.println(entity); + JsonObject result = JsonLd.compact(JsonDocument.of(MediaType.JSON, new StringReader(entity)), + "https://www.w3.org/ns/activitystreams").compactToRelative(false).get(); + String activityType = result.getString("type").intern(); + switch (activityType) { + case "Accept": { + JsonValue resultObject = result.get("object"); + if (resultObject instanceof JsonObject resultObjectAsObject) { + System.out.println(resultObjectAsObject.get("type").toString()); + if (resultObjectAsObject.getString("type").equals("Follow")) { + // TODO: Config. + if (resultObjectAsObject.getString("object").equals("https://relay.brainz.social/actor")) { + // noop + } else { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + } + } else { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + break; + } + case "Reject": { + // TODO: Send undo follow, and remove follow. + break; + } + case "Announce", "Create": { + JsonValue resultObject = result.get("object"); + Map objectToPush = new HashMap<>(); + List contextList = new ArrayList<>(); + contextList.add("https://www.w3.org/ns/activitystreams"); + objectToPush.put("@context", contextList); + // TODO: Config + // TODO: Make a controller for this. + UriComponentsBuilder idBuilder = UriComponentsBuilder + .fromUriString("https://relay.brainz.social/announce"); + idBuilder.queryParam("object", + resultObject instanceof JsonObject resultObjectObject + ? resultObjectObject.getJsonString("id").getChars().toString() + : resultObject instanceof JsonString resultObjectString + ? resultObjectString.getChars().toString() + : resultObject.toString()); + objectToPush.put("id", idBuilder.encode().toUriString()); + // TODO: Config + objectToPush.put("actor", "https://relay.brainz.social/actor"); + objectToPush.put("published", ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT)); + List toList = new ArrayList<>(); + toList.add("https://www.w3.org/ns/activitystreams#Public"); + objectToPush.put("to", toList); + List ccList = new ArrayList<>(); + // TODO: Config + ccList.add("https://relay.brainz.social/"); + ccList.add("https://relay.brainz.social/followers"); + objectToPush.put("cc", ccList); + // JsonObject outputObject = Json.createObjectBuilder(objectToPush).build(); + return new ResponseEntity<>(objectToPush, HttpStatus.OK); + } + case "Follow": + break; + case "Undo": { + JsonValue resultObject = result.get("object"); + if (resultObject instanceof JsonObject resultObjectObject) { + // TODO: Handle Public. + if (resultObjectObject.getJsonString("object").getChars().toString() + .equals("https://relay.brainz.social/actor")) { + Map objectToPush = new HashMap<>(); + List contextList = new ArrayList<>(); + contextList.add("https://www.w3.org/ns/activitystreams"); + objectToPush.put("@context", contextList); + // TODO: Config + UriComponentsBuilder idBuilder = UriComponentsBuilder + .fromUriString("https://relay.brainz.social/undo"); + idBuilder.queryParam("object", + resultObjectObject.getJsonString("id").getChars().toString()); + objectToPush.put("id", idBuilder.encode().toUriString()); + objectToPush.put("type", "Undo"); + // TODO: Config + objectToPush.put("actor", "https://relay.brainz.social/actor"); + Map subObjectToPush = new HashMap<>(); + // TODO: Config + UriComponentsBuilder subIdBuilder = UriComponentsBuilder + .fromUriString("https://relay.brainz.social/follow"); + subIdBuilder.queryParam("target", + resultObjectObject.getJsonString("actor").getChars().toString()); + subObjectToPush.put("id", subIdBuilder.build().toUriString()); + subObjectToPush.put("type", "Follow"); + // TODO: Config + subObjectToPush.put("actor", "https://relay.brainz.social/actor"); + subObjectToPush.put("object", resultObjectObject.getJsonString("actor").getChars().toString()); + objectToPush.put("object", subObjectToPush); + // TODO: Remove listener from relay. + // JsonObject outputObject = Json.createObjectBuilder(objectToPush).build(); + return new ResponseEntity<>(objectToPush, HttpStatus.OK); + } + } + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + case "Delete": + case "Update": + case "Add": + case "Remove": + default: + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + +} diff --git a/src/main/java/social/brainz/relay_server/controllers/IndexController.java b/src/main/java/social/brainz/relay_server/controllers/IndexController.java new file mode 100644 index 0000000..5f3d3e8 --- /dev/null +++ b/src/main/java/social/brainz/relay_server/controllers/IndexController.java @@ -0,0 +1,16 @@ +package social.brainz.relay_server.controllers; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class IndexController { + @GetMapping("/") + public String getIndex(@RequestParam(name = "name", required = false, defaultValue = "World") String name, + Model model) { + model.addAttribute("name", name); + return "index"; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..c187d32 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=Brainz Relay