diff --git a/.eleventy.js b/.eleventy.js
deleted file mode 100644
index 35e3615..0000000
--- a/.eleventy.js
+++ /dev/null
@@ -1,95 +0,0 @@
-const ACTOR = require("./actor").default
-
-module.exports = function(eleventyConfig) {
- // I'm .gitignoring my content for now, so 11ty should not ignore that
- eleventyConfig.setUseGitIgnore(false)
-
- // Filters are in a separate function to try and keep this config less cluttered
- addFilters(eleventyConfig)
- // same with shortcodes
- addShortcodes(eleventyConfig)
- // and collections
- addCollections(eleventyConfig)
-
- // add the actor data, accessible globally
- eleventyConfig.addGlobalData("ACTOR", ACTOR);
-
- // TODO: assets?
- // files to passthrough copy
- eleventyConfig.addPassthroughCopy({"img":"assets/img"})
- eleventyConfig.addPassthroughCopy({"js":"assets/js"})
- eleventyConfig.addPassthroughCopy("css")
- eleventyConfig.addPassthroughCopy("CNAME")
-
- // plugins
- eleventyConfig.addPlugin(require("@11ty/eleventy-plugin-rss"))
- const { EleventyHtmlBasePlugin } = require("@11ty/eleventy")
- eleventyConfig.addPlugin(EleventyHtmlBasePlugin)
-
- // Return your Object options:
- return {
- dir: {
- input: "_content",
- output: "_site"
- },
- htmlTemplateEngine: "njk",
- templateFormats: ["md","html","njk"]
- }
-}
-
-function addCollections(eleventyConfig) {
- eleventyConfig.addCollection("feed", function(collectionApi) {
- return collectionApi.getAllSorted().reverse().filter(item => {
- if(!item.data.published) return false
- return item.filePathStem.startsWith('/posts/')
- }).map(item => {
- item.data.author = ACTOR
- return item
- }).sort((a, b) => new Date(b.published).getTime() - new Date(a.published).getTime())
- })
-}
-
-function addShortcodes(eleventyConfig) {
- eleventyConfig.addNunjucksShortcode("getVar", function(varString){ return this.ctx[varString] })
- eleventyConfig.addShortcode('renderlayoutblock', function(name){ return (this.page.layoutblock || {})[name] || '' })
- eleventyConfig.addPairedShortcode('layoutblock', function(content, name) {
- if (!this.page.layoutblock) this.page.layoutblock = {}
- this.page.layoutblock[name] = content
- return ''
- })
-}
-
-function addFilters(eleventyConfig) {
- eleventyConfig.addFilter("formatDate", formatDateFilter)
- eleventyConfig.addFilter("dateISOString", dateISOStringFilter)
- eleventyConfig.addFilter("dateObj", (value) => new Date(value))
- eleventyConfig.addFilter("log", (value) => { console.log(`[11ty] 📄LOG: `, value); return value })
- eleventyConfig.addFilter("concat", (value, other) => value + '' + other)
- eleventyConfig.addNunjucksAsyncFilter("await", (promise) => promise.then(res => callback(null, res)).catch(err => callback(err)))
-}
-
-// default date formatting
-function formatDateFilter(value) {
- try{
- const date = new Date(value)
- if(date) return date.toISOString().replace('T', ' ').slice(0, -5)
- else throw 'Unrecognized data format'
- }
- catch(e) {
- console.error(`Could not convert "${value}"`, e)
- return value;
- }
-}
-
-// dates as iso string
-function dateISOStringFilter(value) {
- try{
- const date = new Date(value)
- if(date) return date.toISOString()
- else throw 'Unrecognized data format'
- }
- catch(e) {
- console.error(`Could not convert "${value}"`, e)
- return value;
- }
-}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 2d8c964..38aec2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -173,3 +173,4 @@ _content/_data/_inbox
_content/_data/_outbox
_content/posts
_site
+db.sqlite*
diff --git a/_content/_data/_inbox/.gitkeep b/_content/_data/_inbox/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/_content/_data/_outbox/.gitkeep b/_content/_data/_outbox/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/_content/_data/disliked.json b/_content/_data/disliked.json
deleted file mode 100644
index 0637a08..0000000
--- a/_content/_data/disliked.json
+++ /dev/null
@@ -1 +0,0 @@
-[]
\ No newline at end of file
diff --git a/_content/_data/followers.json b/_content/_data/followers.json
deleted file mode 100644
index 0637a08..0000000
--- a/_content/_data/followers.json
+++ /dev/null
@@ -1 +0,0 @@
-[]
\ No newline at end of file
diff --git a/_content/_data/following.json b/_content/_data/following.json
deleted file mode 100644
index 0637a08..0000000
--- a/_content/_data/following.json
+++ /dev/null
@@ -1 +0,0 @@
-[]
\ No newline at end of file
diff --git a/_content/_data/layout.js b/_content/_data/layout.js
deleted file mode 100644
index c98ebc5..0000000
--- a/_content/_data/layout.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = "layout-default.njk"
\ No newline at end of file
diff --git a/_content/_data/liked.json b/_content/_data/liked.json
deleted file mode 100644
index 0637a08..0000000
--- a/_content/_data/liked.json
+++ /dev/null
@@ -1 +0,0 @@
-[]
\ No newline at end of file
diff --git a/_content/_data/shared.json b/_content/_data/shared.json
deleted file mode 100644
index 0637a08..0000000
--- a/_content/_data/shared.json
+++ /dev/null
@@ -1 +0,0 @@
-[]
\ No newline at end of file
diff --git a/_content/_includes/layout-default.njk b/_content/_includes/layout-default.njk
deleted file mode 100644
index 07f180f..0000000
--- a/_content/_includes/layout-default.njk
+++ /dev/null
@@ -1,13 +0,0 @@
----js
-{
- layout: "layout-main.njk",
- ctx: function() { return this.ctx }
-}
----
-{% from "macro-entry.njk" import entryMacro %}
-
-{{ entryMacro(ctx(), author, url, content) }}
-
-{% layoutblock 'foot' %}
-
-{% endlayoutblock %}
diff --git a/_content/_includes/layout-feed.njk b/_content/_includes/layout-feed.njk
deleted file mode 100644
index 9977d86..0000000
--- a/_content/_includes/layout-feed.njk
+++ /dev/null
@@ -1,15 +0,0 @@
----
-title: Feed
-layout: layout-main.njk
-scripts_foot: ''
----
-
-
{{ title }}
- {{ content | safe }}
-
-
-{% include 'partial-pagination.njk' %}
-
-{% layoutblock 'foot' %}
-
-{% endlayoutblock %}
\ No newline at end of file
diff --git a/_content/_includes/layout-main.njk b/_content/_includes/layout-main.njk
deleted file mode 100644
index c0f0795..0000000
--- a/_content/_includes/layout-main.njk
+++ /dev/null
@@ -1,38 +0,0 @@
----
-title: Mon Repos (Death's Domain)
----
-
-
-
-
-
-
-
-
-
-
-
- {#
-
-
-
- #}
-
-
-
-
-
-
- {{ title }}
- {% renderlayoutblock 'head' %}
-
-
-
- {% renderlayoutblock 'header' %}
-
- {{ content | safe }}
-
-
-{% renderlayoutblock 'footer' %}
-{% renderlayoutblock 'foot' %}
-
diff --git a/_content/_includes/macro-author.njk b/_content/_includes/macro-author.njk
deleted file mode 100644
index 141d191..0000000
--- a/_content/_includes/macro-author.njk
+++ /dev/null
@@ -1,10 +0,0 @@
-{% macro authorMacro(author) %}
- {# {{ author | getAuthorData | log }} #}
-
-
-
- {{ author.name }}
- {{ author.preferredUsername }}
-
-
-{% endmacro %}
\ No newline at end of file
diff --git a/_content/_includes/macro-card-head.njk b/_content/_includes/macro-card-head.njk
deleted file mode 100644
index 0260e5b..0000000
--- a/_content/_includes/macro-card-head.njk
+++ /dev/null
@@ -1,9 +0,0 @@
-{% from "macro-author.njk" import authorMacro %}
-{% macro cardHeadMacro(author, date, url) %}
-
-{% endmacro %}
\ No newline at end of file
diff --git a/_content/_includes/macro-entry.njk b/_content/_includes/macro-entry.njk
deleted file mode 100644
index 623a5ce..0000000
--- a/_content/_includes/macro-entry.njk
+++ /dev/null
@@ -1,14 +0,0 @@
-{% from "macro-card-head.njk" import cardHeadMacro %}
-{% from "macro-summary.njk" import summaryMacro %}
-{% macro entryMacro(item, author, url, content, summaryOnly=false) %}
-
- {{ cardHeadMacro(author, item.published, url) }}
-
- {{ summaryMacro(item, url) }}
- {% if item.type == 'article' and summaryOnly %}
- {% elseif content %}
-
{{ content | safe }}
- {% endif %}
-
-
-{% endmacro %}
\ No newline at end of file
diff --git a/_content/_includes/macro-summary.njk b/_content/_includes/macro-summary.njk
deleted file mode 100644
index 6ad8083..0000000
--- a/_content/_includes/macro-summary.njk
+++ /dev/null
@@ -1,83 +0,0 @@
-{% macro summaryMacro(item, url) %}
-{% switch item.type %}
- {% case "article" %} {# article summary: #}
-
- {% if item.summary %}
- {{ item.summary | safe }}
- {% endif %}
-
- {% case "reply" %} {# reply summary: #}
- Reply to {{ item["in-reply-to"] }}
-
- {% case "like" %} {# like summary: #}
- Favourited {{ item['like-of'] }}
-
- {% case "boost" %} {# boost summary: #}
- Boosted {{ item["repost-of"] }}
-
- {% case "bookmark" %} {# bookmark summary: #}
- Bookmarked {{ item["bookmark-of"] }}
-
- {% case "read" %} {# read summary: #}
-
- {% if item["read-status"].toLowerCase() == "to-read" %}
- To Read:
-
- {% elseif item["read-status"].toLowerCase() == "reading" %}
- Currently Reading:
-
- {% elseif item["read-status"].toLowerCase() == "finished" %}
- Finished Reading:
-
- {% endif %}
-
- {% if item["read-of"].startsWith("http") %}
- {{ item["read-of"] }}
- {% else %}
- {{ item["read-of"] }}
- {% endif %}
-
-
- {% case "watch" %} {# watch summary: #}
-
- {% if item["watch-status"].toLowerCase() == "to-watch" %}
- To Watch:
- {% elseif item["watch-status"].toLowerCase() == "watching" %}
- Currently Watching:
- {% elseif item["watch-status"].toLowerCase() == "watched" or item["watch-status"].toLowerCase() == "finished" %}
- Finished watching:
- {% else %}
- Watched:
- {% endif %}
-
-
- {% if item["watch-of"].startsWith("http") %}
- {{ item["watch-of"] }}
- {% else %}
- {{ item["watch-of"] }}
- {% endif %}
-
-
- {% case "rsvp" %} {# rsvp summary: #}
-
-
- {% if item.rsvp.toLowerCase() == "yes" %}
-
- Will attend
- {% elseif item.rsvp.toLowerCase() == "maybe" %}
-
- Might attend
- {% elseif item.rsvp.toLowerCase() == "no" %}
-
- Won't attend
- {% elseif item.rsvp.toLowerCase() == "interested" %}
-
- Interested in
- {% endif %}
-
- {{ item["in-reply-to"] }}
-
- {% endswitch %}
-{% endmacro %}
\ No newline at end of file
diff --git a/_content/_includes/partial-pagination.njk b/_content/_includes/partial-pagination.njk
deleted file mode 100644
index 10f0194..0000000
--- a/_content/_includes/partial-pagination.njk
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if pagination %}
-
-{% endif %}
\ No newline at end of file
diff --git a/_content/_includes/summary-article.njk b/_content/_includes/summary-article.njk
deleted file mode 100644
index edb3ccb..0000000
--- a/_content/_includes/summary-article.njk
+++ /dev/null
@@ -1,6 +0,0 @@
-
-{% if item.summary %}
- {{ item.summary | safe }}
-{% endif %}
\ No newline at end of file
diff --git a/_content/_includes/summary-reply.njk b/_content/_includes/summary-reply.njk
deleted file mode 100644
index e69de29..0000000
diff --git a/_content/atom.njk b/_content/atom.njk
deleted file mode 100644
index e053b60..0000000
--- a/_content/atom.njk
+++ /dev/null
@@ -1,36 +0,0 @@
----json
-{
- "layout": null,
- "permalink": "atom.xml",
- "eleventyExcludeFromCollections": true,
- "metadata": {
- "subtitle": "A feed of all my posts on the fediverse",
- "language": "en"
- }
-}
----
-
-{% from "macro-summary.njk" import summaryMacro %}
-
- {{ ACTOR.name }}'s feed
- {{ metadata.subtitle }}
-
- {{ collections.feed[0].date | dateToRfc3339 }}
- {{ ACTOR.id | absoluteUrl(ACTOR.url) }}
-
- {{ ACTOR.name }}
-
- {%- for post in collections.feed %}
- {%- set absolutePostUrl = post.url | absoluteUrl(ACTOR.url) %}
-
- {{ post.data.title }}
-
- {{ post.data.published | dateObj | dateToRfc3339 }}
- {{ absolutePostUrl }}
-
- {{ summaryMacro(post.data, post.url) | htmlToAbsoluteUrls(absolutePostUrl) }}
- {{ post.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}
-
-
- {%- endfor %}
-
\ No newline at end of file
diff --git a/_content/index.html b/_content/index.html
deleted file mode 100644
index b663fd1..0000000
--- a/_content/index.html
+++ /dev/null
@@ -1,76 +0,0 @@
----
-layout: layout-main.njk
-eleventyExcludeFromCollections: true
----
-
-
-
-
-
- I'm {{ ACTOR.name }}
- {% if ACTOR.name != ACTOR.preferredUsername %}
- (a.k.a {{ ACTOR.preferredUsername }})
- {% endif %}
-
-
- {% if ACTOR.summary %}
- {{ ACTOR.summary }}
- {% else %}
- ...and I am a human on the Internet.
- {% endif %}
-
-
- Go check out some stuff I wrote
- Or stay up-to-date by following me
-
-
-
- ...or elsewhere on the Internet:
-
-
-
-
-{% layoutblock 'foot' %}
-
-
-
-{% endlayoutblock %}
diff --git a/_content/json.njk b/_content/json.njk
deleted file mode 100644
index 623d805..0000000
--- a/_content/json.njk
+++ /dev/null
@@ -1,36 +0,0 @@
----json
-{
- "layout": null,
- "permalink": "feed.json",
- "eleventyExcludeFromCollections": true,
- "metadata": {
- "subtitle": "A feed of all my posts on the fediverse",
- "language": "en"
- }
-}
----
-{ {% from "macro-summary.njk" import summaryMacro %}
- "version": "https://jsonfeed.org/version/1.1",
- "title": "{{ ACTOR.name }}'s feed",
- "language": "{{ metadata.language }}",
- "home_page_url": "{{ ACTOR.url }}/",
- "feed_url": "{{ permalink | absoluteUrl(ACTOR.url) }}",
- "description": "{{ metadata.subtitle }}",
- "author": {
- "name": "{{ ACTOR.name }}",
- "url": "{{ ACTOR.url }}/"
- },
- "items": [
- {%- for post in collections.feed %}
- {%- set absolutePostUrl = post.url | absoluteUrl(ACTOR.url) %}
- {
- "id": "{{ absolutePostUrl }}",
- "url": "{{ absolutePostUrl }}",
- "title": "{{ post.data.title }}",
- "content_html": {{ summaryMacro(post.data, post.url) | concat(post.templateContent) | htmlToAbsoluteUrls(absolutePostUrl) | dump | safe }},
- "date_published": "{{ post.data.published | dateObj | dateToRfc3339 }}"
- }
- {% if not loop.last %},{% endif %}
- {%- endfor %}
- ]
-}
\ No newline at end of file
diff --git a/_content/posts/.gitkeep b/_content/posts/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/_content/posts/index.njk b/_content/posts/index.njk
deleted file mode 100644
index bbd4c63..0000000
--- a/_content/posts/index.njk
+++ /dev/null
@@ -1,18 +0,0 @@
----
-title: My Feed
-layout: layout-feed.njk
-pagination:
- data: collections.feed
- size: 20
-eleventyExcludeFromCollections: true
----
-{% from "macro-entry.njk" import entryMacro %}
-
-
-{% for item in pagination.items %}
- -
- {{ entryMacro(item.data, item.data.author, item.url, item.templateContent, true) }}
-
-{% endfor %}
-
-
diff --git a/_content/rss.njk b/_content/rss.njk
deleted file mode 100644
index 115fc47..0000000
--- a/_content/rss.njk
+++ /dev/null
@@ -1,36 +0,0 @@
----js
-{
- "layout": null,
- "permalink": "rss.xml",
- "eleventyExcludeFromCollections": true,
- "metadata": {
- "subtitle": "A feed of all my posts on the fediverse",
- "language": "en"
- }
-}
----
-
-{% from "macro-summary.njk" import summaryMacro %}
-
-
- {{ ACTOR.name }}'s feed
- {{ ACTOR.url }}
-
- {{ metadata.subtitle }}
- {{ metadata.language }}
- {%- for post in collections.feed | reverse %}
- {%- set absolutePostUrl = post.url | absoluteUrl(ACTOR.url) %}
- -
- {{ post.data.title }}
- {{ absolutePostUrl }}
-
- {{ summaryMacro(post.data, post.url) | htmlToAbsoluteUrls(absolutePostUrl) }}
- {{ post.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}
-
- {{ post.data.published | dateObj | dateToRfc822 }}
- {{ ACTOR.name }}
- {{ absolutePostUrl }}
-
- {%- endfor %}
-
-
diff --git a/actor.ts b/actor.ts
index 04bfb4e..29a7a5f 100644
--- a/actor.ts
+++ b/actor.ts
@@ -8,8 +8,8 @@ export const summary = ""
// avatar image
const icon:{ type:"Image", mediaType:string, url:string, name:string } = {
type: "Image",
- mediaType: "image/svg+xml",
- url: BASE_URL + "/assets/img/avatar-tt.svg",
+ mediaType: "image/png",
+ url: "https://s.gravatar.com/avatar/bc0f8c2d2ecc533cb236ce1f858365a25dbbc80ce42df49b9a95e88f07f91720?s=800",
name: "My profile photo — a pixelated version of me"
}
diff --git a/bun.lockb b/bun.lockb
index 941a179..e22d873 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/css/styles.css b/css/styles.css
deleted file mode 100644
index dfea86d..0000000
--- a/css/styles.css
+++ /dev/null
@@ -1,211 +0,0 @@
-:root {
- --bg-rgb: 238, 238, 238;
- --on-bg-rgb: 4, 4, 4;
- --primary-rgb: 160, 116, 196;
- --on-primary-rgb: 238, 238, 238;
- --bg: rgb(var(--bg-rgb));
- --on-bg: rgb(var(--on-bg-rgb));
- --primary: rgb(var(--primary-rgb));
- --on-primary: rgb(var(--on-primary-rgb));
-
- --fade-alpha: 0.06;
- --mute-alpha: 0.3;
-
- --bg-fade: rgba(var(--bg-rgb), var(--fade-alpha));
- --on-bg-fade: rgba(var(--on-bg-rgb), var(--fade-alpha));
- --primary-fade: rgba(var(--primary-rgb), var(--fade-alpha));
- --on-primary-fade: rgba(var(--on-primary-rgb), var(--fade-alpha));
-
- --bg-muted: rgba(var(--bg-rgb), var(--mute-alpha));
- --on-bg-muted: rgba(var(--on-bg-rgb), var(--mute-alpha));
- --primary-muted: rgba(var(--primary-rgb), var(--mute-alpha));
- --on-primary-muted: rgba(var(--on-primary-rgb), var(--mute-alpha));
-
-}
-@media (prefers-color-scheme: dark) {
- :root{
- --bg-rgb: 17, 17, 17;
- --on-bg-rgb: 251, 251, 251;
- --on-primary-rgb: 251, 251, 251;
-
- --fade-alpha: 0.16;
- }
-}
-html {
- background-color: var(--bg);
- color: var(--on-bg);
-}
-
-a {
- color: var(--primary);
- text-decoration: none;
-}
-
-a:hover {
- text-decoration: underline;
-}
-
-body {
- line-height: 1.6;
- font-family: system-ui, -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
- text-align: center;
- margin: 0;
- min-height: 90vh;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- overflow-x: hidden;
-}
-
-p:last-child {
- margin-block-end: 0;
-}
-
-
-img.u-photo { border-radius: 50%; max-height:200px; }
-/* .p-author img.u-photo{
- max-height: 48px;
-} */
-ul { padding: 0; list-style: none; }
-img.u-featured {
- display:block;
- z-index:0;
- margin-bottom:-125px
-}
-
-button.icon-button {
- line-height: 24px;
- background-color: #666289;
- color:#fff;
- border:none;
- border-radius:4px;
-}
-button.icon-button>img {
- max-height: 24px;
- vertical-align: bottom;
-}
-
-.button-input-container>label{
- display:block;
-}
-.button-input-container>input{
- background-color: #666289;
- color:#fff;
- border:none;
- border-radius: 4px;
- padding: 4px 66px 4px 6px;
- margin-right: -60px;
-}
-.button-input-container>input:focus{
- border:none;
-}
-.button-input-container>button{
- background-color: #666289;
- color:#fff;
- border-radius: 4px;
- border-color: #fff;
-}
-i>img{
- max-height: 1em;
- vertical-align: middle;
-}
-img.tiny-avatar {
- max-height: 17px;
-}
-
-img.small-avatar {
- max-height: 40px;
-}
-
-/* Pagination */
-.pagination {
- margin: 50px auto;
-}
-.pagination li {
- display: inline-block;
-}
-.pagination a {
- padding: 8px 16px;
- text-decoration: none;
- border-radius: 5px;
- border: 1px solid var(--on-bg-fade);
-}
-.pagination a.active {
- background-color: var(--primary);
- color: var(--on-primary);
- }
-
-.pagination a:hover:not(.active) { background-color: var(--on-bg-fade); }
-
-.pagination a.disabled {
- color: var(--primary-fade);
- pointer-events: none;
- }
-
-/* End Pagination */
-
-/* Feed Entries */
-
-.h-entry {
- max-width: 500px;
- min-width: 320px;
- background-color: var(--primary-fade);
- padding: 20px;
- margin: 0;
- border: 1px solid var(--on-bg-fade);
- text-align: left;
-}
-
-.h-entry:not(:last-child) {
- border-bottom: none;
-}
-
-.h-entry .p-author {
- max-width: calc(100% - 80px);
- display: flex;
- align-items: center;
- gap: 10px;
- overflow: hidden;
-}
-
-.h-entry .u-photo {
- vertical-align: baseline;
-}
-
-.h-entry .display-name {
- display: block;
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.h-entry .display-name .p-name {
- white-space: nowrap;
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- font-weight: 700;
-}
-
-.h-entry .display-name .p-nickname {
- white-space: nowrap;
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- color: var(--on-primary-muted);
-}
-
-.h-entry .permalink {
- float:right;
- text-align: right;
- font-size: 0.8em;
- color: var(--on-primary-muted);
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width:80px;
-}
-
-/* End Feed Entries */
\ No newline at end of file
diff --git a/img/Fediverse_logo_proposal.svg b/img/Fediverse_logo_proposal.svg
deleted file mode 100644
index 854e6f0..0000000
--- a/img/Fediverse_logo_proposal.svg
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
diff --git a/img/Obsidian.svg b/img/Obsidian.svg
deleted file mode 100644
index 57c05a2..0000000
--- a/img/Obsidian.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/img/avatar-tiny.png b/img/avatar-tiny.png
deleted file mode 100644
index 390531e..0000000
Binary files a/img/avatar-tiny.png and /dev/null differ
diff --git a/img/avatar-tt-trans.svg b/img/avatar-tt-trans.svg
deleted file mode 100644
index 90e8520..0000000
--- a/img/avatar-tt-trans.svg
+++ /dev/null
@@ -1,37 +0,0 @@
-
\ No newline at end of file
diff --git a/img/avatar-tt.svg b/img/avatar-tt.svg
deleted file mode 100644
index 3712fd0..0000000
--- a/img/avatar-tt.svg
+++ /dev/null
@@ -1,38 +0,0 @@
-
\ No newline at end of file
diff --git a/img/avatar-tt@800.png b/img/avatar-tt@800.png
deleted file mode 100644
index 4929c9c..0000000
Binary files a/img/avatar-tt@800.png and /dev/null differ
diff --git a/img/banner-1500x500.jpg b/img/banner-1500x500.jpg
deleted file mode 100644
index cf44a30..0000000
Binary files a/img/banner-1500x500.jpg and /dev/null differ
diff --git a/js/button-input.js b/js/button-input.js
deleted file mode 100644
index 40b18e9..0000000
--- a/js/button-input.js
+++ /dev/null
@@ -1,61 +0,0 @@
-function buttonInputClick() {
- this.style.display = 'none'
-
- const buttonInput = document.createElement('div')
- buttonInput.classList.add('button-input-container')
-
- if(this.dataset.instruction){
- const label = document.createElement('label')
- buttonInput.appendChild(label)
- label.innerText = this.dataset.instruction
- }
-
- const input = document.createElement('input')
- buttonInput.appendChild(input)
- input.type = 'text'
- if(this.dataset.placeholder){
- input.placeholder = this.dataset.placeholder
- }
-
- const button = document.createElement('button')
- buttonInput.appendChild(button)
- button.innerText = "Submit"
- button.addEventListener('click', () => {
- if(this.onsubmit){
- const event = new Event('button-input-value')
- event.value = input.value
- this.onsubmit(event)
- }
-
- buttonInput.parentNode.removeChild(buttonInput)
- if(this.dataset.success){
- const span = document.createElement('span')
- span.classList.add('success')
- span.innerText = this.dataset.success
- setTimeout(() => {
- span.parentNode.removeChild(span)
- this.style.display = null
- }, 5000);
- this.parentNode.insertBefore(span, this.nextSibling)
- }
- else{
- this.style.display = null
- }
- })
-
- input.addEventListener("keypress", function(event) {
- if (event.key === "Enter") {
- event.preventDefault();
- button.click();
- }
- });
-
- this.parentNode.insertBefore(buttonInput, this.nextSibling)
- input.focus()
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- document.querySelectorAll('.button-input').forEach(button => {
- button.addEventListener('click', buttonInputClick)
- });
-}, false);
\ No newline at end of file
diff --git a/js/follow.js b/js/follow.js
deleted file mode 100644
index f8891c1..0000000
--- a/js/follow.js
+++ /dev/null
@@ -1,35 +0,0 @@
-const SUBSCRIBE_LINK_REL = 'http://ostatus.org/schema/1.0/subscribe'
-function follow(username, handle) {
- if(!handle){
- handle = prompt("Please enter your fediverse / mastodon handle (e.g. '@user@domain.social')", "@")
- }
-
- if(handle) {
- const input = handle
- handle = handle.trim().replace(/^@/,'')
- const split = handle.split('@')
- if(split.length == 2) {
- const resource = `acct:${handle}`
- const domain = split[1]
-
- // look up remote user via webfinger
- const url = `https://${domain}/.well-known/webfinger?resource=${resource}`
- fetch(url, {headers: {
- 'Content-Type': 'application/activity+json'
- }}).then(async result => {
- const json = await result.json()
- const subscribe = json.links.find(link => link.rel && link.rel == SUBSCRIBE_LINK_REL)
- let template = subscribe.template
- window.open(template.replace("{uri}", username), '_blank').focus()
- })
- .catch(e => {
- console.error(e)
- throw `Sorry, we couldn't find a subscribe uri for ${input}.\n\nTry searching for "${username}" on ${domain} (or in your fediverse client of choice)`
- })
-
- }
- else {
- throw 'Please enter your fediverse address in @user@domain.social format'
- }
- }
-}
\ No newline at end of file
diff --git a/js/relative-time.js b/js/relative-time.js
deleted file mode 100644
index 0dfba51..0000000
--- a/js/relative-time.js
+++ /dev/null
@@ -1,6 +0,0 @@
-document.addEventListener('DOMContentLoaded', () => {
- document.querySelectorAll('time').forEach(time => {
- const datetime = luxon.DateTime.fromISO(time.getAttribute('datetime'))
- time.innerText = datetime.toRelative()
- });
-}, false);
\ No newline at end of file
diff --git a/package.json b/package.json
index 1fd791f..4240160 100644
--- a/package.json
+++ b/package.json
@@ -14,9 +14,7 @@
"typescript": "^5.0.0"
},
"dependencies": {
- "@11ty/eleventy": "^2.0.1",
- "@11ty/eleventy-plugin-rss": "^1.2.0",
- "gray-matter": "^4.0.3",
- "node-forge": "^1.3.1"
+ "node-forge": "^1.3.1",
+ "using-statement": "^0.4.2"
}
}
\ No newline at end of file
diff --git a/src/activitypub.ts b/src/activitypub.ts
index dbb03d9..21d4ea7 100644
--- a/src/activitypub.ts
+++ b/src/activitypub.ts
@@ -1,11 +1,14 @@
-import * as db from "./db"
import { verify } from "./request"
import outbox from "./outbox"
import inbox from "./inbox"
import ACTOR from "../actor"
import { activityPubTypes } from "./env"
+import ActivityPubDB from "./db"
-export default (req: Request): Response | Promise | undefined => {
+let db:ActivityPubDB
+
+export default (req: Request, database:ActivityPubDB): Response | Promise | undefined => {
+ db = database
const url = new URL(req.url)
let match
@@ -24,7 +27,7 @@ export default (req: Request): Response | Promise | undefined => {
}
export function reqIsActivityPub(req:Request) {
- const contentType = req.headers.get("Accept")
+ const contentType = req.headers.get("Accept") + ',' + req.headers.get('Content-Type')
return activityPubTypes.some(t => contentType?.includes(t))
}
@@ -47,7 +50,7 @@ const postOutbox = async (req:Request):Promise => {
// ensure that the verified actor matches the actor in the request body
if (ACTOR.id !== body.actor) return new Response("", { status: 401 })
- return await outbox(body)
+ return await outbox(body, db)
}
const postInbox = async (req:Request):Promise => {
@@ -69,21 +72,21 @@ const postInbox = async (req:Request):Promise => {
// ensure that the verified actor matches the actor in the request body
if (from !== body.actor) return new Response("", { status: 401 })
- return await inbox(body)
+ return await inbox(body, db)
}
const getOutbox = async (req:Request):Promise => {
console.log("GetOutbox")
// TODO: Paging?
- const posts = await db.listOutboxActivities()
+ const posts = db.listOutboxActivities()
return Response.json({
"@context": "https://www.w3.org/ns/activitystreams",
id: ACTOR.outbox,
type: "OrderedCollection",
- totalItems: posts.length,
- orderedItems: posts.map((post) => ({
+ totalItems: posts?.length,
+ orderedItems: posts?.map((post) => ({
...post,
actor: ACTOR.id
})).sort( (a,b) => new Date(b.published).getTime() - new Date(a.published).getTime())
@@ -95,13 +98,13 @@ const getFollowers = async (req:Request):Promise => {
const url = new URL(req.url)
const page = url.searchParams.get("page")
- const followers = await db.listFollowers()
+ const followers = db.listFollowers()
if(!page) return Response.json({
"@context": "https://www.w3.org/ns/activitystreams",
id: ACTOR.followers,
type: "OrderedCollection",
- totalItems: followers.length,
+ totalItems: followers?.length,
first: `${ACTOR.followers}?page=1`,
})
else return Response.json({
@@ -109,8 +112,8 @@ const getFollowers = async (req:Request):Promise => {
id: `${ACTOR.followers}?page=${page}`,
type: "OrderedCollectionPage",
partOf: ACTOR.followers,
- totalItems: followers.length,
- orderedItems: followers.map(follower => follower.actor)
+ totalItems: followers?.length,
+ orderedItems: followers?.map(follower => follower?.id)
})
}
@@ -119,13 +122,13 @@ const getFollowing = async (req:Request):Promise => {
const url = new URL(req.url)
const page = url.searchParams.get("page")
- const following = await db.listFollowing()
+ const following = db.listFollowing()
if(!page) return Response.json({
"@context": "https://www.w3.org/ns/activitystreams",
id: ACTOR.following,
type: "OrderedCollection",
- totalItems: following.length,
+ totalItems: following?.length,
first: `${ACTOR.following}?page=1`,
})
else return Response.json({
@@ -133,8 +136,8 @@ const getFollowing = async (req:Request):Promise => {
id: `${ACTOR.following}?page=${page}`,
type: "OrderedCollectionPage",
partOf: ACTOR.following,
- totalItems: following.length,
- orderedItems: following.map(follow => follow.actor)
+ totalItems: following?.length,
+ orderedItems: following?.map(follow => follow.id)
})
}
@@ -142,18 +145,18 @@ const getActor = async (req:Request):Promise => {
console.log("GetActor")
if(reqIsActivityPub(req)) return Response.json(ACTOR, { headers: { "Content-Type": "application/activity+json"}})
- else return Response.json(await db.listPosts())
+ else return Response.json(db.listPosts())
}
const getPost = async (req:Request, id:string):Promise => {
console.log("GetPost", id)
- if(reqIsActivityPub(req)) return Response.json((await db.getOutboxActivity(id)).object, { headers: { "Content-Type": "application/activity+json"}})
- else return Response.json(await db.getPost(id))
+ if(reqIsActivityPub(req)) return Response.json((db.getOutboxActivity(id))?.object, { headers: { "Content-Type": "application/activity+json"}})
+ else return Response.json(db.getPost(id))
}
const getOutboxActivity = async (req:Request, id:string):Promise => {
console.log("GetOutboxActivity", id)
- return Response.json((await db.getOutboxActivity(id)), { headers: { "Content-Type": "application/activity+json"}})
+ return Response.json((db.getOutboxActivity(id)), { headers: { "Content-Type": "application/activity+json"}})
}
\ No newline at end of file
diff --git a/src/admin.ts b/src/admin.ts
index 28d7ce6..83b4797 100644
--- a/src/admin.ts
+++ b/src/admin.ts
@@ -1,11 +1,14 @@
import { idsFromValue } from "./activitypub"
-import * as db from "./db"
import { ADMIN_PASSWORD, ADMIN_USERNAME, activityPubTypes } from "./env"
import outbox from "./outbox"
import { fetchObject } from "./request"
import ACTOR from "../actor"
+import ActivityPubDB from "./db"
-export default (req: Request): Response | Promise | undefined => {
+let db:ActivityPubDB
+
+export default (req: Request, database: ActivityPubDB): Response | Promise | undefined => {
+ db = database
const url = new URL(req.url)
if(!url.pathname.startsWith('/admin')) return undefined
@@ -15,7 +18,7 @@ export default (req: Request): Response | Promise | undefined => {
let match
if(req.method === "GET" && (match = url.pathname.match(/^\/test\/?$/i))) return new Response("", { status: 204 })
- else if(req.method == "POST" && (match = url.pathname.match(/^\/rebuild\/?$/i))) return rebuild(req)
+ // else if(req.method == "POST" && (match = url.pathname.match(/^\/rebuild\/?$/i))) return rebuild(req)
else if(req.method == "POST" && (match = url.pathname.match(/^\/create\/?$/i))) return create(req)
else if(req.method == "POST" && (match = url.pathname.match(/^\/follow\/([^\/]+)\/?$/i))) return follow(req, match[1])
else if(req.method == "DELETE" && (match = url.pathname.match(/^\/follow\/([^\/]+)\/?$/i))) return unfollow(req, match[1])
@@ -43,11 +46,11 @@ const checkAuth = (headers: Headers): Boolean => {
return username === ADMIN_USERNAME && password === ADMIN_PASSWORD
}
-// rebuild the 11ty static pages
-export const rebuild = async(req:Request):Promise => {
- await db.rebuild()
- return new Response("", { status: 201 })
-}
+// // rebuild the 11ty static pages
+// export const rebuild = async(req:Request):Promise => {
+// db.rebuild()
+// return new Response("", { status: 201 })
+// }
// create an activity
const create = async (req:Request, inReplyTo:string|null = null):Promise => {
@@ -82,10 +85,10 @@ const create = async (req:Request, inReplyTo:string|null = null):Promise => {
+const idFromHandle = async(handle:string) => {
let url
if(handle.startsWith('@')) handle = handle.substring(1)
try {
@@ -106,6 +109,11 @@ const follow = async (req:Request, handle:string):Promise => {
url = actorLink.href
}
+ return url
+}
+
+const follow = async (req:Request, handle:string):Promise => {
+ const url = await idFromHandle(handle)
console.log(`Following ${url}`)
// send the follow request to the supplied actor
@@ -115,22 +123,23 @@ const follow = async (req:Request, handle:string):Promise => {
actor: ACTOR.id,
object: url,
to: [url, "https://www.w3.org/ns/activitystreams#Public"]
- })
+ }, db)
}
const unfollow = async (req:Request, handle:string):Promise => {
+ const url = await idFromHandle(handle)
// check to see if we are already following. If not, just return success
- const existing = await db.getFollowing(handle)
+ const existing = db.getFollowing(url)
if (!existing) return new Response("", { status: 204 })
- const activity = await db.getOutboxActivity(existing.id)
+ const activity = db.getOutboxActivity(existing.activity_id)
// outbox will also take care of the deletion
return await outbox({
"@context": "https://www.w3.org/ns/activitystreams",
type: "Undo",
actor: ACTOR.id,
object: activity,
- to: activity.to
- })
+ to: activity?.to
+ }, db)
}
const like = async (req:Request, object_url:string):Promise => {
@@ -143,22 +152,22 @@ const like = async (req:Request, object_url:string):Promise => {
object: object,
to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"],
cc: [ACTOR.followers]
- })
+ }, db)
}
-const unlike = async (req:Request, object_id:string):Promise => {
+const unlike = async (req:Request, activity_id:string):Promise => {
// check to see if we are already following. If not, just return success
- const liked = await db.listLiked()
- let existing = liked.find(o => o.object_id === object_id)
+ const liked = db.listLiked()
+ let existing = liked?.find(o => o?.activity_id === activity_id)
if (!existing){
- const object = await (await fetchObject(object_id)).json()
+ const object = await (await fetchObject(activity_id)).json()
idsFromValue(object).forEach(id => {
- const e = liked.find(o => o.object_id === id)
+ const e = liked?.find(o => o.activity_id === id)
if(e) existing = e
})
}
if (!existing) return new Response("No like found to delete", { status: 204 })
- const activity = await db.getOutboxActivity(existing.id)
+ const activity = db.getOutboxActivity(existing.activity_id)
return undo(activity)
}
@@ -172,22 +181,22 @@ const dislike = async (req:Request, object_url:string):Promise => {
object: object,
to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"],
cc: [ACTOR.followers]
- })
+ }, db)
}
const undislike = async (req:Request, object_id:string):Promise => {
// check to see if we are already following. If not, just return success
- const disliked = await db.listDisliked()
- let existing = disliked.find(o => o.object_id === object_id)
+ const disliked = db.listDisliked()
+ let existing = disliked?.find(o => o.activity_id === object_id)
if (!existing){
const object = await (await fetchObject(object_id)).json()
idsFromValue(object).forEach(id => {
- const e = disliked.find(o => o.object_id === id)
+ const e = disliked?.find(o => o.activity_id === id)
if(e) existing = e
})
}
if (!existing) return new Response("No dislike found to delete", { status: 204 })
- const activity = await db.getOutboxActivity(existing.id)
+ const activity = db.getOutboxActivity(existing.activity_id)
return undo(activity)
}
@@ -201,22 +210,22 @@ const share = async (req:Request, object_url:string):Promise => {
object: object,
to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"],
cc: [ACTOR.followers]
- })
+ }, db)
}
const unshare = async (req:Request, object_id:string):Promise => {
// check to see if we are already following. If not, just return success
- const shared = await db.listShared()
- let existing = shared.find(o => o.object_id === object_id)
+ const shared = db.listShared()
+ let existing = shared?.find(o => o.activity_id === object_id)
if (!existing){
const object = await (await fetchObject(object_id)).json()
idsFromValue(object).forEach(id => {
- const e = shared.find(o => o.object_id === id)
+ const e = shared?.find(o => o.activity_id === id)
if(e) existing = e
})
}
if (!existing) return new Response("No share found to delete", { status: 204 })
- const activity = await db.getOutboxActivity(existing.id)
+ const activity = db.getOutboxActivity(existing.activity_id)
return undo(activity)
}
@@ -229,23 +238,23 @@ const undo = async(activity:any):Promise => {
object: activity,
to: activity.to,
cc: activity.cc
- })
+ }, db)
}
const deletePost = async (req:Request, id:string):Promise => {
- const post = await db.getPostByURL(id)
+ const post = db.getPostByURL(id)
if(!post) return new Response("", { status: 404 })
- const activity = await db.getOutboxActivity(post.local_id)
+ const activity = db.getOutboxActivity(post.activity_id)
return await outbox({
"@context": "https://www.w3.org/ns/activitystreams",
type: "Delete",
actor: ACTOR.id,
- to: activity.to,
- cc: activity.cc,
- audience: activity.audience,
+ to: activity?.to,
+ cc: activity?.cc,
+ // audience: activity?.audience,
object: {
id,
type: "Tombstone"
}
- })
+ }, db)
}
\ No newline at end of file
diff --git a/src/db.ts b/src/db.ts
index b786cf2..ffae1fe 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -1,229 +1,426 @@
-import { ACTIVITY_INBOX_PATH, ACTIVITY_OUTBOX_PATH, CONTENT_PATH, DATA_PATH, POSTS_PATH, STATIC_PATH } from "./env"
-import path from "path"
-import { readdir } from "fs/promises"
-import { unlinkSync } from "node:fs"
-import { fetchObject } from "./request"
-import { idsFromValue } from "./activitypub"
-import ACTOR from "../actor"
-const matter = require('gray-matter')
-const Eleventy = require("@11ty/eleventy")
+import { Database, Statement } from "bun:sqlite"
-// rebuild the 11ty static pages
-export async function rebuild() {
- console.info(`Building 11ty from ${CONTENT_PATH}, to ${STATIC_PATH}`)
- await new Eleventy(CONTENT_PATH, STATIC_PATH, { configPath: '.eleventy.js' }).write()
+export interface ActivityObject {
+ activity_id: string
+ id: string
+ published: string
}
-export async function createInboxActivity(activity:any, object_id:any) {
- const activityFile = Bun.file(path.join(ACTIVITY_INBOX_PATH, `${object_id}.activity.json`))
- await Bun.write(activityFile, JSON.stringify(activity))
+export interface Activity extends ActivityObject {
+ type: string
+ actor: string
+ published: string
+ to: string[]
+ cc?: string[]
+ object?: any
}
-export async function createOutboxActivity(activity:any, object_id:any) {
- const activityFile = Bun.file(path.join(ACTIVITY_OUTBOX_PATH, `${object_id}.activity.json`))
- await Bun.write(activityFile, JSON.stringify(activity))
+export interface Following extends ActivityObject {
+ accepted?: boolean
}
-export async function getInboxActivity(id:string) {
- const file = Bun.file(path.join(ACTIVITY_INBOX_PATH, `${id}.activity.json`))
- return await file.json()
+export interface Post extends ActivityObject {
+ attributedTo: string
+ type: string
+ content?: string
}
-export async function getOutboxActivity(id:string) {
- const file = Bun.file(path.join(ACTIVITY_OUTBOX_PATH, `${id}.activity.json`))
- return await file.json()
-}
+export default class ActivityPubDB {
+ // The Database
+ db: Database
-export async function listInboxActivities() {
- return await Promise.all(
- (await readdir(ACTIVITY_INBOX_PATH)).filter(v => v.endsWith('.activity.json'))
- .map(async filename => await Bun.file(path.join(ACTIVITY_INBOX_PATH, filename)).json())
- )
-}
+ // Cached Statements
-export async function listOutboxActivities() {
- return await Promise.all(
- (await readdir(ACTIVITY_OUTBOX_PATH)).filter(v => v.endsWith('.activity.json'))
- .map(async filename => await Bun.file(path.join(ACTIVITY_OUTBOX_PATH, filename)).json())
- )
-}
-
-export async function createPost(post_object:any, object_id:string) {
- const file = Bun.file(path.join(POSTS_PATH, `${object_id}.md`))
- let {type, object, inReplyTo} = post_object
- if(inReplyTo && typeof inReplyTo === 'string') inReplyTo = await fetchObject(inReplyTo)
-
- if(object){
- let { content, published, id, attributedTo } = object
- if(content as string) content = '> ' + content.replace('\n', '\n> ') + '\n'
- else content = ""
-
- content += post_object.content || ""
- //TODO: add appropriate content for different types (e.g. like, etc)
- const data:any = { id, published, attributedTo, type }
- if(inReplyTo) data.inReplyTo = idsFromValue(inReplyTo).at(0)
- await Bun.write(file, matter.stringify(content, data))
+ constructor() {
+ this.db = new Database('./db.sqlite')
+ this.migrate()
+ this.cacheQueries()
}
- else {
- const { content, published, id, attributedTo } = post_object
- let reply_content = ""
- if(!object && inReplyTo) {
- reply_content = inReplyTo.content
- if(reply_content as string) reply_content = '> ' + reply_content.replace('\n', '\n> ') + '\n'
- else reply_content = ""
+ close() {
+ this.db.close()
+ }
+
+ migrate() {
+ let version = (this.db.query("PRAGMA user_version;").get() as {user_version:number})?.user_version
+ console.log(`Hi from migrate! User version: ${version}`)
+
+ const statements:Statement[] = []
+
+ switch(version) {
+ case 0:
+ console.log("migrating db version 1")
+ this.db.exec("PRAGMA journal_mode = WAL;")
+
+ // Create the inbox table
+ statements.push(this.db.prepare(`CREATE TABLE [inbox] (
+ [activity_id] TEXT NOT NULL PRIMARY KEY,
+ [id] TEXT NOT NULL,
+ [type] TEXT NOT NULL,
+ [actor] TEXT NOT NULL,
+ [published] TEXT NOT NULL,
+ [to] TEXT NOT NULL,
+ [cc] TEXT,
+ [activity] TEXT NOT NULL
+ )`))
+
+ // Create the outbox table
+ statements.push(this.db.prepare(`CREATE TABLE [outbox] (
+ [activity_id] TEXT NOT NULL PRIMARY KEY,
+ [id] TEXT NOT NULL,
+ [type] TEXT NOT NULL,
+ [actor] TEXT NOT NULL,
+ [published] TEXT NOT NULL,
+ [to] TEXT NOT NULL,
+ [cc] TEXT,
+ [activity] TEXT NOT NULL
+ )`))
+
+ // Create the following table
+ statements.push(this.db.prepare(`CREATE TABLE [following] (
+ [activity_id] TEXT NOT NULL PRIMARY KEY,
+ [id] TEXT NOT NULL,
+ [published] TEXT NOT NULL,
+ [accepted] INTEGER
+ )`))
+
+ // Create the followers table
+ statements.push(this.db.prepare(`CREATE TABLE [followers] (
+ [activity_id] TEXT NOT NULL PRIMARY KEY,
+ [id] TEXT NOT NULL,
+ [published] TEXT NOT NULL
+ )`))
+
+ // Create the liked table
+ statements.push(this.db.prepare(`CREATE TABLE [liked] (
+ [activity_id] TEXT NOT NULL PRIMARY KEY,
+ [id] TEXT NOT NULL,
+ [published] TEXT NOT NULL
+ )`))
+
+ // Create the disliked table
+ statements.push(this.db.prepare(`CREATE TABLE [disliked] (
+ [activity_id] TEXT NOT NULL PRIMARY KEY,
+ [id] TEXT NOT NULL,
+ [published] TEXT NOT NULL
+ )`))
+
+ // Create the shared table
+ statements.push(this.db.prepare(`CREATE TABLE [shared] (
+ [activity_id] TEXT NOT NULL PRIMARY KEY,
+ [id] TEXT NOT NULL,
+ [published] TEXT NOT NULL
+ )`))
+
+ version = 1
+ case 1:
+ // Create the posts table
+ statements.push(this.db.prepare(`CREATE TABLE [posts] (
+ [activity_id] TEXT NOT NULL PRIMARY KEY,
+ [id] TEXT NOT NULL,
+ [published] TEXT NOT NULL,
+ [attributedTo] TEXT,
+ [type] TEXT NOT NULL,
+ [content] TEXT,
+ [object] TEXT
+ )`))
+ version = 2
+ case 2: break;
+ default: break;
}
- const data:any = { id, published, attributedTo, type }
- if(inReplyTo) data.inReplyTo = idsFromValue(inReplyTo).at(0)
- await Bun.write(file, matter.stringify((reply_content || "") + (content || ""), data))
+ const migration = this.db.transaction((statements:Statement[]) => {
+ statements.forEach(s => {
+ console.log(s.toString());
+ s.run();
+ s.finalize()
+ })
+ return true
+ })
+ if(migration(statements)) this.db.run(`PRAGMA user_version=${version}`)
}
- rebuild()
-}
-export async function getPost(id:string) {
- const file = Bun.file(path.join(POSTS_PATH, `${id}.md`))
- const { data, content } = matter(await file.text())
- return {
- ...data,
- content: content.trim(),
- local_id: id
+
+ queries:{
+ createInboxActivity?:Statement,
+ getInboxActivity?:Statement,
+ listInboxActivities?:Statement,
+ createOutboxActivity?:Statement,
+ getOutboxActivity?:Statement,
+ listOutboxActivities?:Statement,
+ createFollowing?:Statement,
+ getFollowing?:Statement,
+ deleteFollowing?:Statement,
+ listFollowing?:Statement,
+ acceptFollowing?:Statement,
+ createFollower?:Statement,
+ getFollower?:Statement,
+ deleteFollower?:Statement,
+ listFollowers?:Statement,
+ createLiked?:Statement,
+ getLiked?:Statement,
+ deleteLiked?:Statement,
+ listLiked?:Statement,
+ createDisliked?:Statement,
+ getDisliked?:Statement,
+ deleteDisliked?:Statement,
+ listDisliked?:Statement,
+ createShared?:Statement,
+ getShared?:Statement,
+ deleteShared?:Statement,
+ listShared?:Statement,
+ createPost?:Statement,
+ getPost?:Statement,
+ getPostByURL?:Statement,
+ deletePost?:Statement,
+ listPosts?:Statement
+ } = {}
+
+ cacheQueries() {
+ // inbox queries
+ this.queries.createInboxActivity = this.db.query(`INSERT INTO [inbox]
+ ([activity_id], [id], [type], [actor], [published], [to], [cc], [activity])
+ VALUES ($activity_id, $id, $type, $actor, $published, $to, $cc, $activity)`)
+ this.queries.getInboxActivity = this.db.query(`SELECT * FROM [inbox] WHERE [activity_id]=$activity_id`)
+ this.queries.listInboxActivities = this.db.query(`SELECT * FROM [inbox] ORDER BY [published] DESC`)
+
+ // outbox queries
+ this.queries.createOutboxActivity = this.db.query(`INSERT INTO [outbox]
+ ([activity_id], [id], [type], [actor], [published], [to], [cc], [activity])
+ VALUES ($activity_id, $id, $type, $actor, $published, $to, $cc, $activity)`)
+ this.queries.getOutboxActivity = this.db.query(`SELECT * FROM [outbox] WHERE [activity_id]=$activity_id`)
+ this.queries.listOutboxActivities = this.db.query(`SELECT * FROM [outbox] ORDER BY [published] DESC`)
+
+ // following queries
+ this.queries.createFollowing = this.db.query(`INSERT INTO [following]
+ ([activity_id], [id], [published], [accepted])
+ VALUES ($activity_id, $id, $published, NULL)`)
+ this.queries.getFollowing = this.db.query('SELECT * FROM [following] WHERE [id]=$id')
+ this.queries.deleteFollowing = this.db.query('DELETE FROM [following] WHERE [id]=$id')
+ this.queries.listFollowing = this.db.query('SELECT * FROM [following] ORDER BY [published] DESC')
+ this.queries.acceptFollowing = this.db.query('UPDATE [following] SET [accepted]=TRUE WHERE [id]=$id')
+
+ // follower queries
+ this.queries.createFollower = this.db.query(`INSERT INTO [followers]
+ ([activity_id], [id], [published])
+ VALUES ($activity_id, $id, $published)`)
+ this.queries.getFollower = this.db.query('SELECT * FROM [followers] WHERE [id]=$id')
+ this.queries.deleteFollower = this.db.query('DELETE FROM [followers] WHERE [id]=$id')
+ this.queries.listFollowers = this.db.query('SELECT * FROM [followers] ORDER BY [published] DESC')
+
+ // liked queries
+ this.queries.createLiked = this.db.query(`INSERT INTO [liked]
+ ([activity_id], [id], [published])
+ VALUES ($activity_id, $id, $published)`)
+ this.queries.getLiked = this.db.query('SELECT * FROM [liked] WHERE [id]=$id')
+ this.queries.deleteLiked = this.db.query('DELETE FROM [liked] WHERE [id]=$id')
+ this.queries.listLiked = this.db.query('SELECT * FROM [liked] ORDER BY [published] DESC')
+
+ // disliked queries
+ this.queries.createDisliked = this.db.query(`INSERT INTO [disliked]
+ ([activity_id], [id], [published])
+ VALUES ($activity_id, $id, $published)`)
+ this.queries.getDisliked = this.db.query('SELECT * FROM [disliked] WHERE [id]=$id')
+ this.queries.deleteDisliked = this.db.query('DELETE FROM [disliked] WHERE [id]=$id')
+ this.queries.listDisliked = this.db.query('SELECT * FROM [disliked] ORDER BY [published] DESC')
+
+ // shared queries
+ this.queries.createShared = this.db.query(`INSERT INTO [shared]
+ ([activity_id], [id], [published])
+ VALUES ($activity_id, $id, $published)`)
+ this.queries.getShared = this.db.query('SELECT * FROM [shared] WHERE [id]=$id')
+ this.queries.deleteShared = this.db.query('DELETE FROM [shared] WHERE [id]=$id')
+ this.queries.listShared = this.db.query('SELECT * FROM [shared] ORDER BY [published] DESC')
+
+ // post queries
+ this.queries.createPost = this.db.query(`INSERT INTO [posts]
+ ([activity_id], [id], [published], [attributedTo], [type], [content], [object])
+ VALUES ($activity_id, $id, $published, $attributedTo, $type, $content, $object)`)
+ this.queries.getPost = this.db.query('SELECT * FROM [posts] WHERE [activity_id]=$activity_id')
+ this.queries.getPostByURL = this.db.query('SELECT * FROM [posts] WHERE [id]=$id')
+ this.queries.deletePost = this.db.query('DELETE FROM [posts] WHERE [id]=$id')
+ this.queries.listPosts = this.db.query('SELECT * FROM [posts] ORDER BY [published] DESC')
}
-}
-export async function getPostByURL(url_id:string) {
- if(!url_id || !url_id.startsWith(ACTOR.url + '/posts/')) return null
- const match = url_id.match(/\/([0-9a-f]+)\/?$/)
- const local_id = match ? match[1] : url_id
- return await getPost(local_id)
-}
+ createInboxActivity(activity_id:string, activity:any) {
+ //new Date(activity.published).getTime().toString(36)
+ return this.queries.createInboxActivity?.run({
+ $activity_id: activity_id,
+ $id: activity.id,
+ $type: activity.type,
+ $actor: activity.actor,
+ $published: activity.published || new Date().toISOString(),
+ $to: JSON.stringify(activity.to),
+ $cc: JSON.stringify(activity.cc),
+ $activity: JSON.stringify(activity)
+ })
+ }
-export async function deletePost(id:string) {
- unlinkSync(path.join(POSTS_PATH, id + '.md'))
- rebuild()
-}
+ getInboxActivity(activity_id:string):Activity | undefined {
+ return JSON.parse((this.queries.getInboxActivity?.get(activity_id) as any).activity)
+ }
-export async function listPosts() {
- return await Promise.all((await readdir(POSTS_PATH)).filter(v => v.endsWith('.md')).map(async filename => await getPost(filename.slice(0, -3))))
-}
+ listInboxActivities():Activity[] | undefined {
+ return this.queries.listInboxActivities?.all().map(r => JSON.parse((r as any).activity))
+ }
-export async function createFollowing(handle:string, id:string) {
- const file = Bun.file(path.join(DATA_PATH, `following.json`))
- const following_list = await file.json() as Array
- if(!following_list.find(v => v.id === id || v.handle === handle)) following_list.push({id, handle, createdAt: new Date().toISOString()})
- await Bun.write(file, JSON.stringify(following_list))
- rebuild()
-}
+ createOutboxActivity(activity_id:string, activity:any) {
+ //new Date(activity.published).getTime().toString(36)
+ return this.queries.createOutboxActivity?.run({
+ $activity_id: activity_id,
+ $id: activity.id,
+ $type: activity.type,
+ $actor: activity.actor,
+ $published: activity.published,
+ $to: JSON.stringify(activity.to),
+ $cc: JSON.stringify(activity.cc),
+ $activity: JSON.stringify(activity)
+ })
+ }
-export async function deleteFollowing(handle:string) {
- const file = Bun.file(path.join(DATA_PATH, `following.json`))
- const following_list = await file.json() as Array
- await Bun.write(file, JSON.stringify(following_list.filter(v => v.handle !== handle)))
- rebuild()
-}
+ getOutboxActivity(activity_id:string): Activity | undefined {
+ return JSON.parse((this.queries.getOutboxActivity?.get(activity_id) as any).activity)
+ }
-export async function getFollowing(handle:string) {
- const file = Bun.file(path.join(DATA_PATH, `following.json`))
- const following_list = await file.json() as Array
- return following_list.find(v => v.handle === handle)
-}
+ listOutboxActivities(): Activity[] | undefined {
+ return this.queries.listOutboxActivities?.all().map(r => JSON.parse((r as any).activity))
+ }
-export async function listFollowing(onlyAccepted = true) {
- const file = Bun.file(path.join(DATA_PATH, `following.json`))
- return ((await file.json()) as Array).filter(f => !onlyAccepted || f.accepted)
-}
+ createFollowing(activity_id:string, id:string, published:string | Date) {
+ if(published instanceof Date) published = published.toISOString()
+ return this.queries.createFollowing?.run({
+ $activity_id: activity_id,
+ $id: id,
+ $published: published
+ })
+ }
-export async function acceptFollowing(handle:string) {
- const file = Bun.file(path.join(DATA_PATH, `following.json`))
- const following_list = await file.json() as Array
- const following = following_list.find(v => v.handle === handle)
- if(following) following.accepted = new Date().toISOString()
- await Bun.write(file, JSON.stringify(following_list))
- rebuild()
-}
+ getFollowing(id:string): Following | undefined {
+ return this.queries.getFollowing?.get(id) as any
+ }
-export async function createFollower(actor:string, id:string) {
- const file = Bun.file(path.join(DATA_PATH, `followers.json`))
- const followers_list = await file.json() as Array
- if(!followers_list.find(v => v.id === id || v.actor === actor)) followers_list.push({id, actor, createdAt: new Date().toISOString()})
- await Bun.write(file, JSON.stringify(followers_list))
- rebuild()
-}
+ deleteFollowing(id:string) {
+ return this.queries.deleteFollowing?.run(id)
+ }
-export async function deleteFollower(actor:string) {
- const file = Bun.file(path.join(DATA_PATH, `followers.json`))
- const followers_list = await file.json() as Array
- await Bun.write(file, JSON.stringify(followers_list.filter(v => v.actor !== actor)))
- rebuild()
-}
+ listFollowing(): Following[] | undefined {
+ return this.queries.listFollowing?.all() as Following[]
+ }
-export async function getFollower(actor:string) {
- const file = Bun.file(path.join(DATA_PATH, `followers.json`))
- const followers_list = await file.json() as Array
- return followers_list.find(v => v.actor === actor)
-}
+ acceptFollowing(id:string) {
+ return this.queries.acceptFollowing?.run(id)
+ }
-export async function listFollowers() {
- const file = Bun.file(path.join(DATA_PATH, `followers.json`))
- return await file.json() as Array
-}
+ createFollower(activity_id:string, id:string, published:string | Date) {
+ if(published instanceof Date) published = published.toISOString()
+ return this.queries.createFollower?.run({
+ $activity_id: activity_id,
+ $id: id,
+ $published: published
+ })
+ }
-export async function createLiked(object_id:string, id:string) {
- const file = Bun.file(path.join(DATA_PATH, `liked.json`))
- const liked_list = await file.json() as Array
- if(!liked_list.find(v => v.object_id === object_id)) liked_list.push({id, object_id, createdAt: new Date().toISOString()})
- await Bun.write(file, JSON.stringify(liked_list))
- rebuild()
-}
+ getFollower(id:string): ActivityObject | undefined {
+ return this.queries.getFollower?.get(id) as ActivityObject
+ }
-export async function deleteLiked(object_id:string) {
- const file = Bun.file(path.join(DATA_PATH, `liked.json`))
- const liked_list = await file.json() as Array
- await Bun.write(file, JSON.stringify(liked_list.filter(v => v.object_id !== object_id)))
- rebuild()
-}
+ deleteFollower(id:string) {
+ console.log("! DELETE Follower ", id)
+ return this.queries.deleteFollower?.run(id)
+ }
-export async function listLiked() {
- const file = Bun.file(path.join(DATA_PATH, `liked.json`))
- return await file.json() as Array
-}
+ listFollowers(): ActivityObject[] | undefined {
+ return this.queries.listFollowers?.all().map(x => x as ActivityObject)
+ }
-export async function createDisliked(object_id:string, id:string) {
- const file = Bun.file(path.join(DATA_PATH, `disliked.json`))
- const disliked_list = await file.json() as Array
- if(!disliked_list.find(v => v.object_id === object_id)) disliked_list.push({id, object_id, createdAt: new Date().toISOString()})
- await Bun.write(file, JSON.stringify(disliked_list))
- rebuild()
-}
+ createLiked(activity_id:string, id:string, published:string | Date) {
+ if(published instanceof Date) published = published.toISOString()
+ return this.queries.createLiked?.run({
+ $activity_id: activity_id,
+ $id: id,
+ $published: published
+ })
+ }
-export async function deleteDisliked(object_id:string) {
- const file = Bun.file(path.join(DATA_PATH, `disliked.json`))
- const disliked_list = await file.json() as Array
- await Bun.write(file, JSON.stringify(disliked_list.filter(v => v.object_id !== object_id)))
- rebuild()
-}
+ getLiked(id:string): ActivityObject | undefined {
+ return this.queries.getLiked?.get(id) as ActivityObject
+ }
-export async function listDisliked() {
- const file = Bun.file(path.join(DATA_PATH, `disliked.json`))
- return await file.json() as Array
-}
+ deleteLiked(id:string) {
+ return this.queries.deleteLiked?.run(id)
+ }
-export async function createShared(object_id:string, id:string) {
- const file = Bun.file(path.join(DATA_PATH, `shared.json`))
- const shared_list = await file.json() as Array
- if(!shared_list.find(v => v.object_id === object_id)) shared_list.push({id, object_id, createdAt: new Date().toISOString()})
- await Bun.write(file, JSON.stringify(shared_list))
- rebuild()
-}
+ listLiked(): ActivityObject[] | undefined {
+ return this.queries.listLiked?.all().map(x => x as ActivityObject)
+ }
-export async function deleteShared(object_id:string) {
- const file = Bun.file(path.join(DATA_PATH, `shared.json`))
- const shared_list = await file.json() as Array
- await Bun.write(file, JSON.stringify(shared_list.filter(v => v.object_id !== object_id)))
- rebuild()
-}
+ createDisliked(activity_id:string, id:string, published:string | Date) {
+ if(published instanceof Date) published = published.toISOString()
+ return this.queries.createDisliked?.run({
+ $activity_id: activity_id,
+ $id: id,
+ $published: published
+ })
+ }
-export async function listShared() {
- const file = Bun.file(path.join(DATA_PATH, `shared.json`))
- return await file.json() as Array
+ getDisliked(id:string): ActivityObject | undefined {
+ return this.queries.getDisliked?.get(id) as ActivityObject
+ }
+
+ deleteDisliked(id:string) {
+ return this.queries.deleteDisliked?.run(id)
+ }
+
+ listDisliked(): ActivityObject[] | undefined {
+ return this.queries.listDisliked?.all().map(x => x as ActivityObject)
+ }
+
+ createShared(activity_id:string, id:string, published:string | Date) {
+ if(published instanceof Date) published = published.toISOString()
+ return this.queries.createShared?.run({
+ $activity_id: activity_id,
+ $id: id,
+ $published: published
+ })
+ }
+
+ getShared(id:string): ActivityObject | undefined {
+ return this.queries.getShared?.get(id) as ActivityObject
+ }
+
+ deleteShared(id:string) {
+ return this.queries.deleteShared?.run(id)
+ }
+
+ listShared(): ActivityObject[] | undefined {
+ return this.queries.listShared?.all().map(x => x as ActivityObject)
+ }
+
+ createPost(activity_id:string, object:any) {
+ return this.queries.createPost?.run({
+ $activity_id: activity_id,
+ $id: object.id,
+ $published: object.published,
+ $attributedTo: object.attributedTo,
+ $type: object.type,
+ $content: object.content,
+ $object: JSON.stringify(object)
+ })
+ }
+
+ getPost(activity_id:string): Post | undefined {
+ return JSON.parse((this.queries.getPost?.get(activity_id) as any).object)
+ }
+
+ getPostByURL(id:string): Post | undefined {
+ return JSON.parse((this.queries.getPostByURL?.get(id) as any).object)
+ }
+
+ deletePost(activity_id:string) {
+ return this.queries.deletePost?.run(activity_id)
+ }
+
+ listPosts(): Post[] | undefined {
+ return this.queries.listPosts?.all().map(p => JSON.parse((p as any).object))
+ }
}
\ No newline at end of file
diff --git a/src/env.ts b/src/env.ts
index faaada0..4e3f0be 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -31,6 +31,7 @@ export const STATIC_PATH = path.join('.', '_site')
export const CONTENT_PATH = path.join('.', '_content')
export const POSTS_PATH = path.join(CONTENT_PATH, "posts")
export const DATA_PATH = path.join(CONTENT_PATH, "_data")
+export const DB_PATH = path.join(DATA_PATH, "db.sqlite")
export const ACTIVITY_INBOX_PATH = path.join(DATA_PATH, "_inbox")
export const ACTIVITY_OUTBOX_PATH = path.join(DATA_PATH, "_outbox")
@@ -49,4 +50,4 @@ export const activityPubTypes = [
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'application/activity+json'
]
-export const contentTypeHeader = { 'Content-Type': activityPubTypes[0]}
\ No newline at end of file
+export const contentTypeHeader = { 'Content-Type': activityPubTypes[0] }
\ No newline at end of file
diff --git a/src/inbox.ts b/src/inbox.ts
index fd15ec8..f8559e0 100644
--- a/src/inbox.ts
+++ b/src/inbox.ts
@@ -1,26 +1,29 @@
import { idsFromValue } from "./activitypub"
-import * as db from "./db"
import outbox from "./outbox"
import { send } from "./request"
import ACTOR from "../actor"
+import ActivityPubDB from "./db"
-export default async function inbox(activity:any) {
+let db:ActivityPubDB
+export default async function inbox(activity:any, database:ActivityPubDB) {
+ db = database
const date = new Date()
// get the main recipients ([...new Set()] is to dedupe)
const recipientList = [...new Set([...idsFromValue(activity.to), ...idsFromValue(activity.cc), ...idsFromValue(activity.audience)])]
// if my list of followers in the list of recipients, then forward to them as well
if(recipientList.includes(ACTOR.url + "/followers")) {
- (await db.listFollowers()).forEach(f => send(f, activity, activity.attributedTo))
+ (db.listFollowers())?.forEach(f => send(f.id, activity, activity.attributedTo))
}
// save this activity to my inbox
- const id = `${date.getTime().toString(16)}`
- db.createInboxActivity(activity, id)
+ const activity_id = `${date.getTime().toString(32)}`
+ console.log(`New inbox activity ${activity_id} (${activity.type})`, activity)
+ db.createInboxActivity(activity_id, activity)
// TODO: process the activity and update local data
switch(activity.type) {
- case "Follow": follow(activity, id); break;
+ case "Follow": follow(activity, activity_id); break;
case "Accept": accept(activity); break;
case "Reject": reject(activity); break;
case "Undo": undo(activity); break;
@@ -29,10 +32,10 @@ export default async function inbox(activity:any) {
return new Response("", { status: 204 })
}
-const follow = async (activity:any, id:string) => {
+const follow = async (activity:any, activity_id:string) => {
// someone is following me
// save this follower locally
- db.createFollower(activity.actor, id)
+ db.createFollower(activity_id, activity.actor, activity.published || new Date().toISOString())
// send an accept message to the outbox
await outbox({
"@context": "https://www.w3.org/ns/activitystreams",
@@ -40,26 +43,26 @@ const follow = async (activity:any, id:string) => {
actor: ACTOR.id,
to: [activity.actor],
object: activity,
- });
+ }, db);
}
const undo = async (activity:any) => {
switch (activity.object.type) {
// someone is undoing their follow of me
- case "Follow": await db.deleteFollower(activity.actor); break
+ case "Follow": db.deleteFollower(activity.actor); break
}
}
const accept = async (activity:any) => {
switch (activity.object.type) {
// someone accepted my follow of them
- case "Follow": await db.acceptFollowing(activity.actor); break
+ case "Follow": db.acceptFollowing(activity.actor); break
}
}
const reject = async (activity:any) => {
switch (activity.object.type) {
// someone rejected my follow of them
- case "Follow": await db.deleteFollowing(activity.actor); break
+ case "Follow": db.deleteFollowing(activity.actor); break
}
}
diff --git a/src/index.ts b/src/index.ts
index 53dd718..489ebee 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,13 +1,10 @@
-import { DEFAULT_DOCUMENTS, STATIC_PATH } from "./env"
import admin from './admin'
-import activitypub from "./activitypub"
+import activitypub, { reqIsActivityPub } from "./activitypub"
import { fetchObject } from "./request"
-import path from "path"
-import { BunFile } from "bun"
-import { rebuild } from "./db"
import { handle, webfinger } from "../actor"
+import ActivityPubDB from './db'
-rebuild()
+const db = new ActivityPubDB()
const server = Bun.serve({
port: 3000,
@@ -15,7 +12,7 @@ const server = Bun.serve({
const url = new URL(req.url)
// log the incoming request info
- console.info(`${new Date().toISOString()} 📥 ${req.method} ${req.url}`)
+ console.info(`${new Date().toISOString()} 📥 ${req.method} ${reqIsActivityPub(req)?'[AP] ':''}${req.url}`)
// CORS route (for now, any domain has access)
if(req.method === "OPTIONS") {
@@ -36,6 +33,7 @@ const server = Bun.serve({
// return the webfinger
return Response.json(webfinger, { headers: { 'content-type': 'application/jrd+json' }})
}
+ // Debugging route for fetching ActivityPub documents
else if(req.method === "GET" && url.pathname.startsWith("/fetch/")) {
const object_url = url.pathname.substring(7)
if(!object_url) return new Response("No url supplied", { status: 400})
@@ -43,33 +41,9 @@ const server = Bun.serve({
return fetchObject(object_url)
}
- return admin(req) || activitypub(req) || staticFile(req)
+ // admin and activitypub routes
+ return admin(req, db) || activitypub(req, db) || new Response("", { status: 404 })
},
-});
-const getDefaultDocument = async(base_path: string) => {
- for(const d of DEFAULT_DOCUMENTS){
- const filePath = path.join(base_path, d)
- const file = Bun.file(filePath)
- if(await file.exists()) return file
- }
-}
+})
-const staticFile = async (req:Request): Promise => {
- try{
- const url = new URL(req.url)
- const filePath = path.join(STATIC_PATH, url.pathname)
- let file:BunFile|undefined = Bun.file(filePath)
- // if the file doesn't exist, attempt to get the default document for the path
- if(!(await file.exists())) file = await getDefaultDocument(filePath)
-
- if(file && await file.exists()) return new Response(file)
- // if the file still doesn't exist, just return a 404
- else return new Response("", { status: 404 })
- }
- catch(err) {
- console.error(err)
- return new Response("", { status: 404 })
- }
-}
-
-console.log(`Listening on http://localhost:${server.port} ...`);
\ No newline at end of file
+console.log(`Listening on http://localhost:${server.port} ...`)
\ No newline at end of file
diff --git a/src/outbox.ts b/src/outbox.ts
index 314ca5c..576484b 100644
--- a/src/outbox.ts
+++ b/src/outbox.ts
@@ -1,12 +1,15 @@
import { idsFromValue } from "./activitypub"
-import * as db from "./db"
import { fetchObject, send } from "./request"
import ACTOR from "../actor"
+import ActivityPubDB from "./db"
-export default async function outbox(activity:any):Promise {
+let db:ActivityPubDB
+
+export default async function outbox(activity:any, database:ActivityPubDB):Promise {
+ db = database
const date = new Date()
- const id = `${date.getTime().toString(16)}`
- console.log('outbox', id, activity)
+ const activity_id = `${date.getTime().toString(32)}`
+ console.log('outbox', activity_id, activity)
// https://www.w3.org/TR/activitypub/#object-without-create
if(!activity.actor && !(activity.object || activity.target || activity.result || activity.origin || activity.instrument)) {
@@ -25,7 +28,7 @@ export default async function outbox(activity:any):Promise {
if(audience) activity.audience = audience
}
- activity.id = `${ACTOR.url}/outbox/${id}`
+ activity.id = `${ACTOR.url}/outbox/${activity_id}`
if(!activity.published) activity.published = date.toISOString()
if(activity.type === 'Create' && activity.object && Object(activity.object) === activity.object) {
@@ -44,22 +47,22 @@ export default async function outbox(activity:any):Promise {
// now that has been taken care of, it's time to update our local data, depending on the contents of the activity
switch(activity.type) {
- case "Accept": await accept(activity, id); break;
- case "Follow": await follow(activity, id); break;
- case "Like": await like(activity, id); break;
- case "Dislike": await dislike(activity, id); break;
- case "Annouce": await announce(activity, id); break;
- case "Create": await create(activity, id); break;
+ case "Accept": await accept(activity, activity_id); break;
+ case "Follow": await follow(activity, activity_id); break;
+ case "Like": await like(activity, activity_id); break;
+ case "Dislike": await dislike(activity, activity_id); break;
+ case "Annouce": await announce(activity, activity_id); break;
+ case "Create": await create(activity, activity_id); break;
case "Undo": await undo(activity); break;
case "Delete": await deletePost(activity); break;
// TODO: case "Anncounce": return await share(activity)
}
// save the activity data for the outbox
- await db.createOutboxActivity(activity, id)
+ db.createOutboxActivity(activity_id, activity)
// send to the appropriate recipients
finalRecipientList.forEach((to) => {
- if (to.startsWith(ACTOR.url + "/followers")) db.listFollowers().then(followers => followers.forEach(f => send(f.actor, activity)))
+ if (to.startsWith(ACTOR.url + "/followers")) db.listFollowers()?.forEach(f => send(f.id, activity))
else if (to === "https://www.w3.org/ns/activitystreams#Public") return // there's nothing to "send" to here
else if (to) send(to, activity)
})
@@ -67,57 +70,59 @@ export default async function outbox(activity:any):Promise {
return new Response("", { status: 201, headers: { location: activity.id } })
}
-async function create(activity:any, id:string) {
- activity.object.id = activity.object.url = `${ACTOR.url}/posts/${id}`
- await db.createPost(activity.object, id)
+async function create(activity:any, activity_id:string) {
+ activity.object.id = activity.object.url = `${ACTOR.url}/posts/${activity_id}`
+ db.createPost(activity_id, activity.object)
return true
}
-async function accept(activity:any, id:string) {
+async function accept(activity:any, activity_id:string) {
return true
}
-async function follow(activity:any, id:string) {
- await db.createFollowing(activity.object , id)
+async function follow(activity:any, activity_id:string) {
+ idsFromValue(activity.object).forEach(id => {
+ db.createFollowing(activity_id, id, activity.published)
+ });
return true
}
-async function like(activity:any, id:string) {
+async function like(activity:any, activity_id:string) {
if(typeof activity.object === 'string'){
- await db.createLiked(activity.object, id)
- activity.object = await fetchObject(activity.object)
+ db.createLiked(activity_id, activity.object, activity.published || new Date())
+ activity.object = fetchObject(activity.object)
}
else {
- const liked = await idsFromValue(activity.object)
- liked.forEach(l => db.createLiked(l, id))
+ const liked = idsFromValue(activity.object)
+ liked.forEach(l => db.createLiked(activity_id, l, activity.published || new Date()))
}
- await db.createPost(activity, id)
+ db.createPost(activity_id, activity)
return true
}
-async function dislike(activity:any, id:string) {
+async function dislike(activity:any, activity_id:string) {
if(typeof activity.object === 'string'){
- await db.createDisliked(activity.object, id)
+ db.createDisliked(activity_id, activity.object, activity.published || new Date())
activity.object = await fetchObject(activity.object)
}
else {
const disliked = await idsFromValue(activity.object)
- disliked.forEach(l => db.createDisliked(l, id))
+ disliked.forEach(l => db.createDisliked(activity_id, l, activity.published || new Date()))
}
- await db.createPost(activity, id)
+ db.createPost(activity_id, activity)
return true
}
-async function announce(activity:any, id:string) {
+async function announce(activity:any, activity_id:string) {
if(typeof activity.object === 'string'){
- await db.createShared(activity.object, id)
+ db.createShared(activity_id, activity.object, activity.published || new Date())
activity.object = await fetchObject(activity.object)
}
else {
const shared = await idsFromValue(activity.object)
- shared.forEach(l => db.createShared(l, id))
+ shared.forEach(l => db.createShared(activity_id, l, activity.published || new Date()))
}
- await db.createPost(activity, id)
+ db.createPost(activity_id, activity)
return true
}
@@ -136,17 +141,17 @@ async function announce(activity:any, id:string) {
async function undo(activity:any) {
const id = await idsFromValue(activity.object).at(0)
if (!id) return true
- const match = id.match(/\/([0-9a-f]+)\/?$/)
- const local_id = match ? match[1] : id
- console.log('undo', local_id)
+ const match = id.match(/\/([0-9a-z]+)\/?$/)
+ const activity_id = match ? match[1] : id
+ console.log('undo', activity_id)
try{
- const existing = await db.getOutboxActivity(local_id)
+ const existing = db.getOutboxActivity(activity_id)
switch(activity.object.type) {
- case "Follow": await db.deleteFollowing(existing.object); break;
- case "Like": idsFromValue(existing.object).forEach(async id => await db.deleteLiked(id)); await db.deletePost(local_id); break;
- case "Dislike": idsFromValue(existing.object).forEach(async id => await db.deleteDisliked(id)); await db.deletePost(local_id); break;
- case "Announce": idsFromValue(existing.object).forEach(async id => await db.deleteShared(id)); await db.deletePost(local_id); break;
+ case "Follow": idsFromValue(existing?.object).forEach(async id => db.deleteFollowing(id)); break;
+ case "Like": idsFromValue(existing?.object).forEach(async id => db.deleteLiked(id)); db.deletePost(activity_id); break;
+ case "Dislike": idsFromValue(existing?.object).forEach(async id => db.deleteDisliked(id)); db.deletePost(activity_id); break;
+ case "Announce": idsFromValue(existing?.object).forEach(async id => db.deleteShared(id)); db.deletePost(activity_id); break;
}
}
@@ -160,8 +165,8 @@ async function undo(activity:any) {
async function deletePost(activity:any) {
const id = await idsFromValue(activity.object).at(0)
if(!id) return false
- const post = await db.getPostByURL(id)
+ const post = db.getPostByURL(id)
if(!post) return false
- await db.deletePost(post.local_id)
+ db.deletePost(post.activity_id)
return true
}
\ No newline at end of file