Improve ActivityStream objects

- Add optional fields
- Parse json correctly
- Add print helper
This commit is contained in:
2024-02-22 20:58:36 -03:00
parent 7fbdd031ff
commit 810bf6b830
5 changed files with 399 additions and 34 deletions

View File

@@ -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"));
}
}

78
source/ap/util.d Normal file
View File

@@ -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);
}

View File

@@ -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() {