Compare commits
1 commit
master
...
feature/in
Author | SHA1 | Date | |
---|---|---|---|
Chris Josten | f405f99729 |
9
dub.json
9
dub.json
|
@ -5,11 +5,16 @@
|
|||
"copyright": "Copyright © 2019, Chris Josten",
|
||||
"dependencies": {
|
||||
"dyaml": "~>0.8.0",
|
||||
"htmld": "~>0.3.7",
|
||||
"vibe-d": "~>0.9.0"
|
||||
},
|
||||
"description": "A blog based on Markdown and JSON",
|
||||
"license": "AGPLv3",
|
||||
"mainSourceFile": "source/nl/netsoj/chris/blog/main.d",
|
||||
"name": "mijnblog",
|
||||
"targetType": "executable",
|
||||
"stringImportPaths": ["views", "translations"]
|
||||
"stringImportPaths": [
|
||||
"views",
|
||||
"translations"
|
||||
],
|
||||
"targetType": "executable"
|
||||
}
|
|
@ -7,7 +7,9 @@
|
|||
"dyaml": "0.8.3",
|
||||
"eventcore": "0.9.13",
|
||||
"fswatch": "0.5.0",
|
||||
"htmld": "0.3.7",
|
||||
"libasync": "0.8.6",
|
||||
"libdominator": "1.1.7",
|
||||
"libevent": "2.0.2+2.0.16",
|
||||
"memutils": "1.0.4",
|
||||
"mir-linux-kernel": "1.0.1",
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
/**
|
||||
* Implements and holds caches for several pages.
|
||||
*/
|
||||
module nl.netsoj.chris.blog.cache;
|
||||
|
||||
import std.experimental.logger;
|
||||
import std.traits;
|
||||
|
||||
import article;
|
||||
import page;
|
||||
import project;
|
||||
import nl.netsoj.chris.blog.model.article;
|
||||
import nl.netsoj.chris.blog.model.page;
|
||||
import nl.netsoj.chris.blog.model.project;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -21,7 +26,7 @@ GenericCache!(Project, "a.title < b.title") projects;
|
|||
* again if needed.
|
||||
*/
|
||||
struct GenericCache(T, string sortOrder)
|
||||
if (isImplicitlyConvertible!(T, Page)) {
|
||||
if (is(T : Page)) {
|
||||
public:
|
||||
|
||||
void addItem(T item) {
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
module nl.netsoj.chris.blog.constants;
|
||||
|
||||
/**
|
||||
* Constants which are passed to templates while rendering.
|
||||
*/
|
||||
class Constants {
|
||||
public static immutable string SITE_NAME = "Chris Josten's site";
|
||||
debug {
|
||||
public static immutable string SITE_URL = "https://kortstondig.chris.netsoj.nl";
|
||||
} else {
|
||||
public static immutable string SITE_URL = "https://chris.netsoj.nl";
|
||||
}
|
||||
public static immutable string COPYRIGHT = "© Chris Josten, 2020";
|
||||
}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
module nl.netsoj.chris.blog.interfaces.http;
|
||||
|
||||
import std.experimental.logger;
|
||||
|
||||
import vibe.d;
|
||||
|
||||
import cache;
|
||||
import article;
|
||||
import page;
|
||||
import project;
|
||||
import nl.netsoj.chris.blog.interfaces.indieauth;
|
||||
import nl.netsoj.chris.blog.model.article;
|
||||
import nl.netsoj.chris.blog.model.page;
|
||||
import nl.netsoj.chris.blog.model.project;
|
||||
import nl.netsoj.chris.blog.cache;
|
||||
import nl.netsoj.chris.blog.constants;
|
||||
|
||||
/**
|
||||
* Output types for the content.
|
||||
|
@ -83,12 +89,24 @@ string singleResponseMixin(string arrayName, string templateName) {
|
|||
render!("` ~ templateName ~ `", content);
|
||||
break;
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
@translationContext!TranslateContext
|
||||
class MijnBlog {
|
||||
|
||||
public:
|
||||
|
||||
this() {
|
||||
m_indieAuth = new IndieAuth();
|
||||
}
|
||||
|
||||
/**
|
||||
* IndieAuth subinterface
|
||||
*/
|
||||
@safe
|
||||
@path("/indieweb")
|
||||
@property IndieAuth indieweb() { return m_indieAuth; };
|
||||
|
||||
/**
|
||||
* Generates response for /posts/:slug and /palen/:slug.
|
||||
*/
|
||||
|
@ -140,8 +158,13 @@ public:
|
|||
addCachingHeader(res);
|
||||
// If no slug is supplied, it will be adjusted to "index"
|
||||
req.params.addField("slug", "index");
|
||||
res.headers.addField("Link", "<" ~ Constants.SITE_URL ~ "/indieweb/auth>; rel=\"authorization_endpoint\"");
|
||||
res.headers.addField("Link", "<" ~ Constants.SITE_URL ~ "/indieweb/token>; rel=\"token_endpoint\"");
|
||||
res.headers.addField("Link", "<" ~ Constants.SITE_URL ~ "/indieweb/micropub>; rel=\"micropub\"");
|
||||
mixin(singleResponseMixin("pages", "pages/page.dt"));
|
||||
}
|
||||
private:
|
||||
IndieAuth m_indieAuth;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -149,7 +172,9 @@ public:
|
|||
*/
|
||||
@safe
|
||||
void errorPage(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) {
|
||||
render!("pages/error.dt", error)(res);
|
||||
//render!("pages/error.dt", error)(res);
|
||||
import std.conv;
|
||||
res.writeBody(text("Error ", error.code, ": ", error.message, "\n\n", error.debugMessage), "text/plain");
|
||||
}
|
||||
|
||||
@trusted
|
||||
|
@ -171,4 +196,7 @@ void startHTTPServer() {
|
|||
router.registerWebInterface(new MijnBlog);
|
||||
|
||||
listenHTTP(settings, router);
|
||||
foreach(route; router.getAllRoutes()) {
|
||||
infof("Path: %s", route);
|
||||
}
|
||||
}
|
||||
|
|
92
source/nl/netsoj/chris/blog/interfaces/indieauth.d
Normal file
92
source/nl/netsoj/chris/blog/interfaces/indieauth.d
Normal file
|
@ -0,0 +1,92 @@
|
|||
module nl.netsoj.chris.blog.interfaces.indieauth;
|
||||
|
||||
import nl.netsoj.chris.blog.interfaces.http;
|
||||
import nl.netsoj.chris.blog.interfaces.micropub;
|
||||
import nl.netsoj.chris.blog.microformats.parser;
|
||||
|
||||
import mfd = nl.netsoj.chris.blog.microformats.definitions;
|
||||
|
||||
import vibe.d;
|
||||
|
||||
struct App {
|
||||
string logo;;
|
||||
string name;
|
||||
string url;
|
||||
string clientId;
|
||||
string redirectUri;
|
||||
bool richInfo = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of https://www.w3.org/TR/indieauth/
|
||||
*/
|
||||
@translationContext!TranslateContext
|
||||
class IndieAuth {
|
||||
public:
|
||||
this() {
|
||||
m_microPub = new MicroPub();
|
||||
}
|
||||
|
||||
@queryParam("response_type", "response_type")
|
||||
@queryParam("me", "me")
|
||||
@queryParam("client_id", "client_id")
|
||||
@queryParam("redirect_uri", "redirect_uri")
|
||||
@queryParam("state", "state")
|
||||
void getAuth(HTTPServerRequest req, HTTPServerResponse res,
|
||||
string response_type, string me, string client_id, string redirect_uri, string state) {
|
||||
enforceHTTP(response_type == "code", HTTPStatus.badRequest);
|
||||
|
||||
URL source = URL(client_id);
|
||||
HTTPClientResponse response = requestHTTP(source);
|
||||
mfd.App[] parsedApps = parsePage!(mfd.App)(response.bodyReader.readAllUTF8, source);
|
||||
|
||||
App app;
|
||||
app.name = client_id;
|
||||
app.richInfo = false;
|
||||
if (parsedApps.length > 0) {
|
||||
auto parsedApp = parsedApps[0];
|
||||
app.logo = parsedApp.logo;
|
||||
app.name = parsedApp.name;
|
||||
app.richInfo = true;
|
||||
}
|
||||
|
||||
app.clientId = client_id;
|
||||
URL redirectUrl = URL(redirect_uri);
|
||||
redirectUrl.queryString = redirectUrl.queryString ~ "%scode=%s&state=%s".format(
|
||||
(redirectUrl.queryString.length == 0 ? "" : "&"),
|
||||
"123456", state);
|
||||
app.redirectUri = redirectUrl.toString();
|
||||
render!("pages/indieauth.dt", app);
|
||||
}
|
||||
|
||||
@safe
|
||||
@path("/")
|
||||
void getIndex(HTTPServerResponse res) {
|
||||
res.writeBody("IndieAuth root", "text/plain");
|
||||
}
|
||||
|
||||
@safe
|
||||
void postToken(HTTPServerRequest req, HTTPServerResponse res,
|
||||
string grant_type, string code, string client_id, string redirect_uri, string me) {
|
||||
|
||||
struct OkResponse {
|
||||
string access_token;
|
||||
string token_type = "Bearer";
|
||||
string me;
|
||||
string scope_;
|
||||
}
|
||||
|
||||
OkResponse response;
|
||||
response.me = me;
|
||||
response.scope_ = "foo";
|
||||
response.access_token = "baz";
|
||||
|
||||
res.writeJsonBody(response, HTTPStatus.ok);
|
||||
}
|
||||
|
||||
@safe @property
|
||||
MicroPub micropub() { return m_microPub; };
|
||||
|
||||
private:
|
||||
MicroPub m_microPub;
|
||||
}
|
14
source/nl/netsoj/chris/blog/interfaces/micropub.d
Normal file
14
source/nl/netsoj/chris/blog/interfaces/micropub.d
Normal file
|
@ -0,0 +1,14 @@
|
|||
module nl.netsoj.chris.blog.interfaces.micropub;
|
||||
|
||||
import vibe.d;
|
||||
|
||||
import nl.netsoj.chris.blog.interfaces.http;
|
||||
|
||||
@translationContext!TranslateContext
|
||||
class MicroPub {
|
||||
|
||||
@path("/")
|
||||
void getIndex(HTTPServerRequest req, HTTPServerResponse res) {
|
||||
res.writeBody("MicroPub endpoint", "text/plain");
|
||||
}
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
module nl.netsoj.chris.blog.main;
|
||||
|
||||
import std.experimental.logger;
|
||||
import vibe.d;
|
||||
|
||||
import article;
|
||||
import page;
|
||||
import project;
|
||||
|
||||
import cache;
|
||||
import http;
|
||||
import watcher;
|
||||
import nl.netsoj.chris.blog.interfaces.http;
|
||||
import nl.netsoj.chris.blog.model.article;
|
||||
import nl.netsoj.chris.blog.model.page;
|
||||
import nl.netsoj.chris.blog.model.project;
|
||||
import nl.netsoj.chris.blog.cache;
|
||||
import nl.netsoj.chris.blog.watcher;
|
||||
|
||||
|
||||
void main() {
|
||||
|
|
12
source/nl/netsoj/chris/blog/microformats/definitions.d
Normal file
12
source/nl/netsoj/chris/blog/microformats/definitions.d
Normal file
|
@ -0,0 +1,12 @@
|
|||
module nl.netsoj.chris.blog.microformats.definitions;
|
||||
|
||||
import nl.netsoj.chris.blog.microformats.parser;
|
||||
|
||||
struct App {
|
||||
@MicroFormat(MicroFormat.Type.Url)
|
||||
string logo;
|
||||
@MicroFormat(MicroFormat.Type.PlainText)
|
||||
string name;
|
||||
|
||||
string url;
|
||||
}
|
116
source/nl/netsoj/chris/blog/microformats/parser.d
Normal file
116
source/nl/netsoj/chris/blog/microformats/parser.d
Normal file
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* Parser for microformats.
|
||||
* Standards: http://microformats.org/wiki/microformats2-parsing
|
||||
*/
|
||||
module nl.netsoj.chris.blog.microformats.parser;
|
||||
|
||||
import std.exception;
|
||||
import std.traits;
|
||||
|
||||
import vibe.inet.url;
|
||||
|
||||
import html;
|
||||
|
||||
import nl.netsoj.chris.blog.url;
|
||||
|
||||
class MicroFormatParseException : Exception {
|
||||
mixin basicExceptionCtors;
|
||||
}
|
||||
|
||||
|
||||
struct MicroFormatProperty {
|
||||
enum Type {
|
||||
RootClass,
|
||||
PlainText,
|
||||
Url,
|
||||
DateTime,
|
||||
EmbeddedMarkup
|
||||
};
|
||||
|
||||
Type type = Type.PlainText;
|
||||
|
||||
string getClassPrefix() pure {
|
||||
final switch(type) {
|
||||
case Type.RootClass:
|
||||
return "h-";
|
||||
case Type.PlainText:
|
||||
return "p-";
|
||||
case Type.Url:
|
||||
return "u-";
|
||||
case Type.DateTime:
|
||||
return "dt-";
|
||||
case Type.EmbeddedMarkup:
|
||||
return "e-";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a web page to extract to the given microformat model out of it
|
||||
*/
|
||||
T[] parsePage(T)(string source, URL url) {
|
||||
return parsePage!T(createDocument(source), url);
|
||||
}
|
||||
|
||||
T[] parsePage(T)(Document page, URL url)
|
||||
if (isAggregateType!T) {
|
||||
import std.algorithm;
|
||||
import std.array;
|
||||
import std.conv;
|
||||
import std.range;
|
||||
import std.string;
|
||||
|
||||
string rootClass = "h-" ~ T.stringof.toLower;
|
||||
|
||||
return page.querySelectorAll(".%s".format(rootClass)).map!((node){
|
||||
alias PropType = MicroFormatProperty.Type;
|
||||
T instance = T();
|
||||
MicroFormatProperty uda;
|
||||
string propertyClass;
|
||||
Node propNode;
|
||||
static foreach(sym; getSymbolsByUDA!(T, MicroFormatProperty)) {
|
||||
uda = getUDAs!(sym, MicroFormatProperty)[0];
|
||||
propertyClass = uda.getClassPrefix() ~ sym.stringof.toLower;
|
||||
|
||||
propNode = page.querySelector(".%s".format(propertyClass), node);
|
||||
switch (uda.type) {
|
||||
case PropType.PlainText:
|
||||
if (propNode.firstChild && propNode.firstChild.isTextNode()) {
|
||||
__traits(getMember, instance, sym.stringof) = to!string(propNode.text);
|
||||
}
|
||||
break;
|
||||
case PropType.Url:
|
||||
if (propNode.tag == "a" && propNode.hasAttr("href")) {
|
||||
__traits(getMember, instance, sym.stringof) = resolveURL(url, to!string(propNode["href"])).toString;
|
||||
} else if (propNode.tag == "img" && propNode.hasAttr("src")) {
|
||||
__traits(getMember, instance, sym.stringof) = resolveURL(url, to!string(propNode["src"])).toString;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}).array;
|
||||
}
|
||||
|
||||
unittest {
|
||||
import std.stdio;
|
||||
string page = q"eos
|
||||
<div class="h-app">
|
||||
<img src="/logo.png" class="u-logo">
|
||||
<a href="/" class="u-url p-name">Example App</a>
|
||||
</div>"
|
||||
eos";
|
||||
|
||||
struct App {
|
||||
@MicroFormatProperty(MicroFormatProperty.Type.Url)
|
||||
string logo;
|
||||
@MicroFormatProperty(MicroFormatProperty.Type.PlainText)
|
||||
string name;
|
||||
}
|
||||
|
||||
auto ts = parsePage!App(page, URL("https://example.com/"));
|
||||
assert(ts[0] == App ("https://example.com:443/logo.png", "Example App"));
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
module nl.netsoj.chris.blog.model.article;
|
||||
|
||||
import std.file;
|
||||
import std.stdio;
|
||||
import std.string;
|
||||
|
@ -7,8 +9,8 @@ import std.experimental.logger;
|
|||
import dyaml;
|
||||
import vibe.d;
|
||||
|
||||
import page;
|
||||
import utils;
|
||||
import nl.netsoj.chris.blog.model.page;
|
||||
import nl.netsoj.chris.blog.utils;
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
module nl.netsoj.chris.blog.model.page;
|
||||
|
||||
import std.exception;
|
||||
import std.experimental.logger;
|
||||
import std.file;
|
||||
|
@ -7,7 +9,7 @@ import std.stdio;
|
|||
import dyaml;
|
||||
import vibe.vibe;
|
||||
|
||||
import utils;
|
||||
import nl.netsoj.chris.blog.utils;
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
module nl.netsoj.chris.blog.model.project;
|
||||
|
||||
import std.array;
|
||||
import std.algorithm;
|
||||
import std.typecons;
|
||||
|
@ -5,9 +7,9 @@ import std.typecons;
|
|||
import dyaml;
|
||||
import vibe.vibe;
|
||||
|
||||
import page;
|
||||
import utils;
|
||||
import staticpaths;
|
||||
import nl.netsoj.chris.blog.model.page;
|
||||
import nl.netsoj.chris.blog.staticpaths;
|
||||
import nl.netsoj.chris.blog.utils;
|
||||
|
||||
/**
|
||||
* Represents a project, like an unfinished application
|
||||
|
|
1
source/nl/netsoj/chris/blog/package.d
Normal file
1
source/nl/netsoj/chris/blog/package.d
Normal file
|
@ -0,0 +1 @@
|
|||
module nl.netsoj.chris.blog;
|
|
@ -1,3 +1,5 @@
|
|||
module nl.netsoj.chris.blog.staticpaths;
|
||||
|
||||
/**
|
||||
* Paths to static data.
|
||||
*/
|
||||
|
|
27
source/nl/netsoj/chris/blog/url.d
Normal file
27
source/nl/netsoj/chris/blog/url.d
Normal file
|
@ -0,0 +1,27 @@
|
|||
module nl.netsoj.chris.blog.url;
|
||||
|
||||
import std.exception;
|
||||
|
||||
import vibe.inet.url;
|
||||
|
||||
URL resolveURL(URL source, string other) {
|
||||
URL otherUrl = URL(other);
|
||||
if (otherUrl.schema.length > 0 || otherUrl.host.length > 0) {
|
||||
return otherUrl;
|
||||
}
|
||||
|
||||
if (otherUrl.schema.length == 0) {
|
||||
enforce(source.schema.length > 0, "Source URL must have a scheme to resolve the other URL");
|
||||
otherUrl.schema = source.schema;
|
||||
}
|
||||
|
||||
if (otherUrl.host.length == 0) {
|
||||
otherUrl.host = source.host;
|
||||
otherUrl.port = source.port;
|
||||
if (!otherUrl.path.absolute) {
|
||||
otherUrl.path = source.path ~ otherUrl.path;
|
||||
}
|
||||
}
|
||||
|
||||
return otherUrl;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
module nl.netsoj.chris.blog.utils;
|
||||
|
||||
import std.algorithm;
|
||||
import std.array;
|
||||
import std.conv;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
module nl.netsoj.chris.blog.watcher;
|
||||
|
||||
import std.array;
|
||||
import std.algorithm;
|
||||
import std.experimental.logger;
|
||||
|
@ -7,14 +9,14 @@ import std.traits;
|
|||
|
||||
import vibe.d;
|
||||
|
||||
import cache;
|
||||
import page;
|
||||
import nl.netsoj.chris.blog.cache;
|
||||
import nl.netsoj.chris.blog.model.page;
|
||||
|
||||
/**
|
||||
* Loads pages into memory and sets up a "watcher" to watch a directory for file changes.
|
||||
*/
|
||||
void initPages(T, C)(C *cache, const string directory)
|
||||
if (isImplicitlyConvertible!(T, Page)) {
|
||||
if (is(T : Page)) {
|
||||
|
||||
bool addPage(string path) {
|
||||
try {
|
||||
|
@ -22,7 +24,7 @@ void initPages(T, C)(C *cache, const string directory)
|
|||
logf("Added %s", newPage.slug);
|
||||
cache.addItem(newPage);
|
||||
return true;
|
||||
} catch (page.ArticleParseException e) {
|
||||
} catch (ArticleParseException e) {
|
||||
logf("Could not parse %s: %s", path, e);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
|
@ -72,7 +74,7 @@ void initPages(T, C)(C *cache, const string directory)
|
|||
try {
|
||||
newPage = new T(change.path.toString());
|
||||
cache.changeItem(newPage);
|
||||
} catch(page.ArticleParseException e) {
|
||||
} catch(ArticleParseException e) {
|
||||
warningf("Could not parse %s", change.path.toString());
|
||||
} catch (Exception e) {
|
||||
warningf("Error while updating %s: %s", change.path.toString(), e.msg);
|
||||
|
|
|
@ -12,7 +12,7 @@ block content
|
|||
- if (articleList.length == 0)
|
||||
p No posts found
|
||||
- else
|
||||
- import utils;
|
||||
- import nl.netsoj.chris.blog.utils;
|
||||
- foreach(article; articleList)
|
||||
article
|
||||
header
|
||||
|
|
|
@ -14,7 +14,7 @@ block extra_meta_data
|
|||
|
||||
block content
|
||||
article(itemscope, itemtype="https://schema.org/BlogPosting", lang="#{content.language}")
|
||||
- import utils;
|
||||
- import nl.netsoj.chris.blog.utils;
|
||||
header
|
||||
h1.title(itemprop="headline") #{content.title}
|
||||
p.subtitle
|
||||
|
|
21
views/pages/indieauth.dt
Normal file
21
views/pages/indieauth.dt
Normal file
|
@ -0,0 +1,21 @@
|
|||
extends parts/page
|
||||
|
||||
block header
|
||||
title Aanmelden - Netsoj.nl
|
||||
|
||||
block sidebar
|
||||
|
||||
block content
|
||||
- if (app.richInfo)
|
||||
header.project-header
|
||||
img.project-icon(src="#{app.logo}", alt="Icon of #{app.name}")
|
||||
div
|
||||
h1.title Aanmelden met #[a(href="#{app.clientId}") #{app.name}]
|
||||
span.project-description #{app.clientId}
|
||||
- else
|
||||
header
|
||||
h1.title Aanmelden met #[a(href="#{app.clientId}") #{app.name}]
|
||||
|
||||
p #{app.name} wil uw identiteit bevestigen.
|
||||
|
||||
p #[a(href="#{app.redirectUri}") Aanvaarden en aanmelden] #[a(href="javascript:history.back();") Weigeren]
|
|
@ -1,6 +1,6 @@
|
|||
doctype html
|
||||
html(prefix="og: http://ogp.me/ns#")
|
||||
- import constants;
|
||||
- import nl.netsoj.chris.blog.constants;
|
||||
- import vibe.d;
|
||||
head
|
||||
//- Kick off loading the css as fast as possible
|
||||
|
|
Loading…
Reference in a new issue