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
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// rejection reason or nothing.
|
|
4
4
|
|
|
5
5
|
import { dropFollower } from '../store.mjs';
|
|
6
|
-
import { emojisOf, pollOf, isContentType, trimActivity } from './activity.mjs';
|
|
6
|
+
import { emojisOf, pollOf, isContentType, trimActivity, reactionOf, quoteOf, quotePolicyOf, authorOf } from './activity.mjs';
|
|
7
|
+
|
|
8
|
+
const idOf = (v) => (typeof v === 'string' ? v : v?.id);
|
|
7
9
|
|
|
8
10
|
// §7.6 Add / §7.9 Remove. The side effect would be to add or remove the object
|
|
9
11
|
// to/from the collection named in `target` — but only a collection we own AND
|
|
@@ -104,7 +106,7 @@ export async function onFollow(intake, activity, actor, { trusted = false } = {}
|
|
|
104
106
|
|
|
105
107
|
export async function onUndo(intake, activity, actor, { trusted = false } = {}) {
|
|
106
108
|
const inner = activity.object && typeof activity.object === 'object' ? activity.object : null;
|
|
107
|
-
if (inner?.type === 'Like' || inner?.type === 'Announce') return onUndoReaction(intake, inner, actor, { trusted });
|
|
109
|
+
if (inner?.type === 'Like' || inner?.type === 'Announce' || inner?.type === 'EmojiReact') return onUndoReaction(intake, inner, actor, { trusted });
|
|
108
110
|
// AS2 allows `object` to be a bare IRI, and that IRI is exactly the Follow
|
|
109
111
|
// id we stored. Reading `.type` off a string gives undefined, so the whole
|
|
110
112
|
// Undo was dropped — silently, since handle() reads that as handled, so no
|
|
@@ -290,6 +292,15 @@ export async function onUndoReaction(intake, inner, actor, { trusted = false } =
|
|
|
290
292
|
const objectId = typeof target === 'string' ? target : target?.id;
|
|
291
293
|
if (!objectId) return;
|
|
292
294
|
if (objectId.startsWith(intake.urls.notes)) {
|
|
295
|
+
// An emoji taken back: the one reaction it names, by that sender.
|
|
296
|
+
const reaction = reactionOf(inner);
|
|
297
|
+
if (reaction) {
|
|
298
|
+
const gone = intake.store.removeNotifications(n => n.type === 'reaction' && n.actor === actor
|
|
299
|
+
&& n.noteId === objectId && n.emoji === reaction.emoji);
|
|
300
|
+
if (gone) intake.log(`reaction ${reaction.emoji} withdrawn by ${actor} on ${objectId}`);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (inner.type === 'EmojiReact') return;
|
|
293
304
|
const type = inner.type === 'Like' ? 'favourite' : 'reblog';
|
|
294
305
|
const gone = intake.store.removeNotifications(n => n.type === type && n.actor === actor && n.noteId === objectId);
|
|
295
306
|
if (gone) intake.log(`${inner.type} withdrawn by ${actor} on ${objectId}`);
|
|
@@ -369,6 +380,9 @@ export async function onDelete(intake, activity, actor) {
|
|
|
369
380
|
}
|
|
370
381
|
const s = intake.store.getStatuses().find(x => x.noteId === objectId);
|
|
371
382
|
if (s) await intake.forget(s);
|
|
383
|
+
if (s && intake.onCarriedGone) {
|
|
384
|
+
await intake.onCarriedGone({ noteId: objectId, actor }).catch(e => intake.log(`carried gone ${objectId}: ${e.message}`));
|
|
385
|
+
}
|
|
372
386
|
intake.log(`deleted upstream: ${objectId}`);
|
|
373
387
|
}
|
|
374
388
|
|
|
@@ -401,9 +415,16 @@ export async function onUpdate(intake, activity, actor) {
|
|
|
401
415
|
const attachments = attachmentsOf(note);
|
|
402
416
|
const freshPoll = pollOf(note);
|
|
403
417
|
const freshEmojis = emojisOf(note);
|
|
418
|
+
// A quote's authorization arrives on an Update, once the quoted author
|
|
419
|
+
// has answered: the quote is checked again, and the policy follows the edit.
|
|
420
|
+
const quote = quoteOf(note);
|
|
421
|
+
const quoteState = quote ? await intake.quoteStateOf(note, quote) : null;
|
|
422
|
+
const quotePolicy = quotePolicyOf(note, s.actor);
|
|
404
423
|
intake.store.updateStatus(objectId, {
|
|
405
424
|
content, ...(attachments.length ? { attachments } : {}),
|
|
406
425
|
emojis: freshEmojis.length ? freshEmojis : undefined,
|
|
426
|
+
quote: quote || undefined, quoteState: quoteState || undefined,
|
|
427
|
+
quotePolicy: quotePolicy || undefined,
|
|
407
428
|
// The edit's own stamp when the note carries one; tallies and the
|
|
408
429
|
// content warning follow the edit too. A poll refresh keeps our vote.
|
|
409
430
|
editedAt: note.updated || new Date().toISOString(),
|
|
@@ -412,6 +433,9 @@ export async function onUpdate(intake, activity, actor) {
|
|
|
412
433
|
poll: { ...freshPoll, voted: !!s.poll?.voted, ownVotes: s.poll?.ownVotes || [] },
|
|
413
434
|
} : {}),
|
|
414
435
|
});
|
|
436
|
+
if (intake.onCarriedEdit) {
|
|
437
|
+
await intake.onCarriedEdit({ noteId: objectId, actor, note }).catch(e => intake.log(`carried edit ${objectId}: ${e.message}`));
|
|
438
|
+
}
|
|
415
439
|
intake.log(`edited upstream: ${objectId}`);
|
|
416
440
|
}
|
|
417
441
|
|
|
@@ -427,6 +451,8 @@ export async function onUpdate(intake, activity, actor) {
|
|
|
427
451
|
// the timeline goes quiet. `followActivity` is stored by followActor
|
|
428
452
|
// (lib/social.mjs) for exactly this kind of comparison.
|
|
429
453
|
export async function onReject(intake, activity, actor, { trusted = false } = {}) {
|
|
454
|
+
const asked = quoteAsked(intake, activity);
|
|
455
|
+
if (asked) return onQuoteAnswered(intake, asked, activity, actor, 'Reject');
|
|
430
456
|
if (activity.object?.type && activity.object.type !== 'Follow') return;
|
|
431
457
|
const contacts = intake.store.getContacts();
|
|
432
458
|
const rec = contacts.following.find(f => f.actor === actor);
|
|
@@ -475,6 +501,8 @@ export async function onMove(intake, activity, actor) {
|
|
|
475
501
|
}
|
|
476
502
|
|
|
477
503
|
export async function onAccept(intake, activity, actor, { trusted = false } = {}) {
|
|
504
|
+
const asked = quoteAsked(intake, activity);
|
|
505
|
+
if (asked) return onQuoteAnswered(intake, asked, activity, actor, 'Accept');
|
|
478
506
|
const contacts = intake.store.getContacts();
|
|
479
507
|
const rec = contacts.following.find(f => f.actor === actor);
|
|
480
508
|
// It has to answer the Follow we actually sent. followActor stores that
|
|
@@ -497,3 +525,109 @@ export async function onAccept(intake, activity, actor, { trusted = false } = {}
|
|
|
497
525
|
intake.log(`follow accepted by ${actor}`);
|
|
498
526
|
}
|
|
499
527
|
}
|
|
528
|
+
|
|
529
|
+
// --- quotes (FEP-044f) ---
|
|
530
|
+
|
|
531
|
+
// The post of ours whose quote request this Accept or Reject answers, if any.
|
|
532
|
+
// Matched on the request id we sent, which only the quoted author was given.
|
|
533
|
+
function quoteAsked(intake, activity) {
|
|
534
|
+
const named = idOf(activity.object);
|
|
535
|
+
if (!named) return null;
|
|
536
|
+
return intake.store.getStatuses().find(x => x.kind === 'post' && x.quoteRequest?.id === named) || null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// The quoted author's answer. A yes carries their authorization, a document
|
|
540
|
+
// at their origin naming our post and theirs — fetched and compared, not
|
|
541
|
+
// believed from the envelope; then the note says so to everyone who has it.
|
|
542
|
+
// A no takes the quote off the note.
|
|
543
|
+
async function onQuoteAnswered(intake, s, activity, actor, type) {
|
|
544
|
+
if (actor !== s.quoteRequest?.actor) {
|
|
545
|
+
intake.log(`${type} of our quote request from ${actor}, who is not ${s.quoteRequest?.actor} — ignored`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (type === 'Reject') {
|
|
549
|
+
if (s.quoteState === 'rejected') return;
|
|
550
|
+
await intake.publisher.withdrawQuote(s);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (s.quoteState === 'accepted') return;
|
|
554
|
+
const resultId = idOf(activity.result);
|
|
555
|
+
if (!resultId || !intake.sameIdentity(resultId, actor)) return `Accept of a quote request carries no authorization at ${actor}`;
|
|
556
|
+
const auth = await intake.fetchAP(resultId);
|
|
557
|
+
if (!auth) throw new Error(`cannot fetch quote authorization ${resultId} — will retry`);
|
|
558
|
+
const ok = auth.id === resultId && auth.type === 'QuoteAuthorization'
|
|
559
|
+
&& [].concat(auth.attributedTo || []).map(idOf).includes(actor)
|
|
560
|
+
&& idOf(auth.interactingObject) === s.noteId && idOf(auth.interactionTarget) === s.quote;
|
|
561
|
+
if (!ok) return `quote authorization ${resultId} does not say ${s.noteId} may quote ${s.quote}`;
|
|
562
|
+
await intake.publisher.authorizeQuote(s, resultId);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Someone asks to quote one of our posts. Their post is read at its origin,
|
|
566
|
+
// like every note that arrives — it has to be theirs, and has to quote ours.
|
|
567
|
+
// A post the world can read may be quoted by anyone; a followers-only or
|
|
568
|
+
// direct one by nobody. The answer is an Accept carrying our authorization,
|
|
569
|
+
// written beside the post, or a Reject. The quoting post is then kept the
|
|
570
|
+
// way a mention of ours is, under the one notification a quote raises.
|
|
571
|
+
export async function onQuoteRequest(intake, activity, actor) {
|
|
572
|
+
const objectId = idOf(activity.object);
|
|
573
|
+
const instrumentId = idOf(activity.instrument);
|
|
574
|
+
if (!objectId || !instrumentId) return 'QuoteRequest without object or instrument';
|
|
575
|
+
// A followers-only post lives in the private container; a follower may
|
|
576
|
+
// know its id and ask. It is ours, and the answer below is no.
|
|
577
|
+
const ours = objectId.startsWith(intake.urls.notes)
|
|
578
|
+
|| (intake.urls.privateNotes && objectId.startsWith(intake.urls.privateNotes));
|
|
579
|
+
if (!ours) {
|
|
580
|
+
intake.log(`QuoteRequest from ${actor} names ${objectId}, not a post of ours — ignored`);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (!intake.sameIdentity(instrumentId, actor)) return `QuoteRequest instrument/actor identity mismatch (${instrumentId})`;
|
|
584
|
+
const s = intake.store.getStatuses().find(x => x.noteId === objectId && x.kind === 'post');
|
|
585
|
+
if (!s) return `QuoteRequest for a post we do not hold (${objectId})`;
|
|
586
|
+
// Not retried on failure: the instrument is a URL a stranger chose.
|
|
587
|
+
const note = await intake.fetchAP(instrumentId);
|
|
588
|
+
if (!note) return `quote request: instrument fetch failed (${instrumentId})`;
|
|
589
|
+
if (note.id !== instrumentId || !isContentType(note.type)) return `quote request: instrument is not verifiable content (${instrumentId})`;
|
|
590
|
+
if (authorOf(note, actor) !== actor) return `quote request: ${instrumentId} is not by ${actor}`;
|
|
591
|
+
if (quoteOf(note) !== objectId) return `quote request: ${instrumentId} does not quote ${objectId}`;
|
|
592
|
+
const doc = await intake.fetchAP(actor);
|
|
593
|
+
const inbox = doc?.endpoints?.sharedInbox || doc?.inbox;
|
|
594
|
+
if (!inbox) return `quote request: no inbox for ${actor}`;
|
|
595
|
+
const { quoteAnswerActivity } = await import('../wire-quotes.mjs');
|
|
596
|
+
const open = !s.visibility || s.visibility === 'public' || s.visibility === 'unlisted';
|
|
597
|
+
if (!open) {
|
|
598
|
+
await intake.deliverer.deliver(inbox,
|
|
599
|
+
quoteAnswerActivity({ urls: intake.urls, type: 'Reject', request: activity, to: actor, serial: intake.serial++ }));
|
|
600
|
+
intake.log(`quote refused: ${instrumentId} asked to quote ${objectId}, which is not public`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const authorization = await intake.publisher.grantQuote(objectId, instrumentId);
|
|
604
|
+
await intake.deliverer.deliver(inbox,
|
|
605
|
+
quoteAnswerActivity({ urls: intake.urls, type: 'Accept', request: activity, result: authorization, to: actor, serial: intake.serial++ }));
|
|
606
|
+
const held = intake.store.getStatuses().find(x => x.noteId === instrumentId);
|
|
607
|
+
if (!held) {
|
|
608
|
+
// The origin's copy, already fetched above.
|
|
609
|
+
const rejected = await intake.ingestNote(instrumentId, actor, { inline: note });
|
|
610
|
+
if (rejected) intake.log(`quote of ${objectId} by ${actor}: ${rejected}`);
|
|
611
|
+
} else if (held.quoteState !== 'accepted') {
|
|
612
|
+
intake.store.updateStatus(instrumentId, { quote: objectId, quoteState: 'accepted' });
|
|
613
|
+
}
|
|
614
|
+
intake.store.addNotification({ type: 'quote', actor, noteId: instrumentId });
|
|
615
|
+
intake.log(`quote allowed: ${instrumentId} quotes ${objectId}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// What can be said about a note's quote. The quoted post is ours: allowed if
|
|
619
|
+
// we granted it. Elsewhere: allowed when the note carries an authorization
|
|
620
|
+
// that, fetched from the quoted post's own origin, names this note and that
|
|
621
|
+
// one. Otherwise pending — the honest word for a quote nobody has vouched for.
|
|
622
|
+
export async function quoteStateOf(intake, note, quote) {
|
|
623
|
+
if (quote.startsWith(intake.urls.notes)) {
|
|
624
|
+
const target = intake.store.getStatuses().find(x => x.noteId === quote);
|
|
625
|
+
return (target?.quotedBy || []).some(q => q.note === note.id) ? 'accepted' : 'pending';
|
|
626
|
+
}
|
|
627
|
+
const authId = idOf(note.quoteAuthorization);
|
|
628
|
+
if (!authId || !intake.sameIdentity(authId, quote)) return 'pending';
|
|
629
|
+
const auth = await intake.fetchAP(authId).catch(() => null);
|
|
630
|
+
const ok = auth && auth.id === authId && auth.type === 'QuoteAuthorization'
|
|
631
|
+
&& idOf(auth.interactingObject) === note.id && idOf(auth.interactionTarget) === quote;
|
|
632
|
+
return ok ? 'accepted' : 'pending';
|
|
633
|
+
}
|
|
@@ -130,7 +130,32 @@ export function trimActivity(a) {
|
|
|
130
130
|
if (v) out[k] = String(v).slice(0, 2048);
|
|
131
131
|
}
|
|
132
132
|
const obj = idOf(a.object);
|
|
133
|
-
if (obj)
|
|
133
|
+
if (obj) {
|
|
134
|
+
// An Update says what to change INSIDE its object — a topic's new name, or
|
|
135
|
+
// whether it is closed — so flattening the object to its id alone threw the
|
|
136
|
+
// instruction away and left a bare topic id that meant nothing. Close,
|
|
137
|
+
// reopen and rename all arrived at the queue saying only which topic, and
|
|
138
|
+
// the moderator watched them do nothing. Kept now, bounded, and only the
|
|
139
|
+
// two fields a moderation queue acts on: everything else is still dropped.
|
|
140
|
+
const asked = a.object && typeof a.object === 'object' ? a.object : null;
|
|
141
|
+
const said = {};
|
|
142
|
+
if (asked) {
|
|
143
|
+
if (typeof asked.name === 'string') said.name = asked.name.slice(0, 300);
|
|
144
|
+
// `false` is the whole of a reopen, so presence is what counts, not truth.
|
|
145
|
+
if ('closed' in asked) {
|
|
146
|
+
// A date stays a date; a boolean stays a boolean; and the STRINGS
|
|
147
|
+
// "true"/"false" — which is what a boolean looks like after a form, a
|
|
148
|
+
// template or an RDF round trip — become the booleans they mean,
|
|
149
|
+
// because `!!"false"` is true and would turn a reopen into a close.
|
|
150
|
+
const v = asked.closed;
|
|
151
|
+
said.closed = v === 'false' ? false : v === 'true' ? true
|
|
152
|
+
: typeof v === 'string' ? v.slice(0, 64) : !!v;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
out.object = Object.keys(said).length
|
|
156
|
+
? { id: String(obj).slice(0, 2048), ...said }
|
|
157
|
+
: String(obj).slice(0, 2048);
|
|
158
|
+
}
|
|
134
159
|
// A typed object with no id still says what it was — a Block of nobody, an
|
|
135
160
|
// Undo of a Follow — and that is the whole of what the queue reads.
|
|
136
161
|
else if (a.object && typeof a.object === 'object' && a.object.type) {
|
|
@@ -238,3 +263,56 @@ export function authorOf(note, delivered = null) {
|
|
|
238
263
|
// post. See sameIdentity.
|
|
239
264
|
return sameIdentity(note?.id, author) ? author : null;
|
|
240
265
|
}
|
|
266
|
+
|
|
267
|
+
// A reaction: an emoji somebody put on a post. Pleroma, Akkoma, Misskey and
|
|
268
|
+
// Sharkey send EmojiReact; Misskey also sends a Like whose `content` is the
|
|
269
|
+
// emoji, which is what its own users see as a reaction. Either way the
|
|
270
|
+
// emoji is the content — a unicode character, or `:name:` for a custom one
|
|
271
|
+
// whose image rides the tag list. Null when the activity is a plain Like.
|
|
272
|
+
export const MAX_REACTION_CHARS = 64;
|
|
273
|
+
export function reactionOf(activity) {
|
|
274
|
+
if (!activity || (activity.type !== 'EmojiReact' && activity.type !== 'Like')) return null;
|
|
275
|
+
const content = typeof activity.content === 'string' ? activity.content.trim() : '';
|
|
276
|
+
if (!content) return null;
|
|
277
|
+
const emoji = content.slice(0, MAX_REACTION_CHARS);
|
|
278
|
+
const bare = emoji.replace(/^:|:$/g, '');
|
|
279
|
+
const custom = emojisOf(activity).find(e => e.shortcode === bare);
|
|
280
|
+
return { emoji, ...(custom ? { url: custom.url } : {}) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// What a note quotes, whichever way its server says it. Mastodon writes
|
|
284
|
+
// FEP-044f's `quote` alongside the two older spellings, Fedibird's quoteUri
|
|
285
|
+
// and Misskey's _misskey_quote; Pleroma writes quoteUrl; FEP-e232 puts a Link
|
|
286
|
+
// in the tag list with the Misskey relation. One post, one quote.
|
|
287
|
+
const QUOTE_REL = 'https://misskey-hub.net/ns#_misskey_quote';
|
|
288
|
+
export function quoteOf(note) {
|
|
289
|
+
const idOf = (v) => (typeof v === 'string' ? v : v?.id);
|
|
290
|
+
const named = [note?.quote, note?.quoteUri, note?._misskey_quote, note?.quoteUrl]
|
|
291
|
+
.map(v => idOf(Array.isArray(v) ? v[0] : v)).find(Boolean);
|
|
292
|
+
const linked = [].concat(note?.tag || []).find(t => t?.type === 'Link' && t.href
|
|
293
|
+
&& [].concat(t.rel || []).includes(QUOTE_REL))?.href;
|
|
294
|
+
const q = named || linked;
|
|
295
|
+
return q && q !== note?.id ? httpOnly(String(q).slice(0, MAX_URL_CHARS)) : null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Who the author lets quote a post: their interaction policy's canQuote, in
|
|
299
|
+
// the words a client uses. The public collection is `public`; the author's
|
|
300
|
+
// own followers or following collections are `followers` and `following`.
|
|
301
|
+
// Null when the note says nothing, which is the common case on servers that
|
|
302
|
+
// predate the policy.
|
|
303
|
+
export function quotePolicyOf(note, author) {
|
|
304
|
+
const can = note?.interactionPolicy?.canQuote;
|
|
305
|
+
if (!can || typeof can !== 'object') return null;
|
|
306
|
+
const word = (v) => {
|
|
307
|
+
const id = typeof v === 'string' ? v : v?.id;
|
|
308
|
+
if (!id) return null;
|
|
309
|
+
if (id === 'https://www.w3.org/ns/activitystreams#Public' || id === 'as:Public' || id === 'Public') return 'public';
|
|
310
|
+
if (author && sameIdentity(id, author)) {
|
|
311
|
+
if (/\/followers\/?$/u.test(id)) return 'followers';
|
|
312
|
+
if (/\/following\/?$/u.test(id)) return 'following';
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
};
|
|
316
|
+
const list = (v) => [...new Set([].concat(v || []).map(word).filter(Boolean))];
|
|
317
|
+
return { automatic: list(can.automaticApproval), manual: list(can.manualApproval) };
|
|
318
|
+
}
|
|
@@ -9,6 +9,9 @@ import { trimActivity, MAX_MODQUEUE, MAX_PENDING_REVIEW, CO_MEMBER_TTL_MS, CO_ME
|
|
|
9
9
|
// moderator's ordinary traffic (their posts, likes, follows) is not
|
|
10
10
|
// moderation and takes the normal arms.
|
|
11
11
|
export function isModerationAsk(intake, activity) {
|
|
12
|
+
// What a forum holds beyond a group's own asks: a topic removed or moved,
|
|
13
|
+
// a post deleted out of a topic, a report.
|
|
14
|
+
if (intake.isModerationAskExtra?.(activity)) return true;
|
|
12
15
|
if (activity.type === 'Block') return true;
|
|
13
16
|
if (activity.type === 'Undo') {
|
|
14
17
|
return typeof activity.object === 'object' && activity.object?.type === 'Block';
|
|
@@ -78,14 +81,34 @@ export async function amplify(intake, noteId, { approved = false, activity = nul
|
|
|
78
81
|
// A DM to the group, or a followers-only post it happened to receive, was
|
|
79
82
|
// addressed to less than the world — carrying it would widen the author's
|
|
80
83
|
// audience for them. A group only ever amplifies public posts.
|
|
81
|
-
|
|
84
|
+
//
|
|
85
|
+
// Except a PRIVATE group, where that reasoning runs backwards. Its members
|
|
86
|
+
// write to the group and to nobody else on purpose, and its membership is
|
|
87
|
+
// exactly the people already allowed to read what they write; refusing the
|
|
88
|
+
// carry there would mean a private group could only carry posts its author
|
|
89
|
+
// had published to the world. So a group told it is private carries what is
|
|
90
|
+
// addressed to it. A DM is still a DM and is still never carried.
|
|
91
|
+
if (s.direct || (s.nonPublic && !intake.config.private)) {
|
|
82
92
|
intake.log(`not amplified — ${noteId} was not addressed publicly, and a group never widens a post's audience`);
|
|
83
93
|
return;
|
|
84
94
|
}
|
|
85
95
|
const contacts = intake.store.getContacts();
|
|
86
96
|
if (!contacts.followers.some(f => f.actor === s.actor)) {
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
// A group carries its members and nobody else. A forum may choose to
|
|
98
|
+
// HOLD a stranger's reply for a moderator instead of dropping it, which
|
|
99
|
+
// is what `replyPolicy` says; the group itself still carries nothing it
|
|
100
|
+
// has not been told to.
|
|
101
|
+
if (intake.onStranger) {
|
|
102
|
+
const held = await intake.onStranger({ noteId, actor: s.actor, activity }).catch(() => false);
|
|
103
|
+
if (held) { intake.log(`held for a moderator — ${s.actor} is not a member`); return; }
|
|
104
|
+
}
|
|
105
|
+
// The hook may have made them one — a forum whose categories are open
|
|
106
|
+
// treats posting into one as joining it. So ask the record again rather
|
|
107
|
+
// than the copy taken before the hook ran.
|
|
108
|
+
if (!intake.store.getContacts().followers.some(f => f.actor === s.actor)) {
|
|
109
|
+
intake.log(`not amplified — ${s.actor} is not a member`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
89
112
|
}
|
|
90
113
|
if (intake.store.getMuted().actors.includes(s.actor)) {
|
|
91
114
|
intake.log(`not amplified — ${s.actor} is muted`);
|
|
@@ -155,6 +178,17 @@ export async function amplify(intake, noteId, { approved = false, activity = nul
|
|
|
155
178
|
await intake.publisher.recordOutbox(act);
|
|
156
179
|
intake.store.setPending(intake.store.getPending().filter(p => p.noteId !== noteId));
|
|
157
180
|
intake.log(`amplified ${noteId} → ${inboxes.length} inbox(es)`);
|
|
181
|
+
// What the group carried is what the group's conversation holds. A forum
|
|
182
|
+
// places the post in its topic here — after the carry, so only what the
|
|
183
|
+
// category vouched for is in the topic (FEP-7888 ownership). Its failure
|
|
184
|
+
// never undoes the carry.
|
|
185
|
+
if (intake.onCarried) {
|
|
186
|
+
// Both: what the group said (the Announce) and what its author sent. A
|
|
187
|
+
// forum reads the author's own activity — the name they gave the topic
|
|
188
|
+
// they were opening is on that one, and on no other.
|
|
189
|
+
await intake.onCarried({ noteId, actor: s.actor, activity: act, sent: activity })
|
|
190
|
+
.catch(e => intake.log(`after the carry of ${noteId}: ${e.message}`));
|
|
191
|
+
}
|
|
158
192
|
// The same carry, shown natively to the group's Bluesky followers.
|
|
159
193
|
await intake.bskyGroup?.mirrorCarry(s)
|
|
160
194
|
.catch(e => intake.log(`bluesky mirror of the carry failed: ${e.message}`));
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import * as podInbox from '../../pod/inbox.mjs';
|
|
26
26
|
import { readCapped } from '../../shared/safefetch.mjs';
|
|
27
|
-
import { trimActivity, sameIdentity, sameOrigin, httpUrl, MAX_ITEM_BYTES, MAX_FORWARDS_PER_DRAIN } from './activity.mjs';
|
|
27
|
+
import { trimActivity, sameIdentity, sameOrigin, httpUrl, reactionOf, MAX_EMOJIS, MAX_ITEM_BYTES, MAX_FORWARDS_PER_DRAIN } from './activity.mjs';
|
|
28
28
|
import * as channel from './channel.mjs';
|
|
29
29
|
import * as verify from './verify.mjs';
|
|
30
30
|
import * as group from './group.mjs';
|
|
@@ -322,6 +322,9 @@ export class Intake {
|
|
|
322
322
|
}
|
|
323
323
|
this.lastDrain = new Date().toISOString();
|
|
324
324
|
this.lastDrainAtMs = Date.now();
|
|
325
|
+
// What a drain leaves behind may need acting on: a forum's queue of asks,
|
|
326
|
+
// which is filled here and emptied by whoever can vouch for them.
|
|
327
|
+
if (this.afterDrain) setTimeout(() => this.afterDrain(), 0);
|
|
325
328
|
this._pruneAttempts();
|
|
326
329
|
let all;
|
|
327
330
|
try {
|
|
@@ -548,7 +551,8 @@ export class Intake {
|
|
|
548
551
|
case 'Undo': return this.onUndo(activity, actor, { trusted });
|
|
549
552
|
case 'Create': return this.onCreate(activity, actor, { trusted });
|
|
550
553
|
case 'Accept': return this.onAccept(activity, actor, { trusted });
|
|
551
|
-
case '
|
|
554
|
+
case 'QuoteRequest': return this.onQuoteRequest(activity, actor, { trusted });
|
|
555
|
+
case 'Like': case 'Announce': case 'EmojiReact': {
|
|
552
556
|
// FEP-1b12: a group Announces the member's whole Create, not the note.
|
|
553
557
|
// Without unwrapping we try to ingest a Create as if it were a Note and
|
|
554
558
|
// dead-letter every post a group ever carries — including our own.
|
|
@@ -564,6 +568,29 @@ export class Intake {
|
|
|
564
568
|
&& (wrapped.type === 'Create' || wrapped.type === 'Update')) ? wrapped.object : wrapped;
|
|
565
569
|
const objectId = typeof inner === 'string' ? inner : inner?.id;
|
|
566
570
|
this.log(`${activity.type} from ${actor} on ${objectId}`);
|
|
571
|
+
// An emoji on one of our posts. Recorded the way a Like is — on the
|
|
572
|
+
// sender's word, nothing to dereference — with the emoji itself, and
|
|
573
|
+
// a custom emoji's image added to the post's own list so a client
|
|
574
|
+
// that draws reactions from there finds it. Anything else an
|
|
575
|
+
// EmojiReact names is not ours to notice.
|
|
576
|
+
const reaction = activity.type !== 'Announce' ? reactionOf(activity) : null;
|
|
577
|
+
if (reaction) {
|
|
578
|
+
if (!objectId || !objectId.startsWith(this.urls.notes)) return;
|
|
579
|
+
this.store.addNotification({
|
|
580
|
+
type: 'reaction', actor, noteId: objectId, emoji: reaction.emoji,
|
|
581
|
+
...(reaction.url ? { url: reaction.url } : {}),
|
|
582
|
+
...(this.known(actor) ? {} : { unverified: true }),
|
|
583
|
+
});
|
|
584
|
+
if (reaction.url) {
|
|
585
|
+
const s = this.store.getStatuses().find(x => x.noteId === objectId);
|
|
586
|
+
const shortcode = reaction.emoji.replace(/^:|:$/g, '');
|
|
587
|
+
if (s && !(s.emojis || []).some(e => e.shortcode === shortcode) && (s.emojis || []).length < MAX_EMOJIS) {
|
|
588
|
+
this.store.updateStatus(objectId, { emojis: [...(s.emojis || []), { shortcode, url: reaction.url }] });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (activity.type === 'EmojiReact') return;
|
|
567
594
|
if (objectId && objectId.startsWith(this.urls.notes)) {
|
|
568
595
|
// Nothing vouches for this actor: a Like carries no signature and,
|
|
569
596
|
// unlike a Create, has no object at the sender's origin to re-read.
|
|
@@ -585,6 +612,7 @@ export class Intake {
|
|
|
585
612
|
case 'Move': return this.onMove(activity, actor);
|
|
586
613
|
case 'Add': case 'Remove': return this.onAddRemove(activity, actor);
|
|
587
614
|
case 'Block': return this.onBlock(activity, actor, { trusted });
|
|
615
|
+
case 'Flag': if (this.onReport) return this.onReport(activity, actor, { trusted }); return;
|
|
588
616
|
default: this.log(`ignored ${activity.type} from ${actor}`);
|
|
589
617
|
}
|
|
590
618
|
}
|
|
@@ -641,6 +669,8 @@ export class Intake {
|
|
|
641
669
|
onCreate(...a) { return activities.onCreate(this, ...a); }
|
|
642
670
|
onAnnouncedDelete(...a) { return activities.onAnnouncedDelete(this, ...a); }
|
|
643
671
|
onAnnounce(...a) { return activities.onAnnounce(this, ...a); }
|
|
672
|
+
onQuoteRequest(...a) { return activities.onQuoteRequest(this, ...a); }
|
|
673
|
+
quoteStateOf(...a) { return activities.quoteStateOf(this, ...a); }
|
|
644
674
|
onDelete(...a) { return activities.onDelete(this, ...a); }
|
|
645
675
|
onUpdate(...a) { return activities.onUpdate(this, ...a); }
|
|
646
676
|
onReject(...a) { return activities.onReject(this, ...a); }
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as podNotes from '../../pod/notes.mjs';
|
|
7
7
|
import { PUBLIC } from '../wire.mjs';
|
|
8
8
|
import * as polls from '../polls.mjs';
|
|
9
|
-
import { httpOnly, emojisOf, pollOf, isContentType, authorOf, MAX_MENTIONS, MAX_URL_CHARS, MAX_REPLIES_RECORDED, MAX_FORWARDED, FORWARDABLE, FORWARD_TYPES } from './activity.mjs';
|
|
9
|
+
import { httpOnly, emojisOf, pollOf, isContentType, authorOf, quoteOf, quotePolicyOf, MAX_MENTIONS, MAX_URL_CHARS, MAX_REPLIES_RECORDED, MAX_FORWARDED, FORWARDABLE, FORWARD_TYPES } from './activity.mjs';
|
|
10
10
|
|
|
11
11
|
// Does this activity/note concern us at all? Either it comes from someone
|
|
12
12
|
// we follow, or it names us (to/cc, mention tag) or replies to one of our
|
|
@@ -21,6 +21,9 @@ export function concernsUs(intake, doc, actor) {
|
|
|
21
21
|
const tagged = [].concat(doc?.tag || [])
|
|
22
22
|
.some(t => t?.type === 'Mention' && (t.href === intake.urls.actor || t.name?.includes(intake.urls.actor)));
|
|
23
23
|
if (tagged) return true;
|
|
24
|
+
// A post that quotes one of ours is about us as much as a reply is.
|
|
25
|
+
const quoted = quoteOf(doc);
|
|
26
|
+
if (quoted && quoted.startsWith(intake.urls.notes)) return true;
|
|
24
27
|
const inReplyTo = typeof doc?.inReplyTo === 'string' ? doc.inReplyTo : doc?.inReplyTo?.id;
|
|
25
28
|
if (!inReplyTo) return false;
|
|
26
29
|
if (String(inReplyTo).startsWith(intake.urls.notes)) return true;
|
|
@@ -130,6 +133,9 @@ export async function ingestNote(intake, objectId, actor, { via, inline = null }
|
|
|
130
133
|
// signed Announce to every member.
|
|
131
134
|
const author = authorOf(note, actor);
|
|
132
135
|
if (!author) return `object names an author its origin does not vouch for (${objectId})`;
|
|
136
|
+
// The verified document, kept for whoever acts on this note after the
|
|
137
|
+
// carry: a forum caches it for its readers. Only when someone asked.
|
|
138
|
+
intake.recentNotes?.set(note.id, note);
|
|
133
139
|
// This is the check that catches a blocked actor reaching us through
|
|
134
140
|
// somebody else's boost, or through a hashtag feed.
|
|
135
141
|
if (intake.store.isBlocked(author)) return `blocked author (${author})`;
|
|
@@ -197,6 +203,13 @@ export async function ingestNote(intake, objectId, actor, { via, inline = null }
|
|
|
197
203
|
.filter(t => t.name);
|
|
198
204
|
const emojis = emojisOf(note);
|
|
199
205
|
const poll = pollOf(note);
|
|
206
|
+
// What the note quotes, and what its author lets others quote. The quoted
|
|
207
|
+
// post is fetched and kept so the quote can be shown whole; whether the
|
|
208
|
+
// quote was allowed is checked, never assumed.
|
|
209
|
+
const quote = quoteOf(note);
|
|
210
|
+
const quotePolicy = quotePolicyOf(note, author);
|
|
211
|
+
const quoteState = quote ? await intake.quoteStateOf(note, quote) : null;
|
|
212
|
+
if (quote) await holdQuoted(intake, quote);
|
|
200
213
|
// Explicitly addressed, but to nobody public and to no followers
|
|
201
214
|
// collection: a direct message, which belongs to the conversations view
|
|
202
215
|
// rather than a timeline. A note with no addressing at all is NOT direct —
|
|
@@ -220,15 +233,21 @@ export async function ingestNote(intake, objectId, actor, { via, inline = null }
|
|
|
220
233
|
...(emojis.length ? { emojis } : {}),
|
|
221
234
|
...(mentions.length ? { mentions } : {}),
|
|
222
235
|
...(attachments.length ? { attachments } : {}),
|
|
236
|
+
...(quote ? { quote, quoteState } : {}),
|
|
237
|
+
...(quotePolicy ? { quotePolicy } : {}),
|
|
223
238
|
...(via ? { via } : {}),
|
|
224
239
|
});
|
|
225
240
|
// A notification for anything addressed to this person: a stranger's
|
|
226
241
|
// post, a reply to one of ours, a direct message, or a post that names
|
|
227
242
|
// us — whoever sent it. A direct message from someone followed used to
|
|
228
|
-
// raise nothing and sat unseen in the private mentions.
|
|
243
|
+
// raise nothing and sat unseen in the private mentions. A post quoting
|
|
244
|
+
// one of ours is its own kind of notification.
|
|
229
245
|
const namesUs = mentions.some((m) => m.href === intake.urls.actor);
|
|
230
246
|
const replyToOurs = !!note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes);
|
|
231
|
-
|
|
247
|
+
const quotesUs = !!quote && quote.startsWith(intake.urls.notes);
|
|
248
|
+
if (quotesUs) {
|
|
249
|
+
intake.store.addNotification({ type: 'quote', actor: author, noteId: note.id });
|
|
250
|
+
} else if (!followed || replyToOurs || direct || namesUs) {
|
|
232
251
|
intake.store.addNotification({ type: 'mention', actor: author, noteId: note.id });
|
|
233
252
|
}
|
|
234
253
|
if (note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes)) {
|
|
@@ -238,6 +257,44 @@ export async function ingestNote(intake, objectId, actor, { via, inline = null }
|
|
|
238
257
|
intake.log(`${kind}: ${note.id}${via ? ` (boosted by ${via})` : ''}`);
|
|
239
258
|
}
|
|
240
259
|
|
|
260
|
+
// The post a quote shows: fetched at its origin and kept out of every
|
|
261
|
+
// timeline (`remote`), the way a thread's ancestors are — it is there to be
|
|
262
|
+
// shown inside the quoting post, not on its own. Nothing is fetched twice,
|
|
263
|
+
// and nothing is followed further: a quote of a quote shows one level.
|
|
264
|
+
export async function holdQuoted(intake, quoteId) {
|
|
265
|
+
if (intake.store.getStatuses().some(s => s.noteId === quoteId)) return;
|
|
266
|
+
if (intake.store.isBlocked(quoteId)) return;
|
|
267
|
+
const q = await intake.fetchAP(quoteId).catch(() => null);
|
|
268
|
+
if (!q || q.id !== quoteId || !isContentType(q.type)) {
|
|
269
|
+
intake.log(`quoted post ${quoteId} could not be read — the quote shows without it`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const by = authorOf(q, null);
|
|
273
|
+
if (!by || intake.store.isBlocked(by)) return;
|
|
274
|
+
const { attachmentsOf, titledContent } = await import('../wire.mjs');
|
|
275
|
+
const attachments = attachmentsOf(q);
|
|
276
|
+
const emojis = emojisOf(q);
|
|
277
|
+
const poll = pollOf(q);
|
|
278
|
+
const quotePolicy = quotePolicyOf(q, by);
|
|
279
|
+
const mentions = [].concat(q.tag || [])
|
|
280
|
+
.filter(t => t?.type === 'Mention' && t.href && t.name)
|
|
281
|
+
.slice(0, MAX_MENTIONS)
|
|
282
|
+
.map(t => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) }))
|
|
283
|
+
.filter(m => m.href);
|
|
284
|
+
intake.store.addStatus({
|
|
285
|
+
noteId: q.id, actor: by, content: titledContent(q),
|
|
286
|
+
published: q.published, inReplyTo: q.inReplyTo, kind: 'remote',
|
|
287
|
+
...(q.summary ? { spoiler: String(q.summary).replace(/<[^>]*>/g, '') } : {}),
|
|
288
|
+
...(q.sensitive === true ? { sensitive: true } : {}),
|
|
289
|
+
...(poll ? { poll } : {}),
|
|
290
|
+
...(emojis.length ? { emojis } : {}),
|
|
291
|
+
...(mentions.length ? { mentions } : {}),
|
|
292
|
+
...(attachments.length ? { attachments } : {}),
|
|
293
|
+
...(quotePolicy ? { quotePolicy } : {}),
|
|
294
|
+
});
|
|
295
|
+
intake.log(`quoted post kept: ${q.id}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
241
298
|
// Drop a post we were holding. A group that carried it also unsays its own
|
|
242
299
|
// Announce — forwarding the author's Delete would be signed by us and not by
|
|
243
300
|
// them, which receivers are right to refuse.
|
package/lib/core/lease.mjs
CHANGED
|
@@ -21,11 +21,21 @@ const JITTER = () => 0.85 + Math.random() * 0.3;
|
|
|
21
21
|
const UNREADABLE = Symbol('lease-unreadable');
|
|
22
22
|
|
|
23
23
|
export class Lease {
|
|
24
|
-
|
|
24
|
+
// `id` names the HOLDER, and acquire() lets a holder reclaim its own lease
|
|
25
|
+
// without waiting for the TTL — so whether it is stable decides whether a
|
|
26
|
+
// restart is the same agent coming back or a second one arriving. A process
|
|
27
|
+
// that is gone for good wants a new one each time; a browser does not, since
|
|
28
|
+
// its agent lives in a service worker the browser is free to kill and restart
|
|
29
|
+
// whenever it likes. Left to the default, every one of those restarts looked
|
|
30
|
+
// like another device and asked the owner to take over their own account —
|
|
31
|
+
// which is what switching between the two clients did, because that is a
|
|
32
|
+
// navigation and a navigation can take the worker with it. The browser build
|
|
33
|
+
// passes one it keeps.
|
|
34
|
+
constructor({ url, fetchImpl, log = console.log, id = null }) {
|
|
25
35
|
this.url = url;
|
|
26
36
|
this.fetchImpl = fetchImpl;
|
|
27
37
|
this.log = log;
|
|
28
|
-
this.id = crypto.randomUUID();
|
|
38
|
+
this.id = id || crypto.randomUUID();
|
|
29
39
|
this.stopped = false;
|
|
30
40
|
this.timer = null;
|
|
31
41
|
this.heldUntil = 0; // wall-clock end of the lease we last wrote
|