Improve ActivityStream objects
- Add optional fields - Parse json correctly - Add print helper
This commit is contained in:
3
dub.sdl
3
dub.sdl
@@ -8,5 +8,4 @@ dependency "handy-httpd" version="~>8.2.0"
|
|||||||
dependency "ddbc" version="~>0.5.8"
|
dependency "ddbc" version="~>0.5.8"
|
||||||
dependency "requests" version="~>2.1.3"
|
dependency "requests" version="~>2.1.3"
|
||||||
dependency "hibernated" version="~>0.4.0"
|
dependency "hibernated" version="~>0.4.0"
|
||||||
// subConfiguration "ddbc" "SQLite"
|
dependency "openssl" version="~>3.3.3"
|
||||||
// subConfiguration "ddbc" "PGSQL"
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"httparsed": "1.2.1",
|
"httparsed": "1.2.1",
|
||||||
"mysql-native": "3.1.0",
|
"mysql-native": "3.1.0",
|
||||||
"odbc": "1.0.0",
|
"odbc": "1.0.0",
|
||||||
|
"openssl": "3.3.3",
|
||||||
"path-matcher": "1.1.4",
|
"path-matcher": "1.1.4",
|
||||||
"requests": "2.1.3",
|
"requests": "2.1.3",
|
||||||
"slf4d": "3.0.0",
|
"slf4d": "3.0.0",
|
||||||
|
|||||||
@@ -1,23 +1,70 @@
|
|||||||
module ap.activity_stream;
|
module ap.activity_stream;
|
||||||
|
|
||||||
|
import std.algorithm;
|
||||||
|
import std.array;
|
||||||
|
import std.datetime;
|
||||||
|
import std.format;
|
||||||
import std.json;
|
import std.json;
|
||||||
|
|
||||||
import util;
|
import util;
|
||||||
|
|
||||||
|
union Store {
|
||||||
|
string str;
|
||||||
|
ASObject[] array;
|
||||||
|
Link[] links;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ObjectType {
|
||||||
|
Object,
|
||||||
|
String,
|
||||||
|
Array,
|
||||||
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
* Basic ActivityStream Object
|
* Basic ActivityStream Object
|
||||||
* https://www.w3.org/TR/activitypub/#obj
|
* https://www.w3.org/TR/activitypub/#obj
|
||||||
+/
|
+/
|
||||||
class ASObject {
|
class ASObject {
|
||||||
// Required fields (for root objects)
|
// 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 id; /// AP requirement for unique identifier
|
||||||
string type; /// AP requirement for type of object
|
string type; /// AP requirement for type of object
|
||||||
|
|
||||||
// Optional fields
|
// 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;
|
JSONValue raw;
|
||||||
|
|
||||||
|
protected Store m_store;
|
||||||
|
protected ObjectType m_objType;
|
||||||
|
|
||||||
|
this() {
|
||||||
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
+ Constructs the object according to json source
|
+ Constructs the object according to json source
|
||||||
+ Params:
|
+ Params:
|
||||||
@@ -25,28 +72,262 @@ class ASObject {
|
|||||||
+ Returns: Object
|
+ Returns: Object
|
||||||
+/
|
+/
|
||||||
this(JSONValue json) {
|
this(JSONValue json) {
|
||||||
with (this) {
|
switch (json.type) {
|
||||||
const(JSONValue)* ascontext = "@context" in json;
|
case JSONType.ARRAY:
|
||||||
|
this.m_objType = ObjectType.Array;
|
||||||
|
this.m_store.array = json.array.map!(item => new ASObject(item)).array;
|
||||||
|
break;
|
||||||
|
|
||||||
if (ascontext) {
|
case JSONType.OBJECT:
|
||||||
switch ((*ascontext).type) {
|
this.m_objType = ObjectType.Object;
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// String properties
|
||||||
|
optional(json, "context", context);
|
||||||
optional(json, "id", id);
|
optional(json, "id", id);
|
||||||
optional(json, "type", type);
|
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
78
source/ap/util.d
Normal 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);
|
||||||
|
}
|
||||||
@@ -12,9 +12,12 @@ import db.db;
|
|||||||
import net.request_pool;
|
import net.request_pool;
|
||||||
import webfinger;
|
import webfinger;
|
||||||
import ap.actor;
|
import ap.actor;
|
||||||
import ap.util;
|
|
||||||
|
// import ap.util;
|
||||||
import std.math.remainder;
|
import std.math.remainder;
|
||||||
import util;
|
import util;
|
||||||
|
import ap.activity_stream;
|
||||||
|
import ap.util;
|
||||||
|
|
||||||
void commonInit() {
|
void commonInit() {
|
||||||
auto provider = new DefaultProvider(true, Levels.DEBUG);
|
auto provider = new DefaultProvider(true, Levels.DEBUG);
|
||||||
@@ -42,6 +45,8 @@ void initDatabase(Config cfg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
import std.stdio;
|
||||||
|
|
||||||
commonInit();
|
commonInit();
|
||||||
|
|
||||||
scope (exit) {
|
scope (exit) {
|
||||||
@@ -49,23 +54,24 @@ void main() {
|
|||||||
Rp.stop();
|
Rp.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
generateRSA();
|
ASObject actor = apResolveRemoteUsername("@marisa@ak.gensokyo.shop");
|
||||||
return;
|
// writeln(actor.raw.toJSON(true));
|
||||||
|
writeln(actor.stringRep);
|
||||||
|
|
||||||
Actor marisa = apResolveRemoteUsername("@admin@localhost:8080");
|
// Actor marisa = apResolveRemoteUsername("@admin@localhost:8080");
|
||||||
infoF!"Actor type: %s"(marisa.type);
|
// infoF!"Actor type: %s"(marisa.type);
|
||||||
infoF!"Actor preferredUsername: %s"(marisa.preferredUsername);
|
// infoF!"Actor preferredUsername: %s"(marisa.preferredUsername);
|
||||||
infoF!"Actor sharedInbox: %s"(marisa.endpoints.sharedInbox);
|
// infoF!"Actor sharedInbox: %s"(marisa.endpoints.sharedInbox);
|
||||||
|
|
||||||
PRequest followers = PRequest();
|
// PRequest followers = PRequest();
|
||||||
|
|
||||||
with (followers) {
|
// with (followers) {
|
||||||
url = marisa.followers;
|
// url = marisa.followers;
|
||||||
headers["Accept"] = "application/activity+json";
|
// headers["Accept"] = "application/activity+json";
|
||||||
}
|
// }
|
||||||
|
|
||||||
Response rs = Rp.request(followers, true);
|
// Response rs = Rp.request(followers, true);
|
||||||
infoF!"followers: %s"(rs.responseBody);
|
// infoF!"followers: %s"(rs.responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
//int main() {
|
//int main() {
|
||||||
|
|||||||
Reference in New Issue
Block a user