fedipod-server 0.21.0 → 0.28.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 +5 -4
- package/dist/claims.d.ts +3 -1
- package/dist/claims.js +20 -7
- package/dist/components/context.jsonld +6 -0
- package/dist/handler.d.ts +29 -1
- package/dist/handler.js +89 -4
- package/dist/handler.jsonld +36 -1
- package/lib/client/c2s.mjs +100 -3
- package/lib/client/masto/accounts.mjs +44 -4
- package/lib/client/masto/body.mjs +90 -3
- package/lib/client/masto/index.mjs +5 -1
- package/lib/client/masto/instance.mjs +11 -3
- package/lib/client/masto/media.mjs +3 -57
- package/lib/client/masto/oauth.mjs +66 -9
- package/lib/client/masto/render.mjs +62 -5
- package/lib/client/masto/statuses.mjs +25 -1
- package/lib/client/masto/timelines.mjs +128 -6
- package/lib/connections/tagfeed.mjs +42 -5
- package/lib/core/as2.mjs +36 -5
- package/lib/core/contexts/index.mjs +14 -0
- package/lib/core/contexts/litepub-0.1.json +60 -0
- package/lib/core/contexts/map.json +3 -1
- package/lib/core/contexts/quotes.json +18 -0
- package/lib/core/graphview.mjs +41 -3
- package/lib/core/intake/activities.mjs +136 -2
- package/lib/core/intake/activity.mjs +79 -1
- package/lib/core/intake/group.mjs +37 -3
- package/lib/core/intake/index.mjs +32 -2
- package/lib/core/intake/notes.mjs +60 -3
- package/lib/core/lease.mjs +12 -2
- package/lib/core/profile-page.mjs +104 -0
- package/lib/core/publisher/collections.mjs +6 -1
- package/lib/core/publisher/index.mjs +35 -17
- package/lib/core/publisher/notes.mjs +104 -3
- package/lib/core/shapes/activitystreams.ttl +28 -0
- package/lib/core/shapes/index.mjs +26 -1
- package/lib/core/wire-quotes.mjs +71 -0
- package/lib/core/wire.mjs +59 -103
- package/lib/device/admin/routes/lifecycle.mjs +9 -0
- package/lib/device/admin/static.mjs +4 -1
- package/lib/device/admin/surface.mjs +26 -2
- package/lib/device/certs.mjs +18 -2
- package/lib/device/cli/commands/setup.mjs +6 -2
- package/lib/device/cli/commands/state.mjs +3 -1
- package/lib/device/gateway-move.mjs +58 -0
- package/lib/device/migrate.mjs +3 -1
- package/lib/device/setup.mjs +67 -7
- package/lib/gateway/front-core.mjs +307 -36
- package/lib/gateway/gateway-core.mjs +15 -6
- package/lib/gateway/notices.mjs +84 -0
- package/lib/gateway/quiet.mjs +184 -0
- package/lib/pod/root.mjs +6 -3
- package/lib/pod/state.mjs +8 -7
- package/lib/pod/transport.mjs +5 -1
- package/lib/session/README.md +123 -0
- package/lib/session/demo.html +94 -0
- package/lib/session/fedi-account.mjs +346 -0
- package/lib/session/fedi-login.mjs +123 -0
- package/lib/session/oidc-session.mjs +233 -0
- package/lib/session/package.json +41 -0
- package/package.json +1 -1
- package/run-agent.mjs +28 -0
- package/vendor/gate.cjs +6 -3
- package/web/admin/bar.css +66 -0
- package/web/admin/bar.js +38 -0
- package/web/admin/client/index.html +31 -3
- package/web/admin/client-pick.js +79 -0
- package/web/admin/gateway.js +22 -0
- package/web/admin/index.html +54 -13
- package/web/admin/notices-bar.js +90 -0
- package/web/admin/oauth-signin.mjs +37 -0
- package/web/admin/record.js +13 -1
- package/web/admin/setup/index.html +31 -3
- package/web/admin/tokens.css +15 -3
- package/web/admin/upkeep.js +4 -5
- package/web/app/README.md +14 -10
- package/web/app/admin-facade.mjs +51 -12
- package/web/app/agent.mjs +157 -19
- package/web/app/authorize_interaction.html +56 -0
- package/web/app/authorize_interaction.js +69 -0
- package/web/app/boot.mjs +233 -95
- package/web/app/dist/boot.js +477 -569
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +1861 -532
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/gateway-move.mjs +60 -0
- package/web/app/index.html +40 -49
- package/web/app/keys-browser.mjs +12 -15
- package/web/app/keystore.mjs +19 -36
- package/web/app/oidc-session.mjs +5 -188
- package/web/app/signup.mjs +172 -132
- package/web/app/sw-src.mjs +1 -1
- package/web/app/update.js +66 -0
- package/web/front/admin.html +5 -3
- package/web/front/new-account.html +2 -2
- package/web/front/notices.html +69 -0
- package/web/front/notices.js +107 -0
- package/web/front/run.html +15 -8
- package/web/front/run.js +124 -36
- package/web/icons/forum.svg +11 -0
- package/web/icons/gateway.svg +14 -0
- package/web/icons/local.svg +11 -0
- package/web/app/index.html~ +0 -152
- package/web/app/site/_headers +0 -9
- package/web/app/site/_redirects +0 -22
- package/web/app/site/admin/actors.js +0 -145
- package/web/app/site/admin/bar.css +0 -60
- package/web/app/site/admin/bar.js +0 -61
- package/web/app/site/admin/client/client.js +0 -66
- package/web/app/site/admin/client/index.html +0 -43
- package/web/app/site/admin/common.js +0 -23
- package/web/app/site/admin/connections.js +0 -112
- package/web/app/site/admin/gateway.js +0 -111
- package/web/app/site/admin/group.js +0 -258
- package/web/app/site/admin/index.html +0 -564
- package/web/app/site/admin/record.js +0 -378
- package/web/app/site/admin/setup/index.html +0 -215
- package/web/app/site/admin/setup/setup.js +0 -326
- package/web/app/site/admin/tokens.css +0 -56
- package/web/app/site/admin/upkeep.js +0 -178
- package/web/app/site/admin/window.css +0 -55
- package/web/app/site/admin/window.js +0 -136
- package/web/app/site/app/404.html +0 -40
- package/web/app/site/app/_headers +0 -2
- package/web/app/site/app/apple-touch-icon.png +0 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js +0 -2
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +0 -1
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +0 -183
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
- package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +0 -2
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +0 -1
- package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
- package/web/app/site/app/assets/chunk-aKtaBQYM.js +0 -1
- package/web/app/site/app/assets/compose-CFEx7wb4.js +0 -2
- package/web/app/site/app/assets/compose-CFEx7wb4.js.map +0 -1
- package/web/app/site/app/assets/compose-ZJN5xSb4.js +0 -35
- package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +0 -1
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js +0 -52
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +0 -1
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js +0 -2
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +0 -1
- package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +0 -1
- package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +0 -1
- package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
- package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +0 -1
- package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
- package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
- package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
- package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
- package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
- package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
- package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
- package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
- package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +0 -1
- package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +0 -1
- package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
- package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +0 -1
- package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
- package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +0 -1
- package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
- package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
- package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +0 -1
- package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
- package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +0 -1
- package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
- package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
- package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
- package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
- package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
- package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +0 -1
- package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +0 -1
- package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +0 -1
- package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +0 -1
- package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +0 -1
- package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
- package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +0 -1
- package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
- package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +0 -1
- package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +0 -1
- package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +0 -1
- package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
- package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
- package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +0 -1
- package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +0 -1
- package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
- package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
- package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +0 -1
- package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +0 -1
- package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +0 -1
- package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
- package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +0 -1
- package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +0 -1
- package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +0 -1
- package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
- package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
- package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
- package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +0 -1
- package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +0 -1
- package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +0 -1
- package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +0 -1
- package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +0 -1
- package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
- package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
- package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +0 -1
- package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +0 -1
- package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +0 -1
- package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +0 -1
- package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +0 -1
- package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +0 -1
- package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +0 -1
- package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +0 -1
- package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
- package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +0 -1
- package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +0 -1
- package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +0 -1
- package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
- package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +0 -1
- package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +0 -1
- package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
- package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
- package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
- package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +0 -1
- package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +0 -1
- package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
- package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +0 -1
- package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +0 -1
- package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +0 -1
- package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +0 -1
- package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +0 -1
- package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +0 -1
- package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +0 -1
- package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +0 -1
- package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +0 -1
- package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
- package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +0 -1
- package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
- package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
- package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
- package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +0 -1
- package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
- package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +0 -1
- package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +0 -1
- package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
- package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +0 -1
- package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
- package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +0 -1
- package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
- package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +0 -1
- package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +0 -1
- package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
- package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +0 -1
- package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +0 -1
- package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
- package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +0 -1
- package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +0 -1
- package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +0 -1
- package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
- package/web/app/site/app/assets/instances-BNPptnn-.json +0 -333
- package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +0 -1
- package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
- package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +0 -1
- package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +0 -1
- package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +0 -1
- package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +0 -1
- package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +0 -1
- package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +0 -1
- package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +0 -1
- package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +0 -1
- package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +0 -1
- package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +0 -1
- package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +0 -1
- package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +0 -1
- package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
- package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
- package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +0 -1
- package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
- package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +0 -1
- package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +0 -1
- package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
- package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/tok-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +0 -1
- package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +0 -1
- package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/main-BdqNbG-a.js +0 -26
- package/web/app/site/app/assets/main-BdqNbG-a.js.map +0 -1
- package/web/app/site/app/assets/mock-home-DGhR__He.js +0 -2
- package/web/app/site/app/assets/mock-home-DGhR__He.js.map +0 -1
- package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
- package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +0 -2
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +0 -1
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +0 -20
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +0 -1
- package/web/app/site/app/assets/style-B1iLPTPR.css +0 -2
- package/web/app/site/app/assets/temml-DuyIQMei.js +0 -97
- package/web/app/site/app/assets/temml-DuyIQMei.js.map +0 -1
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +0 -2
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
- package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
- package/web/app/site/app/assets/year-in-posts-DJv_star.js +0 -3
- package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +0 -1
- package/web/app/site/app/compose/index.html +0 -20
- package/web/app/site/app/favicon.ico +0 -0
- package/web/app/site/app/index.html +0 -73
- package/web/app/site/app/logo-192.png +0 -0
- package/web/app/site/app/logo-512.png +0 -0
- package/web/app/site/app/logo-badge-72.png +0 -0
- package/web/app/site/app/logo-maskable-512.png +0 -0
- package/web/app/site/app/logo-monochrome-512.png +0 -0
- package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
- package/web/app/site/app/manifest.webmanifest +0 -1
- package/web/app/site/app/og-image-2.jpg +0 -0
- package/web/app/site/app/og-image.png +0 -0
- package/web/app/site/app/robots.txt +0 -2
- package/web/app/site/app/version.json +0 -1
- package/web/app/site/boot.js +0 -34277
- package/web/app/site/index.html +0 -251
- package/web/app/site/sw.js +0 -71331
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// profile-page.mjs — the human page a browser opens for an actor: who the
|
|
2
|
+
// account is, its fields, when it joined, what it pinned, and a remote-follow
|
|
3
|
+
// control. The actor document is for servers; this is the address you hand
|
|
4
|
+
// to a person. The agent writes it beside the actor (publisher
|
|
5
|
+
// publishProfilePage) only when its content changed.
|
|
6
|
+
|
|
7
|
+
const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>' };
|
|
8
|
+
|
|
9
|
+
// `fields` are the profile's name/value rows, `joined` an ISO date, `pinned`
|
|
10
|
+
// the pinned posts as { content (sanitized HTML), published, url }.
|
|
11
|
+
export function profilePageHtml({ name, address, summary = null, icon = null, image = null,
|
|
12
|
+
fields = [], joined = null, pinned = [], kind = 'person' }) {
|
|
13
|
+
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, c => ({ ...HTML_ESCAPES, '"': '"' }[c]));
|
|
14
|
+
const what = kind === 'group' ? 'a group' : 'an account';
|
|
15
|
+
const month = (iso) => {
|
|
16
|
+
const d = new Date(iso);
|
|
17
|
+
return Number.isNaN(d.getTime()) ? null
|
|
18
|
+
: d.toLocaleDateString('en', { month: 'long', year: 'numeric', timeZone: 'UTC' });
|
|
19
|
+
};
|
|
20
|
+
const day = (iso) => {
|
|
21
|
+
const d = new Date(iso);
|
|
22
|
+
return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10);
|
|
23
|
+
};
|
|
24
|
+
const fieldRows = fields.filter(f => f?.name && f?.value !== undefined && f?.value !== null && String(f.value) !== '');
|
|
25
|
+
const joinedText = joined ? month(joined) : null;
|
|
26
|
+
const pins = pinned.filter(x => x && typeof x.content === 'string');
|
|
27
|
+
return `<!doctype html>
|
|
28
|
+
<html lang="en">
|
|
29
|
+
<head>
|
|
30
|
+
<meta charset="utf-8">
|
|
31
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
32
|
+
<title>${esc(name)} (${esc(address)})</title>
|
|
33
|
+
<style>
|
|
34
|
+
:root { color-scheme: light dark; }
|
|
35
|
+
body { font: 112.5%/1.5 system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
|
|
36
|
+
img.header { width: 100%; max-height: 12rem; object-fit: cover; border-radius: 1rem; }
|
|
37
|
+
img.avatar { width: 6rem; height: 6rem; border-radius: 1rem; object-fit: cover; margin-top: 1rem; }
|
|
38
|
+
h1 { margin: .5rem 0 0; }
|
|
39
|
+
.address { font-size: 1.1rem; user-select: all; }
|
|
40
|
+
dl.fields { display: grid; grid-template-columns: max-content 1fr; gap: .4rem 1rem; margin: 1.5rem 0; }
|
|
41
|
+
dl.fields dt { font-weight: 600; }
|
|
42
|
+
dl.fields dd { margin: 0; overflow-wrap: anywhere; }
|
|
43
|
+
.joined { margin: 1rem 0; }
|
|
44
|
+
section.pinned h2 { font-size: 1.1rem; margin: 2rem 0 .5rem; }
|
|
45
|
+
article.pin { border: 1px solid #8884; border-radius: 1rem; padding: 1rem 1.2rem; margin: 1rem 0; }
|
|
46
|
+
article.pin p:last-child { margin-bottom: 0; }
|
|
47
|
+
article.pin .when { font-size: 1rem; }
|
|
48
|
+
form { margin-top: 2rem; }
|
|
49
|
+
label { display: block; margin-bottom: .3rem; }
|
|
50
|
+
input { font: inherit; padding: .5rem; width: 14rem; max-width: 100%; }
|
|
51
|
+
button { font: inherit; padding: .5rem 1rem; }
|
|
52
|
+
.hint { color: #666; font-size: 1rem; }
|
|
53
|
+
.err { color: #b00020; }
|
|
54
|
+
.err:empty { display: none; }
|
|
55
|
+
@media (prefers-color-scheme: dark) {
|
|
56
|
+
.hint { color: #aaa; }
|
|
57
|
+
.err { color: #ff8a80; }
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
60
|
+
</head>
|
|
61
|
+
<body>
|
|
62
|
+
<main>
|
|
63
|
+
${image ? `<img class="header" src="${esc(image)}" alt="">` : ''}
|
|
64
|
+
${icon ? `<img class="avatar" src="${esc(icon)}" alt="">` : ''}
|
|
65
|
+
<h1>${esc(name)}</h1>
|
|
66
|
+
<p class="address">${esc(address)}</p>
|
|
67
|
+
${summary ? `<div>${summary}</div>` : ''}
|
|
68
|
+
${fieldRows.length ? `<dl class="fields">
|
|
69
|
+
${fieldRows.map(f => `<dt>${esc(f.name)}</dt><dd>${esc(f.value)}</dd>`).join('\n')}
|
|
70
|
+
</dl>` : ''}
|
|
71
|
+
${joinedText ? `<p class="joined">Joined ${esc(joinedText)}</p>` : ''}
|
|
72
|
+
${pins.length ? `<section class="pinned">
|
|
73
|
+
<h2>Pinned</h2>
|
|
74
|
+
${pins.map(x => `<article class="pin">
|
|
75
|
+
${x.content}
|
|
76
|
+
<p class="when">${x.url ? `<a href="${esc(x.url)}">${esc(day(x.published))}</a>` : esc(day(x.published))}</p>
|
|
77
|
+
</article>`).join('\n')}
|
|
78
|
+
</section>` : ''}
|
|
79
|
+
<p>This is ${what} on the Fediverse. To follow it, paste the address above
|
|
80
|
+
into the search box of Mastodon or any Fediverse app — or use the form.</p>
|
|
81
|
+
<form id="follow">
|
|
82
|
+
<label for="server">your server</label>
|
|
83
|
+
<input id="server" type="text" placeholder="mastodon.social" autocomplete="off"
|
|
84
|
+
required aria-describedby="follow-err">
|
|
85
|
+
<button type="submit">Follow</button>
|
|
86
|
+
<p class="err" id="follow-err" role="alert"></p>
|
|
87
|
+
</form>
|
|
88
|
+
<p class="hint">The form sends you to your own server's follow screen.</p>
|
|
89
|
+
</main>
|
|
90
|
+
<script>
|
|
91
|
+
document.getElementById('follow').addEventListener('submit', (ev) => {
|
|
92
|
+
ev.preventDefault();
|
|
93
|
+
const s = document.getElementById('server').value.trim().replace(/^https?:\\/\\//, '').replace(/\\/.*$/, '');
|
|
94
|
+
const err = document.getElementById('follow-err');
|
|
95
|
+
const addr = document.querySelector('.address').textContent.trim();
|
|
96
|
+
if (!s) { err.textContent = 'Enter your server, e.g. mastodon.social'; return; }
|
|
97
|
+
err.textContent = '';
|
|
98
|
+
location.href = 'https://' + s + '/authorize_interaction?uri=' + encodeURIComponent(addr);
|
|
99
|
+
});
|
|
100
|
+
</script>
|
|
101
|
+
</body>
|
|
102
|
+
</html>
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
@@ -15,7 +15,7 @@ import * as podPrivate from '../../pod/private.mjs';
|
|
|
15
15
|
// still gets everything, so a missed call site degrades to the old cost rather
|
|
16
16
|
// than silently publishing nothing.
|
|
17
17
|
export const ALL_COLLECTIONS = { followers: true, following: true, outbox: true, acls: true,
|
|
18
|
-
pending: true, blocked: true };
|
|
18
|
+
pending: true, blocked: true, featured: true };
|
|
19
19
|
|
|
20
20
|
// Publish the collections a change actually TOUCHED.
|
|
21
21
|
//
|
|
@@ -152,6 +152,11 @@ export async function publishCollections(publisher, which = ALL_COLLECTIONS) {
|
|
|
152
152
|
}
|
|
153
153
|
if (which.pending) await publisher.publishPending();
|
|
154
154
|
if (which.blocked) await publisher.publishBlocked();
|
|
155
|
+
// The pinned posts, written even when there are none. The actor names this
|
|
156
|
+
// collection, and a server showing the profile reads it whether or not
|
|
157
|
+
// anything was ever pinned; with no document there the pod refused every
|
|
158
|
+
// reader, a refusal nothing caches, and every server asked again.
|
|
159
|
+
if (which.featured) await publisher.publishFeatured();
|
|
155
160
|
if (which.outbox) {
|
|
156
161
|
const outbox = publisher.store.read('outbox.json', []);
|
|
157
162
|
// Reconcile reads every published page, which is the expensive part of a
|
|
@@ -91,7 +91,11 @@ export class Publisher {
|
|
|
91
91
|
// Every actor document this identity publishes, from one place: the
|
|
92
92
|
// profile publish and a Move both write it, and a Move that rebuilt it
|
|
93
93
|
// by hand dropped the gateway inbox, the outbox door and the collections.
|
|
94
|
-
|
|
94
|
+
// `clientOrigin` is overridden only by the agent serving a client its own
|
|
95
|
+
// local actor: standalone, the published document cannot name a loopback
|
|
96
|
+
// address, but a client already talking to that address can be told it.
|
|
97
|
+
actorDocFor({ priv = false, moderators = null, inbox = null, movedTo = this.config.movedTo || null,
|
|
98
|
+
clientOrigin = this.clientOrigin } = {}) {
|
|
95
99
|
const { urls } = this;
|
|
96
100
|
return wire.actorDoc({
|
|
97
101
|
urls, handle: wire.publicHandle(this.config), name: this.config.name, publicKeyPem: this.publicKeyPem,
|
|
@@ -102,6 +106,9 @@ export class Publisher {
|
|
|
102
106
|
image: this.config.image || null, fields: this.config.fields || [],
|
|
103
107
|
webId: this.remote.webId || null,
|
|
104
108
|
aliases: this.config.aliases || [],
|
|
109
|
+
// A group says whether only its moderators may open posts (Lemmy's
|
|
110
|
+
// term); a person's actor carries nothing of the kind.
|
|
111
|
+
postingRestrictedToMods: this.config.kind === 'group' ? !!this.config.postingRestrictedToMods : null,
|
|
105
112
|
moderators,
|
|
106
113
|
pendingFollowers: priv ? urls.pendingFollowers : null,
|
|
107
114
|
pendingFollowing: priv ? urls.pendingFollowing : null,
|
|
@@ -110,11 +117,11 @@ export class Publisher {
|
|
|
110
117
|
// The agent's own outbox endpoint, where it is reachable: a client
|
|
111
118
|
// following the actor must arrive somewhere that will take a write.
|
|
112
119
|
// Otherwise the Gateway's outbox door, when one is attached.
|
|
113
|
-
outbox:
|
|
120
|
+
outbox: clientOrigin ? `${clientOrigin}ap/outbox` : this.gatewayOutbox(),
|
|
114
121
|
// How a client-to-server client finds the way in with nothing configured
|
|
115
122
|
// by hand. Advertised only where the surface is publicly reachable.
|
|
116
|
-
oauthAuthorize:
|
|
117
|
-
oauthToken:
|
|
123
|
+
oauthAuthorize: clientOrigin ? `${clientOrigin}oauth/authorize` : null,
|
|
124
|
+
oauthToken: clientOrigin ? `${clientOrigin}oauth/token` : null,
|
|
118
125
|
});
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -177,7 +184,9 @@ export class Publisher {
|
|
|
177
184
|
// actor's inbox; deliveries reach the pod inbox through it. Absent → today.
|
|
178
185
|
const gw = this.config.gateway;
|
|
179
186
|
const gwActive = gw && gw.url && gw.mode && gw.mode !== 'off';
|
|
180
|
-
|
|
187
|
+
// A forum's category advertises the forum's one inbox (config.inboxUrl):
|
|
188
|
+
// deliveries to any category land there, and the forum routes them.
|
|
189
|
+
const actorDoc = this.actorDocFor({ priv, moderators, inbox: gwActive ? gw.url : (this.config.inboxUrl || null) });
|
|
181
190
|
const surface = crypto.createHash('sha256').update(JSON.stringify({
|
|
182
191
|
actor: actorDoc, handle: this.config.handle, host,
|
|
183
192
|
quiesced: !!this.config.quiescedAt, version: AGENT_VERSION,
|
|
@@ -191,16 +200,21 @@ export class Publisher {
|
|
|
191
200
|
return { unreachable: [], updated: 0, skipped: true };
|
|
192
201
|
}
|
|
193
202
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
203
|
+
// Discovery is the pod's, and a pod has one WebFinger document: a forum's
|
|
204
|
+
// category (config.forum names the forum) is not the pod's identity, so it
|
|
205
|
+
// writes none — the forum's own actor, or a Gateway, answers for it.
|
|
206
|
+
if (!this.config.forum) {
|
|
207
|
+
await discovery.writeWebfinger(this.remote, urls,
|
|
208
|
+
wire.jrd({ handle: this.config.handle, host, actor: urls.actor, page: urls.profileHtml }));
|
|
209
|
+
await discovery.writeHostMeta(this.remote, urls, wire.hostMeta(urls.base));
|
|
197
210
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
211
|
+
const nodeinfoDocUrl = urls.home + 'ap/nodeinfo-2.0';
|
|
212
|
+
const localPosts = this.store.getStatuses().filter(s => s.kind === 'post').length;
|
|
213
|
+
await discovery.writeNodeinfo(this.remote, urls, {
|
|
214
|
+
pointer: wire.nodeinfoPointer(nodeinfoDocUrl),
|
|
215
|
+
doc: wire.nodeinfoDoc({ version: AGENT_VERSION, localPosts }),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
204
218
|
|
|
205
219
|
const actor = actorDoc; // built above, for the digest
|
|
206
220
|
await podActor.write(this.remote, urls, actor);
|
|
@@ -218,7 +232,7 @@ export class Publisher {
|
|
|
218
232
|
// WebID → actor: the profile card lists the actor as a foaf:account.
|
|
219
233
|
// Best-effort — a profile that cannot be read or edited does not stop the
|
|
220
234
|
// publish, it is logged and the rest of the surface still goes up.
|
|
221
|
-
try {
|
|
235
|
+
if (this.config.forum) { /* a category is not the pod owner's account */ } else try {
|
|
222
236
|
const wrote = await podActor.linkInWebIdProfile(this.remote, {
|
|
223
237
|
actorUrl: urls.actor,
|
|
224
238
|
accountName: `@${this.config.handle}@${host}`,
|
|
@@ -304,12 +318,13 @@ export class Publisher {
|
|
|
304
318
|
async verifyPublicSurface() {
|
|
305
319
|
const { urls } = this;
|
|
306
320
|
const targets = [
|
|
307
|
-
|
|
308
|
-
['host-meta', urls.base + '.well-known/host-meta'],
|
|
321
|
+
// Discovery documents are the pod's, not a forum category's (see publishProfile).
|
|
322
|
+
...(this.config.forum ? [] : [['webfinger', urls.webfinger], ['host-meta', urls.base + '.well-known/host-meta']]),
|
|
309
323
|
['actor', urls.actor],
|
|
310
324
|
['notes', urls.notes],
|
|
311
325
|
['followers', urls.followers],
|
|
312
326
|
['following', urls.following],
|
|
327
|
+
['featured', urls.featured],
|
|
313
328
|
['outbox', urls.outbox],
|
|
314
329
|
];
|
|
315
330
|
const unreachable = [];
|
|
@@ -468,6 +483,9 @@ export class Publisher {
|
|
|
468
483
|
privateReady(...a) { return notes.privateReady(this, ...a); }
|
|
469
484
|
_mentionsFor(...a) { return notes.mentionsFor(this, ...a); }
|
|
470
485
|
publishNote(...a) { return notes.publishNote(this, ...a); }
|
|
486
|
+
authorizeQuote(...a) { return notes.authorizeQuote(this, ...a); }
|
|
487
|
+
withdrawQuote(...a) { return notes.withdrawQuote(this, ...a); }
|
|
488
|
+
grantQuote(...a) { return notes.grantQuote(this, ...a); }
|
|
471
489
|
publishObject(...a) { return notes.publishObject(this, ...a); }
|
|
472
490
|
updateNote(...a) { return notes.updateNote(this, ...a); }
|
|
473
491
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import * as wire from '../wire.mjs';
|
|
7
|
+
import * as wireQuotes from '../wire-quotes.mjs';
|
|
7
8
|
import * as containers from '../../pod/containers.mjs';
|
|
8
9
|
import * as podNotes from '../../pod/notes.mjs';
|
|
9
10
|
|
|
@@ -159,7 +160,10 @@ export function rowContent(obj) {
|
|
|
159
160
|
return `<p><a href="${esc(obj?.id || '')}">${esc(obj?.type || 'object')}</a></p>`;
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
|
|
163
|
+
// `quote`: the post this one quotes, `{ id, actor }` — the quoted author is
|
|
164
|
+
// asked to allow it (FEP-044f). A quote of one of this actor's own posts is
|
|
165
|
+
// allowed on the spot.
|
|
166
|
+
export async function publishNote(publisher, content, { inReplyTo, attachments, visibility = 'public', spoilerText = null, sensitive = false, slug: wanted = null, also = [], deliverTo = [], quote = null } = {}) {
|
|
163
167
|
const { urls } = publisher;
|
|
164
168
|
const priv = visibility === 'private' || visibility === 'direct';
|
|
165
169
|
if (priv) {
|
|
@@ -171,10 +175,16 @@ export async function publishNote(publisher, content, { inReplyTo, attachments,
|
|
|
171
175
|
const mentions = await publisher._mentionsFor(content, inReplyTo);
|
|
172
176
|
if (visibility === 'direct') assertDirectAddressed(content, mentions);
|
|
173
177
|
const reply = await replyTarget(publisher, inReplyTo, mentions, visibility);
|
|
178
|
+
const selfQuote = quote?.id && quote.actor === urls.actor;
|
|
179
|
+
const quoted = quote?.id ? {
|
|
180
|
+
id: quote.id,
|
|
181
|
+
// Our own post: the authorization is minted here, before the note goes out.
|
|
182
|
+
authorization: selfQuote ? await grantQuote(publisher, quote.id, (priv ? urls.privateNotes : urls.notes) + slug) : null,
|
|
183
|
+
} : null;
|
|
174
184
|
const note = wire.noteDoc({ urls, slug, content, published, inReplyTo, attachments, mentions,
|
|
175
185
|
visibility, summary: spoilerText, sensitive, container: priv ? urls.privateNotes : urls.notes,
|
|
176
|
-
also: [...also, ...(reply ? [reply.actor] : [])] });
|
|
177
|
-
const addressedInboxes = await inboxesFor(publisher, [...also, ...deliverTo]);
|
|
186
|
+
also: [...also, ...(reply ? [reply.actor] : []), ...(quote?.actor && !selfQuote ? [quote.actor] : [])], quote: quoted });
|
|
187
|
+
const addressedInboxes = await inboxesFor(publisher, [...also, ...deliverTo, ...(quote?.actor && !selfQuote ? [quote.actor] : [])]);
|
|
178
188
|
|
|
179
189
|
await podNotes.write(publisher.remote, note.id, note);
|
|
180
190
|
// Empty, but present: a dangling `replies` that 404s is worse than none.
|
|
@@ -183,6 +193,9 @@ export async function publishNote(publisher, content, { inReplyTo, attachments,
|
|
|
183
193
|
// The outbox is the PUBLIC index; a private or direct post is not in it.
|
|
184
194
|
if (!priv) await publisher.recordOutbox(note.id);
|
|
185
195
|
|
|
196
|
+
const request = quote?.id && !selfQuote
|
|
197
|
+
? wireQuotes.quoteRequestActivity({ urls, note, quoted: quote.id, quotedActor: quote.actor, serial: Date.now() })
|
|
198
|
+
: null;
|
|
186
199
|
publisher.store.addStatus({
|
|
187
200
|
noteId: note.id, actor: urls.actor, content: note.content, published, inReplyTo,
|
|
188
201
|
kind: 'post', slug, text: content, visibility,
|
|
@@ -190,6 +203,12 @@ export async function publishNote(publisher, content, { inReplyTo, attachments,
|
|
|
190
203
|
...(note.sensitive ? { sensitive: true } : {}),
|
|
191
204
|
...(attachments?.length ? { attachments } : {}),
|
|
192
205
|
...rowTags(note),
|
|
206
|
+
...(quote?.id ? {
|
|
207
|
+
quote: quote.id,
|
|
208
|
+
quoteState: selfQuote ? 'accepted' : 'pending',
|
|
209
|
+
...(quoted.authorization ? { quoteAuthorization: quoted.authorization } : {}),
|
|
210
|
+
...(request ? { quoteRequest: { id: request.id, actor: quote.actor } } : {}),
|
|
211
|
+
} : {}),
|
|
193
212
|
});
|
|
194
213
|
// The row is what the author's own timeline reads. Land it before answering
|
|
195
214
|
// — the same PUT the debounce would send in 300 ms — so a worker killed or a
|
|
@@ -212,6 +231,13 @@ export async function publishNote(publisher, content, { inReplyTo, attachments,
|
|
|
212
231
|
].filter(Boolean))];
|
|
213
232
|
await publisher.deliverer.deliverToAll(inboxes, create);
|
|
214
233
|
publisher.log(`note published: ${note.id} → ${inboxes.length} inbox(es)`);
|
|
234
|
+
// The quoted author is asked after the note is out: their Accept names the
|
|
235
|
+
// note, so it has to exist where they will look for it.
|
|
236
|
+
if (request) {
|
|
237
|
+
const [inbox] = await inboxesFor(publisher, [quote.actor]);
|
|
238
|
+
if (inbox) await publisher.deliverer.deliver(inbox, request);
|
|
239
|
+
else publisher.log(`quote of ${quote.id}: no inbox found for ${quote.actor} — the quote stays pending`);
|
|
240
|
+
}
|
|
215
241
|
|
|
216
242
|
// Bluesky mirror: PUBLIC posts only — unlisted, followers-only and direct
|
|
217
243
|
// are never carried off the fediverse. A mirror failure never fails the
|
|
@@ -328,6 +354,7 @@ export async function updateNote(publisher, s, { content, spoilerText = null, se
|
|
|
328
354
|
attachments: atts, mentions, visibility: s.visibility || 'public',
|
|
329
355
|
summary: spoilerText, sensitive: sensitive ?? !!s.sensitive, updated, container,
|
|
330
356
|
also: reply ? [reply.actor] : [],
|
|
357
|
+
quote: rowQuote(s),
|
|
331
358
|
});
|
|
332
359
|
await podNotes.write(publisher.remote, note.id, note);
|
|
333
360
|
await podNotes.writeCreate(publisher.remote, wire.createActivityId(note.id), wire.createActivity(note, urls));
|
|
@@ -350,3 +377,77 @@ export async function updateNote(publisher, s, { content, spoilerText = null, se
|
|
|
350
377
|
publisher.log(`note edited: ${note.id} → ${inboxes.length} inbox(es)`);
|
|
351
378
|
return patched;
|
|
352
379
|
}
|
|
380
|
+
|
|
381
|
+
// What a note's document says about its quote, read off the timeline row: the
|
|
382
|
+
// quoted post and, once granted, the authorization. A quote the author
|
|
383
|
+
// refused is no longer said.
|
|
384
|
+
export function rowQuote(s) {
|
|
385
|
+
if (!s?.quote || s.quoteState === 'rejected') return null;
|
|
386
|
+
return { id: s.quote, ...(s.quoteAuthorization ? { authorization: s.quoteAuthorization } : {}) };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// The note's document again, unchanged in what it says, changed in what it
|
|
390
|
+
// says about its quote. Not an edit — no `updated` stamp — so nobody shows it
|
|
391
|
+
// as edited; the Update goes where the Create went, plus to the quoted author.
|
|
392
|
+
async function restateNote(publisher, s, { quote }) {
|
|
393
|
+
const { urls } = publisher;
|
|
394
|
+
const content = s.text ?? '';
|
|
395
|
+
const mentions = await publisher._mentionsFor(content, s.inReplyTo);
|
|
396
|
+
const container = String(s.noteId).startsWith(urls.privateNotes) ? urls.privateNotes : urls.notes;
|
|
397
|
+
const slug = s.slug || String(s.noteId).slice(container.length);
|
|
398
|
+
const reply = await replyTarget(publisher, s.inReplyTo, mentions, s.visibility || 'public');
|
|
399
|
+
const note = wire.noteDoc({
|
|
400
|
+
urls, slug, content, published: s.published, inReplyTo: s.inReplyTo,
|
|
401
|
+
attachments: s.attachments || [], mentions, visibility: s.visibility || 'public',
|
|
402
|
+
summary: s.spoiler || null, sensitive: !!s.sensitive, updated: s.editedAt || null, container,
|
|
403
|
+
also: [...(reply ? [reply.actor] : []), ...(s.quoteRequest?.actor ? [s.quoteRequest.actor] : [])],
|
|
404
|
+
quote,
|
|
405
|
+
});
|
|
406
|
+
await podNotes.write(publisher.remote, note.id, note);
|
|
407
|
+
await podNotes.writeCreate(publisher.remote, wire.createActivityId(note.id), wire.createActivity(note, urls));
|
|
408
|
+
const update = wire.updateActivity(note, urls, { serial: Date.now() });
|
|
409
|
+
const contacts = publisher.store.getContacts();
|
|
410
|
+
const inboxes = [...new Set([
|
|
411
|
+
...(s.visibility === 'direct' ? [] : contacts.followers.map(f => f.sharedInbox || f.inbox)),
|
|
412
|
+
...mentions.map(m => m.inbox),
|
|
413
|
+
reply?.inbox,
|
|
414
|
+
...(await inboxesFor(publisher, s.quoteRequest?.actor ? [s.quoteRequest.actor] : [])),
|
|
415
|
+
].filter(Boolean))];
|
|
416
|
+
await publisher.deliverer.deliverToAll(inboxes, update);
|
|
417
|
+
return inboxes.length;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// The quoted author said yes: their authorization goes onto the note, and
|
|
421
|
+
// everyone who has the note is told.
|
|
422
|
+
export async function authorizeQuote(publisher, s, authorizationId) {
|
|
423
|
+
const patched = publisher.store.updateStatus(s.noteId, { quoteState: 'accepted', quoteAuthorization: authorizationId });
|
|
424
|
+
const n = await restateNote(publisher, patched || s, { quote: { id: s.quote, authorization: authorizationId } });
|
|
425
|
+
publisher.log(`quote allowed by ${s.quoteRequest?.actor || 'the author'}: ${s.noteId} → ${n} inbox(es)`);
|
|
426
|
+
return patched;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// The quoted author said no: the note stops naming their post.
|
|
430
|
+
export async function withdrawQuote(publisher, s) {
|
|
431
|
+
const patched = publisher.store.updateStatus(s.noteId, { quoteState: 'rejected', quoteAuthorization: undefined });
|
|
432
|
+
const n = await restateNote(publisher, patched || s, { quote: null });
|
|
433
|
+
publisher.log(`quote refused by ${s.quoteRequest?.actor || 'the author'}: ${s.noteId} → ${n} inbox(es)`);
|
|
434
|
+
return patched;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Somebody may quote one of this actor's posts: the authorization is written
|
|
438
|
+
// beside the post — public-Read, like the post — and remembered on its row,
|
|
439
|
+
// so a receiver that checks the quote finds it and a re-asked question gets
|
|
440
|
+
// the same answer. Returns the authorization's id.
|
|
441
|
+
const MAX_QUOTED_BY = 200;
|
|
442
|
+
export async function grantQuote(publisher, noteId, instrumentId) {
|
|
443
|
+
const { urls } = publisher;
|
|
444
|
+
const id = wireQuotes.quoteAuthorizationId(noteId, instrumentId);
|
|
445
|
+
await podNotes.write(publisher.remote, id, wireQuotes.quoteAuthorizationDoc({ urls, id, instrument: instrumentId, target: noteId }));
|
|
446
|
+
const s = publisher.store.getStatuses().find(x => x.noteId === noteId);
|
|
447
|
+
if (s) {
|
|
448
|
+
const quotedBy = (s.quotedBy || []).filter(q => q.note !== instrumentId);
|
|
449
|
+
quotedBy.unshift({ note: instrumentId, authorization: id });
|
|
450
|
+
publisher.store.updateStatus(noteId, { quotedBy: quotedBy.slice(0, MAX_QUOTED_BY) });
|
|
451
|
+
}
|
|
452
|
+
return id;
|
|
453
|
+
}
|
|
@@ -127,3 +127,31 @@
|
|
|
127
127
|
sh:minCount 1 ;
|
|
128
128
|
sh:maxCount 1 ;
|
|
129
129
|
] .
|
|
130
|
+
|
|
131
|
+
<#Update>
|
|
132
|
+
a sh:NodeShape ;
|
|
133
|
+
sh:targetClass as:Update ;
|
|
134
|
+
sh:name "Update" ;
|
|
135
|
+
sh:description "A change to the object it names, carrying the parts that are now different." ;
|
|
136
|
+
sh:property [
|
|
137
|
+
sh:path as:actor ;
|
|
138
|
+
sh:name "actor" ;
|
|
139
|
+
sh:description "The one that performed the activity." ;
|
|
140
|
+
sh:nodeKind sh:BlankNodeOrIRI ;
|
|
141
|
+
sh:minCount 1 ;
|
|
142
|
+
sh:maxCount 1 ;
|
|
143
|
+
] ;
|
|
144
|
+
sh:property [
|
|
145
|
+
sh:path as:object ;
|
|
146
|
+
sh:name "object" ;
|
|
147
|
+
sh:description "The thing the activity is about." ;
|
|
148
|
+
sh:minCount 1 ;
|
|
149
|
+
sh:maxCount 1 ;
|
|
150
|
+
] ;
|
|
151
|
+
sh:property [
|
|
152
|
+
sh:path ( as:object as:closed ) ;
|
|
153
|
+
sh:name "closed" ;
|
|
154
|
+
sh:description "The time the object stopped taking anything more." ;
|
|
155
|
+
sh:datatype xsd:dateTime ;
|
|
156
|
+
sh:maxCount 1 ;
|
|
157
|
+
] .
|
|
@@ -59,6 +59,26 @@ function asTerm(q) {
|
|
|
59
59
|
return rdf.quad(term(q.subject), term(q.predicate), term(q.object));
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* What to call the property that did not fit.
|
|
64
|
+
*
|
|
65
|
+
* A single-predicate path names itself: the IRI, shortened. A sequence path is
|
|
66
|
+
* a list, and its head is a blank node that names nothing — so the failing
|
|
67
|
+
* property shape is asked for its own `sh:name` instead.
|
|
68
|
+
*/
|
|
69
|
+
const SH_NAME = 'http://www.w3.org/ns/shacl#name';
|
|
70
|
+
function pathName(r) {
|
|
71
|
+
const direct = r.path?.value ?? null;
|
|
72
|
+
if (direct && /[#/]/u.test(direct)) return direct;
|
|
73
|
+
const shape = r.sourceShape;
|
|
74
|
+
if (shape) {
|
|
75
|
+
for (const q of shacl().$shapes?.dataset ?? []) {
|
|
76
|
+
if (q.subject.equals(shape) && q.predicate.value === SH_NAME) return q.object.value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return direct;
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
/**
|
|
63
83
|
* Check a document's graph against the shapes.
|
|
64
84
|
*
|
|
@@ -91,7 +111,12 @@ export async function checkShapes(quads) {
|
|
|
91
111
|
conforms: false,
|
|
92
112
|
results: report.results.slice(0, 10).map((r) => ({
|
|
93
113
|
focus: r.focusNode?.value ?? null,
|
|
94
|
-
path:
|
|
114
|
+
// A SEQUENCE path — `( as:object as:closed )` — is an RDF list, so its
|
|
115
|
+
// node is blank and `.value` is a generated label like "n3-13". Reported
|
|
116
|
+
// as that, the record named nothing a reader could act on. The shape's
|
|
117
|
+
// own `sh:name` is what it is called, so that is used when the path
|
|
118
|
+
// cannot name itself.
|
|
119
|
+
path: pathName(r),
|
|
95
120
|
message: r.message?.[0]?.value ?? String(r.sourceConstraintComponent?.value ?? 'does not fit the shape'),
|
|
96
121
|
shape: r.sourceShape?.value ?? null,
|
|
97
122
|
})),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// wire-quotes.mjs — the documents and activities a quote is made of on the
|
|
2
|
+
// wire (FEP-044f): the request to the quoted author, the authorization they
|
|
3
|
+
// publish, and the Accept or Reject that answers. The quote terms a note
|
|
4
|
+
// itself carries live in wire.mjs with the note.
|
|
5
|
+
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import { AS_CTX, QUOTE_CTX, POLICY_CTX } from './wire.mjs';
|
|
8
|
+
|
|
9
|
+
// The activity and object types, and the two terms an authorization names
|
|
10
|
+
// its parties by — GoToSocial's, declared as Mastodon declares them.
|
|
11
|
+
export const QUOTE_TYPES_CTX = {
|
|
12
|
+
QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest',
|
|
13
|
+
QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization',
|
|
14
|
+
gts: 'https://gotosocial.org/ns#',
|
|
15
|
+
interactingObject: { '@id': 'gts:interactingObject', '@type': '@id' },
|
|
16
|
+
interactionTarget: { '@id': 'gts:interactionTarget', '@type': '@id' },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// FEP-044f: asking the quoted post's author to allow the quote. The whole
|
|
20
|
+
// quoting note rides along as the instrument, so their server can see what
|
|
21
|
+
// is being asked without fetching it; `object` is their post.
|
|
22
|
+
export function quoteRequestActivity({ urls, note, quoted, quotedActor, serial }) {
|
|
23
|
+
const { '@context': ctx, ...instrument } = note; // eslint-disable-line no-unused-vars
|
|
24
|
+
return {
|
|
25
|
+
'@context': [AS_CTX, { ...QUOTE_CTX, ...POLICY_CTX, QuoteRequest: QUOTE_TYPES_CTX.QuoteRequest }],
|
|
26
|
+
id: urls.actor + '#quote-request-' + serial,
|
|
27
|
+
type: 'QuoteRequest',
|
|
28
|
+
actor: urls.actor,
|
|
29
|
+
to: [quotedActor],
|
|
30
|
+
object: quoted,
|
|
31
|
+
instrument,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The authorization a quoted author publishes (FEP-044f): a document of its
|
|
36
|
+
// own, at the quoted post's origin, naming the quoting post and the quoted
|
|
37
|
+
// one. A receiver checks the quote by fetching this and comparing both.
|
|
38
|
+
export function quoteAuthorizationId(noteId, instrumentId) {
|
|
39
|
+
return noteId + '-quote-' + crypto.createHash('sha256').update(String(instrumentId)).digest('hex').slice(0, 16);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function quoteAuthorizationDoc({ urls, id, instrument, target }) {
|
|
43
|
+
return {
|
|
44
|
+
'@context': [AS_CTX, QUOTE_TYPES_CTX],
|
|
45
|
+
id,
|
|
46
|
+
type: 'QuoteAuthorization',
|
|
47
|
+
attributedTo: urls.actor,
|
|
48
|
+
interactingObject: instrument,
|
|
49
|
+
interactionTarget: target,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The answer to a QuoteRequest: Accept, carrying the authorization as its
|
|
54
|
+
// result, or Reject. The request is echoed by its id, type, actor, object
|
|
55
|
+
// and the instrument's id — enough for the asker to match it, and nothing a
|
|
56
|
+
// stranger wrote is repeated whole.
|
|
57
|
+
export function quoteAnswerActivity({ urls, type, request, result = null, to, serial }) {
|
|
58
|
+
const idOf = (v) => (typeof v === 'string' ? v : v?.id);
|
|
59
|
+
return {
|
|
60
|
+
'@context': [AS_CTX, { QuoteRequest: QUOTE_TYPES_CTX.QuoteRequest }],
|
|
61
|
+
id: urls.actor + '#' + type.toLowerCase() + '-' + serial,
|
|
62
|
+
type,
|
|
63
|
+
actor: urls.actor,
|
|
64
|
+
to: [to],
|
|
65
|
+
object: {
|
|
66
|
+
id: idOf(request), type: 'QuoteRequest', actor: idOf(request?.actor),
|
|
67
|
+
object: idOf(request?.object), instrument: idOf(request?.instrument),
|
|
68
|
+
},
|
|
69
|
+
...(result ? { result } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|