Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1c36d1848 |
2
dub.sdl
2
dub.sdl
@@ -8,4 +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"
|
||||||
dependency "openssl" version="~>3.3.3"
|
subConfiguration "ddbc" "PGSQL"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"fileVersion": 1,
|
"fileVersion": 1,
|
||||||
"versions": {
|
"versions": {
|
||||||
|
"automem": "0.6.9",
|
||||||
"cachetools": "0.3.1",
|
"cachetools": "0.3.1",
|
||||||
"d-unit": "0.10.2",
|
"d-unit": "0.10.2",
|
||||||
"ddbc": "0.5.9",
|
"ddbc": "0.5.9",
|
||||||
@@ -11,11 +12,12 @@
|
|||||||
"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.3",
|
||||||
"path-matcher": "1.1.4",
|
|
||||||
"requests": "2.1.3",
|
"requests": "2.1.3",
|
||||||
"slf4d": "3.0.0",
|
"slf4d": "3.0.0",
|
||||||
"streams": "3.5.0",
|
"streams": "3.5.0",
|
||||||
"undead": "1.1.8"
|
"test_allocator": "0.3.4",
|
||||||
|
"undead": "1.1.8",
|
||||||
|
"unit-threaded": "0.10.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
module ap.errors;
|
|
||||||
|
|
||||||
/++
|
|
||||||
* Base exception for ActivityPub
|
|
||||||
+/
|
|
||||||
class APException : Exception {
|
|
||||||
this(string msg) {
|
|
||||||
super(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
module ap.types.actor;
|
|
||||||
|
|
||||||
import std.json;
|
|
||||||
|
|
||||||
import ap.types.object;
|
|
||||||
import util;
|
|
||||||
|
|
||||||
/++
|
|
||||||
* Represents an ActivityPub Actor
|
|
||||||
* https://www.w3.org/TR/activitypub/#actors
|
|
||||||
+/
|
|
||||||
class Actor : ASObject {
|
|
||||||
/* Required fields */
|
|
||||||
string inbox; /// Link to messages received by this actor. Required
|
|
||||||
string outbox; /// Link to messages produced by this actor. Required
|
|
||||||
string following; /// Link to a collection of the actors that this actor is following. Required
|
|
||||||
string followers; /// Link to a collection of the actors that follow this actor. Required
|
|
||||||
|
|
||||||
/* Optional fields */
|
|
||||||
string liked; /// Link to a collection of objects this actor has liked
|
|
||||||
string streams; /// Supplementary list of Collections of interest
|
|
||||||
string preferredUsername; /// A short username for referencing this actor
|
|
||||||
ActorEndpoints endpoints; /// Useful endpoints for this actor
|
|
||||||
|
|
||||||
JSONValue raw;
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Creates an Actor object based on its json representation
|
|
||||||
+ Params:
|
|
||||||
+ json = ActivityPub actor JSON representation
|
|
||||||
+/
|
|
||||||
this(JSONValue json) {
|
|
||||||
super(json);
|
|
||||||
|
|
||||||
with (this) {
|
|
||||||
required(json, "inbox", inbox);
|
|
||||||
required(json, "outbox", outbox);
|
|
||||||
required(json, "following", following);
|
|
||||||
required(json, "followers", followers);
|
|
||||||
|
|
||||||
optional(json, "liked", liked);
|
|
||||||
optional(json, "streams", streams);
|
|
||||||
optional(json, "preferredUsername", preferredUsername);
|
|
||||||
|
|
||||||
endpoints = ActorEndpoints.fromJson(json.optional!JSONValue("endpoints"));
|
|
||||||
|
|
||||||
raw = json;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ActorEndpoints {
|
|
||||||
string proxyUrl;
|
|
||||||
string oauthAuthorizationEndpoint;
|
|
||||||
string oauthTokenEndpoint;
|
|
||||||
string provideClientKey;
|
|
||||||
string signClientKey;
|
|
||||||
string sharedInbox;
|
|
||||||
|
|
||||||
static ActorEndpoints fromJson(JSONValue json) {
|
|
||||||
ActorEndpoints endpoints = ActorEndpoints();
|
|
||||||
|
|
||||||
with (endpoints) {
|
|
||||||
optional(json, "proxyUrl", proxyUrl);
|
|
||||||
optional(json, "oauthAuthorizationEndpoint", oauthAuthorizationEndpoint);
|
|
||||||
optional(json, "oauthTokenEndpoint", oauthTokenEndpoint);
|
|
||||||
optional(json, "provideClientKey", provideClientKey);
|
|
||||||
optional(json, "signClientKey", signClientKey);
|
|
||||||
optional(json, "sharedInbox", sharedInbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
return endpoints;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
module ap.types.collection;
|
|
||||||
|
|
||||||
import std.json;
|
|
||||||
|
|
||||||
import ap.types.object;
|
|
||||||
import ap.types.collection_page;
|
|
||||||
import util;
|
|
||||||
|
|
||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
module ap.types.collection_page;
|
|
||||||
|
|
||||||
import std.json;
|
|
||||||
|
|
||||||
import ap.types.collection;
|
|
||||||
import ap.types.object;
|
|
||||||
import util;
|
|
||||||
|
|
||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
module ap.types.link;
|
|
||||||
|
|
||||||
import std.algorithm;
|
|
||||||
import std.array;
|
|
||||||
import std.format;
|
|
||||||
import std.json;
|
|
||||||
|
|
||||||
import ap.types.object;
|
|
||||||
import util;
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
module ap.types.object;
|
|
||||||
|
|
||||||
import std.algorithm;
|
|
||||||
import std.array;
|
|
||||||
import std.datetime;
|
|
||||||
import std.format;
|
|
||||||
import std.json;
|
|
||||||
|
|
||||||
import ap.types.link;
|
|
||||||
import ap.types.collection;
|
|
||||||
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)
|
|
||||||
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:
|
|
||||||
+ json = the source json
|
|
||||||
+ Returns: Object
|
|
||||||
+/
|
|
||||||
this(JSONValue 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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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.types.object;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,15 @@
|
|||||||
module db.db;
|
module db.db;
|
||||||
|
|
||||||
import std.json;
|
|
||||||
|
|
||||||
import slf4d;
|
import slf4d;
|
||||||
|
|
||||||
import ddbc.core;
|
import ddbc.core;
|
||||||
import hibernated.core;
|
import hibernated.core;
|
||||||
|
|
||||||
import db.types;
|
import db.types;
|
||||||
|
|
||||||
enum DBConnector {
|
enum DBConnector {
|
||||||
Mysql,
|
DB_MYSQL,
|
||||||
Postgres,
|
DB_PGSQL,
|
||||||
Sqlite,
|
DB_SQLITE,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DBSettings {
|
struct DBSettings {
|
||||||
@@ -22,42 +19,9 @@ struct DBSettings {
|
|||||||
string username;
|
string username;
|
||||||
string password;
|
string password;
|
||||||
string dbname;
|
string dbname;
|
||||||
|
|
||||||
static DBSettings fromJson(JSONValue cfg) {
|
|
||||||
DBSettings settings;
|
|
||||||
|
|
||||||
with (settings) {
|
|
||||||
dbname = cfg["dbName"].str;
|
|
||||||
|
|
||||||
switch (cfg["connector"].str) {
|
|
||||||
case "postgresql":
|
|
||||||
connector = DBConnector.Postgres;
|
|
||||||
break;
|
|
||||||
case "sqlite":
|
|
||||||
connector = DBConnector.Sqlite;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Exception("Database connector `" ~ cfg["connector"].str ~ "` not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("host" in cfg)
|
|
||||||
host = cfg["host"].str;
|
|
||||||
|
|
||||||
if ("port" in cfg)
|
|
||||||
port = cast(ushort) cfg["port"].integer;
|
|
||||||
|
|
||||||
if ("username" in cfg)
|
|
||||||
username = cfg["username"].str;
|
|
||||||
|
|
||||||
if ("password" in cfg)
|
|
||||||
username = cfg["password"].str;
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Database {
|
class DB {
|
||||||
protected DBSettings m_settings;
|
protected DBSettings m_settings;
|
||||||
protected EntityMetaData m_schema;
|
protected EntityMetaData m_schema;
|
||||||
protected DataSource m_ds;
|
protected DataSource m_ds;
|
||||||
@@ -76,7 +40,7 @@ class Database {
|
|||||||
Dialect dialect;
|
Dialect dialect;
|
||||||
|
|
||||||
switch (this.m_settings.connector) {
|
switch (this.m_settings.connector) {
|
||||||
case DBConnector.Postgres:
|
case DBConnector.DB_PGSQL:
|
||||||
debugF!"Using PGSQL driver";
|
debugF!"Using PGSQL driver";
|
||||||
|
|
||||||
import ddbc.drivers.pgsqlddbc;
|
import ddbc.drivers.pgsqlddbc;
|
||||||
@@ -91,22 +55,6 @@ class Database {
|
|||||||
dialect = new PGSQLDialect();
|
dialect = new PGSQLDialect();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DBConnector.Sqlite:
|
|
||||||
debugF!"Using SQLite driver";
|
|
||||||
|
|
||||||
import ddbc.drivers.sqliteddbc;
|
|
||||||
|
|
||||||
driver = new SQLITEDriver();
|
|
||||||
url = this.m_settings.dbname;
|
|
||||||
|
|
||||||
static import std.file;
|
|
||||||
|
|
||||||
if (std.file.exists(url))
|
|
||||||
std.file.remove(url);
|
|
||||||
|
|
||||||
dialect = new SQLiteDialect();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Exception("Database connector not supported (yet)");
|
throw new Exception("Database connector not supported (yet)");
|
||||||
}
|
}
|
||||||
|
|||||||
122
source/main.d
122
source/main.d
@@ -1,116 +1,48 @@
|
|||||||
module main;
|
module main;
|
||||||
|
|
||||||
import std.json;
|
|
||||||
|
|
||||||
import requests;
|
|
||||||
import slf4d;
|
import slf4d;
|
||||||
import slf4d.default_provider;
|
import slf4d.default_provider;
|
||||||
|
|
||||||
import config;
|
import config;
|
||||||
import singletons;
|
import singletons;
|
||||||
import db.db;
|
import db.db;
|
||||||
import net.request_pool;
|
|
||||||
import webfinger;
|
|
||||||
import ap.actor;
|
|
||||||
|
|
||||||
// import ap.util;
|
int main() {
|
||||||
import std.math.remainder;
|
|
||||||
import util;
|
|
||||||
import ap.activity_stream;
|
|
||||||
import ap.util;
|
|
||||||
|
|
||||||
void commonInit() {
|
|
||||||
auto provider = new DefaultProvider(true, Levels.DEBUG);
|
auto provider = new DefaultProvider(true, Levels.DEBUG);
|
||||||
configureLoggingProvider(provider);
|
configureLoggingProvider(provider);
|
||||||
|
|
||||||
Config cfg = loadConfig();
|
|
||||||
initRequestPool(cfg);
|
|
||||||
initDatabase(cfg);
|
|
||||||
}
|
|
||||||
|
|
||||||
Config loadConfig() {
|
|
||||||
Config cfg = new Config();
|
Config cfg = new Config();
|
||||||
cfg.load();
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
void initRequestPool(Config cfg) {
|
try {
|
||||||
RP = new RequestPool();
|
cfg.load();
|
||||||
RP.startBackground();
|
} catch (Exception e) {
|
||||||
}
|
error(e);
|
||||||
|
return 21;
|
||||||
void initDatabase(Config cfg) {
|
|
||||||
DB = new Database(DBSettings.fromJson(cfg.v["db"]));
|
|
||||||
DB.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
import std.stdio;
|
|
||||||
|
|
||||||
commonInit();
|
|
||||||
|
|
||||||
scope (exit) {
|
|
||||||
DB.close();
|
|
||||||
RP.stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ASObject actor = apResolveRemoteUsername("@marisa@ak.gensokyo.shop");
|
DBSettings dbSettings;
|
||||||
// writeln(actor.raw.toJSON(true));
|
auto dbCfg = cfg.v["db"];
|
||||||
writeln(actor.stringRep);
|
with (dbSettings) {
|
||||||
|
host = dbCfg["host"].str;
|
||||||
|
port = cast(ushort) dbCfg["port"].integer;
|
||||||
|
username = dbCfg["username"].str;
|
||||||
|
password = dbCfg["password"].str;
|
||||||
|
dbname = dbCfg["dbName"].str;
|
||||||
|
|
||||||
// Actor marisa = apResolveRemoteUsername("@admin@localhost:8080");
|
switch (dbCfg["connector"].str) {
|
||||||
// infoF!"Actor type: %s"(marisa.type);
|
case "postgresql":
|
||||||
// infoF!"Actor preferredUsername: %s"(marisa.preferredUsername);
|
connector = DBConnector.DB_PGSQL;
|
||||||
// infoF!"Actor sharedInbox: %s"(marisa.endpoints.sharedInbox);
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PRequest followers = PRequest();
|
Db = new DB(dbSettings);
|
||||||
|
Db.connect();
|
||||||
|
|
||||||
// with (followers) {
|
scope (exit)
|
||||||
// url = marisa.followers;
|
Db.close();
|
||||||
// headers["Accept"] = "application/activity+json";
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Response rs = RP.request(followers, true);
|
return 0;
|
||||||
// infoF!"followers: %s"(rs.responseBody);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//int main() {
|
|
||||||
// auto provider = new DefaultProvider(true, Levels.DEBUG);
|
|
||||||
// configureLoggingProvider(provider);
|
|
||||||
//
|
|
||||||
// Config cfg = new Config();
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// cfg.load();
|
|
||||||
// } catch (Exception e) {
|
|
||||||
// error(e);
|
|
||||||
// return 21;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// DBSettings dbSettings;
|
|
||||||
// auto dbCfg = cfg.v["db"];
|
|
||||||
// with (dbSettings) {
|
|
||||||
// host = dbCfg["host"].str;
|
|
||||||
// port = cast(ushort) dbCfg["port"].integer;
|
|
||||||
// username = dbCfg["username"].str;
|
|
||||||
// password = dbCfg["password"].str;
|
|
||||||
// dbname = dbCfg["dbName"].str;
|
|
||||||
//
|
|
||||||
// switch (dbCfg["connector"].str) {
|
|
||||||
// case "postgresql":
|
|
||||||
// connector = DBConnector.DB_PGSQL;
|
|
||||||
// break;
|
|
||||||
// default:
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// DB = new DB(dbSettings);
|
|
||||||
// DB.connect();
|
|
||||||
//
|
|
||||||
// scope (exit)
|
|
||||||
// DB.close();
|
|
||||||
//
|
|
||||||
// return 0;
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
|
|||||||
@@ -1,82 +1,67 @@
|
|||||||
module net.request_pool;
|
module net.request_pool;
|
||||||
|
|
||||||
|
import core.thread;
|
||||||
|
import core.sync.semaphore;
|
||||||
|
|
||||||
import std.container;
|
import std.container;
|
||||||
import std.parallelism;
|
import std.uuid;
|
||||||
|
|
||||||
import requests;
|
import requests;
|
||||||
import slf4d;
|
import slf4d;
|
||||||
|
|
||||||
struct PRequest {
|
struct PRequest {
|
||||||
string url;
|
|
||||||
string method = "GET";
|
string method = "GET";
|
||||||
QueryParam[] params;
|
string url;
|
||||||
string[string] headers;
|
string[string] params;
|
||||||
|
UUID uuid; /// Used internally
|
||||||
string body = "";
|
|
||||||
string contentType = "text/plain";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequestPool {
|
class RequestPool {
|
||||||
private int m_totalWorkers;
|
private int m_totalWorkers;
|
||||||
private TaskPool m_taskPool;
|
private ThreadGroup m_threads;
|
||||||
private Logger _l;
|
private bool m_shouldRun = false;
|
||||||
private Request[] m_requests;
|
private Semaphore m_semaphore;
|
||||||
|
|
||||||
|
protected DList!PRequest m_requestsQueue;
|
||||||
|
|
||||||
this(int totalWorkers = 4) {
|
this(int totalWorkers = 4) {
|
||||||
this.m_totalWorkers = totalWorkers;
|
this.m_totalWorkers = totalWorkers;
|
||||||
this._l = getLogger();
|
this.m_semaphore = new Semaphore();
|
||||||
|
|
||||||
for (int i = 0; i < totalWorkers + 1; i++) {
|
|
||||||
Request rq = Request();
|
|
||||||
// TODO: add custom fields (such as user agent)
|
|
||||||
rq.addHeaders(["User-Agent": "apd (development)"]);
|
|
||||||
this.m_requests ~= rq;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void startBackground() {
|
void startBackground() {
|
||||||
_l.debugF!"Starting RequestPool with %d workers"(this.m_totalWorkers);
|
debugF!"Starting RequestPool with %d workers"(this.m_totalWorkers);
|
||||||
this.m_taskPool = new TaskPool(this.m_totalWorkers);
|
this.m_shouldRun = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < this.m_totalWorkers; i++) {
|
||||||
|
Thread t = new Thread(() => this.m_run(i));
|
||||||
|
this.m_threads.add(t);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Response request(PRequest request, bool blocking = false) {
|
void enqueue(PRequest request) {
|
||||||
auto t = task(&this.m_run, request);
|
request.uuid = randomUUID();
|
||||||
this.m_taskPool.put(t);
|
this.m_requestsQueue.insertBack(request);
|
||||||
|
this.m_semaphore.notify();
|
||||||
if (blocking)
|
|
||||||
return t.yieldForce();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
this.m_taskPool.finish(true);
|
this.m_shouldRun = false;
|
||||||
|
this.m_threads.joinAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
Response m_run(PRequest request) {
|
private void m_run(immutable int tid) {
|
||||||
Request rq = this.m_requests[this.m_taskPool.workerIndex];
|
Request rq = Request();
|
||||||
rq.addHeaders(request.headers);
|
|
||||||
|
|
||||||
_l.debugF!"[%d][%s] %s"(this.m_taskPool.workerIndex, request.method, request.url);
|
while (this.m_shouldRun) {
|
||||||
|
this.m_semaphore.wait();
|
||||||
|
PRequest request = this.m_requestsQueue.front();
|
||||||
|
this.m_requestsQueue.removeFront();
|
||||||
|
|
||||||
Response rs;
|
debugF!"Requesting %s with tid %d"(request.url, tid);
|
||||||
|
|
||||||
switch (request.method) {
|
Response rs = rq.execute(request.method, request.url);
|
||||||
case "GET":
|
|
||||||
rs = rq.get(request.url, request.params);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "POST":
|
|
||||||
rs = rq.post(request.url, request.body, request.contentType);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
errorF!"Unknown request method: %s"(request.method);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_l.debugF!"[%d][%s] %s result code: %d"(this.m_taskPool.workerIndex, request.method, request.url, rs
|
|
||||||
.code);
|
|
||||||
return rs;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
module singletons;
|
module singletons;
|
||||||
|
|
||||||
import db.db;
|
import db.db;
|
||||||
import net.request_pool;
|
|
||||||
|
|
||||||
Database DB;
|
DB Db;
|
||||||
RequestPool RP;
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
module util;
|
|
||||||
|
|
||||||
import std.format;
|
|
||||||
import std.json;
|
|
||||||
|
|
||||||
import slf4d;
|
|
||||||
|
|
||||||
import deimos.openssl.rsa;
|
|
||||||
import deimos.openssl.pem;
|
|
||||||
|
|
||||||
void optional(T)(ref JSONValue val, string key, ref T receiver) {
|
|
||||||
const(JSONValue)* p = key in val;
|
|
||||||
|
|
||||||
if (p == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
static if (is(JSONValue == T))
|
|
||||||
receiver = *p;
|
|
||||||
else
|
|
||||||
receiver = (*p).get!T;
|
|
||||||
}
|
|
||||||
|
|
||||||
T optional(T)(ref JSONValue val, string key) {
|
|
||||||
T t;
|
|
||||||
optional(val, key, t);
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
void required(T)(ref JSONValue val, string key, ref T receiver) {
|
|
||||||
receiver = val[key].get!T;
|
|
||||||
}
|
|
||||||
|
|
||||||
void generateRSA() {
|
|
||||||
RSA* rsa = RSA_generate_key(2048, 3, null, null);
|
|
||||||
BIO* bio = BIO_new(BIO_s_mem());
|
|
||||||
PEM_write_bio_RSAPrivateKey(bio, rsa, null, null, 0, null, null);
|
|
||||||
|
|
||||||
int keyLen = BIO_pending(bio);
|
|
||||||
char[] pemKey = new char[keyLen + 1];
|
|
||||||
BIO_read(bio, pemKey.ptr, keyLen);
|
|
||||||
|
|
||||||
BIO_free_all(bio);
|
|
||||||
RSA_free(rsa);
|
|
||||||
|
|
||||||
infoF!"private key: %s"(pemKey);
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
module webfinger;
|
|
||||||
|
|
||||||
import std.algorithm;
|
|
||||||
import std.array;
|
|
||||||
import std.format;
|
|
||||||
import std.json;
|
|
||||||
import std.typecons;
|
|
||||||
|
|
||||||
import requests;
|
|
||||||
|
|
||||||
import net.request_pool;
|
|
||||||
import singletons;
|
|
||||||
|
|
||||||
enum AcceptedWebfingerContentType = "application/jrd+json, application/json";
|
|
||||||
|
|
||||||
private bool checkValidWebfingerResponse(Response rs) {
|
|
||||||
|
|
||||||
return AcceptedWebfingerContentType.canFind(rs.responseHeaders()["content-type"].split(";")[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PRequest buildRequest(string uri, string[string] params) {
|
|
||||||
PRequest rq = PRequest();
|
|
||||||
string schema = "https";
|
|
||||||
|
|
||||||
if (uri.startsWith("localhost"))
|
|
||||||
schema = "http";
|
|
||||||
|
|
||||||
rq.method = "GET";
|
|
||||||
rq.url = format("%s://%s/.well-known/webfinger", schema, uri);
|
|
||||||
rq.headers["Accept"] = AcceptedWebfingerContentType;
|
|
||||||
|
|
||||||
QueryParam[] p;
|
|
||||||
|
|
||||||
foreach (k, v; params)
|
|
||||||
p ~= tuple!("key", "value")(k, v);
|
|
||||||
|
|
||||||
rq.params = p;
|
|
||||||
|
|
||||||
return rq;
|
|
||||||
}
|
|
||||||
|
|
||||||
PRequest buildAcctRequest(string uri, string acct) {
|
|
||||||
string[string] params = ["resource": format("acct:%s@%s", acct, uri)];
|
|
||||||
|
|
||||||
return buildRequest(uri, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONValue wfRequestAccount(string uri, string acct) {
|
|
||||||
Response rs = RP.request(buildAcctRequest(uri, acct), true);
|
|
||||||
|
|
||||||
if (checkValidWebfingerResponse(rs) == false)
|
|
||||||
throw new Exception("Invalid webfinger response");
|
|
||||||
|
|
||||||
return parseJSON(rs.responseBody().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONValue wfRequestAccount(string handle) {
|
|
||||||
string[] uriAcct = handle.split("@")[$ - 2 .. $];
|
|
||||||
|
|
||||||
return wfRequestAccount(uriAcct[1], uriAcct[0]);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user