From 810bf6b830a696ff1cef9d850cd6b669a8af04da Mon Sep 17 00:00:00 2001 From: marisa Date: Thu, 22 Feb 2024 20:58:36 -0300 Subject: [PATCH] Improve ActivityStream objects - Add optional fields - Parse json correctly - Add print helper --- dub.sdl | 3 +- dub.selections.json | 1 + source/ap/activity_stream.d | 317 ++++++++++++++++++++++++++++++++++-- source/ap/util.d | 78 +++++++++ source/main.d | 34 ++-- 5 files changed, 399 insertions(+), 34 deletions(-) create mode 100644 source/ap/util.d diff --git a/dub.sdl b/dub.sdl index 12e5264..360c4f1 100644 --- a/dub.sdl +++ b/dub.sdl @@ -8,5 +8,4 @@ dependency "handy-httpd" version="~>8.2.0" dependency "ddbc" version="~>0.5.8" dependency "requests" version="~>2.1.3" dependency "hibernated" version="~>0.4.0" -// subConfiguration "ddbc" "SQLite" -// subConfiguration "ddbc" "PGSQL" +dependency "openssl" version="~>3.3.3" diff --git a/dub.selections.json b/dub.selections.json index f48c731..2792c8f 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -11,6 +11,7 @@ "httparsed": "1.2.1", "mysql-native": "3.1.0", "odbc": "1.0.0", + "openssl": "3.3.3", "path-matcher": "1.1.4", "requests": "2.1.3", "slf4d": "3.0.0", diff --git a/source/ap/activity_stream.d b/source/ap/activity_stream.d index 2d05850..9e3e5e0 100644 --- a/source/ap/activity_stream.d +++ b/source/ap/activity_stream.d @@ -1,23 +1,70 @@ module ap.activity_stream; +import std.algorithm; +import std.array; +import std.datetime; +import std.format; import std.json; import util; +union Store { + string str; + ASObject[] array; + Link[] links; +} + +enum ObjectType { + Object, + String, + Array, +} + /++ * Basic ActivityStream Object * https://www.w3.org/TR/activitypub/#obj +/ class ASObject { // Required fields (for root objects) - string context; /// Must be activitystream context + JSONValue context; /// Must be activitystream context string id; /// AP requirement for unique identifier string type; /// AP requirement for type of object // Optional fields + ASObject attachment; + ASObject attributedTo; + ASObject audience; + string content; + string name; + DateTime endTime; + ASObject generator; + ASObject icon; + ASObject image; + ASObject inReplyTo; + ASObject location; + ASObject preview; + DateTime published; + Collection replies; + DateTime startTime; + string summary; + ASObject tag; + DateTime updated; + Link url; + ASObject to; + ASObject bto; + ASObject cc; + ASObject bcc; + string mediaType; + string duration; JSONValue raw; + protected Store m_store; + protected ObjectType m_objType; + + this() { + } + /++ + Constructs the object according to json source + Params: @@ -25,28 +72,262 @@ class ASObject { + Returns: Object +/ this(JSONValue json) { - with (this) { - const(JSONValue)* ascontext = "@context" in json; + switch (json.type) { + case JSONType.ARRAY: + this.m_objType = ObjectType.Array; + this.m_store.array = json.array.map!(item => new ASObject(item)).array; + break; - if (ascontext) { - switch ((*ascontext).type) { - case JSONType.ARRAY: - context = (*ascontext)[0].str; - break; - - case JSONType.STRING: - context = (*ascontext).str; - break; - - default: - throw new Exception("Invalid @context type for ActivityStream Object"); - } - } + case JSONType.OBJECT: + this.m_objType = ObjectType.Object; + // String properties + optional(json, "context", context); optional(json, "id", id); optional(json, "type", type); + optional(json, "content", content); + optional(json, "name", name); + optional(json, "summary", summary); + optional(json, "mediaType", mediaType); + optional(json, "duration", duration); - raw = json; + // AS Object properties + generator = new ASObject(json.optional!JSONValue("generator")); + icon = new ASObject(json.optional!JSONValue("icon")); + image = new ASObject(json.optional!JSONValue("image")); + inReplyTo = new ASObject(json.optional!JSONValue("inReplyTo")); + location = new ASObject(json.optional!JSONValue("location")); + preview = new ASObject(json.optional!JSONValue("preview")); + tag = new ASObject(json.optional!JSONValue("tag")); + to = new ASObject(json.optional!JSONValue("to")); + bto = new ASObject(json.optional!JSONValue("bto")); + cc = new ASObject(json.optional!JSONValue("cc")); + bcc = new ASObject(json.optional!JSONValue("bcc")); + + const(JSONValue)* j; + + // Date properties + // if ((j = "endTime" in json) != null) + // endTime = DateTime.fromISOString(json["endTime"].str); + + // if ((j = "published" in json) != null) + // published = DateTime.fromISOString(json["published"].str); + + // if ((j = "startTime" in json) != null) + // startTime = DateTime.fromISOString(json["startTime"].str); + + // if ((j = "updated" in json) != null) + // updated = DateTime.fromISOString(json["updated"].str); + + url = new Link(json.optional!JSONValue("url")); + + break; + + case JSONType.STRING: + this.m_objType = ObjectType.String; + this.m_store.str = json.str; + break; + + case JSONType.NULL: + break; + + default: + throw new Exception("Unrecognized ActivityStream Object JSON format: " ~ json.type); } + + raw = json; + } + + string stringRep(int indentation = 0, string indentStr = " ") const { + string[] output; + string indent = ""; + string j; + + if (this.m_objType == ObjectType.String) + return format("%s%s\n", indent, this.m_store.str); + + if (this.m_objType == ObjectType.Array) { + output ~= "["; + + foreach (obj; this.m_store.array) + output ~= format("%s%s%s%s,", indent, indentStr, indentStr, obj.stringRep(indentation + 1)); + + output ~= format("%s%s]", indent, indentStr); + + return output.length == 2 ? "" : output.join("\n"); + } + + for (int i = 0; i < indentation; i++) + indent ~= indentStr; + + output ~= format("%s {", this.type); + + if (id) + output ~= format("%s%sid = %s", indent, indentStr, id); + + if (content) + output ~= format("%s%scontent = %s", indent, indentStr, content); + + if (name) + output ~= format("%s%sname = %s", indent, indentStr, name); + + // if (summary) + // output ~= format("%s%ssummary = %s", indent, indentStr, summary); + + if (mediaType) + output ~= format("%s%smediaType = %s", indent, indentStr, mediaType); + + if (duration) + output ~= format("%s%sduration = %s", indent, indentStr, duration); + + if (url && (j = url.stringRep(indentation + 1)) != string.init) + output ~= format("%s%surl = %s", indent, indentStr, j); + + if (icon && (j = icon.stringRep(indentation + 1)) != string.init) + output ~= format("%s%sicon = %s", indent, indentStr, j); + + if (image && (j = image.stringRep(indentation + 1)) != string.init) + output ~= format("%s%simage = %s", indent, indentStr, j); + + if (tag && (j = tag.stringRep(indentation + 1)) != string.init) + output ~= format("%s%stag = %s", indent, indentStr, j); + + output ~= indent ~ "}"; + + return output.length == 2 ? "" : output.join("\n"); + } +} + +class Link { + string type; + string href; + string[] rel; + string mediaType; + string name; + int height, width; + ASObject preview; + + protected Store m_store; + protected ObjectType m_objType; + + JSONValue raw; + + this() { + } + + this(JSONValue json) { + switch (json.type) { + case JSONType.ARRAY: + m_objType = ObjectType.Array; + m_store.links = json.array.map!(item => new Link(item)).array; + break; + + case JSONType.OBJECT: + m_objType = ObjectType.Object; + + optional(json, "type", type); + optional(json, "href", href); + optional(json, "mediaType", mediaType); + optional(json, "name", name); + optional(json, "height", height); + optional(json, "width", width); + + const(JSONValue)* j; + if ((j = "rel" in json) != null) + rel = (*j).array.map!(item => item.str).array; + + preview = new ASObject(json.optional!JSONValue("preview")); + break; + + case JSONType.STRING: + m_objType = ObjectType.String; + m_store.str = json.str; + break; + + case JSONType.NULL: + break; + + default: + throw new Exception("Unrecognized ActivityStream Link JSON format"); + } + + this.raw = json; + } + + string stringRep(int indentation = 0, string indentStr = " ") const { + string[] output; + string indent = ""; + string j; + + if (this.m_objType == ObjectType.String) + return format("%s%s", indent, this.m_store.str); + + for (int i = 0; i < indentation; i++) + indent ~= indentStr; + + output ~= format("%s {", this.type); + + if (href) + output ~= format("%s%shref = %s", indent, indentStr, href); + + if (mediaType) + output ~= format("%s%smediaType = %s", indent, indentStr, mediaType); + + if (name) + output ~= format("%s%sname = %s", indent, indentStr, name); + + if (height) + output ~= format("%s%sheight = %d", indent, indentStr, height); + + if (width) + output ~= format("%s%swidth = %d", indent, indentStr, width); + + if (preview && (j = preview.stringRep(indentation + 1)) != string.init) + output ~= format("%s%spreview = %s", indent, indentStr, j); + + output ~= indent ~ "}"; + + if (output.length == 2) + return ""; + + return output.join("\n"); + } +} + +class Collection : ASObject { + int totalItems; + CollectionPage current; + CollectionPage first; + CollectionPage last; + ASObject items; + + this(JSONValue json) { + super(json); + + if (this.m_objType != ObjectType.Object) + throw new Exception("Wrong Collection format?"); + + optional(json, "totalItems", totalItems); + current = new CollectionPage(json.optional!JSONValue("current")); + first = new CollectionPage(json.optional!JSONValue("first")); + last = new CollectionPage(json.optional!JSONValue("last")); + items = new ASObject(json.optional!JSONValue("items")); + } +} + +class CollectionPage : Collection { + CollectionPage partOf; + CollectionPage next; + CollectionPage prev; + + this(JSONValue json) { + super(json); + + if (this.m_objType != ObjectType.Object) + throw new Exception("Wrong CollectionPage format?"); + + partOf = new CollectionPage(json.optional!JSONValue("partOf")); + next = new CollectionPage(json.optional!JSONValue("next")); + prev = new CollectionPage(json.optional!JSONValue("prev")); } } diff --git a/source/ap/util.d b/source/ap/util.d new file mode 100644 index 0000000..9b35686 --- /dev/null +++ b/source/ap/util.d @@ -0,0 +1,78 @@ +module ap.util; + +import std.format; +import std.json; + +import requests; +import slf4d; + +import singletons; +import net.request_pool; +import webfinger; +import ap.errors; +import ap.actor; +import ap.activity_stream; + +enum { + ActivityJson = "application/activity+json", + ActivityJsonLd = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`, +} + +/++ + + Fetches remote actor based on WebFinger link + + Params: + + wf = WebFinger link to user + + Returns: User Actor JSON representation + +/ +JSONValue apFetchRemoteUser(JSONValue wf) { + PRequest rq = PRequest(); + rq.url = wf["href"].str; + rq.headers["Accept"] = wf["type"].str; + + Response rs = Rp.request(rq, true); + return parseJSON(cast(string) rs.responseBody); +} + +/++ + + Fetches remote ActivityStream object by its id + + Params: + + id = the id of ActivityStream object + + Returns: ActivityStream object + +/ +ASObject apFetchASObject(string id, string contentType = ActivityJson) { + PRequest rq = PRequest(); + rq.url = id; + rq.headers["Accept"] = contentType; + + Response rs = Rp.request(rq, true); + return new ASObject(parseJSON(cast(string) rs.responseBody)); +} + +/++ + + Resolves remote user to Actor based on username handle + + Params: + + username = Account username + + Returns: ActivityPub Actor + +/ +ASObject apResolveRemoteUsername(string username) { + JSONValue resource = wfRequestAccount(username); + JSONValue accountLink; + + foreach (link; resource["links"].array) + if ( + link["type"].str == ActivityJsonLd || + link["type"].str == ActivityJson + ) { + accountLink = link; + break; + } + + if (accountLink == JSONValue()) + throw new Exception("No valid activity streams href found for " ~ username); + + string id = accountLink["href"].str; + string contentType = accountLink["type"].str; + + // return new Actor(apFetchASObject(id, contentType)); + return apFetchASObject(id, contentType); +} diff --git a/source/main.d b/source/main.d index 82c767e..bbb8a52 100644 --- a/source/main.d +++ b/source/main.d @@ -12,9 +12,12 @@ import db.db; import net.request_pool; import webfinger; import ap.actor; -import ap.util; + +// import ap.util; import std.math.remainder; import util; +import ap.activity_stream; +import ap.util; void commonInit() { auto provider = new DefaultProvider(true, Levels.DEBUG); @@ -42,6 +45,8 @@ void initDatabase(Config cfg) { } void main() { + import std.stdio; + commonInit(); scope (exit) { @@ -49,23 +54,24 @@ void main() { Rp.stop(); } - generateRSA(); - return; + ASObject actor = apResolveRemoteUsername("@marisa@ak.gensokyo.shop"); + // writeln(actor.raw.toJSON(true)); + writeln(actor.stringRep); - Actor marisa = apResolveRemoteUsername("@admin@localhost:8080"); - infoF!"Actor type: %s"(marisa.type); - infoF!"Actor preferredUsername: %s"(marisa.preferredUsername); - infoF!"Actor sharedInbox: %s"(marisa.endpoints.sharedInbox); + // Actor marisa = apResolveRemoteUsername("@admin@localhost:8080"); + // infoF!"Actor type: %s"(marisa.type); + // infoF!"Actor preferredUsername: %s"(marisa.preferredUsername); + // infoF!"Actor sharedInbox: %s"(marisa.endpoints.sharedInbox); - PRequest followers = PRequest(); + // PRequest followers = PRequest(); - with (followers) { - url = marisa.followers; - headers["Accept"] = "application/activity+json"; - } + // with (followers) { + // url = marisa.followers; + // headers["Accept"] = "application/activity+json"; + // } - Response rs = Rp.request(followers, true); - infoF!"followers: %s"(rs.responseBody); + // Response rs = Rp.request(followers, true); + // infoF!"followers: %s"(rs.responseBody); } //int main() {