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.icon.name if author.icon.name else - - {{ 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) %} -
- - {{ authorMacro(author) }} -
-{% 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: #} -

- {{ item.name if item.name else item.title }} -

- {% 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 @@ -

- {{ item.name if item.name else item.title }} -

-{% 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 ---- - - - -{% 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 %} - - - 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 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - 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 @@ - -Made with Pixels to Svg https://codepen.io/shshaw/pen/XbxvNj - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 @@ - -Made with Pixels to Svg https://codepen.io/shshaw/pen/XbxvNj - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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