@geekity/cms 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2217 -214
- package/admin/components/fields.njk +165 -0
- package/admin/layouts/base.njk +1 -1
- package/admin/layouts/settings-page.njk +26 -0
- package/admin/layouts/shell.njk +17 -3
- package/admin/pages/account/forgot.njk +47 -0
- package/admin/pages/account/login.njk +26 -0
- package/admin/pages/account/reset.njk +38 -0
- package/admin/pages/account/setup.njk +28 -0
- package/admin/pages/appearance/themes.njk +69 -0
- package/admin/pages/comments/all.njk +133 -0
- package/admin/{layouts/dashboard.njk → pages/dashboard/home.njk} +8 -15
- package/admin/{layouts/document-conflict.njk → pages/documents/conflict.njk} +1 -0
- package/admin/{layouts/document-editor.njk → pages/documents/editor.njk} +75 -59
- package/admin/{layouts/document-list.njk → pages/documents/list.njk} +19 -2
- package/admin/pages/documents/taxonomy.njk +95 -0
- package/admin/pages/federation/followers.njk +238 -0
- package/admin/pages/federation/settings.njk +77 -0
- package/admin/pages/media/library.njk +160 -0
- package/admin/pages/messages/all.njk +110 -0
- package/admin/pages/navigation/menus.njk +129 -0
- package/admin/{layouts → pages}/placeholder.njk +5 -1
- package/admin/pages/settings/discussion.njk +99 -0
- package/admin/pages/settings/email.njk +203 -0
- package/admin/pages/settings/general.njk +29 -0
- package/admin/pages/settings/permalinks.njk +50 -0
- package/admin/pages/settings/reading.njk +64 -0
- package/admin/pages/tools/content-index.njk +89 -0
- package/admin/pages/users/edit.njk +239 -0
- package/admin/pages/users/list.njk +51 -0
- package/admin/pages/users/new.njk +41 -0
- package/admin/static/admin.css +558 -3
- package/admin/static/copy.js +52 -0
- package/admin/static/editor.js +16 -16
- package/admin/static/slug.js +61 -0
- package/dist/access-log.d.ts +44 -0
- package/dist/access-log.d.ts.map +1 -0
- package/dist/access-log.js +71 -0
- package/dist/access-log.js.map +1 -0
- package/dist/admin/accounts.d.ts +413 -0
- package/dist/admin/accounts.d.ts.map +1 -0
- package/dist/admin/accounts.js +680 -0
- package/dist/admin/accounts.js.map +1 -0
- package/dist/admin/appearance.d.ts +80 -0
- package/dist/admin/appearance.d.ts.map +1 -0
- package/dist/admin/appearance.js +136 -0
- package/dist/admin/appearance.js.map +1 -0
- package/dist/admin/comments.d.ts +90 -0
- package/dist/admin/comments.d.ts.map +1 -0
- package/dist/admin/comments.js +244 -0
- package/dist/admin/comments.js.map +1 -0
- package/dist/admin/credentials.d.ts +26 -2
- package/dist/admin/credentials.d.ts.map +1 -1
- package/dist/admin/credentials.js +35 -2
- package/dist/admin/credentials.js.map +1 -1
- package/dist/admin/documents.d.ts +117 -10
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +328 -41
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/federation.d.ts +161 -50
- package/dist/admin/federation.d.ts.map +1 -1
- package/dist/admin/federation.js +235 -93
- package/dist/admin/federation.js.map +1 -1
- package/dist/admin/formatting.d.ts +20 -0
- package/dist/admin/formatting.d.ts.map +1 -0
- package/dist/admin/formatting.js +72 -0
- package/dist/admin/formatting.js.map +1 -0
- package/dist/admin/headers.d.ts +65 -0
- package/dist/admin/headers.d.ts.map +1 -0
- package/dist/admin/headers.js +104 -0
- package/dist/admin/headers.js.map +1 -0
- package/dist/admin/index.d.ts +38 -12
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +25 -7
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/media.d.ts +168 -0
- package/dist/admin/media.d.ts.map +1 -0
- package/dist/admin/media.js +295 -0
- package/dist/admin/media.js.map +1 -0
- package/dist/admin/menu.d.ts +79 -0
- package/dist/admin/menu.d.ts.map +1 -0
- package/dist/admin/menu.js +143 -0
- package/dist/admin/menu.js.map +1 -0
- package/dist/admin/messages.d.ts +80 -0
- package/dist/admin/messages.d.ts.map +1 -0
- package/dist/admin/messages.js +152 -0
- package/dist/admin/messages.js.map +1 -0
- package/dist/admin/navigation.d.ts +155 -0
- package/dist/admin/navigation.d.ts.map +1 -0
- package/dist/admin/navigation.js +292 -0
- package/dist/admin/navigation.js.map +1 -0
- package/dist/admin/preview.d.ts.map +1 -1
- package/dist/admin/preview.js +31 -7
- package/dist/admin/preview.js.map +1 -1
- package/dist/admin/recovery.d.ts +92 -0
- package/dist/admin/recovery.d.ts.map +1 -0
- package/dist/admin/recovery.js +229 -0
- package/dist/admin/recovery.js.map +1 -0
- package/dist/admin/routes.d.ts +0 -20
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +164 -105
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/session.d.ts +17 -3
- package/dist/admin/session.d.ts.map +1 -1
- package/dist/admin/session.js +19 -5
- package/dist/admin/session.js.map +1 -1
- package/dist/admin/settings-discussion.d.ts +37 -0
- package/dist/admin/settings-discussion.d.ts.map +1 -0
- package/dist/admin/settings-discussion.js +117 -0
- package/dist/admin/settings-discussion.js.map +1 -0
- package/dist/admin/settings-email.d.ts +54 -0
- package/dist/admin/settings-email.d.ts.map +1 -0
- package/dist/admin/settings-email.js +189 -0
- package/dist/admin/settings-email.js.map +1 -0
- package/dist/admin/settings-federation.d.ts +48 -0
- package/dist/admin/settings-federation.d.ts.map +1 -0
- package/dist/admin/settings-federation.js +123 -0
- package/dist/admin/settings-federation.js.map +1 -0
- package/dist/admin/settings-general.d.ts +12 -0
- package/dist/admin/settings-general.d.ts.map +1 -0
- package/dist/admin/settings-general.js +48 -0
- package/dist/admin/settings-general.js.map +1 -0
- package/dist/admin/settings-page.d.ts +119 -0
- package/dist/admin/settings-page.d.ts.map +1 -0
- package/dist/admin/settings-page.js +132 -0
- package/dist/admin/settings-page.js.map +1 -0
- package/dist/admin/settings-pages.d.ts +30 -0
- package/dist/admin/settings-pages.d.ts.map +1 -0
- package/dist/admin/settings-pages.js +43 -0
- package/dist/admin/settings-pages.js.map +1 -0
- package/dist/admin/settings-permalinks.d.ts +16 -0
- package/dist/admin/settings-permalinks.d.ts.map +1 -0
- package/dist/admin/settings-permalinks.js +32 -0
- package/dist/admin/settings-permalinks.js.map +1 -0
- package/dist/admin/settings-reading.d.ts +34 -0
- package/dist/admin/settings-reading.d.ts.map +1 -0
- package/dist/admin/settings-reading.js +49 -0
- package/dist/admin/settings-reading.js.map +1 -0
- package/dist/admin/settings.d.ts +339 -96
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +600 -308
- package/dist/admin/settings.js.map +1 -1
- package/dist/admin/store.d.ts +606 -152
- package/dist/admin/store.d.ts.map +1 -1
- package/dist/admin/store.js +863 -268
- package/dist/admin/store.js.map +1 -1
- package/dist/admin/taxonomy.d.ts +114 -0
- package/dist/admin/taxonomy.d.ts.map +1 -0
- package/dist/admin/taxonomy.js +289 -0
- package/dist/admin/taxonomy.js.map +1 -0
- package/dist/admin/templates.d.ts +46 -14
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +46 -14
- package/dist/admin/templates.js.map +1 -1
- package/dist/admin/throttle.d.ts +84 -0
- package/dist/admin/throttle.d.ts.map +1 -0
- package/dist/admin/throttle.js +143 -0
- package/dist/admin/throttle.js.map +1 -0
- package/dist/admin/tools.d.ts +111 -0
- package/dist/admin/tools.d.ts.map +1 -0
- package/dist/admin/tools.js +182 -0
- package/dist/admin/tools.js.map +1 -0
- package/dist/admin/uploads.d.ts +28 -2
- package/dist/admin/uploads.d.ts.map +1 -1
- package/dist/admin/uploads.js +43 -4
- package/dist/admin/uploads.js.map +1 -1
- package/dist/admin/users.d.ts +100 -13
- package/dist/admin/users.d.ts.map +1 -1
- package/dist/admin/users.js +412 -39
- package/dist/admin/users.js.map +1 -1
- package/dist/cache.d.ts +141 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +207 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +14 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +429 -21
- package/dist/cli.js.map +1 -1
- package/dist/comments/akismet.d.ts +130 -0
- package/dist/comments/akismet.d.ts.map +1 -0
- package/dist/comments/akismet.js +274 -0
- package/dist/comments/akismet.js.map +1 -0
- package/dist/comments/conversation.d.ts +29 -0
- package/dist/comments/conversation.d.ts.map +1 -0
- package/dist/comments/conversation.js +60 -0
- package/dist/comments/conversation.js.map +1 -0
- package/dist/comments/form.d.ts +123 -0
- package/dist/comments/form.d.ts.map +1 -0
- package/dist/comments/form.js +79 -0
- package/dist/comments/form.js.map +1 -0
- package/dist/comments/index.d.ts +16 -0
- package/dist/comments/index.d.ts.map +1 -0
- package/dist/comments/index.js +10 -0
- package/dist/comments/index.js.map +1 -0
- package/dist/comments/markdown.d.ts +8 -0
- package/dist/comments/markdown.d.ts.map +1 -0
- package/dist/comments/markdown.js +85 -0
- package/dist/comments/markdown.js.map +1 -0
- package/dist/comments/moderate.d.ts +71 -0
- package/dist/comments/moderate.d.ts.map +1 -0
- package/dist/comments/moderate.js +88 -0
- package/dist/comments/moderate.js.map +1 -0
- package/dist/comments/policy.d.ts +57 -0
- package/dist/comments/policy.d.ts.map +1 -0
- package/dist/comments/policy.js +80 -0
- package/dist/comments/policy.js.map +1 -0
- package/dist/comments/records.d.ts +266 -0
- package/dist/comments/records.d.ts.map +1 -0
- package/dist/comments/records.js +487 -0
- package/dist/comments/records.js.map +1 -0
- package/dist/comments/routes.d.ts +60 -0
- package/dist/comments/routes.d.ts.map +1 -0
- package/dist/comments/routes.js +212 -0
- package/dist/comments/routes.js.map +1 -0
- package/dist/comments/submission.d.ts +297 -0
- package/dist/comments/submission.d.ts.map +1 -0
- package/dist/comments/submission.js +280 -0
- package/dist/comments/submission.js.map +1 -0
- package/dist/comments/viewer.d.ts +21 -0
- package/dist/comments/viewer.d.ts.map +1 -0
- package/dist/comments/viewer.js +45 -0
- package/dist/comments/viewer.js.map +1 -0
- package/dist/config.d.ts +225 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +121 -6
- package/dist/config.js.map +1 -1
- package/dist/contact/delivery.d.ts +60 -0
- package/dist/contact/delivery.d.ts.map +1 -0
- package/dist/contact/delivery.js +87 -0
- package/dist/contact/delivery.js.map +1 -0
- package/dist/contact/form.d.ts +130 -0
- package/dist/contact/form.d.ts.map +1 -0
- package/dist/contact/form.js +163 -0
- package/dist/contact/form.js.map +1 -0
- package/dist/contact/index.d.ts +17 -0
- package/dist/contact/index.d.ts.map +1 -0
- package/dist/contact/index.js +13 -0
- package/dist/contact/index.js.map +1 -0
- package/dist/contact/records.d.ts +124 -0
- package/dist/contact/records.d.ts.map +1 -0
- package/dist/contact/records.js +218 -0
- package/dist/contact/records.js.map +1 -0
- package/dist/contact/routes.d.ts +17 -0
- package/dist/contact/routes.d.ts.map +1 -0
- package/dist/contact/routes.js +127 -0
- package/dist/contact/routes.js.map +1 -0
- package/dist/contact/submission.d.ts +103 -0
- package/dist/contact/submission.d.ts.map +1 -0
- package/dist/contact/submission.js +166 -0
- package/dist/contact/submission.js.map +1 -0
- package/dist/content/document.d.ts +7 -2
- package/dist/content/document.d.ts.map +1 -1
- package/dist/content/document.js +1 -0
- package/dist/content/document.js.map +1 -1
- package/dist/content/index.d.ts +7 -2
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -1
- package/dist/content/index.js.map +1 -1
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +24 -2
- package/dist/content/markdown.js.map +1 -1
- package/dist/content/parser.d.ts.map +1 -1
- package/dist/content/parser.js +11 -4
- package/dist/content/parser.js.map +1 -1
- package/dist/content/save.d.ts +5 -0
- package/dist/content/save.d.ts.map +1 -1
- package/dist/content/save.js +44 -1
- package/dist/content/save.js.map +1 -1
- package/dist/content/schedule.d.ts +125 -0
- package/dist/content/schedule.d.ts.map +1 -0
- package/dist/content/schedule.js +207 -0
- package/dist/content/schedule.js.map +1 -0
- package/dist/content/search.d.ts +64 -0
- package/dist/content/search.d.ts.map +1 -0
- package/dist/content/search.js +136 -0
- package/dist/content/search.js.map +1 -0
- package/dist/content/store.d.ts +220 -7
- package/dist/content/store.d.ts.map +1 -1
- package/dist/content/store.js +392 -67
- package/dist/content/store.js.map +1 -1
- package/dist/content/sync.d.ts +7 -1
- package/dist/content/sync.d.ts.map +1 -1
- package/dist/content/sync.js +18 -10
- package/dist/content/sync.js.map +1 -1
- package/dist/content/time.d.ts +56 -0
- package/dist/content/time.d.ts.map +1 -0
- package/dist/content/time.js +141 -0
- package/dist/content/time.js.map +1 -0
- package/dist/content/writer.d.ts.map +1 -1
- package/dist/content/writer.js +3 -0
- package/dist/content/writer.js.map +1 -1
- package/dist/env.d.ts +70 -5
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/actor.d.ts +95 -41
- package/dist/federation/actor.d.ts.map +1 -1
- package/dist/federation/actor.js +180 -53
- package/dist/federation/actor.js.map +1 -1
- package/dist/federation/article.d.ts +44 -17
- package/dist/federation/article.d.ts.map +1 -1
- package/dist/federation/article.js +98 -46
- package/dist/federation/article.js.map +1 -1
- package/dist/federation/delivery.d.ts +63 -15
- package/dist/federation/delivery.d.ts.map +1 -1
- package/dist/federation/delivery.js +154 -80
- package/dist/federation/delivery.js.map +1 -1
- package/dist/federation/federation.d.ts +22 -17
- package/dist/federation/federation.d.ts.map +1 -1
- package/dist/federation/federation.js +98 -77
- package/dist/federation/federation.js.map +1 -1
- package/dist/federation/followers.d.ts +3 -2
- package/dist/federation/followers.d.ts.map +1 -1
- package/dist/federation/followers.js +6 -5
- package/dist/federation/followers.js.map +1 -1
- package/dist/federation/import-wordpress.d.ts +128 -0
- package/dist/federation/import-wordpress.d.ts.map +1 -0
- package/dist/federation/import-wordpress.js +426 -0
- package/dist/federation/import-wordpress.js.map +1 -0
- package/dist/federation/inbox.d.ts +36 -13
- package/dist/federation/inbox.d.ts.map +1 -1
- package/dist/federation/inbox.js +120 -49
- package/dist/federation/inbox.js.map +1 -1
- package/dist/federation/index.d.ts +22 -10
- package/dist/federation/index.d.ts.map +1 -1
- package/dist/federation/index.js +13 -8
- package/dist/federation/index.js.map +1 -1
- package/dist/federation/keys.d.ts +98 -20
- package/dist/federation/keys.d.ts.map +1 -1
- package/dist/federation/keys.js +269 -39
- package/dist/federation/keys.js.map +1 -1
- package/dist/federation/mount.d.ts +46 -10
- package/dist/federation/mount.d.ts.map +1 -1
- package/dist/federation/mount.js +300 -28
- package/dist/federation/mount.js.map +1 -1
- package/dist/federation/paths.d.ts +23 -35
- package/dist/federation/paths.d.ts.map +1 -1
- package/dist/federation/paths.js +26 -39
- package/dist/federation/paths.js.map +1 -1
- package/dist/federation/records.d.ts +223 -0
- package/dist/federation/records.d.ts.map +1 -0
- package/dist/federation/records.js +506 -0
- package/dist/federation/records.js.map +1 -0
- package/dist/federation/relays.d.ts +114 -0
- package/dist/federation/relays.d.ts.map +1 -0
- package/dist/federation/relays.js +290 -0
- package/dist/federation/relays.js.map +1 -0
- package/dist/federation/replies.d.ts +80 -0
- package/dist/federation/replies.d.ts.map +1 -0
- package/dist/federation/replies.js +166 -0
- package/dist/federation/replies.js.map +1 -0
- package/dist/federation/wordpress.d.ts +131 -0
- package/dist/federation/wordpress.d.ts.map +1 -0
- package/dist/federation/wordpress.js +341 -0
- package/dist/federation/wordpress.js.map +1 -0
- package/dist/files/atomic.d.ts +103 -0
- package/dist/files/atomic.d.ts.map +1 -0
- package/dist/files/atomic.js +150 -0
- package/dist/files/atomic.js.map +1 -0
- package/dist/files/index.d.ts +3 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +2 -0
- package/dist/files/index.js.map +1 -0
- package/dist/forms/protection.d.ts +85 -0
- package/dist/forms/protection.d.ts.map +1 -0
- package/dist/forms/protection.js +116 -0
- package/dist/forms/protection.js.map +1 -0
- package/dist/images/icons.d.ts +59 -0
- package/dist/images/icons.d.ts.map +1 -0
- package/dist/images/icons.js +131 -0
- package/dist/images/icons.js.map +1 -0
- package/dist/images/index.d.ts +7 -0
- package/dist/images/index.d.ts.map +1 -0
- package/dist/images/index.js +4 -0
- package/dist/images/index.js.map +1 -0
- package/dist/images/markup.d.ts +47 -0
- package/dist/images/markup.d.ts.map +1 -0
- package/dist/images/markup.js +175 -0
- package/dist/images/markup.js.map +1 -0
- package/dist/images/paths.d.ts +34 -0
- package/dist/images/paths.d.ts.map +1 -0
- package/dist/images/paths.js +41 -0
- package/dist/images/paths.js.map +1 -0
- package/dist/images/variants.d.ts +104 -0
- package/dist/images/variants.d.ts.map +1 -0
- package/dist/images/variants.js +320 -0
- package/dist/images/variants.js.map +1 -0
- package/dist/index.d.ts +123 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +344 -25
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +18 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +34 -3
- package/dist/init.js.map +1 -1
- package/dist/mail/brevo.d.ts +35 -0
- package/dist/mail/brevo.d.ts.map +1 -0
- package/dist/mail/brevo.js +85 -0
- package/dist/mail/brevo.js.map +1 -0
- package/dist/mail/credentials.d.ts +62 -0
- package/dist/mail/credentials.d.ts.map +1 -0
- package/dist/mail/credentials.js +86 -0
- package/dist/mail/credentials.js.map +1 -0
- package/dist/mail/index.d.ts +15 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +8 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/mail/memory.d.ts +35 -0
- package/dist/mail/memory.d.ts.map +1 -0
- package/dist/mail/memory.js +29 -0
- package/dist/mail/memory.js.map +1 -0
- package/dist/mail/provider.d.ts +80 -0
- package/dist/mail/provider.d.ts.map +1 -0
- package/dist/mail/provider.js +23 -0
- package/dist/mail/provider.js.map +1 -0
- package/dist/mail/service.d.ts +156 -0
- package/dist/mail/service.d.ts.map +1 -0
- package/dist/mail/service.js +265 -0
- package/dist/mail/service.js.map +1 -0
- package/dist/mail/smtp.d.ts +39 -0
- package/dist/mail/smtp.d.ts.map +1 -0
- package/dist/mail/smtp.js +58 -0
- package/dist/mail/smtp.js.map +1 -0
- package/dist/mail/templates.d.ts +91 -0
- package/dist/mail/templates.d.ts.map +1 -0
- package/dist/mail/templates.js +69 -0
- package/dist/mail/templates.js.map +1 -0
- package/dist/notifications/comments.d.ts +66 -0
- package/dist/notifications/comments.d.ts.map +1 -0
- package/dist/notifications/comments.js +151 -0
- package/dist/notifications/comments.js.map +1 -0
- package/dist/notifications/digest.d.ts +135 -0
- package/dist/notifications/digest.d.ts.map +1 -0
- package/dist/notifications/digest.js +287 -0
- package/dist/notifications/digest.js.map +1 -0
- package/dist/notifications/index.d.ts +13 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +8 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/links.d.ts +60 -0
- package/dist/notifications/links.d.ts.map +1 -0
- package/dist/notifications/links.js +63 -0
- package/dist/notifications/links.js.map +1 -0
- package/dist/notifications/optouts.d.ts +48 -0
- package/dist/notifications/optouts.d.ts.map +1 -0
- package/dist/notifications/optouts.js +93 -0
- package/dist/notifications/optouts.js.map +1 -0
- package/dist/notifications/preferences.d.ts +158 -0
- package/dist/notifications/preferences.d.ts.map +1 -0
- package/dist/notifications/preferences.js +195 -0
- package/dist/notifications/preferences.js.map +1 -0
- package/dist/notifications/routes.d.ts +28 -0
- package/dist/notifications/routes.d.ts.map +1 -0
- package/dist/notifications/routes.js +184 -0
- package/dist/notifications/routes.js.map +1 -0
- package/dist/notifications/tokens.d.ts +60 -0
- package/dist/notifications/tokens.d.ts.map +1 -0
- package/dist/notifications/tokens.js +125 -0
- package/dist/notifications/tokens.js.map +1 -0
- package/dist/notify.d.ts +84 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +149 -0
- package/dist/notify.js.map +1 -0
- package/dist/web/archive.d.ts +49 -0
- package/dist/web/archive.d.ts.map +1 -0
- package/dist/web/archive.js +59 -0
- package/dist/web/archive.js.map +1 -0
- package/dist/web/assets.d.ts +6 -5
- package/dist/web/assets.d.ts.map +1 -1
- package/dist/web/assets.js +5 -7
- package/dist/web/assets.js.map +1 -1
- package/dist/web/authors.d.ts +143 -0
- package/dist/web/authors.d.ts.map +1 -0
- package/dist/web/authors.js +178 -0
- package/dist/web/authors.js.map +1 -0
- package/dist/web/comments.d.ts +60 -0
- package/dist/web/comments.d.ts.map +1 -0
- package/dist/web/comments.js +207 -0
- package/dist/web/comments.js.map +1 -0
- package/dist/web/context.d.ts +162 -28
- package/dist/web/context.d.ts.map +1 -1
- package/dist/web/context.js +123 -26
- package/dist/web/context.js.map +1 -1
- package/dist/web/conversation.d.ts +209 -0
- package/dist/web/conversation.d.ts.map +1 -0
- package/dist/web/conversation.js +557 -0
- package/dist/web/conversation.js.map +1 -0
- package/dist/web/documents.d.ts +36 -6
- package/dist/web/documents.d.ts.map +1 -1
- package/dist/web/documents.js +52 -10
- package/dist/web/documents.js.map +1 -1
- package/dist/web/feed-atom.d.ts +22 -0
- package/dist/web/feed-atom.d.ts.map +1 -0
- package/dist/web/feed-atom.js +83 -0
- package/dist/web/feed-atom.js.map +1 -0
- package/dist/web/feed-item.d.ts +145 -0
- package/dist/web/feed-item.d.ts.map +1 -0
- package/dist/web/feed-item.js +120 -0
- package/dist/web/feed-item.js.map +1 -0
- package/dist/web/feed-json.d.ts +79 -0
- package/dist/web/feed-json.d.ts.map +1 -0
- package/dist/web/feed-json.js +53 -0
- package/dist/web/feed-json.js.map +1 -0
- package/dist/web/feed-rss.d.ts +26 -0
- package/dist/web/feed-rss.d.ts.map +1 -0
- package/dist/web/feed-rss.js +203 -0
- package/dist/web/feed-rss.js.map +1 -0
- package/dist/web/feed-source.d.ts +244 -0
- package/dist/web/feed-source.d.ts.map +1 -0
- package/dist/web/feed-source.js +201 -0
- package/dist/web/feed-source.js.map +1 -0
- package/dist/web/feed-xml.d.ts +63 -0
- package/dist/web/feed-xml.d.ts.map +1 -0
- package/dist/web/feed-xml.js +116 -0
- package/dist/web/feed-xml.js.map +1 -0
- package/dist/web/feeds.d.ts +31 -96
- package/dist/web/feeds.d.ts.map +1 -1
- package/dist/web/feeds.js +94 -198
- package/dist/web/feeds.js.map +1 -1
- package/dist/web/health.d.ts +31 -0
- package/dist/web/health.d.ts.map +1 -0
- package/dist/web/health.js +44 -0
- package/dist/web/health.js.map +1 -0
- package/dist/web/index.d.ts +30 -10
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +18 -7
- package/dist/web/index.js.map +1 -1
- package/dist/web/navigation.d.ts +195 -0
- package/dist/web/navigation.d.ts.map +1 -0
- package/dist/web/navigation.js +300 -0
- package/dist/web/navigation.js.map +1 -0
- package/dist/web/negotiate.d.ts +31 -0
- package/dist/web/negotiate.d.ts.map +1 -1
- package/dist/web/negotiate.js +31 -11
- package/dist/web/negotiate.js.map +1 -1
- package/dist/web/recent.d.ts +43 -0
- package/dist/web/recent.d.ts.map +1 -0
- package/dist/web/recent.js +37 -0
- package/dist/web/recent.js.map +1 -0
- package/dist/web/render.d.ts +203 -9
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +341 -17
- package/dist/web/render.js.map +1 -1
- package/dist/web/routes.d.ts +25 -8
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +854 -93
- package/dist/web/routes.js.map +1 -1
- package/dist/web/sanitize.d.ts +19 -0
- package/dist/web/sanitize.d.ts.map +1 -0
- package/dist/web/sanitize.js +272 -0
- package/dist/web/sanitize.js.map +1 -0
- package/dist/web/search.d.ts +79 -0
- package/dist/web/search.d.ts.map +1 -0
- package/dist/web/search.js +94 -0
- package/dist/web/search.js.map +1 -0
- package/dist/web/sitemap.d.ts +120 -0
- package/dist/web/sitemap.d.ts.map +1 -0
- package/dist/web/sitemap.js +199 -0
- package/dist/web/sitemap.js.map +1 -0
- package/dist/web/taxonomy.d.ts +152 -0
- package/dist/web/taxonomy.d.ts.map +1 -0
- package/dist/web/taxonomy.js +237 -0
- package/dist/web/taxonomy.js.map +1 -0
- package/dist/web/templates.d.ts +54 -16
- package/dist/web/templates.d.ts.map +1 -1
- package/dist/web/templates.js +109 -28
- package/dist/web/templates.js.map +1 -1
- package/dist/web/themes.d.ts +226 -0
- package/dist/web/themes.d.ts.map +1 -0
- package/dist/web/themes.js +314 -0
- package/dist/web/themes.js.map +1 -0
- package/dist/webmention/discovery.d.ts +51 -0
- package/dist/webmention/discovery.d.ts.map +1 -0
- package/dist/webmention/discovery.js +176 -0
- package/dist/webmention/discovery.js.map +1 -0
- package/dist/webmention/html.d.ts +71 -0
- package/dist/webmention/html.d.ts.map +1 -0
- package/dist/webmention/html.js +285 -0
- package/dist/webmention/html.js.map +1 -0
- package/dist/webmention/index.d.ts +12 -0
- package/dist/webmention/index.d.ts.map +1 -0
- package/dist/webmention/index.js +8 -0
- package/dist/webmention/index.js.map +1 -0
- package/dist/webmention/links.d.ts +18 -0
- package/dist/webmention/links.d.ts.map +1 -0
- package/dist/webmention/links.js +64 -0
- package/dist/webmention/links.js.map +1 -0
- package/dist/webmention/microformats.d.ts +93 -0
- package/dist/webmention/microformats.d.ts.map +1 -0
- package/dist/webmention/microformats.js +376 -0
- package/dist/webmention/microformats.js.map +1 -0
- package/dist/webmention/receive.d.ts +141 -0
- package/dist/webmention/receive.d.ts.map +1 -0
- package/dist/webmention/receive.js +264 -0
- package/dist/webmention/receive.js.map +1 -0
- package/dist/webmention/routes.d.ts +37 -0
- package/dist/webmention/routes.d.ts.map +1 -0
- package/dist/webmention/routes.js +75 -0
- package/dist/webmention/routes.js.map +1 -0
- package/dist/webmention/service.d.ts +94 -0
- package/dist/webmention/service.d.ts.map +1 -0
- package/dist/webmention/service.js +207 -0
- package/dist/webmention/service.js.map +1 -0
- package/package.json +11 -6
- package/templates/site/content/_data/site.json +8 -0
- package/templates/site/content/pages/about.md +7 -0
- package/templates/site/content/posts/2026-01-01-hello-world.md +5 -3
- package/templates/site/geekity.config.ts +11 -1
- package/templates/site/tsconfig.json +1 -1
- package/themes/default/README.md +1216 -50
- package/themes/default/layouts/404.njk +2 -3
- package/themes/default/layouts/author.njk +36 -0
- package/themes/default/layouts/base.njk +161 -31
- package/themes/default/layouts/category.njk +27 -0
- package/themes/default/layouts/front-page.njk +56 -0
- package/themes/default/layouts/home.njk +17 -1
- package/themes/default/layouts/page.njk +57 -5
- package/themes/default/layouts/post.njk +78 -19
- package/themes/default/layouts/search.njk +57 -0
- package/themes/default/layouts/tag.njk +3 -6
- package/themes/default/mail/comment-digest.html.njk +37 -0
- package/themes/default/mail/comment-digest.subject.njk +3 -0
- package/themes/default/mail/comment-digest.txt.njk +40 -0
- package/themes/default/mail/comment-pending.html.njk +27 -0
- package/themes/default/mail/comment-pending.subject.njk +2 -0
- package/themes/default/mail/comment-pending.txt.njk +24 -0
- package/themes/default/mail/comment-reply.html.njk +21 -0
- package/themes/default/mail/comment-reply.subject.njk +2 -0
- package/themes/default/mail/comment-reply.txt.njk +22 -0
- package/themes/default/mail/contact-message.html.njk +24 -0
- package/themes/default/mail/contact-message.subject.njk +5 -0
- package/themes/default/mail/contact-message.txt.njk +20 -0
- package/themes/default/mail/password-changed.html.njk +18 -0
- package/themes/default/mail/password-changed.subject.njk +2 -0
- package/themes/default/mail/password-changed.txt.njk +17 -0
- package/themes/default/mail/password-reset.html.njk +22 -0
- package/themes/default/mail/password-reset.subject.njk +2 -0
- package/themes/default/mail/password-reset.txt.njk +18 -0
- package/themes/default/mail/test.html.njk +14 -0
- package/themes/default/mail/test.subject.njk +2 -0
- package/themes/default/mail/test.txt.njk +12 -0
- package/themes/default/partials/archive.njk +27 -0
- package/themes/default/partials/bio.njk +71 -0
- package/themes/default/partials/comment-form.njk +137 -0
- package/themes/default/partials/contact-form.njk +88 -0
- package/themes/default/partials/conversation.njk +139 -0
- package/themes/default/partials/feeds.njk +26 -0
- package/themes/default/partials/jsonld.njk +120 -0
- package/themes/default/partials/menu.njk +32 -0
- package/themes/default/partials/pagination.njk +12 -6
- package/themes/default/partials/post-list.njk +51 -18
- package/themes/default/partials/search-form.njk +22 -0
- package/themes/default/partials/tags.njk +26 -5
- package/themes/default/static/highlight.js +4 -0
- package/themes/default/static/style.css +1142 -194
- package/themes/default/theme.json +9 -0
- package/admin/layouts/federation.njk +0 -145
- package/admin/layouts/login.njk +0 -23
- package/admin/layouts/settings.njk +0 -116
- package/admin/layouts/setup.njk +0 -27
- package/admin/layouts/users.njk +0 -91
- /package/admin/{layouts/_flash.njk → components/flash.njk} +0 -0
package/dist/admin/store.js
CHANGED
|
@@ -1,32 +1,27 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
4
2
|
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
/** How long a session id is, in bytes. 32 is the 256 bits doc-5 asks for. */
|
|
8
|
-
export const SESSION_ID_BYTES = 32;
|
|
3
|
+
import { databaseFile, openDatabase } from '../cache.js';
|
|
4
|
+
import { REPLY_ACTIVITY_TYPE, replyTargetOf } from '../federation/replies.js';
|
|
9
5
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* of the fediverse verify; `Ed25519` signs FEP-8b32 object integrity proofs.
|
|
14
|
-
* They are Fedify's own spellings, so a row round-trips into
|
|
15
|
-
* `generateCryptoKeyPair` without translation.
|
|
6
|
+
* Which rows of the inbox log are replies: a `Create` that named something it
|
|
7
|
+
* answers. Spelled once, because a count and a listing that disagreed about it
|
|
8
|
+
* would show a post a number no page of comments could produce.
|
|
16
9
|
*/
|
|
17
|
-
|
|
10
|
+
const IS_REPLY = `activity_type = '${REPLY_ACTIVITY_TYPE}' AND in_reply_to IS NOT NULL`;
|
|
11
|
+
/** How long a session id is, in bytes. 32 is the 256 bits doc-5 asks for. */
|
|
12
|
+
export const SESSION_ID_BYTES = 32;
|
|
18
13
|
/** How one delivery of one activity to one follower ended. */
|
|
19
14
|
export const DELIVERY_STATUSES = ['sent', 'queued', 'failed'];
|
|
20
|
-
/**
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
/** Where a relay subscription stands (FEP-ae0c). */
|
|
16
|
+
export const RELAY_STATES = ['pending', 'accepted', 'rejected'];
|
|
17
|
+
/** How one webmention this site sent to one page ended. */
|
|
18
|
+
export const WEBMENTION_SEND_STATUSES = ['sent', 'none', 'failed'];
|
|
19
|
+
/** Where a comment came from. */
|
|
20
|
+
export const COMMENT_SOURCES = ['comment', 'webmention'];
|
|
21
|
+
/** What a comment is: something written, or a wordless pointer at the post. */
|
|
22
|
+
export const COMMENT_KINDS = ['reply', 'like', 'boost', 'mention', 'repost'];
|
|
23
|
+
/** Where a comment stands with the moderator. */
|
|
24
|
+
export const COMMENT_STATUSES = ['pending', 'approved', 'spam'];
|
|
30
25
|
/**
|
|
31
26
|
* Open (and if needed create) the admin tables in `dataDir`, applying every
|
|
32
27
|
* migration the package ships. Applying them is idempotent.
|
|
@@ -35,20 +30,9 @@ export class DuplicateUsernameError extends Error {
|
|
|
35
30
|
* index holding a second one on the same file.
|
|
36
31
|
*/
|
|
37
32
|
export function openAdminStore(options) {
|
|
38
|
-
const file =
|
|
39
|
-
|
|
40
|
-
const db = new DatabaseSync(file);
|
|
41
|
-
db.exec('PRAGMA journal_mode = WAL');
|
|
42
|
-
db.exec('PRAGMA foreign_keys = ON');
|
|
43
|
-
migrate(db);
|
|
33
|
+
const file = databaseFile(options.dataDir);
|
|
34
|
+
const db = openDatabase({ dataDir: options.dataDir, ledger: LEDGER, migrations: MIGRATIONS });
|
|
44
35
|
const statements = {
|
|
45
|
-
countUsers: db.prepare('SELECT COUNT(*) AS count FROM users'),
|
|
46
|
-
listUsers: db.prepare('SELECT id, username, created_at FROM users ORDER BY username'),
|
|
47
|
-
userByName: db.prepare('SELECT * FROM users WHERE username = ?'),
|
|
48
|
-
userById: db.prepare('SELECT id, username, created_at FROM users WHERE id = ?'),
|
|
49
|
-
insertUser: db.prepare('INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?)'),
|
|
50
|
-
setPassword: db.prepare('UPDATE users SET password_hash = ? WHERE id = ?'),
|
|
51
|
-
deleteUser: db.prepare('DELETE FROM users WHERE id = ?'),
|
|
52
36
|
deleteSessionsForUser: db.prepare('DELETE FROM sessions WHERE user_id = ? AND id IS NOT ?'),
|
|
53
37
|
insertSession: db.prepare(`
|
|
54
38
|
INSERT INTO sessions (id, user_id, csrf_token, created_at, expires_at)
|
|
@@ -59,33 +43,38 @@ export function openAdminStore(options) {
|
|
|
59
43
|
pruneSessions: db.prepare('DELETE FROM sessions WHERE expires_at <= ?'),
|
|
60
44
|
readFlash: db.prepare('SELECT flash FROM sessions WHERE id = ?'),
|
|
61
45
|
writeFlash: db.prepare('UPDATE sessions SET flash = ? WHERE id = ?'),
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?)
|
|
66
|
-
ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
46
|
+
insertPasswordReset: db.prepare(`
|
|
47
|
+
INSERT INTO password_resets (token_hash, user_id, created_at, expires_at)
|
|
48
|
+
VALUES (?, ?, ?, ?)
|
|
67
49
|
`),
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
50
|
+
passwordResetByHash: db.prepare('SELECT * FROM password_resets WHERE token_hash = ?'),
|
|
51
|
+
deletePasswordReset: db.prepare('DELETE FROM password_resets WHERE token_hash = ?'),
|
|
52
|
+
deletePasswordResetsForUser: db.prepare('DELETE FROM password_resets WHERE user_id = ?'),
|
|
53
|
+
prunePasswordResets: db.prepare('DELETE FROM password_resets WHERE expires_at <= ?'),
|
|
54
|
+
spendToken: db.prepare(`
|
|
55
|
+
INSERT OR IGNORE INTO spent_tokens (token_hash, spent_at, expires_at) VALUES (?, ?, ?)
|
|
56
|
+
`),
|
|
57
|
+
pruneSpentTokens: db.prepare('DELETE FROM spent_tokens WHERE expires_at <= ?'),
|
|
58
|
+
getState: db.prepare('SELECT value FROM cms_state WHERE key = ?'),
|
|
59
|
+
putState: db.prepare(`
|
|
60
|
+
INSERT INTO cms_state (key, value, updated_at) VALUES (?, ?, ?)
|
|
61
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
76
62
|
`),
|
|
77
|
-
|
|
63
|
+
// `?1 IS NULL` is how one statement answers both questions: naming a user
|
|
64
|
+
// counts theirs, naming nobody counts the site's.
|
|
65
|
+
countFollowers: db.prepare('SELECT COUNT(*) AS count FROM followers WHERE ?1 IS NULL OR username = ?1'),
|
|
78
66
|
listFollowers: db.prepare(`
|
|
79
67
|
SELECT * FROM followers
|
|
68
|
+
WHERE ?1 IS NULL OR username = ?1
|
|
80
69
|
ORDER BY followed_at DESC, actor_id DESC
|
|
81
|
-
LIMIT ? OFFSET ?
|
|
70
|
+
LIMIT ?2 OFFSET ?3
|
|
82
71
|
`),
|
|
83
|
-
followerById: db.prepare('SELECT * FROM followers WHERE actor_id = ?'),
|
|
72
|
+
followerById: db.prepare('SELECT * FROM followers WHERE username = ? AND actor_id = ?'),
|
|
84
73
|
putFollower: db.prepare(`
|
|
85
74
|
INSERT INTO followers (
|
|
86
|
-
actor_id, inbox_id, shared_inbox_id, handle, name, icon_url, url, followed_at
|
|
87
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
88
|
-
ON CONFLICT (actor_id) DO UPDATE SET
|
|
75
|
+
username, actor_id, inbox_id, shared_inbox_id, handle, name, icon_url, url, followed_at
|
|
76
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
77
|
+
ON CONFLICT (username, actor_id) DO UPDATE SET
|
|
89
78
|
inbox_id = excluded.inbox_id,
|
|
90
79
|
shared_inbox_id = excluded.shared_inbox_id,
|
|
91
80
|
handle = excluded.handle,
|
|
@@ -93,7 +82,10 @@ export function openAdminStore(options) {
|
|
|
93
82
|
icon_url = excluded.icon_url,
|
|
94
83
|
url = excluded.url
|
|
95
84
|
`),
|
|
96
|
-
deleteFollower: db.prepare('DELETE FROM followers WHERE actor_id = ?'),
|
|
85
|
+
deleteFollower: db.prepare('DELETE FROM followers WHERE username = ? AND actor_id = ?'),
|
|
86
|
+
clearFollowers: db.prepare('DELETE FROM followers'),
|
|
87
|
+
clearInboxActivities: db.prepare('DELETE FROM ap_inbox'),
|
|
88
|
+
resetInboxSequence: db.prepare("DELETE FROM sqlite_sequence WHERE name = 'ap_inbox'"),
|
|
97
89
|
countInboxActivities: db.prepare('SELECT COUNT(*) AS count FROM ap_inbox'),
|
|
98
90
|
listInboxActivities: db.prepare(`
|
|
99
91
|
SELECT * FROM ap_inbox
|
|
@@ -101,37 +93,80 @@ export function openAdminStore(options) {
|
|
|
101
93
|
LIMIT ? OFFSET ?
|
|
102
94
|
`),
|
|
103
95
|
logInboxActivity: db.prepare(`
|
|
104
|
-
INSERT INTO ap_inbox (
|
|
105
|
-
|
|
96
|
+
INSERT INTO ap_inbox (
|
|
97
|
+
activity_id, activity_type, actor_id, object_id, in_reply_to, recipient, received_at, json
|
|
98
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
106
99
|
ON CONFLICT (activity_id) DO UPDATE SET
|
|
107
100
|
activity_type = excluded.activity_type,
|
|
108
101
|
actor_id = excluded.actor_id,
|
|
109
102
|
object_id = excluded.object_id,
|
|
103
|
+
in_reply_to = excluded.in_reply_to,
|
|
104
|
+
recipient = excluded.recipient,
|
|
110
105
|
received_at = excluded.received_at,
|
|
111
106
|
json = excluded.json
|
|
112
107
|
RETURNING *
|
|
113
108
|
`),
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
SELECT * FROM
|
|
117
|
-
ORDER BY
|
|
109
|
+
countReplies: db.prepare(`SELECT COUNT(*) AS count FROM ap_inbox WHERE ${IS_REPLY}`),
|
|
110
|
+
listReplies: db.prepare(`
|
|
111
|
+
SELECT * FROM ap_inbox WHERE ${IS_REPLY}
|
|
112
|
+
ORDER BY received_at DESC, id DESC
|
|
118
113
|
LIMIT ? OFFSET ?
|
|
119
114
|
`),
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
115
|
+
countRepliesTo: db.prepare(`
|
|
116
|
+
SELECT COUNT(*) AS count FROM ap_inbox WHERE ${IS_REPLY} AND in_reply_to = ?
|
|
117
|
+
`),
|
|
118
|
+
listRepliesTo: db.prepare(`
|
|
119
|
+
SELECT * FROM ap_inbox WHERE ${IS_REPLY} AND in_reply_to = ?
|
|
120
|
+
ORDER BY received_at DESC, id DESC
|
|
121
|
+
LIMIT ? OFFSET ?
|
|
122
|
+
`),
|
|
123
|
+
listCommentsFor: db.prepare(`
|
|
124
|
+
SELECT * FROM comments WHERE slug = ? ORDER BY submitted_at, id
|
|
125
|
+
`),
|
|
126
|
+
commentById: db.prepare('SELECT * FROM comments WHERE id = ?'),
|
|
127
|
+
countCommentsFor: db.prepare('SELECT COUNT(*) AS count FROM comments WHERE slug = ? AND status = ?'),
|
|
128
|
+
countCommentsByStatus: db.prepare('SELECT status, COUNT(*) AS count FROM comments GROUP BY status'),
|
|
129
|
+
hasApprovedAuthor: db.prepare(`
|
|
130
|
+
SELECT 1 FROM comments
|
|
131
|
+
WHERE status = 'approved' AND author_name = ? AND author_email IS ?
|
|
132
|
+
LIMIT 1
|
|
133
|
+
`),
|
|
134
|
+
putComment: db.prepare(`
|
|
135
|
+
INSERT INTO comments (
|
|
136
|
+
id, slug, permalink, source, kind, status,
|
|
137
|
+
author_name, author_url, author_email,
|
|
138
|
+
markdown, html, submitted_at, address_hash, in_reply_to, url, author_avatar, notify
|
|
139
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
140
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
127
141
|
slug = excluded.slug,
|
|
128
|
-
|
|
129
|
-
|
|
142
|
+
permalink = excluded.permalink,
|
|
143
|
+
source = excluded.source,
|
|
144
|
+
kind = excluded.kind,
|
|
145
|
+
status = excluded.status,
|
|
146
|
+
author_name = excluded.author_name,
|
|
147
|
+
author_url = excluded.author_url,
|
|
148
|
+
author_email = excluded.author_email,
|
|
149
|
+
markdown = excluded.markdown,
|
|
150
|
+
html = excluded.html,
|
|
151
|
+
submitted_at = excluded.submitted_at,
|
|
152
|
+
address_hash = excluded.address_hash,
|
|
153
|
+
in_reply_to = excluded.in_reply_to,
|
|
154
|
+
url = excluded.url,
|
|
155
|
+
author_avatar = excluded.author_avatar,
|
|
156
|
+
notify = excluded.notify
|
|
130
157
|
`),
|
|
158
|
+
deleteComment: db.prepare('DELETE FROM comments WHERE id = ?'),
|
|
159
|
+
clearComments: db.prepare('DELETE FROM comments'),
|
|
131
160
|
recordDelivery: db.prepare(`
|
|
132
|
-
INSERT INTO ap_deliveries (
|
|
133
|
-
|
|
161
|
+
INSERT INTO ap_deliveries (
|
|
162
|
+
activity_id, activity_type, object_id, slug,
|
|
163
|
+
actor_id, inbox_id, status, error, attempted_at
|
|
164
|
+
)
|
|
165
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
134
166
|
ON CONFLICT (activity_id, actor_id) DO UPDATE SET
|
|
167
|
+
activity_type = excluded.activity_type,
|
|
168
|
+
object_id = excluded.object_id,
|
|
169
|
+
slug = excluded.slug,
|
|
135
170
|
inbox_id = excluded.inbox_id,
|
|
136
171
|
status = excluded.status,
|
|
137
172
|
error = excluded.error,
|
|
@@ -144,8 +179,68 @@ export function openAdminStore(options) {
|
|
|
144
179
|
countDeliveriesByStatus: db.prepare(`
|
|
145
180
|
SELECT status, COUNT(*) AS count FROM ap_deliveries WHERE activity_id = ? GROUP BY status
|
|
146
181
|
`),
|
|
182
|
+
lastDeliveryToInbox: db.prepare(`
|
|
183
|
+
SELECT * FROM ap_deliveries WHERE inbox_id = ?
|
|
184
|
+
ORDER BY attempted_at DESC, rowid DESC
|
|
185
|
+
LIMIT 1
|
|
186
|
+
`),
|
|
187
|
+
lastDeliveryToObject: db.prepare(`
|
|
188
|
+
SELECT * FROM ap_deliveries WHERE object_id = ?
|
|
189
|
+
ORDER BY attempted_at DESC, rowid DESC
|
|
190
|
+
LIMIT 1
|
|
191
|
+
`),
|
|
192
|
+
recordSentWebmention: db.prepare(`
|
|
193
|
+
INSERT INTO webmentions_sent (
|
|
194
|
+
slug, source, target, endpoint, status, error, attempted_at
|
|
195
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
196
|
+
ON CONFLICT (slug, target) DO UPDATE SET
|
|
197
|
+
source = excluded.source,
|
|
198
|
+
endpoint = excluded.endpoint,
|
|
199
|
+
status = excluded.status,
|
|
200
|
+
error = excluded.error,
|
|
201
|
+
attempted_at = excluded.attempted_at
|
|
202
|
+
RETURNING *
|
|
203
|
+
`),
|
|
204
|
+
listSentWebmentions: db.prepare('SELECT * FROM webmentions_sent WHERE slug = ? ORDER BY target'),
|
|
205
|
+
countSentWebmentionsByStatus: db.prepare(`
|
|
206
|
+
SELECT status, COUNT(*) AS count FROM webmentions_sent WHERE slug = ? GROUP BY status
|
|
207
|
+
`),
|
|
208
|
+
listRelays: db.prepare('SELECT * FROM ap_relays ORDER BY created_at, inbox_id'),
|
|
209
|
+
relayByInbox: db.prepare('SELECT * FROM ap_relays WHERE inbox_id = ?'),
|
|
210
|
+
relayByFollow: db.prepare('SELECT * FROM ap_relays WHERE follow_id = ?'),
|
|
211
|
+
putRelay: db.prepare(`
|
|
212
|
+
INSERT INTO ap_relays (
|
|
213
|
+
inbox_id, actor_id, state, reason, follow_id, created_at, updated_at
|
|
214
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
215
|
+
ON CONFLICT (inbox_id) DO UPDATE SET
|
|
216
|
+
actor_id = excluded.actor_id,
|
|
217
|
+
state = excluded.state,
|
|
218
|
+
reason = excluded.reason,
|
|
219
|
+
follow_id = excluded.follow_id,
|
|
220
|
+
updated_at = excluded.updated_at
|
|
221
|
+
RETURNING *
|
|
222
|
+
`),
|
|
223
|
+
deleteRelay: db.prepare('DELETE FROM ap_relays WHERE inbox_id = ?'),
|
|
147
224
|
};
|
|
148
225
|
let open = true;
|
|
226
|
+
/**
|
|
227
|
+
* Run a body with every write in it committed together, or none of them.
|
|
228
|
+
*
|
|
229
|
+
* The rebuilds are what need it: emptying an index and filling it again is
|
|
230
|
+
* one step to everything reading it, and a half-emptied `followers` is a
|
|
231
|
+
* site that has quietly stopped delivering to half its followers.
|
|
232
|
+
*/
|
|
233
|
+
function inTransaction(body) {
|
|
234
|
+
db.exec('BEGIN');
|
|
235
|
+
try {
|
|
236
|
+
body();
|
|
237
|
+
db.exec('COMMIT');
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
db.exec('ROLLBACK');
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
149
244
|
/**
|
|
150
245
|
* The messages queued on a session. A column that will not parse is treated
|
|
151
246
|
* as empty: a flash is a convenience, and losing one is better than a 500 on
|
|
@@ -164,81 +259,8 @@ export function openAdminStore(options) {
|
|
|
164
259
|
return [];
|
|
165
260
|
}
|
|
166
261
|
}
|
|
167
|
-
function storedUser(username) {
|
|
168
|
-
const row = statements.userByName.get(username);
|
|
169
|
-
if (row === undefined)
|
|
170
|
-
return undefined;
|
|
171
|
-
return {
|
|
172
|
-
id: Number(row['id']),
|
|
173
|
-
username: String(row['username']),
|
|
174
|
-
passwordHash: String(row['password_hash']),
|
|
175
|
-
createdAt: String(row['created_at']),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
262
|
return {
|
|
179
263
|
file,
|
|
180
|
-
countUsers() {
|
|
181
|
-
const row = statements.countUsers.get();
|
|
182
|
-
return Number(row?.['count'] ?? 0);
|
|
183
|
-
},
|
|
184
|
-
listUsers() {
|
|
185
|
-
return statements.listUsers.all().map((row) => ({
|
|
186
|
-
id: Number(row['id']),
|
|
187
|
-
username: String(row['username']),
|
|
188
|
-
createdAt: String(row['created_at']),
|
|
189
|
-
}));
|
|
190
|
-
},
|
|
191
|
-
getUser(username) {
|
|
192
|
-
return storedUser(username);
|
|
193
|
-
},
|
|
194
|
-
getUserById(id) {
|
|
195
|
-
const row = statements.userById.get(id);
|
|
196
|
-
if (row === undefined)
|
|
197
|
-
return undefined;
|
|
198
|
-
return {
|
|
199
|
-
id: Number(row['id']),
|
|
200
|
-
username: String(row['username']),
|
|
201
|
-
createdAt: String(row['created_at']),
|
|
202
|
-
};
|
|
203
|
-
},
|
|
204
|
-
createUser(input) {
|
|
205
|
-
const createdAt = new Date().toISOString();
|
|
206
|
-
const hash = hashPassword(input.password);
|
|
207
|
-
try {
|
|
208
|
-
statements.insertUser.run(input.username, hash, createdAt);
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
if (isUniqueViolation(error))
|
|
212
|
-
throw new DuplicateUsernameError(input.username);
|
|
213
|
-
throw error;
|
|
214
|
-
}
|
|
215
|
-
const created = storedUser(input.username);
|
|
216
|
-
if (created === undefined) {
|
|
217
|
-
throw new Error(`The user "${input.username}" vanished between insert and read.`);
|
|
218
|
-
}
|
|
219
|
-
return { id: created.id, username: created.username, createdAt: created.createdAt };
|
|
220
|
-
},
|
|
221
|
-
verifyPassword(username, password) {
|
|
222
|
-
const user = storedUser(username);
|
|
223
|
-
if (user === undefined) {
|
|
224
|
-
// Hash anyway, so a missing user and a wrong password take the same
|
|
225
|
-
// time and the login form does not become a user enumerator.
|
|
226
|
-
verifyPasswordHash(DUMMY_HASH, password);
|
|
227
|
-
return undefined;
|
|
228
|
-
}
|
|
229
|
-
if (!verifyPasswordHash(user.passwordHash, password))
|
|
230
|
-
return undefined;
|
|
231
|
-
return { id: user.id, username: user.username, createdAt: user.createdAt };
|
|
232
|
-
},
|
|
233
|
-
setPassword(userId, password) {
|
|
234
|
-
// Hashed before the update rather than inside it, so a hash that throws
|
|
235
|
-
// leaves the stored one alone.
|
|
236
|
-
const hash = hashPassword(password);
|
|
237
|
-
return statements.setPassword.run(hash, userId).changes > 0;
|
|
238
|
-
},
|
|
239
|
-
deleteUser(userId) {
|
|
240
|
-
return statements.deleteUser.run(userId).changes > 0;
|
|
241
|
-
},
|
|
242
264
|
createSession(input) {
|
|
243
265
|
const now = input.now ?? new Date();
|
|
244
266
|
const session = {
|
|
@@ -271,52 +293,87 @@ export function openAdminStore(options) {
|
|
|
271
293
|
// and spares nothing instead of matching nothing.
|
|
272
294
|
return Number(statements.deleteSessionsForUser.run(userId, options.except ?? null).changes);
|
|
273
295
|
},
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
296
|
+
createPasswordReset(input) {
|
|
297
|
+
const now = input.now ?? new Date();
|
|
298
|
+
const token = randomToken();
|
|
299
|
+
const reset = {
|
|
300
|
+
token,
|
|
301
|
+
userId: input.userId,
|
|
302
|
+
createdAt: now.toISOString(),
|
|
303
|
+
expiresAt: new Date(now.getTime() + input.lifetimeSeconds * 1000).toISOString(),
|
|
304
|
+
};
|
|
305
|
+
statements.insertPasswordReset.run(hashToken(token), reset.userId, reset.createdAt, reset.expiresAt);
|
|
306
|
+
return reset;
|
|
307
|
+
},
|
|
308
|
+
getPasswordReset(token, now = new Date()) {
|
|
309
|
+
const hash = hashToken(token);
|
|
310
|
+
const row = statements.passwordResetByHash.get(hash);
|
|
311
|
+
if (row === undefined)
|
|
312
|
+
return undefined;
|
|
313
|
+
const reset = {
|
|
314
|
+
userId: Number(row['user_id']),
|
|
282
315
|
createdAt: String(row['created_at']),
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
316
|
+
expiresAt: String(row['expires_at']),
|
|
317
|
+
};
|
|
318
|
+
// The same rule the sessions follow: an expired row is deleted rather
|
|
319
|
+
// than ignored, so a clock that goes backwards cannot revive a link.
|
|
320
|
+
if (reset.expiresAt <= now.toISOString()) {
|
|
321
|
+
statements.deletePasswordReset.run(hash);
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
return reset;
|
|
325
|
+
},
|
|
326
|
+
deletePasswordReset(token) {
|
|
327
|
+
return statements.deletePasswordReset.run(hashToken(token)).changes > 0;
|
|
328
|
+
},
|
|
329
|
+
deletePasswordResetsForUser(userId) {
|
|
330
|
+
return Number(statements.deletePasswordResetsForUser.run(userId).changes);
|
|
331
|
+
},
|
|
332
|
+
prunePasswordResets(now = new Date()) {
|
|
333
|
+
return Number(statements.prunePasswordResets.run(now.toISOString()).changes);
|
|
334
|
+
},
|
|
335
|
+
spendToken(token, expiresAt, now = new Date()) {
|
|
336
|
+
// `INSERT OR IGNORE` rather than a read then a write: two clicks on the
|
|
337
|
+
// same link arriving together must not both find nothing and both act.
|
|
338
|
+
const written = statements.spendToken.run(hashToken(token), now.toISOString(), expiresAt);
|
|
339
|
+
return Number(written.changes) > 0;
|
|
340
|
+
},
|
|
341
|
+
pruneSpentTokens(now = new Date()) {
|
|
342
|
+
return Number(statements.pruneSpentTokens.run(now.toISOString()).changes);
|
|
343
|
+
},
|
|
344
|
+
countFollowers(username) {
|
|
345
|
+
const row = statements.countFollowers.get(username ?? null);
|
|
297
346
|
return Number(row?.['count'] ?? 0);
|
|
298
347
|
},
|
|
299
|
-
listFollowers(options = {}) {
|
|
300
|
-
const rows = statements.listFollowers.all(options.limit ?? NO_LIMIT, options.offset ?? 0);
|
|
348
|
+
listFollowers(username, options = {}) {
|
|
349
|
+
const rows = statements.listFollowers.all(username ?? null, options.limit ?? NO_LIMIT, options.offset ?? 0);
|
|
301
350
|
return rows.map(toFollower);
|
|
302
351
|
},
|
|
303
|
-
getFollower(actorId) {
|
|
304
|
-
const row = statements.followerById.get(actorId);
|
|
352
|
+
getFollower(username, actorId) {
|
|
353
|
+
const row = statements.followerById.get(username, actorId);
|
|
305
354
|
return row === undefined ? undefined : toFollower(row);
|
|
306
355
|
},
|
|
307
356
|
putFollower(follower) {
|
|
308
|
-
statements.putFollower.run(follower.actorId, follower.inboxId, follower.sharedInboxId, follower.handle, follower.name, follower.iconUrl, follower.url, follower.followedAt ?? new Date().toISOString());
|
|
357
|
+
statements.putFollower.run(follower.username, follower.actorId, follower.inboxId, follower.sharedInboxId, follower.handle, follower.name, follower.iconUrl, follower.url, follower.followedAt ?? new Date().toISOString());
|
|
309
358
|
// Read back rather than returning what was written: the upsert leaves an
|
|
310
359
|
// existing `followed_at` alone, so the row is the only thing that knows
|
|
311
360
|
// when the follow really began.
|
|
312
|
-
const row = statements.followerById.get(follower.actorId);
|
|
361
|
+
const row = statements.followerById.get(follower.username, follower.actorId);
|
|
313
362
|
if (row === undefined) {
|
|
314
363
|
throw new Error(`The follower "${follower.actorId}" vanished between insert and read.`);
|
|
315
364
|
}
|
|
316
365
|
return toFollower(row);
|
|
317
366
|
},
|
|
318
|
-
deleteFollower(actorId) {
|
|
319
|
-
return statements.deleteFollower.run(actorId).changes > 0;
|
|
367
|
+
deleteFollower(username, actorId) {
|
|
368
|
+
return statements.deleteFollower.run(username, actorId).changes > 0;
|
|
369
|
+
},
|
|
370
|
+
replaceFollowers(followers) {
|
|
371
|
+
inTransaction(() => {
|
|
372
|
+
statements.clearFollowers.run();
|
|
373
|
+
for (const follower of followers) {
|
|
374
|
+
statements.putFollower.run(follower.username, follower.actorId, follower.inboxId, follower.sharedInboxId, follower.handle, follower.name, follower.iconUrl, follower.url, follower.followedAt ?? new Date().toISOString());
|
|
375
|
+
}
|
|
376
|
+
});
|
|
320
377
|
},
|
|
321
378
|
countInboxActivities() {
|
|
322
379
|
const row = statements.countInboxActivities.get();
|
|
@@ -327,33 +384,110 @@ export function openAdminStore(options) {
|
|
|
327
384
|
return rows.map(toInboxActivity);
|
|
328
385
|
},
|
|
329
386
|
logInboxActivity(activity) {
|
|
330
|
-
const row = statements.logInboxActivity.get(activity.activityId, activity.activityType, activity.actorId, activity.objectId,
|
|
387
|
+
const row = statements.logInboxActivity.get(activity.activityId, activity.activityType, activity.actorId, activity.objectId,
|
|
388
|
+
// Derived here rather than passed in, so the column cannot say
|
|
389
|
+
// something the stored activity does not.
|
|
390
|
+
replyTargetOf(activity.json), activity.recipient ?? null, activity.receivedAt ?? new Date().toISOString(), activity.json);
|
|
331
391
|
if (row === undefined) {
|
|
332
392
|
throw new Error(`The activity "${activity.activityType}" was not written to the log.`);
|
|
333
393
|
}
|
|
334
394
|
return toInboxActivity(row);
|
|
335
395
|
},
|
|
336
|
-
|
|
337
|
-
|
|
396
|
+
replaceInboxActivities(activities) {
|
|
397
|
+
inTransaction(() => {
|
|
398
|
+
statements.clearInboxActivities.run();
|
|
399
|
+
// The AUTOINCREMENT high-water mark goes with the rows, so the same
|
|
400
|
+
// log always produces the same ids however many times it is rebuilt.
|
|
401
|
+
statements.resetInboxSequence.run();
|
|
402
|
+
for (const activity of activities) {
|
|
403
|
+
statements.logInboxActivity.run(activity.activityId, activity.activityType, activity.actorId, activity.objectId, replyTargetOf(activity.json), activity.recipient ?? null, activity.receivedAt ?? new Date().toISOString(), activity.json);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
},
|
|
407
|
+
countReplies() {
|
|
408
|
+
const row = statements.countReplies.get();
|
|
338
409
|
return Number(row?.['count'] ?? 0);
|
|
339
410
|
},
|
|
340
|
-
|
|
341
|
-
const rows = statements.
|
|
342
|
-
return rows.map(
|
|
411
|
+
listReplies(options = {}) {
|
|
412
|
+
const rows = statements.listReplies.all(options.limit ?? NO_LIMIT, options.offset ?? 0);
|
|
413
|
+
return rows.map(toInboxActivity);
|
|
343
414
|
},
|
|
344
|
-
|
|
345
|
-
const row = statements.
|
|
346
|
-
return row
|
|
415
|
+
countRepliesTo(objectId) {
|
|
416
|
+
const row = statements.countRepliesTo.get(objectId);
|
|
417
|
+
return Number(row?.['count'] ?? 0);
|
|
347
418
|
},
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
419
|
+
listRepliesTo(objectId, options = {}) {
|
|
420
|
+
const rows = statements.listRepliesTo.all(objectId, options.limit ?? NO_LIMIT, options.offset ?? 0);
|
|
421
|
+
return rows.map(toInboxActivity);
|
|
422
|
+
},
|
|
423
|
+
listActivitiesAbout(objectIds) {
|
|
424
|
+
if (objectIds.length === 0)
|
|
425
|
+
return [];
|
|
426
|
+
// Prepared here rather than beside the others because the number of
|
|
427
|
+
// placeholders is the number of ids: a conversation asks about the post,
|
|
428
|
+
// then about the notes that answered it, and neither count is known when
|
|
429
|
+
// the statements are built.
|
|
430
|
+
const placeholders = objectIds.map(() => '?').join(', ');
|
|
431
|
+
const rows = db
|
|
432
|
+
.prepare(`SELECT * FROM ap_inbox
|
|
433
|
+
WHERE object_id IN (${placeholders}) OR in_reply_to IN (${placeholders})
|
|
434
|
+
ORDER BY id ASC`)
|
|
435
|
+
.all(...objectIds, ...objectIds);
|
|
436
|
+
return rows.map(toInboxActivity);
|
|
437
|
+
},
|
|
438
|
+
listCommentsFor(slug) {
|
|
439
|
+
return statements.listCommentsFor.all(slug).map(toComment);
|
|
440
|
+
},
|
|
441
|
+
listComments(options = {}) {
|
|
442
|
+
// Prepared here rather than beside the others because the `WHERE` is not
|
|
443
|
+
// fixed: the screen asks for one status and the site-wide feed asks for
|
|
444
|
+
// approved comments across every post.
|
|
445
|
+
const where = options.status === undefined ? '' : 'WHERE status = ?';
|
|
446
|
+
const parameters = options.status === undefined ? [] : [options.status];
|
|
447
|
+
const rows = db
|
|
448
|
+
.prepare(`SELECT * FROM comments ${where}
|
|
449
|
+
ORDER BY submitted_at DESC, id DESC
|
|
450
|
+
LIMIT ? OFFSET ?`)
|
|
451
|
+
.all(...parameters, options.limit ?? NO_LIMIT, options.offset ?? 0);
|
|
452
|
+
return rows.map(toComment);
|
|
453
|
+
},
|
|
454
|
+
countCommentsFor(slug, status) {
|
|
455
|
+
const row = statements.countCommentsFor.get(slug, status);
|
|
456
|
+
return Number(row?.['count'] ?? 0);
|
|
457
|
+
},
|
|
458
|
+
getComment(id) {
|
|
459
|
+
const row = statements.commentById.get(id);
|
|
460
|
+
return row === undefined ? undefined : toComment(row);
|
|
461
|
+
},
|
|
462
|
+
countCommentsByStatus() {
|
|
463
|
+
const counts = { pending: 0, approved: 0, spam: 0 };
|
|
464
|
+
for (const row of statements.countCommentsByStatus.all()) {
|
|
465
|
+
counts[commentStatus(row['status'])] += Number(row['count'] ?? 0);
|
|
352
466
|
}
|
|
353
|
-
return
|
|
467
|
+
return counts;
|
|
468
|
+
},
|
|
469
|
+
hasApprovedAuthor(name, email) {
|
|
470
|
+
// `IS` rather than `=`, so an author who gave no email is matched by the
|
|
471
|
+
// rows that gave none either instead of matching nothing.
|
|
472
|
+
return statements.hasApprovedAuthor.get(name, email) !== undefined;
|
|
473
|
+
},
|
|
474
|
+
putComment(comment) {
|
|
475
|
+
statements.putComment.run(comment.id, comment.slug, comment.permalink, comment.source, comment.kind, comment.status, comment.author.name, comment.author.url, comment.author.email, comment.content.markdown, comment.content.html, comment.submitted, comment.addressHash, comment.inReplyTo, comment.url, comment.author.avatar, comment.notify ? 1 : 0);
|
|
476
|
+
return comment;
|
|
477
|
+
},
|
|
478
|
+
deleteComment(id) {
|
|
479
|
+
return statements.deleteComment.run(id).changes > 0;
|
|
480
|
+
},
|
|
481
|
+
replaceComments(comments) {
|
|
482
|
+
inTransaction(() => {
|
|
483
|
+
statements.clearComments.run();
|
|
484
|
+
for (const comment of comments) {
|
|
485
|
+
statements.putComment.run(comment.id, comment.slug, comment.permalink, comment.source, comment.kind, comment.status, comment.author.name, comment.author.url, comment.author.email, comment.content.markdown, comment.content.html, comment.submitted, comment.addressHash, comment.inReplyTo, comment.url, comment.author.avatar, comment.notify ? 1 : 0);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
354
488
|
},
|
|
355
489
|
recordDelivery(delivery) {
|
|
356
|
-
const row = statements.recordDelivery.get(delivery.activityId, delivery.actorId, delivery.inboxId, delivery.status, delivery.error, delivery.attemptedAt ?? new Date().toISOString());
|
|
490
|
+
const row = statements.recordDelivery.get(delivery.activityId, delivery.activityType, delivery.objectId, delivery.slug, delivery.actorId, delivery.inboxId, delivery.status, delivery.error, delivery.attemptedAt ?? new Date().toISOString());
|
|
357
491
|
if (row === undefined) {
|
|
358
492
|
throw new Error(`The delivery of "${delivery.activityId}" to "${delivery.actorId}" was not recorded.`);
|
|
359
493
|
}
|
|
@@ -371,35 +505,108 @@ export function openAdminStore(options) {
|
|
|
371
505
|
}
|
|
372
506
|
return counts;
|
|
373
507
|
},
|
|
374
|
-
|
|
375
|
-
|
|
508
|
+
lastDeliveryToInbox(inboxId) {
|
|
509
|
+
const row = statements.lastDeliveryToInbox.get(inboxId);
|
|
510
|
+
return row === undefined ? undefined : toDelivery(row);
|
|
376
511
|
},
|
|
377
|
-
|
|
378
|
-
const row = statements.
|
|
379
|
-
return
|
|
512
|
+
lastDeliveryToObject(objectId) {
|
|
513
|
+
const row = statements.lastDeliveryToObject.get(objectId);
|
|
514
|
+
return row === undefined ? undefined : toDelivery(row);
|
|
380
515
|
},
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
516
|
+
recordSentWebmention(sent) {
|
|
517
|
+
const row = statements.recordSentWebmention.get(sent.slug, sent.source, sent.target, sent.endpoint, sent.status, sent.error, sent.attemptedAt ?? new Date().toISOString());
|
|
518
|
+
if (row === undefined) {
|
|
519
|
+
throw new Error(`The webmention to ${sent.target} could not be recorded.`);
|
|
385
520
|
}
|
|
386
|
-
return
|
|
387
|
-
},
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
db.exec('COMMIT');
|
|
521
|
+
return toSentWebmention(row);
|
|
522
|
+
},
|
|
523
|
+
listSentWebmentions(slug) {
|
|
524
|
+
const rows = statements.listSentWebmentions.all(slug);
|
|
525
|
+
return rows.map(toSentWebmention);
|
|
526
|
+
},
|
|
527
|
+
countSentWebmentionsByStatus(slug) {
|
|
528
|
+
const counts = { sent: 0, none: 0, failed: 0 };
|
|
529
|
+
const rows = statements.countSentWebmentionsByStatus.all(slug);
|
|
530
|
+
for (const row of rows) {
|
|
531
|
+
counts[webmentionSendStatus(row['status'])] = Number(row['count']);
|
|
398
532
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
533
|
+
return counts;
|
|
534
|
+
},
|
|
535
|
+
listRelays() {
|
|
536
|
+
return statements.listRelays.all().map(toRelay);
|
|
537
|
+
},
|
|
538
|
+
getRelay(inboxId) {
|
|
539
|
+
const row = statements.relayByInbox.get(inboxId);
|
|
540
|
+
return row === undefined ? undefined : toRelay(row);
|
|
541
|
+
},
|
|
542
|
+
getRelayByFollow(followId) {
|
|
543
|
+
const row = statements.relayByFollow.get(followId);
|
|
544
|
+
return row === undefined ? undefined : toRelay(row);
|
|
545
|
+
},
|
|
546
|
+
putRelay(relay) {
|
|
547
|
+
const now = new Date().toISOString();
|
|
548
|
+
const row = statements.putRelay.get(relay.inboxId, relay.actorId, relay.state, relay.reason, relay.followId, relay.createdAt ?? now, now);
|
|
549
|
+
if (row === undefined) {
|
|
550
|
+
throw new Error(`The relay subscription to "${relay.inboxId}" was not written.`);
|
|
402
551
|
}
|
|
552
|
+
return toRelay(row);
|
|
553
|
+
},
|
|
554
|
+
deleteRelay(inboxId) {
|
|
555
|
+
return statements.deleteRelay.run(inboxId).changes > 0;
|
|
556
|
+
},
|
|
557
|
+
pruneSessions(now = new Date()) {
|
|
558
|
+
return Number(statements.pruneSessions.run(now.toISOString()).changes);
|
|
559
|
+
},
|
|
560
|
+
legacySettings() {
|
|
561
|
+
// Prepared here rather than with the rest, because the table this reads
|
|
562
|
+
// is one that will not be there: a statement over a missing table throws
|
|
563
|
+
// when it is prepared, which would take the whole store down.
|
|
564
|
+
if (!hasTable(db, 'settings'))
|
|
565
|
+
return undefined;
|
|
566
|
+
return db.prepare('SELECT key, value, updated_at FROM settings').all().map((row) => ({
|
|
567
|
+
key: String(row['key']),
|
|
568
|
+
value: String(row['value']),
|
|
569
|
+
updatedAt: String(row['updated_at']),
|
|
570
|
+
}));
|
|
571
|
+
},
|
|
572
|
+
legacyActorKeys() {
|
|
573
|
+
// Prepared here for the same reason `legacySettings` is: the table is one
|
|
574
|
+
// that will not be there, and a statement over a missing table throws
|
|
575
|
+
// when it is prepared rather than when it is run.
|
|
576
|
+
if (!hasTable(db, 'actor_keys'))
|
|
577
|
+
return undefined;
|
|
578
|
+
return db.prepare('SELECT identifier, algorithm, private_jwk FROM actor_keys').all().map((row) => ({
|
|
579
|
+
identifier: String(row['identifier']),
|
|
580
|
+
algorithm: String(row['algorithm']),
|
|
581
|
+
privateJwk: String(row['private_jwk']),
|
|
582
|
+
}));
|
|
583
|
+
},
|
|
584
|
+
legacyUsers() {
|
|
585
|
+
// Prepared here for the same reason `legacySettings` is: the table is
|
|
586
|
+
// one that will not be there, and a statement over a missing table
|
|
587
|
+
// throws when it is prepared rather than when it is run.
|
|
588
|
+
if (!hasTable(db, 'users'))
|
|
589
|
+
return undefined;
|
|
590
|
+
return db
|
|
591
|
+
.prepare('SELECT id, username, password_hash, created_at FROM users ORDER BY id')
|
|
592
|
+
.all().map((row) => ({
|
|
593
|
+
id: Number(row['id']),
|
|
594
|
+
username: String(row['username']),
|
|
595
|
+
passwordHash: String(row['password_hash']),
|
|
596
|
+
createdAt: String(row['created_at']),
|
|
597
|
+
}));
|
|
598
|
+
},
|
|
599
|
+
dropLegacyTable(name) {
|
|
600
|
+
// The name is never a caller's to invent: it is one of this file's own
|
|
601
|
+
// shipped table names, so quoting it is enough.
|
|
602
|
+
db.exec(`DROP TABLE IF EXISTS "${name.replace(/"/g, '""')}"`);
|
|
603
|
+
},
|
|
604
|
+
getState(key) {
|
|
605
|
+
const row = statements.getState.get(key);
|
|
606
|
+
return row === undefined ? undefined : String(row['value']);
|
|
607
|
+
},
|
|
608
|
+
setState(key, value) {
|
|
609
|
+
statements.putState.run(key, value, new Date().toISOString());
|
|
403
610
|
},
|
|
404
611
|
pushFlash(sessionId, entry) {
|
|
405
612
|
const queued = [...readFlash(sessionId), entry];
|
|
@@ -420,11 +627,6 @@ export function openAdminStore(options) {
|
|
|
420
627
|
},
|
|
421
628
|
};
|
|
422
629
|
}
|
|
423
|
-
/**
|
|
424
|
-
* A well-formed hash of a password nobody has, verified against when the
|
|
425
|
-
* username is unknown so that both failures cost the same.
|
|
426
|
-
*/
|
|
427
|
-
const DUMMY_HASH = hashPassword('a password no user has, hashed once so that verification is constant time');
|
|
428
630
|
function isFlashMessage(value) {
|
|
429
631
|
if (typeof value !== 'object' || value === null)
|
|
430
632
|
return false;
|
|
@@ -449,6 +651,7 @@ function nullableText(value) {
|
|
|
449
651
|
}
|
|
450
652
|
function toFollower(row) {
|
|
451
653
|
return {
|
|
654
|
+
username: String(row['username']),
|
|
452
655
|
actorId: String(row['actor_id']),
|
|
453
656
|
inboxId: String(row['inbox_id']),
|
|
454
657
|
sharedInboxId: nullableText(row['shared_inbox_id']),
|
|
@@ -466,23 +669,18 @@ function toInboxActivity(row) {
|
|
|
466
669
|
activityType: String(row['activity_type']),
|
|
467
670
|
actorId: String(row['actor_id']),
|
|
468
671
|
objectId: nullableText(row['object_id']),
|
|
672
|
+
inReplyTo: nullableText(row['in_reply_to']),
|
|
673
|
+
recipient: nullableText(row['recipient']),
|
|
469
674
|
receivedAt: String(row['received_at']),
|
|
470
675
|
json: String(row['json']),
|
|
471
676
|
};
|
|
472
677
|
}
|
|
473
|
-
function
|
|
678
|
+
function toDelivery(row) {
|
|
474
679
|
return {
|
|
475
680
|
activityId: String(row['activity_id']),
|
|
476
681
|
activityType: String(row['activity_type']),
|
|
477
682
|
objectId: String(row['object_id']),
|
|
478
683
|
slug: nullableText(row['slug']),
|
|
479
|
-
createdAt: String(row['created_at']),
|
|
480
|
-
json: String(row['json']),
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
function toDelivery(row) {
|
|
484
|
-
return {
|
|
485
|
-
activityId: String(row['activity_id']),
|
|
486
684
|
actorId: String(row['actor_id']),
|
|
487
685
|
inboxId: String(row['inbox_id']),
|
|
488
686
|
status: deliveryStatus(row['status']),
|
|
@@ -490,6 +688,78 @@ function toDelivery(row) {
|
|
|
490
688
|
attemptedAt: String(row['attempted_at']),
|
|
491
689
|
};
|
|
492
690
|
}
|
|
691
|
+
function toSentWebmention(row) {
|
|
692
|
+
return {
|
|
693
|
+
slug: String(row['slug']),
|
|
694
|
+
source: String(row['source']),
|
|
695
|
+
target: String(row['target']),
|
|
696
|
+
endpoint: nullableText(row['endpoint']),
|
|
697
|
+
status: webmentionSendStatus(row['status']),
|
|
698
|
+
error: nullableText(row['error']),
|
|
699
|
+
attemptedAt: String(row['attempted_at']),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/** A stored send status, or `failed` for one this version does not know. */
|
|
703
|
+
function webmentionSendStatus(value) {
|
|
704
|
+
return oneOf(value, WEBMENTION_SEND_STATUSES, 'failed');
|
|
705
|
+
}
|
|
706
|
+
function toComment(row) {
|
|
707
|
+
return {
|
|
708
|
+
id: String(row['id']),
|
|
709
|
+
slug: String(row['slug']),
|
|
710
|
+
permalink: String(row['permalink']),
|
|
711
|
+
source: oneOf(row['source'], COMMENT_SOURCES, 'comment'),
|
|
712
|
+
kind: oneOf(row['kind'], COMMENT_KINDS, 'reply'),
|
|
713
|
+
status: commentStatus(row['status']),
|
|
714
|
+
author: {
|
|
715
|
+
name: String(row['author_name']),
|
|
716
|
+
url: nullableText(row['author_url']),
|
|
717
|
+
email: nullableText(row['author_email']),
|
|
718
|
+
avatar: nullableText(row['author_avatar']),
|
|
719
|
+
},
|
|
720
|
+
content: { markdown: String(row['markdown']), html: String(row['html']) },
|
|
721
|
+
submitted: String(row['submitted_at']),
|
|
722
|
+
addressHash: nullableText(row['address_hash']),
|
|
723
|
+
inReplyTo: nullableText(row['in_reply_to']),
|
|
724
|
+
url: nullableText(row['url']),
|
|
725
|
+
notify: Number(row['notify'] ?? 0) === 1,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* A stored comment status, or `pending` for one this version does not know.
|
|
730
|
+
*
|
|
731
|
+
* Pending is the safe reading: showing a comment nobody approved is the one
|
|
732
|
+
* mistake a moderation queue cannot make, and the screen is right there for
|
|
733
|
+
* the row.
|
|
734
|
+
*/
|
|
735
|
+
function commentStatus(value) {
|
|
736
|
+
return oneOf(value, COMMENT_STATUSES, 'pending');
|
|
737
|
+
}
|
|
738
|
+
/** A stored enumeration value, or the fallback for one this version does not know. */
|
|
739
|
+
function oneOf(value, allowed, fallback) {
|
|
740
|
+
return allowed.includes(value) ? value : fallback;
|
|
741
|
+
}
|
|
742
|
+
function toRelay(row) {
|
|
743
|
+
return {
|
|
744
|
+
inboxId: String(row['inbox_id']),
|
|
745
|
+
actorId: nullableText(row['actor_id']),
|
|
746
|
+
state: relayState(row['state']),
|
|
747
|
+
reason: nullableText(row['reason']),
|
|
748
|
+
followId: nullableText(row['follow_id']),
|
|
749
|
+
createdAt: String(row['created_at']),
|
|
750
|
+
updatedAt: String(row['updated_at']),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* A stored relay state, or `pending` for one this version does not know.
|
|
755
|
+
*
|
|
756
|
+
* Pending is the safe reading of an unknown state: a subscription this version
|
|
757
|
+
* cannot make sense of is one it should not be delivering public posts to, and
|
|
758
|
+
* Retry is on the screen for exactly that row.
|
|
759
|
+
*/
|
|
760
|
+
function relayState(value) {
|
|
761
|
+
return RELAY_STATES.includes(value) ? value : 'pending';
|
|
762
|
+
}
|
|
493
763
|
/**
|
|
494
764
|
* A stored status, or `failed` for one this version does not know.
|
|
495
765
|
*
|
|
@@ -510,13 +780,33 @@ function toSession(row) {
|
|
|
510
780
|
expiresAt: String(row['expires_at']),
|
|
511
781
|
};
|
|
512
782
|
}
|
|
513
|
-
/**
|
|
783
|
+
/**
|
|
784
|
+
* 256 unguessable bits, hex encoded. Session ids, CSRF tokens and password
|
|
785
|
+
* reset tokens are all this.
|
|
786
|
+
*/
|
|
514
787
|
function randomToken() {
|
|
515
788
|
return randomBytes(SESSION_ID_BYTES).toString('hex');
|
|
516
789
|
}
|
|
517
|
-
|
|
518
|
-
|
|
790
|
+
/**
|
|
791
|
+
* A reset token as the database keeps it: its SHA-256, hex encoded.
|
|
792
|
+
*
|
|
793
|
+
* Plain SHA-256 rather than argon2 on purpose. What argon2 buys is time
|
|
794
|
+
* against somebody guessing a password people chose, and there is nothing to
|
|
795
|
+
* guess here: the token is 256 bits out of `randomBytes`, so the only way to
|
|
796
|
+
* hold a valid one is to have been sent it. What the hash is for is that a
|
|
797
|
+
* copy of `geekity.db` — a backup, a stray file — is not a stack of working
|
|
798
|
+
* reset links.
|
|
799
|
+
*/
|
|
800
|
+
function hashToken(token) {
|
|
801
|
+
return createHash('sha256').update(token, 'utf8').digest('hex');
|
|
802
|
+
}
|
|
803
|
+
/** Whether the database has a table by that name. */
|
|
804
|
+
function hasTable(db, name) {
|
|
805
|
+
return (db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").all(name)
|
|
806
|
+
.length > 0);
|
|
519
807
|
}
|
|
808
|
+
/** The table this store's applied versions are recorded in. */
|
|
809
|
+
const LEDGER = 'admin_migrations';
|
|
520
810
|
/**
|
|
521
811
|
* Schema versions for the admin tables, applied in order. They keep their own
|
|
522
812
|
* ledger so their numbering never collides with the content index's.
|
|
@@ -525,6 +815,13 @@ function isUniqueViolation(error) {
|
|
|
525
815
|
*/
|
|
526
816
|
const MIGRATIONS = [
|
|
527
817
|
{
|
|
818
|
+
// Users and sessions, when SQLite still held both. decision-9 moved the
|
|
819
|
+
// accounts into `data/users.json`, so nothing reads the users table any
|
|
820
|
+
// more: `migrateUsersToFile` writes its rows out on the first boot of that
|
|
821
|
+
// version and drops it. The migration stays exactly as it shipped, because
|
|
822
|
+
// a shipped migration is never edited — which does mean a brand new
|
|
823
|
+
// database creates the table here and drops it a moment later. Migration
|
|
824
|
+
// 11 is where sessions lose the foreign key into it.
|
|
528
825
|
version: 1,
|
|
529
826
|
sql: `
|
|
530
827
|
CREATE TABLE users (
|
|
@@ -557,10 +854,12 @@ const MIGRATIONS = [
|
|
|
557
854
|
sql: `ALTER TABLE sessions ADD COLUMN flash TEXT`,
|
|
558
855
|
},
|
|
559
856
|
{
|
|
560
|
-
// Site settings
|
|
561
|
-
// content/_data/site.json
|
|
562
|
-
//
|
|
563
|
-
//
|
|
857
|
+
// Site settings, when SQLite was still the source of them. decision-9
|
|
858
|
+
// moved them into content/_data/site.json, so nothing reads this table
|
|
859
|
+
// any more: `migrateSettingsToFile` writes its rows out on the first boot
|
|
860
|
+
// of that version and drops it. The migration stays exactly as it shipped,
|
|
861
|
+
// because a shipped migration is never edited — which does mean a brand
|
|
862
|
+
// new database creates the table here and drops it a moment later.
|
|
564
863
|
version: 3,
|
|
565
864
|
sql: `
|
|
566
865
|
CREATE TABLE settings (
|
|
@@ -571,11 +870,12 @@ const MIGRATIONS = [
|
|
|
571
870
|
`,
|
|
572
871
|
},
|
|
573
872
|
{
|
|
574
|
-
// The site actor's signing keys
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
873
|
+
// The site actor's signing keys, when SQLite still held them. decision-9
|
|
874
|
+
// moved them into JWK files under `data/keys`, so nothing reads this table
|
|
875
|
+
// any more: `migrateActorKeysToFiles` writes its rows out on the first boot
|
|
876
|
+
// of that version and drops it. The migration stays exactly as it shipped,
|
|
877
|
+
// because a shipped migration is never edited — which does mean a brand new
|
|
878
|
+
// database creates the table here and drops it a moment later.
|
|
579
879
|
version: 4,
|
|
580
880
|
sql: `
|
|
581
881
|
CREATE TABLE actor_keys (
|
|
@@ -673,32 +973,327 @@ const MIGRATIONS = [
|
|
|
673
973
|
CREATE INDEX ap_deliveries_status ON ap_deliveries (status);
|
|
674
974
|
`,
|
|
675
975
|
},
|
|
976
|
+
{
|
|
977
|
+
// What a logged reply answers, so a post's comments can be counted and
|
|
978
|
+
// listed without reading every activity the site was ever sent.
|
|
979
|
+
//
|
|
980
|
+
// It is an index of the JSON beside it rather than a fact of its own: the
|
|
981
|
+
// backfill runs the very function the writer runs, so a database rebuilt
|
|
982
|
+
// from the inbox log (TASK-32) holds what this one holds.
|
|
983
|
+
version: 8,
|
|
984
|
+
sql: `
|
|
985
|
+
ALTER TABLE ap_inbox ADD COLUMN in_reply_to TEXT;
|
|
986
|
+
CREATE INDEX ap_inbox_in_reply_to ON ap_inbox (in_reply_to);
|
|
987
|
+
`,
|
|
988
|
+
run(db) {
|
|
989
|
+
const rows = db.prepare('SELECT id, json FROM ap_inbox').all();
|
|
990
|
+
const update = db.prepare('UPDATE ap_inbox SET in_reply_to = ? WHERE id = ?');
|
|
991
|
+
for (const row of rows) {
|
|
992
|
+
update.run(replyTargetOf(String(row['json'])), Number(row['id']));
|
|
993
|
+
}
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
// Relay subscriptions (FEP-ae0c, TASK-40). The list of relays is a
|
|
998
|
+
// setting mirrored to site.json; this is where each handshake stands,
|
|
999
|
+
// which is operational state and derivable again from the file — a relay
|
|
1000
|
+
// in the list with no row here is simply followed again on the next boot.
|
|
1001
|
+
//
|
|
1002
|
+
// Keyed by the inbox rather than by the relay's actor id, because the
|
|
1003
|
+
// inbox is the only thing known when the `Follow` goes out: the actor id
|
|
1004
|
+
// arrives with the `Accept`, days later if a human has to approve it.
|
|
1005
|
+
version: 9,
|
|
1006
|
+
sql: `
|
|
1007
|
+
CREATE TABLE ap_relays (
|
|
1008
|
+
inbox_id TEXT PRIMARY KEY,
|
|
1009
|
+
actor_id TEXT,
|
|
1010
|
+
state TEXT NOT NULL,
|
|
1011
|
+
reason TEXT,
|
|
1012
|
+
follow_id TEXT,
|
|
1013
|
+
created_at TEXT NOT NULL,
|
|
1014
|
+
updated_at TEXT NOT NULL
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
CREATE INDEX ap_relays_follow_id ON ap_relays (follow_id);
|
|
1018
|
+
CREATE INDEX ap_relays_actor_id ON ap_relays (actor_id);
|
|
1019
|
+
`,
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
// The CMS's own operational state, as opposed to the site's settings. It
|
|
1023
|
+
// is a table of its own rather than more rows in `settings` because the
|
|
1024
|
+
// number of settings is what decides whether a site is seeded from
|
|
1025
|
+
// content/_data/site.json, and because nothing here belongs in that file.
|
|
1026
|
+
version: 10,
|
|
1027
|
+
sql: `
|
|
1028
|
+
CREATE TABLE cms_state (
|
|
1029
|
+
key TEXT PRIMARY KEY,
|
|
1030
|
+
value TEXT NOT NULL,
|
|
1031
|
+
updated_at TEXT NOT NULL
|
|
1032
|
+
);
|
|
1033
|
+
`,
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
// Sessions without the foreign key into `users`, because there is no users
|
|
1037
|
+
// table any more: decision-9 moved the accounts into `data/users.json` and
|
|
1038
|
+
// `migrateUsersToFile` drops the table on the boot after this migration
|
|
1039
|
+
// runs. The constraint could not survive that either way — with
|
|
1040
|
+
// `foreign_keys` on, dropping the parent cascades every login away, and an
|
|
1041
|
+
// insert afterwards fails with "no such table: main.users" — and SQLite
|
|
1042
|
+
// cannot drop a constraint in place, so the table is rebuilt. Every row is
|
|
1043
|
+
// copied across, so a site upgrading keeps the logins it had. What the key
|
|
1044
|
+
// was doing is done in the admin guard instead: a session naming a user
|
|
1045
|
+
// the file no longer holds is not a session.
|
|
1046
|
+
version: 11,
|
|
1047
|
+
sql: `
|
|
1048
|
+
CREATE TABLE sessions_without_users (
|
|
1049
|
+
id TEXT PRIMARY KEY,
|
|
1050
|
+
user_id INTEGER,
|
|
1051
|
+
csrf_token TEXT NOT NULL,
|
|
1052
|
+
created_at TEXT NOT NULL,
|
|
1053
|
+
expires_at TEXT NOT NULL,
|
|
1054
|
+
flash TEXT
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
INSERT INTO sessions_without_users (
|
|
1058
|
+
id, user_id, csrf_token, created_at, expires_at, flash
|
|
1059
|
+
)
|
|
1060
|
+
SELECT id, user_id, csrf_token, created_at, expires_at, flash FROM sessions;
|
|
1061
|
+
|
|
1062
|
+
DROP TABLE sessions;
|
|
1063
|
+
ALTER TABLE sessions_without_users RENAME TO sessions;
|
|
1064
|
+
|
|
1065
|
+
CREATE INDEX sessions_expires_at ON sessions (expires_at);
|
|
1066
|
+
CREATE INDEX sessions_user_id ON sessions (user_id);
|
|
1067
|
+
`,
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
// The outcomes without the payloads. decision-9 replaces "send this stored
|
|
1071
|
+
// activity again" with "send this post as it now reads", so nothing needs
|
|
1072
|
+
// the JSON-LD any more and `ap_outbound` goes: an activity rebuilt from
|
|
1073
|
+
// the file is the current answer, and a copy of what went out last time
|
|
1074
|
+
// could only ever be a staler one.
|
|
1075
|
+
//
|
|
1076
|
+
// What that table held about an activity that an outcome cannot do
|
|
1077
|
+
// without — its type, the object it was about and the slug — moves onto
|
|
1078
|
+
// the outcome row, which is now the whole of what SQLite remembers about
|
|
1079
|
+
// anything the site has sent. The foreign key has to go with the parent,
|
|
1080
|
+
// and SQLite cannot drop a constraint in place, so `ap_deliveries` is
|
|
1081
|
+
// rebuilt the way migration 11 rebuilt `sessions`. Every row that has an
|
|
1082
|
+
// activity to name is carried across as a courtesy: this table is a cache
|
|
1083
|
+
// and is allowed to be empty, so a row whose activity is somehow missing
|
|
1084
|
+
// is dropped rather than invented a type for.
|
|
1085
|
+
version: 12,
|
|
1086
|
+
sql: `
|
|
1087
|
+
CREATE TABLE ap_deliveries_standalone (
|
|
1088
|
+
activity_id TEXT NOT NULL,
|
|
1089
|
+
activity_type TEXT NOT NULL,
|
|
1090
|
+
object_id TEXT NOT NULL,
|
|
1091
|
+
slug TEXT,
|
|
1092
|
+
actor_id TEXT NOT NULL,
|
|
1093
|
+
inbox_id TEXT NOT NULL,
|
|
1094
|
+
status TEXT NOT NULL,
|
|
1095
|
+
error TEXT,
|
|
1096
|
+
attempted_at TEXT NOT NULL,
|
|
1097
|
+
PRIMARY KEY (activity_id, actor_id)
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
INSERT INTO ap_deliveries_standalone (
|
|
1101
|
+
activity_id, activity_type, object_id, slug,
|
|
1102
|
+
actor_id, inbox_id, status, error, attempted_at
|
|
1103
|
+
)
|
|
1104
|
+
SELECT
|
|
1105
|
+
ap_deliveries.activity_id,
|
|
1106
|
+
ap_outbound.activity_type,
|
|
1107
|
+
ap_outbound.object_id,
|
|
1108
|
+
ap_outbound.slug,
|
|
1109
|
+
ap_deliveries.actor_id,
|
|
1110
|
+
ap_deliveries.inbox_id,
|
|
1111
|
+
ap_deliveries.status,
|
|
1112
|
+
ap_deliveries.error,
|
|
1113
|
+
ap_deliveries.attempted_at
|
|
1114
|
+
FROM ap_deliveries
|
|
1115
|
+
JOIN ap_outbound ON ap_outbound.activity_id = ap_deliveries.activity_id;
|
|
1116
|
+
|
|
1117
|
+
DROP TABLE ap_deliveries;
|
|
1118
|
+
ALTER TABLE ap_deliveries_standalone RENAME TO ap_deliveries;
|
|
1119
|
+
|
|
1120
|
+
CREATE INDEX ap_deliveries_attempted_at ON ap_deliveries (attempted_at);
|
|
1121
|
+
CREATE INDEX ap_deliveries_status ON ap_deliveries (status);
|
|
1122
|
+
CREATE INDEX ap_deliveries_object_id ON ap_deliveries (object_id, attempted_at);
|
|
1123
|
+
|
|
1124
|
+
DROP TABLE ap_outbound;
|
|
1125
|
+
`,
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
// What a post's conversation is read by (TASK-49). `in_reply_to` already
|
|
1129
|
+
// had an index because a post's replies are counted on every feed; the
|
|
1130
|
+
// object an activity is about had none, and that is the column a like, a
|
|
1131
|
+
// boost, a `Delete` of a reply and an `Undo` of a like are all found by.
|
|
1132
|
+
version: 13,
|
|
1133
|
+
sql: `
|
|
1134
|
+
CREATE INDEX ap_inbox_object_id ON ap_inbox (object_id);
|
|
1135
|
+
`,
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
// Native comments (TASK-50), which are files under
|
|
1139
|
+
// `content/_data/comments/` and nothing else: this table is an index of
|
|
1140
|
+
// them, emptied and read back on every boot exactly as `followers` and
|
|
1141
|
+
// `ap_inbox` are. Nothing here is a fact the files do not carry.
|
|
1142
|
+
//
|
|
1143
|
+
// The id is the comment's own name rather than a row number, because it is
|
|
1144
|
+
// what a reply names, what a moderation form posts and what the page
|
|
1145
|
+
// anchors on — all three have to survive a rebuilt index. The author's
|
|
1146
|
+
// name and email are indexed together because that pair is the whole of
|
|
1147
|
+
// WordPress's auto-approval rule.
|
|
1148
|
+
version: 14,
|
|
1149
|
+
sql: `
|
|
1150
|
+
CREATE TABLE comments (
|
|
1151
|
+
id TEXT PRIMARY KEY,
|
|
1152
|
+
slug TEXT NOT NULL,
|
|
1153
|
+
permalink TEXT NOT NULL,
|
|
1154
|
+
source TEXT NOT NULL,
|
|
1155
|
+
kind TEXT NOT NULL,
|
|
1156
|
+
status TEXT NOT NULL,
|
|
1157
|
+
author_name TEXT NOT NULL,
|
|
1158
|
+
author_url TEXT,
|
|
1159
|
+
author_email TEXT,
|
|
1160
|
+
markdown TEXT NOT NULL,
|
|
1161
|
+
html TEXT NOT NULL,
|
|
1162
|
+
submitted_at TEXT NOT NULL,
|
|
1163
|
+
address_hash TEXT,
|
|
1164
|
+
in_reply_to TEXT
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
CREATE INDEX comments_slug ON comments (slug, submitted_at);
|
|
1168
|
+
CREATE INDEX comments_status ON comments (status, submitted_at);
|
|
1169
|
+
CREATE INDEX comments_author ON comments (author_name, author_email, status);
|
|
1170
|
+
`,
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
// Webmentions (TASK-51), which are two different things and so two changes.
|
|
1174
|
+
//
|
|
1175
|
+
// A comment may now say where it lives, because a webmention lives on
|
|
1176
|
+
// somebody else's page: it is what a later webmention from that page is
|
|
1177
|
+
// recognised by, and what the thread's permalink points at for an entry
|
|
1178
|
+
// this site did not host. Its author may now have a face, out of the source
|
|
1179
|
+
// page's `h-card`. Both columns are an index of the comment files like
|
|
1180
|
+
// every other column of this table, so nothing backfills them — the rebuild
|
|
1181
|
+
// on the next boot does.
|
|
1182
|
+
//
|
|
1183
|
+
// What the site has sent is the other half, and it is a cache of outcomes
|
|
1184
|
+
// exactly as `ap_deliveries` is: no payload, one row per (post, target),
|
|
1185
|
+
// and a resend moves the row rather than adding to it. It is a table of
|
|
1186
|
+
// its own rather than a use of `ap_deliveries` because the federation
|
|
1187
|
+
// screen counts that table per activity, and a webmention is not one.
|
|
1188
|
+
version: 15,
|
|
1189
|
+
sql: `
|
|
1190
|
+
ALTER TABLE comments ADD COLUMN url TEXT;
|
|
1191
|
+
ALTER TABLE comments ADD COLUMN author_avatar TEXT;
|
|
1192
|
+
|
|
1193
|
+
CREATE TABLE webmentions_sent (
|
|
1194
|
+
slug TEXT NOT NULL,
|
|
1195
|
+
source TEXT NOT NULL,
|
|
1196
|
+
target TEXT NOT NULL,
|
|
1197
|
+
endpoint TEXT,
|
|
1198
|
+
status TEXT NOT NULL,
|
|
1199
|
+
error TEXT,
|
|
1200
|
+
attempted_at TEXT NOT NULL,
|
|
1201
|
+
PRIMARY KEY (slug, target)
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
CREATE INDEX webmentions_sent_attempted_at ON webmentions_sent (attempted_at);
|
|
1205
|
+
`,
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
// Password reset tokens (TASK-54). They sit beside the sessions, and for
|
|
1209
|
+
// the same reason: a reset link is a second, shorter-lived way of proving
|
|
1210
|
+
// you are somebody, and it is a cache — losing one costs the person
|
|
1211
|
+
// nothing but a second click on Forgot password. So `data/users.json`
|
|
1212
|
+
// stays the whole of what a site must back up, and a restart, a rebuild or
|
|
1213
|
+
// a `geekity rebuild` invalidates every link in flight, which is the safe
|
|
1214
|
+
// direction to fail in.
|
|
1215
|
+
//
|
|
1216
|
+
// The key is the token's SHA-256 rather than the token, so the database
|
|
1217
|
+
// holds nothing a working link could be rebuilt from: somebody who reads
|
|
1218
|
+
// this table — a backup, a stray copy of the file — learns that a reset
|
|
1219
|
+
// was asked for and nothing else. It is a plain hash rather than argon2
|
|
1220
|
+
// because the token is 256 random bits and there is no guess to slow down;
|
|
1221
|
+
// what argon2 buys on a password it cannot buy here.
|
|
1222
|
+
version: 16,
|
|
1223
|
+
sql: `
|
|
1224
|
+
CREATE TABLE password_resets (
|
|
1225
|
+
token_hash TEXT PRIMARY KEY,
|
|
1226
|
+
user_id INTEGER NOT NULL,
|
|
1227
|
+
created_at TEXT NOT NULL,
|
|
1228
|
+
expires_at TEXT NOT NULL
|
|
1229
|
+
);
|
|
1230
|
+
|
|
1231
|
+
CREATE INDEX password_resets_expires_at ON password_resets (expires_at);
|
|
1232
|
+
CREATE INDEX password_resets_user_id ON password_resets (user_id);
|
|
1233
|
+
`,
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
// The two halves of a notification's one-click links (TASK-55).
|
|
1237
|
+
//
|
|
1238
|
+
// `spent_tokens` is what makes such a link single use. The link itself is
|
|
1239
|
+
// not stored — it is an HMAC-signed claim, so that a moderation link in an
|
|
1240
|
+
// inbox goes on working across a rebuilt or restored cache, which is a
|
|
1241
|
+
// thing decision-9 says a site may do whenever it likes. Only the fact
|
|
1242
|
+
// that one has been used is written down, keyed by its SHA-256 for the
|
|
1243
|
+
// reason `password_resets` is, and swept once the signature has expired.
|
|
1244
|
+
// Losing this table forgets which links were spent, and that costs a site
|
|
1245
|
+
// nothing: every action a link performs is idempotent, so doing it a
|
|
1246
|
+
// second time is doing it once.
|
|
1247
|
+
//
|
|
1248
|
+
// `comments.notify` is the commenter's "tell me about replies", which the
|
|
1249
|
+
// file says and this column indexes like every other column of that table.
|
|
1250
|
+
// Nothing backfills it: the rebuild on the next boot does, and a comment
|
|
1251
|
+
// written before this shipped never asked to be told.
|
|
1252
|
+
version: 17,
|
|
1253
|
+
sql: `
|
|
1254
|
+
CREATE TABLE spent_tokens (
|
|
1255
|
+
token_hash TEXT PRIMARY KEY,
|
|
1256
|
+
spent_at TEXT NOT NULL,
|
|
1257
|
+
expires_at TEXT NOT NULL
|
|
1258
|
+
);
|
|
1259
|
+
|
|
1260
|
+
CREATE INDEX spent_tokens_expires_at ON spent_tokens (expires_at);
|
|
1261
|
+
|
|
1262
|
+
ALTER TABLE comments ADD COLUMN notify INTEGER NOT NULL DEFAULT 0;
|
|
1263
|
+
`,
|
|
1264
|
+
},
|
|
1265
|
+
{
|
|
1266
|
+
// Followers belong to a user, and the inbox log says who was told
|
|
1267
|
+
// (decision-14, TASK-68). The site actor is gone, so there is nothing to
|
|
1268
|
+
// backfill: the `followers` table is rebuilt on every boot from the files
|
|
1269
|
+
// under `content/_data/federation/{username}/`, and the rows a site
|
|
1270
|
+
// carried for the site actor name nobody. The table is therefore dropped
|
|
1271
|
+
// and made again with the new key rather than altered — `actor_id` was its
|
|
1272
|
+
// primary key, and one actor may follow two of this site's users.
|
|
1273
|
+
version: 18,
|
|
1274
|
+
sql: `
|
|
1275
|
+
DROP TABLE followers;
|
|
1276
|
+
|
|
1277
|
+
CREATE TABLE followers (
|
|
1278
|
+
username TEXT NOT NULL,
|
|
1279
|
+
actor_id TEXT NOT NULL,
|
|
1280
|
+
inbox_id TEXT NOT NULL,
|
|
1281
|
+
shared_inbox_id TEXT,
|
|
1282
|
+
handle TEXT,
|
|
1283
|
+
name TEXT,
|
|
1284
|
+
icon_url TEXT,
|
|
1285
|
+
url TEXT,
|
|
1286
|
+
followed_at TEXT NOT NULL,
|
|
1287
|
+
PRIMARY KEY (username, actor_id)
|
|
1288
|
+
);
|
|
1289
|
+
|
|
1290
|
+
CREATE INDEX followers_followed_at ON followers (followed_at);
|
|
1291
|
+
CREATE INDEX followers_actor_id ON followers (actor_id);
|
|
1292
|
+
|
|
1293
|
+
ALTER TABLE ap_inbox ADD COLUMN recipient TEXT;
|
|
1294
|
+
|
|
1295
|
+
CREATE INDEX ap_inbox_recipient ON ap_inbox (recipient);
|
|
1296
|
+
`,
|
|
1297
|
+
},
|
|
676
1298
|
];
|
|
677
|
-
function migrate(db) {
|
|
678
|
-
db.exec(`
|
|
679
|
-
CREATE TABLE IF NOT EXISTS admin_migrations (
|
|
680
|
-
version INTEGER PRIMARY KEY,
|
|
681
|
-
applied_at TEXT NOT NULL
|
|
682
|
-
)
|
|
683
|
-
`);
|
|
684
|
-
const applied = new Set(db
|
|
685
|
-
.prepare('SELECT version FROM admin_migrations')
|
|
686
|
-
.all()
|
|
687
|
-
.map((row) => Number(row['version'])));
|
|
688
|
-
const record = db.prepare('INSERT INTO admin_migrations (version, applied_at) VALUES (?, ?)');
|
|
689
|
-
for (const migration of MIGRATIONS) {
|
|
690
|
-
if (applied.has(migration.version))
|
|
691
|
-
continue;
|
|
692
|
-
db.exec('BEGIN');
|
|
693
|
-
try {
|
|
694
|
-
db.exec(migration.sql);
|
|
695
|
-
record.run(migration.version, new Date().toISOString());
|
|
696
|
-
db.exec('COMMIT');
|
|
697
|
-
}
|
|
698
|
-
catch (error) {
|
|
699
|
-
db.exec('ROLLBACK');
|
|
700
|
-
throw error;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
1299
|
//# sourceMappingURL=store.js.map
|