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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// body.mjs — reading what a client sent: JSON
|
|
2
|
-
// poll a compose request carries, and the text of a post back out
|
|
3
|
-
// HTML.
|
|
1
|
+
// body.mjs — reading what a client sent: JSON, form-encoded or multipart
|
|
2
|
+
// bodies, the poll a compose request carries, and the text of a post back out
|
|
3
|
+
// of its HTML.
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
@@ -51,6 +51,12 @@ export function readBody(req) {
|
|
|
51
51
|
const ct = String(req.headers['content-type'] || '');
|
|
52
52
|
try {
|
|
53
53
|
if (ct.includes('application/json')) return resolve(data ? JSON.parse(data) : {});
|
|
54
|
+
// A multipart body is not a query string: read as one it becomes a
|
|
55
|
+
// single nonsense key, so every field a client sent that way arrived
|
|
56
|
+
// undefined. Sengi registers its OAuth client with a FormData, which
|
|
57
|
+
// is multipart, so /api/v1/apps saw no redirect_uris and handed back
|
|
58
|
+
// the out-of-band URN, leaving the client nowhere to come back to.
|
|
59
|
+
if (ct.includes('multipart/form-data')) return resolve(textFields(data, ct));
|
|
54
60
|
// A form-encoded list is the same key repeated, spelled with a
|
|
55
61
|
// trailing `[]`. Reading it as a plain object kept only the last one,
|
|
56
62
|
// so a client sending its poll or its media that way lost all but the
|
|
@@ -67,3 +73,84 @@ export function readBody(req) {
|
|
|
67
73
|
req.on('error', reject);
|
|
68
74
|
});
|
|
69
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The text fields of a multipart body that arrived as text. readBody
|
|
79
|
+
* accumulates the body as a string, so this reads the fields out of that
|
|
80
|
+
* string rather than out of bytes — which is all a client sending a plain
|
|
81
|
+
* form this way has in it. A body with a FILE in it is not this reader's:
|
|
82
|
+
* a text round trip corrupts binary, and the upload endpoints take their
|
|
83
|
+
* bytes from readMultipart below.
|
|
84
|
+
*/
|
|
85
|
+
function textFields(data, contentType) {
|
|
86
|
+
const m = /boundary=(?:"([^"]+)"|([^;]+))/.exec(contentType);
|
|
87
|
+
if (!m) return {};
|
|
88
|
+
const out = {};
|
|
89
|
+
for (const part of data.split('--' + (m[1] || m[2]).trim())) {
|
|
90
|
+
const sep = part.indexOf('\r\n\r\n');
|
|
91
|
+
if (sep < 0) continue;
|
|
92
|
+
const head = part.slice(0, sep);
|
|
93
|
+
const name = /name="([^"]*)"/.exec(head)?.[1];
|
|
94
|
+
// A file part is skipped rather than kept as mangled text.
|
|
95
|
+
if (!name || /filename="/.test(head)) continue;
|
|
96
|
+
const value = part.slice(sep + 4).replace(/\r\n$/, '');
|
|
97
|
+
if (name.endsWith('[]')) (out[name] ||= []).push(value);
|
|
98
|
+
else out[name] = value;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Minimal multipart/form-data reader for media uploads: string fields plus
|
|
104
|
+
// at most one file part (Mastodon's media endpoints send exactly one).
|
|
105
|
+
export function readMultipart(req, limit = 12e6) {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const chunks = [];
|
|
108
|
+
let n = 0;
|
|
109
|
+
req.on('data', c => {
|
|
110
|
+
n += c.length;
|
|
111
|
+
if (n > limit) { reject(new Error('upload too large')); req.destroy(); return; }
|
|
112
|
+
chunks.push(c);
|
|
113
|
+
});
|
|
114
|
+
req.on('error', reject);
|
|
115
|
+
req.on('end', () => {
|
|
116
|
+
try {
|
|
117
|
+
const m = /boundary=(?:"([^"]+)"|([^;]+))/.exec(String(req.headers['content-type'] || ''));
|
|
118
|
+
if (!m) return resolve({ fields: {}, file: null });
|
|
119
|
+
const buf = Buffer.concat(chunks);
|
|
120
|
+
const boundary = Buffer.from('--' + (m[1] || m[2]).trim());
|
|
121
|
+
const fields = {};
|
|
122
|
+
// `file` is the last one seen, which is what the single-file media
|
|
123
|
+
// upload wants. `files` keys them by field name, because the profile
|
|
124
|
+
// editor submits an avatar and a header in one request.
|
|
125
|
+
const files = {};
|
|
126
|
+
let file = null;
|
|
127
|
+
let i = buf.indexOf(boundary);
|
|
128
|
+
while (i >= 0) {
|
|
129
|
+
const start = i + boundary.length;
|
|
130
|
+
if (buf.slice(start, start + 2).toString() === '--') break;
|
|
131
|
+
const next = buf.indexOf(boundary, start);
|
|
132
|
+
if (next < 0) break;
|
|
133
|
+
const part = buf.slice(start + 2, next - 2); // strip the CRLFs framing the part
|
|
134
|
+
const sep = part.indexOf('\r\n\r\n');
|
|
135
|
+
if (sep >= 0) {
|
|
136
|
+
const head = part.slice(0, sep).toString();
|
|
137
|
+
const body = part.slice(sep + 4);
|
|
138
|
+
const name = /name="([^"]*)"/.exec(head)?.[1];
|
|
139
|
+
const filename = /filename="([^"]*)"/.exec(head)?.[1];
|
|
140
|
+
if (filename !== undefined) {
|
|
141
|
+
file = {
|
|
142
|
+
filename,
|
|
143
|
+
contentType: /content-type:\s*([^\r\n]+)/i.exec(head)?.[1]?.trim() || 'application/octet-stream',
|
|
144
|
+
data: body,
|
|
145
|
+
};
|
|
146
|
+
if (name) files[name] = file;
|
|
147
|
+
} else if (name) fields[name] = body.toString();
|
|
148
|
+
}
|
|
149
|
+
i = next;
|
|
150
|
+
}
|
|
151
|
+
resolve({ fields, file, files });
|
|
152
|
+
} catch (e) { reject(e); }
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
@@ -31,7 +31,7 @@ export { pollParams } from './body.mjs';
|
|
|
31
31
|
export { attachmentType, extensionFor } from './media.mjs';
|
|
32
32
|
|
|
33
33
|
export class MastoApi {
|
|
34
|
-
constructor({ agent, log = console.log, allowed = null, scheme = null, embedded = false,
|
|
34
|
+
constructor({ agent, log = console.log, allowed = null, scheme = null, embedded = false, throughDoor = null,
|
|
35
35
|
mount = '', streaming = true, webPush = true, scheduling = true }) {
|
|
36
36
|
this.agent = agent;
|
|
37
37
|
// The path this identity's surface answers under, when it shares its origin
|
|
@@ -45,6 +45,9 @@ export class MastoApi {
|
|
|
45
45
|
this.embedded = embedded;
|
|
46
46
|
this.log = log;
|
|
47
47
|
this.allowed = allowed; // authorities a redirect_uri may name
|
|
48
|
+
// Whether a request already passed the owner's door (its key or cookie):
|
|
49
|
+
// an owner who opened the management page is the owner at the sign-in too.
|
|
50
|
+
this.throughDoor = throughDoor;
|
|
48
51
|
// The scheme this identity is reached on, when the socket cannot say —
|
|
49
52
|
// a server behind a TLS proxy terminates cleartext and still is https.
|
|
50
53
|
this.scheme = scheme;
|
|
@@ -183,6 +186,7 @@ export class MastoApi {
|
|
|
183
186
|
acctAction(...a) { return render.acctAction(this, ...a); }
|
|
184
187
|
acctReply(...a) { return render.acctReply(this, ...a); }
|
|
185
188
|
status(...a) { return render.status(this, ...a); }
|
|
189
|
+
quoteApproval(...a) { return render.quoteApproval(this, ...a); }
|
|
186
190
|
filtersFor(...a) { return render.filtersFor(this, ...a); }
|
|
187
191
|
lookup(...a) { return render.lookup(this, ...a); }
|
|
188
192
|
statusOrBoost(...a) { return render.statusOrBoost(this, ...a); }
|
|
@@ -14,7 +14,7 @@ const STUBS = new Map(Object.entries({
|
|
|
14
14
|
'/api/v1/custom_emojis': [],
|
|
15
15
|
'/api/v1/announcements': [],
|
|
16
16
|
'/api/v1/instance/peers': [],
|
|
17
|
-
'/api/v1/trends/
|
|
17
|
+
'/api/v1/trends/links': [],
|
|
18
18
|
'/api/v2/suggestions': [],
|
|
19
19
|
'/api/v1/preferences': {},
|
|
20
20
|
})); // followed_tags is served live from the tag feed, not stubbed
|
|
@@ -49,9 +49,9 @@ export function instanceTitle(api) {
|
|
|
49
49
|
// A Mastodon Tag object. The client reads `following` in its Followed
|
|
50
50
|
// Hashtags view and toggles it with the follow/unfollow endpoints. No usage
|
|
51
51
|
// history — a single-actor instance has no firehose stats to report.
|
|
52
|
-
export function tagObject(api, name, following, req) {
|
|
52
|
+
export function tagObject(api, name, following, req, history = []) {
|
|
53
53
|
const host = req?.headers?.host || api.host;
|
|
54
|
-
return { name, url: `https://${host}/tags/${name}`, history
|
|
54
|
+
return { name, url: `https://${host}/tags/${name}`, history, following: !!following };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function instanceBlurb(api) {
|
|
@@ -59,6 +59,12 @@ export function instanceBlurb(api) {
|
|
|
59
59
|
return `Solid pod ActivityPub ${kind}`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// The Mastodon API version this facade answers to. Clients read it for what
|
|
63
|
+
// the server can do beyond the version string: grouped notifications from 2,
|
|
64
|
+
// a filter's blur action from 5, quote posts from 7. Everything up to 7 is
|
|
65
|
+
// served here; nothing past it is claimed.
|
|
66
|
+
const API_VERSIONS = { mastodon: 7 };
|
|
67
|
+
|
|
62
68
|
export async function handle(api, ctx) {
|
|
63
69
|
const { req, res, pathname, url, send } = ctx; // eslint-disable-line no-unused-vars
|
|
64
70
|
|
|
@@ -68,6 +74,7 @@ export async function handle(api, ctx) {
|
|
|
68
74
|
return send(200, {
|
|
69
75
|
uri: api.host, title: api.instanceTitle(), short_description: api.instanceBlurb(),
|
|
70
76
|
description: api.instanceBlurb(), email: '', version: '4.2.0 (compatible; fedipod)',
|
|
77
|
+
api_versions: API_VERSIONS,
|
|
71
78
|
urls: su ? { streaming_api: su } : {},
|
|
72
79
|
stats: { user_count: 1, status_count: api.store.countStatuses(), domain_count: 1 },
|
|
73
80
|
languages: ['en'], registrations: false, approval_required: false, invites_enabled: false,
|
|
@@ -79,6 +86,7 @@ export async function handle(api, ctx) {
|
|
|
79
86
|
const su = api.streamingUrl(req);
|
|
80
87
|
return send(200, {
|
|
81
88
|
domain: api.host, title: api.instanceTitle(), version: '4.2.0 (compatible; fedipod)',
|
|
89
|
+
api_versions: API_VERSIONS,
|
|
82
90
|
source_url: 'https://github.com/jeff-zucker/FediPod', description: api.instanceBlurb(),
|
|
83
91
|
usage: { users: { active_month: 1 } },
|
|
84
92
|
thumbnail: { url: TRANSPARENT_PNG },
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// media.mjs — an upload on its way to the pod's media container: what an
|
|
2
|
-
// attachment is allowed to be
|
|
3
|
-
//
|
|
2
|
+
// attachment is allowed to be and the two media endpoints. The multipart
|
|
3
|
+
// reader moved to body.mjs, which is where a client's body is read.
|
|
4
4
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import * as podMedia from '../../pod/media.mjs';
|
|
7
|
-
import { readBody } from './body.mjs';
|
|
7
|
+
import { readBody, readMultipart } from './body.mjs';
|
|
8
8
|
|
|
9
9
|
// What an attachment is allowed to BE. Anything else is stored as bytes, which
|
|
10
10
|
// a browser downloads rather than runs.
|
|
@@ -34,60 +34,6 @@ export function extensionFor(mediaType, filename = '') {
|
|
|
34
34
|
return given && (given === sub || sub.startsWith(given) || given.startsWith(sub)) ? given : (sub || 'bin');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Minimal multipart/form-data reader for media uploads: string fields plus
|
|
38
|
-
// at most one file part (Mastodon's media endpoints send exactly one).
|
|
39
|
-
export function readMultipart(req, limit = 12e6) {
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
const chunks = [];
|
|
42
|
-
let n = 0;
|
|
43
|
-
req.on('data', c => {
|
|
44
|
-
n += c.length;
|
|
45
|
-
if (n > limit) { reject(new Error('upload too large')); req.destroy(); return; }
|
|
46
|
-
chunks.push(c);
|
|
47
|
-
});
|
|
48
|
-
req.on('error', reject);
|
|
49
|
-
req.on('end', () => {
|
|
50
|
-
try {
|
|
51
|
-
const m = /boundary=(?:"([^"]+)"|([^;]+))/.exec(String(req.headers['content-type'] || ''));
|
|
52
|
-
if (!m) return resolve({ fields: {}, file: null });
|
|
53
|
-
const buf = Buffer.concat(chunks);
|
|
54
|
-
const boundary = Buffer.from('--' + (m[1] || m[2]).trim());
|
|
55
|
-
const fields = {};
|
|
56
|
-
// `file` is the last one seen, which is what the single-file media
|
|
57
|
-
// upload wants. `files` keys them by field name, because the profile
|
|
58
|
-
// editor submits an avatar and a header in one request.
|
|
59
|
-
const files = {};
|
|
60
|
-
let file = null;
|
|
61
|
-
let i = buf.indexOf(boundary);
|
|
62
|
-
while (i >= 0) {
|
|
63
|
-
const start = i + boundary.length;
|
|
64
|
-
if (buf.slice(start, start + 2).toString() === '--') break;
|
|
65
|
-
const next = buf.indexOf(boundary, start);
|
|
66
|
-
if (next < 0) break;
|
|
67
|
-
const part = buf.slice(start + 2, next - 2); // strip the CRLFs framing the part
|
|
68
|
-
const sep = part.indexOf('\r\n\r\n');
|
|
69
|
-
if (sep >= 0) {
|
|
70
|
-
const head = part.slice(0, sep).toString();
|
|
71
|
-
const body = part.slice(sep + 4);
|
|
72
|
-
const name = /name="([^"]*)"/.exec(head)?.[1];
|
|
73
|
-
const filename = /filename="([^"]*)"/.exec(head)?.[1];
|
|
74
|
-
if (filename !== undefined) {
|
|
75
|
-
file = {
|
|
76
|
-
filename,
|
|
77
|
-
contentType: /content-type:\s*([^\r\n]+)/i.exec(head)?.[1]?.trim() || 'application/octet-stream',
|
|
78
|
-
data: body,
|
|
79
|
-
};
|
|
80
|
-
if (name) files[name] = file;
|
|
81
|
-
} else if (name) fields[name] = body.toString();
|
|
82
|
-
}
|
|
83
|
-
i = next;
|
|
84
|
-
}
|
|
85
|
-
resolve({ fields, file, files });
|
|
86
|
-
} catch (e) { reject(e); }
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
37
|
export async function handle(api, ctx) {
|
|
92
38
|
const { req, res, pathname, url, send } = ctx; // eslint-disable-line no-unused-vars
|
|
93
39
|
|
|
@@ -42,6 +42,42 @@ const escapeHtml = (s) => String(s).replace(/[&<>"']/g, c =>
|
|
|
42
42
|
const parseRedirects = (v) => (Array.isArray(v) ? v : String(v || '').split(/\s+/))
|
|
43
43
|
.map(s => s.trim()).filter(Boolean);
|
|
44
44
|
|
|
45
|
+
// The sign-in page for an account whose owner signs in at their pod: no
|
|
46
|
+
// password of ours exists, and none is asked for. The page carries the
|
|
47
|
+
// client's request in its address, signs the owner in at the pod with the
|
|
48
|
+
// fediverse-account library, and proves that sign-in back to /oauth/authorize.
|
|
49
|
+
function sendPodSigninPage(res, client, webId, mount = '') {
|
|
50
|
+
let asking = '';
|
|
51
|
+
if (client && (client.name || client.redirect)) {
|
|
52
|
+
let where = '';
|
|
53
|
+
try { where = client.redirect ? new URL(client.redirect).host : ''; } catch { /* oob or blank */ }
|
|
54
|
+
const who = client.name ? escapeHtml(client.name) : (where ? escapeHtml(where) : 'A client');
|
|
55
|
+
asking = `<p><strong>${who}</strong> is asking to use your account`
|
|
56
|
+
+ `${where ? `, and will be sent back to <code>${escapeHtml(where)}</code>` : ''}.</p>`;
|
|
57
|
+
}
|
|
58
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' });
|
|
59
|
+
res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
60
|
+
<title>Sign in with your pod</title>
|
|
61
|
+
<style>:root{color-scheme:light dark;--heading:#1a4f8a;--btn:#3d6b35;--line:#9a9a9a}
|
|
62
|
+
body{font:20px Arial,Helvetica,sans-serif;max-width:28rem;margin:12vh auto;padding:0 1rem}
|
|
63
|
+
h1{color:var(--heading);font-size:1.5rem}
|
|
64
|
+
code{word-break:break-all;background:rgba(120,120,120,.12);padding:.05em .35em;border-radius:4px}
|
|
65
|
+
button{font:inherit;padding:.6rem 1.1rem;background:var(--btn);color:#fff;border:none;border-radius:9px;cursor:pointer}
|
|
66
|
+
button[hidden]{display:none}
|
|
67
|
+
#signin-status{min-height:1.5em;padding:.6rem .8rem;border:1px solid var(--line);border-radius:8px;margin-top:1rem}
|
|
68
|
+
@media (prefers-color-scheme:dark){:root{--heading:#7fb3e8;--btn:#5a8a4f}}</style></head><body>
|
|
69
|
+
<main>
|
|
70
|
+
<h1>Sign in with your pod</h1>
|
|
71
|
+
${asking}
|
|
72
|
+
<p>This account belongs to <a id="webid" href="${escapeHtml(webId)}">${escapeHtml(webId)}</a>. Sign in at that pod to allow it.</p>
|
|
73
|
+
<p><button type="button" id="signin" hidden>Sign in with your pod</button></p>
|
|
74
|
+
<p id="signin-status" role="status" aria-live="polite"></p>
|
|
75
|
+
</main>
|
|
76
|
+
<script type="module" src="${escapeHtml(mount)}/oauth/session/signin.mjs"></script>
|
|
77
|
+
</body></html>`);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
45
81
|
function sendLoginForm(res, params, error = '', client = null, status = null, headers = {}) {
|
|
46
82
|
const hidden = [...params.entries()].filter(([k]) => k !== 'password')
|
|
47
83
|
.map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}">`).join('\n');
|
|
@@ -378,6 +414,21 @@ export async function handle(api, ctx) {
|
|
|
378
414
|
let body = null;
|
|
379
415
|
if (req.method === 'POST') { body = await readBody(req); params = new URLSearchParams(body); }
|
|
380
416
|
const redirect = params.get('redirect_uri') || '';
|
|
417
|
+
// The owner's pod sign-in, proved: a Solid-OIDC token with its DPoP proof,
|
|
418
|
+
// from the WebID this account belongs to. It stands in for the password
|
|
419
|
+
// everywhere below.
|
|
420
|
+
let proved = false;
|
|
421
|
+
if (req.method === 'POST' && req.headers.authorization && req.headers.dpop && api.podAuth) {
|
|
422
|
+
const proof = await api.podAuth(req, pathname);
|
|
423
|
+
if (!proof.ok) return send(proof.status || 401, { error: proof.error });
|
|
424
|
+
proved = true;
|
|
425
|
+
}
|
|
426
|
+
const ownerWebId = api.ownerWebId?.() || null;
|
|
427
|
+
// The way back from the pod's login: no client named, only the pod's
|
|
428
|
+
// code. The page finishes the sign-in and returns to the client's request.
|
|
429
|
+
if (req.method === 'GET' && !params.get('client_id') && params.get('code') && params.get('state') && ownerWebId) {
|
|
430
|
+
return sendPodSigninPage(res, null, ownerWebId, api.mount || '');
|
|
431
|
+
}
|
|
381
432
|
const app = api.findApp(params.get('client_id') || '');
|
|
382
433
|
// A client that published its own metadata document needs no
|
|
383
434
|
// registration here: the document is its name and says where it may be
|
|
@@ -419,14 +470,16 @@ export async function handle(api, ctx) {
|
|
|
419
470
|
// port is the owner" is a statement about the machine rather than
|
|
420
471
|
// about a web page. Anything pointing elsewhere has to be approved by
|
|
421
472
|
// somebody who knows the password.
|
|
422
|
-
|
|
473
|
+
const client = { name: app?.name || doc?.name || null, redirect, scope: params.get('scope') || 'read' };
|
|
474
|
+
if (external && !proved && !api.store.getConfig()?.uiPassword && !api.redirectAllowed(redirect)) {
|
|
475
|
+
// With a pod to sign in at, the owner approves the client there.
|
|
476
|
+
if (req.method === 'GET' && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || '');
|
|
423
477
|
api.log(`authorize refused: no UI password, and "${redirect}" is not an address of this agent`);
|
|
424
478
|
return send(403, {
|
|
425
479
|
error: 'this client asks to be sent somewhere other than this agent, and no password is '
|
|
426
480
|
+ 'set to approve that with. Run `fedipod passwd` and try again.',
|
|
427
481
|
});
|
|
428
482
|
}
|
|
429
|
-
const client = { name: app?.name || doc?.name || null, redirect, scope: params.get('scope') || 'read' };
|
|
430
483
|
if (app) {
|
|
431
484
|
if (!app.redirectUris.includes(redirect)) {
|
|
432
485
|
api.log(`authorize refused: redirect_uri "${redirect}" not registered for ${app.clientId}`);
|
|
@@ -448,7 +501,7 @@ export async function handle(api, ctx) {
|
|
|
448
501
|
api.log(`authorize refused: redirect_uri "${redirect}" is not this agent`);
|
|
449
502
|
return send(400, { error: 'redirect_uri must be an address of this agent' });
|
|
450
503
|
}
|
|
451
|
-
if (req.method === 'POST') {
|
|
504
|
+
if (req.method === 'POST' && !proved) {
|
|
452
505
|
if (api.rateLimited()) {
|
|
453
506
|
api.log('authorize rate limited');
|
|
454
507
|
// 429, not 401: a client that reads this as a wrong password will
|
|
@@ -460,11 +513,13 @@ export async function handle(api, ctx) {
|
|
|
460
513
|
if (!pw || !checkPassword(pw, body.password || '')) {
|
|
461
514
|
return sendLoginForm(res, params, 'wrong password — try again', client);
|
|
462
515
|
}
|
|
463
|
-
} else if (pw) {
|
|
516
|
+
} else if (pw && !proved) {
|
|
464
517
|
// The login/approve screen names what is asking before the owner types
|
|
465
518
|
// the password — a cross-site client cannot forge past that.
|
|
466
519
|
return sendLoginForm(res, params, '', client);
|
|
467
|
-
} else if (api.allowed && !api.allowed.isLocalRequest(req)) {
|
|
520
|
+
} else if (!proved && api.allowed && !api.allowed.isLocalRequest(req) && !api.throughDoor?.(req)) {
|
|
521
|
+
// With a pod to sign in at, the owner does that instead of a password.
|
|
522
|
+
if (req.method === 'GET' && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || '');
|
|
468
523
|
// No password set, and this request did not come from this machine.
|
|
469
524
|
//
|
|
470
525
|
// The instant-authorize path is honest theatre on loopback: whoever can
|
|
@@ -477,10 +532,9 @@ export async function handle(api, ctx) {
|
|
|
477
532
|
// dispatched before that check ever runs, so it needs its own.
|
|
478
533
|
api.log(`authorize refused: no UI password, and "${req.headers.host}" is not this machine`);
|
|
479
534
|
return send(403, {
|
|
480
|
-
error:
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
: 'run `fedipod passwd` before logging in over that address'),
|
|
535
|
+
error: api.embedded
|
|
536
|
+
? 'Open your account\'s management page first, then sign in from there.'
|
|
537
|
+
: 'Signing in from another machine needs a password. On the machine that runs your account, run: fedipod passwd',
|
|
484
538
|
});
|
|
485
539
|
}
|
|
486
540
|
// External clients get a bound code; the local flow keeps code==token.
|
|
@@ -493,6 +547,9 @@ export async function handle(api, ctx) {
|
|
|
493
547
|
const target = new URL(redirect);
|
|
494
548
|
target.searchParams.set('code', code);
|
|
495
549
|
if (params.get('state')) target.searchParams.set('state', params.get('state'));
|
|
550
|
+
// A proved sign-in came from the page's own request, which cannot follow
|
|
551
|
+
// a redirect to a client's address; the page is told where to go.
|
|
552
|
+
if (proved) return send(200, { redirect: target.href });
|
|
496
553
|
res.writeHead(302, { location: target.href });
|
|
497
554
|
res.end();
|
|
498
555
|
return true;
|
|
@@ -9,9 +9,25 @@ import { sanitizeHtml, followsNeedApproval, publicHandle } from '../../core/wire
|
|
|
9
9
|
import { profileUrl, postUrl } from '../../connections/bskyfeed.mjs';
|
|
10
10
|
import { htmlToText } from './body.mjs';
|
|
11
11
|
|
|
12
|
-
// 1x1 transparent PNG — placeholder
|
|
12
|
+
// 1x1 transparent PNG — placeholder header, and the instance thumbnail. A
|
|
13
|
+
// banner nobody set should take up no room and show nothing; an avatar is a
|
|
14
|
+
// different matter (below).
|
|
13
15
|
export const TRANSPARENT_PNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
|
14
16
|
|
|
17
|
+
// The avatar for an account with no icon of its own: a head and shoulders on a
|
|
18
|
+
// slate square, 64x64. It used to be the transparent pixel above, which is
|
|
19
|
+
// correct JSON and invisible on screen — in Sengi's left bar the account button
|
|
20
|
+
// (the one control that opens the account) rendered as an empty square, so
|
|
21
|
+
// clicking it looked like nothing happening.
|
|
22
|
+
//
|
|
23
|
+
// A PNG rather than the SVG this was first written as, and not by preference:
|
|
24
|
+
// Angular sanitizes every `<img src>` binding and trusts raster data URIs only,
|
|
25
|
+
// because an SVG can carry script. An `svg+xml` one comes back out of it as
|
|
26
|
+
// `unsafe:data:image/svg+xml` and draws nothing. Sengi is Angular.
|
|
27
|
+
//
|
|
28
|
+
// Redraw it with `node claude/scratch/make-default-avatar.mjs`.
|
|
29
|
+
export const DEFAULT_AVATAR = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAABd0lEQVR42u3ZoW4CQRDG8Xk6NBLbVCJ5gBpMTQ2iBlFR29RXNsFhz1JHXV9hydfkEkoobW5n2F34i0kIHMfN73ZvdwYbTabpmsMAAAAAAAAAAAAAAAAAAACiY3wzS/ePz+n1bZW6zTZ9fH59h17rPX2mYy4OQEktnl5+JP1b6Bgdey6IcIDb2Ty9r7s/Ez8MfUffbRpACfznrp8aDdEIFjnsh9z5YyMhcjqEAWge5ybfh87VFIDuWM7QPzYVokZBCICWM6/k+9A5mwHQmu4NoHM2A+A5/PenQTMA3sn3AQBTgIcgyyAbIbbCFEOUwzREaInRFKUtDgAAFQLcPSxDCqH9Z4V+o0oAXVhU4ofhiWCtJe+NYKV3e6V3idkAHvv9nDqhKIBnxVeqUrRaav5SvQKrqetTols0GGDdbaoB0LWcFUBP31qS72PoimCtD//caWC1tL1Ltc2ttbXfe08wCKC25HP+OgMAAAAAAAAAAAAAAAAArhNgB1PE/x/JOiuNAAAAAElFTkSuQmCC';
|
|
30
|
+
|
|
15
31
|
const selfIcon = (api, self, cached) =>
|
|
16
32
|
(self ? api.store.getConfig()?.icon : null) || cached.icon;
|
|
17
33
|
|
|
@@ -49,8 +65,8 @@ export function account(api, actorUrl, { selfAcct } = {}) {
|
|
|
49
65
|
created_at: (self && api.store.getConfig()?.createdAt) || '2026-01-01T00:00:00.000Z',
|
|
50
66
|
note: (self ? api.store.getConfig()?.summary : cached.summary) || '',
|
|
51
67
|
url: actorUrl, uri: actorUrl,
|
|
52
|
-
avatar: selfIcon(api, self, cached) ||
|
|
53
|
-
avatar_static: selfIcon(api, self, cached) ||
|
|
68
|
+
avatar: selfIcon(api, self, cached) || DEFAULT_AVATAR,
|
|
69
|
+
avatar_static: selfIcon(api, self, cached) || DEFAULT_AVATAR,
|
|
54
70
|
header: (self ? api.store.getConfig()?.image : null) || TRANSPARENT_PNG,
|
|
55
71
|
header_static: (self ? api.store.getConfig()?.image : null) || TRANSPARENT_PNG,
|
|
56
72
|
// A remote actor's counts are whatever its own collections said when we
|
|
@@ -217,7 +233,37 @@ export async function acctReply(api, send, body, parent, visibility) {
|
|
|
217
233
|
} catch (e) { return send(422, { error: e.message }); }
|
|
218
234
|
}
|
|
219
235
|
|
|
220
|
-
|
|
236
|
+
// The post a status quotes, and how the quote stands: `pending` until the
|
|
237
|
+
// quoted author allows it, `accepted` once they have, `rejected` if they
|
|
238
|
+
// refused. The quoted post is shown whenever it is held — a reader wants to
|
|
239
|
+
// see what was quoted, whatever the state says. One level deep: a quote
|
|
240
|
+
// inside a quote is named by id, not rendered, so the shape stays finite.
|
|
241
|
+
export function quoteJson(api, s, { all, depth = 0 } = {}) {
|
|
242
|
+
if (!s.quote) return null;
|
|
243
|
+
const state = s.quoteState || 'pending';
|
|
244
|
+
const quoted = state !== 'rejected' && (all || api.store.getStatuses()).find(x => x.noteId === s.quote);
|
|
245
|
+
if (depth > 0) return { state, quoted_status_id: quoted ? api.store.idFor(quoted.noteId) : null };
|
|
246
|
+
return { state, quoted_status: quoted ? api.status(quoted, { all, depth: depth + 1 }) : null };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Who may quote a post, in the client's words, and what that means for this
|
|
250
|
+
// account. Our own posts: anyone, when the world can read it; nobody
|
|
251
|
+
// otherwise. Another's: whatever policy the post carried — `unknown` when it
|
|
252
|
+
// carried none, which is what a client hides the quote button on.
|
|
253
|
+
export function quoteApproval(api, s) {
|
|
254
|
+
if (s.actor === api.urls?.actor) {
|
|
255
|
+
const open = !s.visibility || s.visibility === 'public' || s.visibility === 'unlisted';
|
|
256
|
+
return { automatic: open ? ['public'] : [], manual: [], current_user: open ? 'automatic' : 'denied' };
|
|
257
|
+
}
|
|
258
|
+
const p = s.quotePolicy;
|
|
259
|
+
if (!p) return { automatic: [], manual: [], current_user: 'unknown' };
|
|
260
|
+
const follows = () => api.store.getContacts().following.some(f => f.actor === s.actor && f.accepted);
|
|
261
|
+
const may = (list) => list.includes('public') || (list.includes('followers') && follows());
|
|
262
|
+
const current = may(p.automatic) ? 'automatic' : may(p.manual) ? 'manual' : 'denied';
|
|
263
|
+
return { automatic: p.automatic, manual: p.manual, current_user: current };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function status(api, s, { all, depth = 0 } = {}) {
|
|
221
267
|
const replies = (all || api.store.getStatuses()).filter(x => x.inReplyTo === s.noteId).length;
|
|
222
268
|
return {
|
|
223
269
|
id: api.store.idFor(s.noteId),
|
|
@@ -258,6 +304,8 @@ export function status(api, s, { all } = {}) {
|
|
|
258
304
|
shortcode: e.shortcode, url: e.url, static_url: e.url, visible_in_picker: false,
|
|
259
305
|
})),
|
|
260
306
|
card: null, poll: s.poll ? api.pollJson(s) : null,
|
|
307
|
+
quote: quoteJson(api, s, { all, depth }),
|
|
308
|
+
quote_approval: quoteApproval(api, s),
|
|
261
309
|
// What a filter matched, if any. Mastodon's clients read this and do the
|
|
262
310
|
// hiding or warning; they do NOT match keywords themselves — Phanpy does
|
|
263
311
|
// not — so filters that were stored and served but never applied were a
|
|
@@ -346,6 +394,7 @@ export function pushNotify(api, n) {
|
|
|
346
394
|
mention: 'mentioned you', favourite: 'favourited your post',
|
|
347
395
|
reblog: 'boosted your post', follow: 'followed you',
|
|
348
396
|
'follow-request': 'asked to follow you', move: 'moved account',
|
|
397
|
+
reaction: `reacted ${n.emoji || ''} to your post`, quote: 'quoted your post',
|
|
349
398
|
};
|
|
350
399
|
const s = n.noteId && api.store.getStatuses().find(x => x.noteId === n.noteId);
|
|
351
400
|
return api.push.notify(n, {
|
|
@@ -419,10 +468,18 @@ export function relationship(api, actorUrl) {
|
|
|
419
468
|
// and a client that does not know the type shows "Unknown notification type"
|
|
420
469
|
// — which is what every Mastodon client did with a follow request here. The
|
|
421
470
|
// stored spelling is left alone so existing state keeps reading.
|
|
422
|
-
|
|
471
|
+
// Mastodon's spellings for what is stored here. A reaction is what Pleroma's
|
|
472
|
+
// API calls an emoji reaction, and what clients render as one.
|
|
473
|
+
export function notificationType(api, t) {
|
|
474
|
+
return t === 'follow-request' ? 'follow_request' : t === 'reaction' ? 'emoji_reaction' : t;
|
|
475
|
+
}
|
|
423
476
|
|
|
424
477
|
export function notification(api, n) {
|
|
425
478
|
const out = { id: n.id, type: api.notificationType(n.type), created_at: n.at, account: api.account(n.actor) };
|
|
479
|
+
if (n.type === 'reaction') {
|
|
480
|
+
out.emoji = n.emoji;
|
|
481
|
+
if (n.url) out.emoji_url = n.url;
|
|
482
|
+
}
|
|
426
483
|
if (n.noteId) {
|
|
427
484
|
const s = api.store.getStatuses().find(x => x.noteId === n.noteId);
|
|
428
485
|
if (s) out.status = api.status(s);
|
|
@@ -42,10 +42,28 @@ export async function handle(api, ctx) {
|
|
|
42
42
|
const media = api.store.getMedia();
|
|
43
43
|
const attachments = mediaIds.map(id => media[id] && { id, ...media[id] }).filter(Boolean);
|
|
44
44
|
|
|
45
|
+
// A quote: the post named is one this account holds, and its author has
|
|
46
|
+
// said it may be quoted. The author's answer decides what the reader sees;
|
|
47
|
+
// the reasons a quote cannot even be asked for are the client's to show.
|
|
48
|
+
let quote = null;
|
|
49
|
+
if (body.quoted_status_id) {
|
|
50
|
+
const quotedUrl = api.store.urlFor(String(body.quoted_status_id));
|
|
51
|
+
const quoted = quotedUrl && api.store.getStatuses().find(x => x.noteId === quotedUrl);
|
|
52
|
+
if (!quoted) return send(404, { error: 'Record not found' });
|
|
53
|
+
if (['bsky', 'acct'].includes(quoted.kind)) return send(422, { error: 'that post cannot be quoted from this account' });
|
|
54
|
+
if (visibility === 'private' || visibility === 'direct') return send(422, { error: 'a quote post is public or unlisted' });
|
|
55
|
+
const may = api.quoteApproval(quoted).current_user;
|
|
56
|
+
if (may === 'denied') return send(422, { error: 'the author of that post does not allow quotes' });
|
|
57
|
+
if (may === 'unknown') return send(422, { error: 'the author of that post has not said whether it may be quoted' });
|
|
58
|
+
if (body.scheduled_at) return send(422, { error: 'a quote cannot be scheduled' });
|
|
59
|
+
quote = { id: quoted.noteId, actor: quoted.actor };
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
const asking = pollParams(body);
|
|
46
63
|
if (asking) {
|
|
47
64
|
// Mastodon's own rules, stated here rather than discovered inside the
|
|
48
65
|
// publisher, so a client gets the reason back on the request it made.
|
|
66
|
+
if (quote) return send(422, { error: 'a poll cannot quote a post' });
|
|
49
67
|
if (attachments.length) return send(422, { error: 'a poll cannot carry media' });
|
|
50
68
|
if (body.scheduled_at) return send(422, { error: 'a poll cannot be scheduled' });
|
|
51
69
|
if (asking.options.length < 2) return send(422, { error: 'a poll needs at least two options' });
|
|
@@ -99,7 +117,7 @@ export async function handle(api, ctx) {
|
|
|
99
117
|
let note;
|
|
100
118
|
try {
|
|
101
119
|
note = await api.agent.publisher.publishNote(body.status,
|
|
102
|
-
{ inReplyTo, attachments, visibility, spoilerText, sensitive });
|
|
120
|
+
{ inReplyTo, attachments, visibility, spoilerText, sensitive, quote });
|
|
103
121
|
} catch (e) {
|
|
104
122
|
// A direct message nobody could be found for: the sender's mistake to
|
|
105
123
|
// fix, said in the client, not a fault of the server.
|
|
@@ -110,6 +128,12 @@ export async function handle(api, ctx) {
|
|
|
110
128
|
return send(200, api.status(s));
|
|
111
129
|
}
|
|
112
130
|
|
|
131
|
+
// Who may quote a post is the same for every post here — anyone, when the
|
|
132
|
+
// world can read it — and is not set per post.
|
|
133
|
+
if (/^\/api\/v1\/statuses\/[a-f0-9]+\/interaction_policy$/.test(pathname) && req.method === 'PUT') {
|
|
134
|
+
return send(422, { error: 'the quote policy is the same for every post here: anyone may quote a public or unlisted post, nobody a followers-only or direct one' });
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
// The composer reads the raw text back before an edit.
|
|
114
138
|
const mSource = /^\/api\/v1\/statuses\/([a-f0-9]+)\/source$/.exec(pathname);
|
|
115
139
|
if (mSource && req.method === 'GET') {
|