fedipod-server 0.29.0 → 0.31.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 +16 -6
- package/dist/claims.d.ts +2 -0
- package/dist/claims.js +13 -4
- package/dist/door.d.ts +4 -0
- package/dist/door.js +31 -0
- package/dist/fedipod.d.ts +15 -0
- package/dist/fedipod.js +13 -0
- package/dist/front.d.ts +3 -0
- package/dist/front.js +92 -0
- package/dist/handler.d.ts +36 -101
- package/dist/handler.js +93 -644
- package/dist/handler.jsonld +2 -30
- package/dist/identities.d.ts +71 -0
- package/dist/identities.js +236 -0
- package/dist/mounts.d.ts +43 -0
- package/dist/mounts.js +123 -0
- package/dist/opt-in.d.ts +46 -0
- package/dist/opt-in.js +176 -0
- package/dist/private-read.d.ts +4 -0
- package/dist/private-read.js +34 -0
- package/package.json +12 -20
- package/lib/client/c2s.mjs +0 -636
- package/lib/client/localapi.mjs +0 -87
- package/lib/client/masto/accounts.mjs +0 -310
- package/lib/client/masto/body.mjs +0 -156
- package/lib/client/masto/index.mjs +0 -206
- package/lib/client/masto/instance.mjs +0 -112
- package/lib/client/masto/media.mjs +0 -79
- package/lib/client/masto/oauth.mjs +0 -656
- package/lib/client/masto/render.mjs +0 -518
- package/lib/client/masto/statuses.mjs +0 -372
- package/lib/client/masto/timelines.mjs +0 -438
- package/lib/client/oidc-auth.mjs +0 -62
- package/lib/client/streaming.mjs +0 -154
- package/lib/client/webpush.mjs +0 -98
- package/lib/connections/acctfeed.mjs +0 -257
- package/lib/connections/atproto.mjs +0 -309
- package/lib/connections/bskyfeed.mjs +0 -243
- package/lib/connections/bskygroup.mjs +0 -162
- package/lib/connections/fediacct.mjs +0 -253
- package/lib/connections/import.mjs +0 -357
- package/lib/connections/tagfeed.mjs +0 -212
- package/lib/connections/vault.mjs +0 -114
- package/lib/core/as2.mjs +0 -206
- package/lib/core/contexts/activitystreams.json +0 -379
- package/lib/core/contexts/anno.json +0 -126
- package/lib/core/contexts/did-v1.json +0 -57
- package/lib/core/contexts/fep-5711.json +0 -36
- package/lib/core/contexts/gotosocial.json +0 -86
- package/lib/core/contexts/identity-v1.json +0 -152
- package/lib/core/contexts/index.mjs +0 -63
- package/lib/core/contexts/join-lemmy.json +0 -33
- package/lib/core/contexts/joinmastodon.json +0 -28
- package/lib/core/contexts/litepub-0.1.json +0 -60
- package/lib/core/contexts/map.json +0 -19
- package/lib/core/contexts/miscellany.json +0 -19
- package/lib/core/contexts/quotes.json +0 -18
- package/lib/core/contexts/schemaorg.json +0 -8845
- package/lib/core/contexts/security-data-integrity-v1.json +0 -78
- package/lib/core/contexts/security-data-integrity-v2.json +0 -81
- package/lib/core/contexts/security-multikey-v1.json +0 -35
- package/lib/core/contexts/security-v1.json +0 -74
- package/lib/core/contexts/webfinger.json +0 -10
- package/lib/core/deliver.mjs +0 -387
- package/lib/core/graphview.mjs +0 -327
- package/lib/core/intake/activities.mjs +0 -649
- package/lib/core/intake/activity.mjs +0 -318
- package/lib/core/intake/channel.mjs +0 -144
- package/lib/core/intake/group.mjs +0 -258
- package/lib/core/intake/index.mjs +0 -732
- package/lib/core/intake/notes.mjs +0 -367
- package/lib/core/intake/verify.mjs +0 -142
- package/lib/core/keys.mjs +0 -153
- package/lib/core/lease.mjs +0 -206
- package/lib/core/place.mjs +0 -82
- package/lib/core/polls.mjs +0 -105
- package/lib/core/profile-page.mjs +0 -104
- package/lib/core/proof.mjs +0 -79
- package/lib/core/publisher/collections.mjs +0 -272
- package/lib/core/publisher/index.mjs +0 -522
- package/lib/core/publisher/notes.mjs +0 -519
- package/lib/core/publisher/own.mjs +0 -143
- package/lib/core/publisher/questions.mjs +0 -241
- package/lib/core/publisher/restore.mjs +0 -224
- package/lib/core/shapes/activitystreams.ttl +0 -157
- package/lib/core/shapes/index.mjs +0 -132
- package/lib/core/shapes/shapes-text.mjs +0 -13
- package/lib/core/social.mjs +0 -561
- package/lib/core/storage.mjs +0 -176
- package/lib/core/store.mjs +0 -578
- package/lib/core/wire-quotes.mjs +0 -71
- package/lib/core/wire.mjs +0 -925
- package/lib/device/account.mjs +0 -87
- package/lib/device/admin/index.mjs +0 -13
- package/lib/device/admin/origins.mjs +0 -35
- package/lib/device/admin/routes/connections.mjs +0 -144
- package/lib/device/admin/routes/gateway.mjs +0 -199
- package/lib/device/admin/routes/lifecycle.mjs +0 -200
- package/lib/device/admin/routes/owner.mjs +0 -322
- package/lib/device/admin/routes/setup.mjs +0 -428
- package/lib/device/admin/routes/social.mjs +0 -188
- package/lib/device/admin/server.mjs +0 -95
- package/lib/device/admin/static.mjs +0 -247
- package/lib/device/admin/surface.mjs +0 -315
- package/lib/device/certs.mjs +0 -281
- package/lib/device/cli/commands/account.mjs +0 -586
- package/lib/device/cli/commands/run.mjs +0 -278
- package/lib/device/cli/commands/service.mjs +0 -221
- package/lib/device/cli/commands/setup.mjs +0 -489
- package/lib/device/cli/commands/state.mjs +0 -561
- package/lib/device/cli/context.mjs +0 -288
- package/lib/device/export-collections.mjs +0 -161
- package/lib/device/gateway-move.mjs +0 -58
- package/lib/device/home.mjs +0 -161
- package/lib/device/migrate.mjs +0 -115
- package/lib/device/ports.mjs +0 -27
- package/lib/device/remote.mjs +0 -54
- package/lib/device/setup.mjs +0 -496
- package/lib/device/update.mjs +0 -96
- package/lib/gateway/caches.mjs +0 -36
- package/lib/gateway/directory.mjs +0 -90
- package/lib/gateway/front-core.mjs +0 -966
- package/lib/gateway/gateway-core.mjs +0 -244
- package/lib/gateway/headers.mjs +0 -49
- package/lib/gateway/httpsig.mjs +0 -159
- package/lib/gateway/notices.mjs +0 -84
- package/lib/gateway/quiet.mjs +0 -189
- package/lib/gateway/relay-extras.mjs +0 -89
- package/lib/gateway/token-claims.mjs +0 -16
- package/lib/pod/README.md +0 -61
- package/lib/pod/actor.mjs +0 -96
- package/lib/pod/collection.mjs +0 -77
- package/lib/pod/containers.mjs +0 -140
- package/lib/pod/discovery.mjs +0 -46
- package/lib/pod/featured.mjs +0 -18
- package/lib/pod/followers.mjs +0 -24
- package/lib/pod/following.mjs +0 -10
- package/lib/pod/http.mjs +0 -74
- package/lib/pod/inbox.mjs +0 -166
- package/lib/pod/links.mjs +0 -35
- package/lib/pod/location.mjs +0 -52
- package/lib/pod/media.mjs +0 -27
- package/lib/pod/notes.mjs +0 -78
- package/lib/pod/notifications.mjs +0 -89
- package/lib/pod/outbox.mjs +0 -18
- package/lib/pod/package.json +0 -19
- package/lib/pod/policy.mjs +0 -58
- package/lib/pod/private.mjs +0 -19
- package/lib/pod/root.mjs +0 -104
- package/lib/pod/state.mjs +0 -40
- package/lib/pod/transport.mjs +0 -674
- package/lib/pod/type-index.mjs +0 -101
- package/lib/pod/urls.mjs +0 -91
- package/lib/server/embed.mjs +0 -424
- package/lib/session/README.md +0 -137
- package/lib/session/demo.html +0 -94
- package/lib/session/fedi-account.mjs +0 -417
- package/lib/session/fedi-login.mjs +0 -123
- package/lib/session/oidc-session.mjs +0 -233
- package/lib/session/package.json +0 -49
- package/lib/shared/guard.mjs +0 -213
- package/lib/shared/links.mjs +0 -9
- package/lib/shared/safefetch.mjs +0 -235
- package/lib/shared/ua.mjs +0 -29
- package/phanpy/dist/404.html +0 -40
- package/phanpy/dist/_headers +0 -2
- package/phanpy/dist/apple-touch-icon.png +0 -0
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js +0 -2
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +0 -1
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +0 -183
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
- package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +0 -2
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +0 -1
- package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
- package/phanpy/dist/assets/chunk-aKtaBQYM.js +0 -1
- package/phanpy/dist/assets/compose-CFEx7wb4.js +0 -2
- package/phanpy/dist/assets/compose-CFEx7wb4.js.map +0 -1
- package/phanpy/dist/assets/compose-ZJN5xSb4.js +0 -35
- package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +0 -1
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js +0 -52
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +0 -1
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js +0 -2
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +0 -1
- package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +0 -1
- package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +0 -1
- package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
- package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +0 -1
- package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
- package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
- package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
- package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
- package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
- package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
- package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
- package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
- package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +0 -1
- package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +0 -1
- package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
- package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +0 -1
- package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
- package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +0 -1
- package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
- package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
- package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +0 -1
- package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
- package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +0 -1
- package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
- package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
- package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
- package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
- package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
- package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +0 -1
- package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +0 -1
- package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +0 -1
- package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +0 -1
- package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +0 -1
- package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
- package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +0 -1
- package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
- package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +0 -1
- package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +0 -1
- package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +0 -1
- package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
- package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
- package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +0 -1
- package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +0 -1
- package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
- package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
- package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +0 -1
- package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +0 -1
- package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +0 -1
- package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
- package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +0 -1
- package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +0 -1
- package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +0 -1
- package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
- package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
- package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
- package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +0 -1
- package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +0 -1
- package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +0 -1
- package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +0 -1
- package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +0 -1
- package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
- package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
- package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +0 -1
- package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +0 -1
- package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +0 -1
- package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +0 -1
- package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +0 -1
- package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +0 -1
- package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +0 -1
- package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +0 -1
- package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
- package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +0 -1
- package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +0 -1
- package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +0 -1
- package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
- package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +0 -1
- package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +0 -1
- package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
- package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
- package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
- package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +0 -1
- package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +0 -1
- package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
- package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +0 -1
- package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +0 -1
- package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +0 -1
- package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +0 -1
- package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +0 -1
- package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +0 -1
- package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +0 -1
- package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +0 -1
- package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +0 -1
- package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
- package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +0 -1
- package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
- package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
- package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
- package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +0 -1
- package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
- package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +0 -1
- package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +0 -1
- package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
- package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +0 -1
- package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
- package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +0 -1
- package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
- package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +0 -1
- package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +0 -1
- package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
- package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +0 -1
- package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +0 -1
- package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
- package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +0 -1
- package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +0 -1
- package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +0 -1
- package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
- package/phanpy/dist/assets/instances-BNPptnn-.json +0 -333
- package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +0 -1
- package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
- package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +0 -1
- package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +0 -1
- package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +0 -1
- package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +0 -1
- package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +0 -1
- package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +0 -1
- package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +0 -1
- package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +0 -1
- package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +0 -1
- package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +0 -1
- package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +0 -1
- package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +0 -1
- package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
- package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
- package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +0 -1
- package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
- package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +0 -1
- package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +0 -1
- package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
- package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/tok-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +0 -1
- package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +0 -1
- package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/main-BdqNbG-a.js +0 -26
- package/phanpy/dist/assets/main-BdqNbG-a.js.map +0 -1
- package/phanpy/dist/assets/mock-home-DGhR__He.js +0 -2
- package/phanpy/dist/assets/mock-home-DGhR__He.js.map +0 -1
- package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
- package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +0 -2
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +0 -1
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +0 -20
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +0 -1
- package/phanpy/dist/assets/style-B1iLPTPR.css +0 -2
- package/phanpy/dist/assets/temml-DuyIQMei.js +0 -97
- package/phanpy/dist/assets/temml-DuyIQMei.js.map +0 -1
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +0 -2
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
- package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
- package/phanpy/dist/assets/year-in-posts-DJv_star.js +0 -3
- package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +0 -1
- package/phanpy/dist/compose/index.html +0 -20
- package/phanpy/dist/favicon.ico +0 -0
- package/phanpy/dist/index.html +0 -73
- package/phanpy/dist/logo-192.png +0 -0
- package/phanpy/dist/logo-512.png +0 -0
- package/phanpy/dist/logo-badge-72.png +0 -0
- package/phanpy/dist/logo-maskable-512.png +0 -0
- package/phanpy/dist/logo-monochrome-512.png +0 -0
- package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
- package/phanpy/dist/manifest.webmanifest +0 -1
- package/phanpy/dist/og-image-2.jpg +0 -0
- package/phanpy/dist/og-image.png +0 -0
- package/phanpy/dist/robots.txt +0 -2
- package/phanpy/dist/sw.js +0 -2
- package/phanpy/dist/sw.js.map +0 -1
- package/phanpy/dist/version.json +0 -1
- package/run-agent.mjs +0 -899
- package/vendor/gate.cjs +0 -138
- package/vendor/idp-grant.cjs +0 -394
- package/vendor/jfetch.cjs +0 -16
- package/web/admin/actors.js +0 -147
- package/web/admin/bar.css +0 -126
- package/web/admin/bar.js +0 -99
- package/web/admin/client/client.js +0 -66
- package/web/admin/client/index.html +0 -69
- package/web/admin/client-pick.js +0 -79
- package/web/admin/common.js +0 -23
- package/web/admin/connections.js +0 -112
- package/web/admin/gateway.js +0 -133
- package/web/admin/group.js +0 -258
- package/web/admin/index.html +0 -612
- package/web/admin/notices-bar.js +0 -90
- package/web/admin/oauth-signin.mjs +0 -37
- package/web/admin/record.js +0 -393
- package/web/admin/setup/index.html +0 -257
- package/web/admin/setup/setup.js +0 -339
- package/web/admin/tokens.css +0 -68
- package/web/admin/upkeep.js +0 -177
- package/web/admin/window.css +0 -55
- package/web/admin/window.js +0 -136
- package/web/app/README.md +0 -81
- package/web/app/admin-facade.mjs +0 -588
- package/web/app/agent.mjs +0 -672
- package/web/app/atproto-browser.mjs +0 -86
- package/web/app/authorize_interaction.html +0 -56
- package/web/app/authorize_interaction.js +0 -69
- package/web/app/boot.mjs +0 -616
- package/web/app/deliver-relay.mjs +0 -129
- package/web/app/dist/boot.js +0 -34562
- package/web/app/dist/boot.js.map +0 -7
- package/web/app/dist/sw.js +0 -73456
- package/web/app/dist/sw.js.map +0 -7
- package/web/app/fediacct-browser.mjs +0 -146
- package/web/app/gateway-move.mjs +0 -60
- package/web/app/idb-kv.mjs +0 -35
- package/web/app/index.html +0 -258
- package/web/app/keys-browser.mjs +0 -71
- package/web/app/keystore.mjs +0 -82
- package/web/app/oidc-session.mjs +0 -6
- package/web/app/pod-auth.mjs +0 -248
- package/web/app/pod-remote.mjs +0 -84
- package/web/app/shims/_globals.mjs +0 -2
- package/web/app/shims/fedify-sig.mjs +0 -77
- package/web/app/shims/node-crypto.mjs +0 -113
- package/web/app/shims/node-fs.mjs +0 -17
- package/web/app/shims/node-path.mjs +0 -19
- package/web/app/shims/node-url.mjs +0 -7
- package/web/app/shims/prelude.js +0 -58
- package/web/app/shims/safefetch.mjs +0 -58
- package/web/app/shims/shapes-text.mjs +0 -8
- package/web/app/shims/web-push.mjs +0 -14
- package/web/app/signup.mjs +0 -357
- package/web/app/sw-src.mjs +0 -220
- package/web/app/update.js +0 -70
- package/web/front/admin.html +0 -55
- package/web/front/admin.js +0 -89
- package/web/front/install.sh +0 -69
- package/web/front/new-account.html +0 -35
- package/web/front/new-account.js +0 -13
- package/web/front/notices.html +0 -69
- package/web/front/notices.js +0 -107
- package/web/front/run.html +0 -64
- package/web/front/run.js +0 -221
- package/web/front/solid-oidc-client.js +0 -6
- package/web/icons/forum.svg +0 -11
- package/web/icons/gateway.svg +0 -14
- package/web/icons/local.svg +0 -11
|
@@ -1,649 +0,0 @@
|
|
|
1
|
-
// activities.mjs — one handler per inbound activity type. Each is reached
|
|
2
|
-
// from Intake.handle() after the sender passed the door checks, and returns a
|
|
3
|
-
// rejection reason or nothing.
|
|
4
|
-
|
|
5
|
-
import { dropFollower } from '../store.mjs';
|
|
6
|
-
import { emojisOf, pollOf, isContentType, trimActivity, reactionOf, quoteOf, quotePolicyOf, authorOf } from './activity.mjs';
|
|
7
|
-
|
|
8
|
-
// How long a follower's refollow waits before it is answered again.
|
|
9
|
-
const REACCEPT_MS = 60 * 60 * 1000;
|
|
10
|
-
|
|
11
|
-
const idOf = (v) => (typeof v === 'string' ? v : v?.id);
|
|
12
|
-
|
|
13
|
-
// §7.6 Add / §7.9 Remove. The side effect would be to add or remove the object
|
|
14
|
-
// to/from the collection named in `target` — but only a collection we own AND
|
|
15
|
-
// that the sender is authorised to modify. No remote is granted write to our
|
|
16
|
-
// collections (membership is Follow/Undo, pins are ours to set), so there is
|
|
17
|
-
// nothing an inbound Add or Remove may change here. It is a valid activity,
|
|
18
|
-
// not garbage: acknowledge it, make no change, and never dead-letter it.
|
|
19
|
-
export function onAddRemove(intake, activity, actor) {
|
|
20
|
-
const target = typeof activity.target === 'string' ? activity.target : activity.target?.id;
|
|
21
|
-
const ours = target && [intake.urls.followers, intake.urls.following, intake.urls.featured]
|
|
22
|
-
.filter(Boolean).includes(target);
|
|
23
|
-
intake.log(ours
|
|
24
|
-
? `${activity.type} from ${actor} targets our ${target} — no remote may modify it; acknowledged`
|
|
25
|
-
: `${activity.type} from ${actor} targets ${target || 'no collection of ours'}; nothing here to change`);
|
|
26
|
-
return; // accepted, no side effect
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function onFollow(intake, activity, actor, { trusted = false } = {}) {
|
|
30
|
-
const doc = await intake.fetchAP(actor); // origin must vouch for the actor
|
|
31
|
-
if (!doc) return `actor fetch failed (${actor})`;
|
|
32
|
-
if (doc.id !== actor) return `actor id mismatch (${actor} vs ${doc.id})`;
|
|
33
|
-
if (!doc.inbox) return `actor has no inbox (${actor})`;
|
|
34
|
-
const contacts = intake.store.getContacts();
|
|
35
|
-
const existing = contacts.followers.find(f => f.actor === actor);
|
|
36
|
-
// NOTHING binds a delivered Follow to the actor it names. LDN bodies carry
|
|
37
|
-
// no signature, and unlike Create, Delete and Update there is no object at
|
|
38
|
-
// the origin to re-fetch and compare — dereferencing the actor proves only
|
|
39
|
-
// that the actor EXISTS. So anyone at all could Append a Follow naming
|
|
40
|
-
// anyone at all, and we would sign an Accept, deliver it to that person,
|
|
41
|
-
// and send them everything published from then on.
|
|
42
|
-
//
|
|
43
|
-
// Until deliveries terminate somewhere their signature survives, a follow
|
|
44
|
-
// we cannot verify is a REQUEST, waiting in the same queue a gated group
|
|
45
|
-
// uses. The requester's client shows "Requested", which is the ordinary
|
|
46
|
-
// locked-account state that manuallyApprovesFollowers tells it to expect.
|
|
47
|
-
// `autoAcceptFollows: true` in config restores the old behaviour.
|
|
48
|
-
// A GROUP is left alone: `approveJoins: false` is its operator saying, in
|
|
49
|
-
// as many words, that anyone may join, and mute/eject are the remedy there.
|
|
50
|
-
// A person has no such setting, so this is their default.
|
|
51
|
-
//
|
|
52
|
-
// A gateway-verified Follow (trust mode) is no longer unverifiable — the
|
|
53
|
-
// door proved the sender — so it does not need the OK that unverifiability
|
|
54
|
-
// alone demanded. An explicit `approveJoins` still holds: verified or not,
|
|
55
|
-
// the operator asked to see joins.
|
|
56
|
-
const unverifiedNeedsOk = intake.config.kind !== 'group' && !intake.config.autoAcceptFollows && !trusted;
|
|
57
|
-
const mustApprove = intake.config.approveJoins || unverifiedNeedsOk;
|
|
58
|
-
if (mustApprove && !existing) {
|
|
59
|
-
const reqs = intake.store.getRequests();
|
|
60
|
-
if (!reqs.some(r => r.actor === actor)) {
|
|
61
|
-
reqs.unshift({
|
|
62
|
-
actor, inbox: doc.inbox, sharedInbox: doc.endpoints?.sharedInbox,
|
|
63
|
-
activity: trimActivity(activity), at: new Date().toISOString(),
|
|
64
|
-
});
|
|
65
|
-
intake.store.setRequests(reqs.slice(0, 500));
|
|
66
|
-
intake.store.addNotification({ type: 'follow-request', actor });
|
|
67
|
-
await intake.republish({ pending: true });
|
|
68
|
-
}
|
|
69
|
-
intake.log(`join requested: ${actor}`);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (existing) {
|
|
73
|
-
// Deliberately NOT updating followId. An inbound Follow is unverifiable —
|
|
74
|
-
// that is what the queue above exists for — so letting one rewrite the id
|
|
75
|
-
// of a follower we already hold hands an attacker the exact value onUndo
|
|
76
|
-
// matches on: POST a Follow naming any follower in the published
|
|
77
|
-
// collection, then POST an Undo naming the id you just chose, and they are
|
|
78
|
-
// gone permanently. A genuine refollow needs nothing from us but the
|
|
79
|
-
// Accept below, which is idempotent.
|
|
80
|
-
} else {
|
|
81
|
-
// A Bluesky member who bridges later arrives here as a second, different
|
|
82
|
-
// actor: the bridge follows on their behalf from bsky.brid.gy/ap/<did>,
|
|
83
|
-
// while the native join is recorded under bsky.app/profile/<did>. Left
|
|
84
|
-
// alone that is one person listed twice, carried twice, and ejectable
|
|
85
|
-
// only half at a time. The bridged record supersedes the native one —
|
|
86
|
-
// it reaches the fediverse side, which the native one never could.
|
|
87
|
-
const bridgedDid = /^https:\/\/bsky\.brid\.gy\/ap\/(did:[^/]+)$/.exec(actor)?.[1];
|
|
88
|
-
if (bridgedDid) {
|
|
89
|
-
const before = contacts.followers.length;
|
|
90
|
-
contacts.followers = contacts.followers.filter(f => f.bsky?.did !== bridgedDid);
|
|
91
|
-
if (contacts.followers.length < before) {
|
|
92
|
-
intake.log(`bluesky member ${bridgedDid} is bridged now — the native record gives way to it`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
contacts.followers.push({
|
|
96
|
-
actor, inbox: doc.inbox, sharedInbox: doc.endpoints?.sharedInbox, followId: activity.id,
|
|
97
|
-
...(bridgedDid ? { bsky: { did: bridgedDid, bridged: true } } : {}),
|
|
98
|
-
});
|
|
99
|
-
intake.store.setContacts(contacts);
|
|
100
|
-
intake.store.addNotification({ type: 'follow', actor });
|
|
101
|
-
await intake.republish({ followers: true });
|
|
102
|
-
intake.log(`new follower: ${actor}`);
|
|
103
|
-
}
|
|
104
|
-
// A refollow from someone already following is answered again — their server
|
|
105
|
-
// may have lost our Accept — but not every time: the same Follow sent over and
|
|
106
|
-
// over would otherwise make us send, and record, an Accept for each one.
|
|
107
|
-
if (existing) {
|
|
108
|
-
const c = intake.store.getContacts();
|
|
109
|
-
const f = c.followers.find(x => x.actor === actor);
|
|
110
|
-
if (f?.acceptedAt && Date.now() - Date.parse(f.acceptedAt) < REACCEPT_MS) {
|
|
111
|
-
intake.log(`refollow from ${actor} — already accepted within the hour, not answered again`);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (f) { f.acceptedAt = new Date().toISOString(); intake.store.setContacts(c); }
|
|
115
|
-
}
|
|
116
|
-
const { acceptActivity } = await import('../wire.mjs');
|
|
117
|
-
await intake.deliverer.deliver(doc.inbox,
|
|
118
|
-
acceptActivity({ urls: intake.urls, followActivity: activity, serial: intake.serial++ }));
|
|
119
|
-
intake.log(`Accept sent → ${doc.inbox}`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export async function onUndo(intake, activity, actor, { trusted = false } = {}) {
|
|
123
|
-
const inner = activity.object && typeof activity.object === 'object' ? activity.object : null;
|
|
124
|
-
if (inner?.type === 'Like' || inner?.type === 'Announce' || inner?.type === 'EmojiReact') return onUndoReaction(intake, inner, actor, { trusted });
|
|
125
|
-
// AS2 allows `object` to be a bare IRI, and that IRI is exactly the Follow
|
|
126
|
-
// id we stored. Reading `.type` off a string gives undefined, so the whole
|
|
127
|
-
// Undo was dropped — silently, since handle() reads that as handled, so no
|
|
128
|
-
// dead letter was kept and the item was DELETEd. The follower stayed, we
|
|
129
|
-
// kept delivering to them, and their server had recorded the unfollow as
|
|
130
|
-
// done and would never resend. Only a TYPED non-Follow is not ours.
|
|
131
|
-
if (typeof activity.object === 'object' && activity.object?.type
|
|
132
|
-
&& activity.object.type !== 'Follow') return;
|
|
133
|
-
// And it must NAME something. Widening the type test to admit a bare IRI
|
|
134
|
-
// also admitted `object: undefined`, `null` and `{}` — which land on the
|
|
135
|
-
// no-followId carve-out below and evict, which is the very hole the
|
|
136
|
-
// followId check was added to close. An Undo that identifies nothing is
|
|
137
|
-
// not an Undo of ours.
|
|
138
|
-
const named = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
139
|
-
// A gateway-verified Undo need not name a stored id — the door proved the
|
|
140
|
-
// sender, so an Undo{Follow} that names us as its object is enough. An
|
|
141
|
-
// unverified one must still identify something (the eviction-hole guard).
|
|
142
|
-
if (!named && !trusted) return;
|
|
143
|
-
// Deliveries arrive unordered: an Undo may land AFTER the refollow it
|
|
144
|
-
// predates. It names the Follow id it revokes — only honor it when it
|
|
145
|
-
// matches the follow we currently hold for that actor.
|
|
146
|
-
const undoneId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
147
|
-
const contacts = intake.store.getContacts();
|
|
148
|
-
const rec = contacts.followers.find(f => f.actor === actor);
|
|
149
|
-
// Withdrawing a request that was never answered: drop it, or it sits in the
|
|
150
|
-
// operator's queue forever asking about someone who left.
|
|
151
|
-
if (!rec) {
|
|
152
|
-
const reqs = intake.store.getRequests();
|
|
153
|
-
const pending = reqs.find(r => r.actor === actor);
|
|
154
|
-
if (pending) {
|
|
155
|
-
// Bound the same way the follower eviction below is, and for the same
|
|
156
|
-
// reason: `actor` is a field in an unsigned body, so without a match
|
|
157
|
-
// anyone could withdraw anyone else's waiting request. The victim's
|
|
158
|
-
// server believes the Follow is still pending and never resends, so
|
|
159
|
-
// they simply never get followed and nobody sees why.
|
|
160
|
-
const theirs = pending.activity?.id;
|
|
161
|
-
if (!trusted && (!theirs || named !== theirs)) {
|
|
162
|
-
intake.log(`Undo from ${actor} does not name the request we hold — ignored`);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
intake.store.setRequests(reqs.filter(r => r.actor !== actor));
|
|
166
|
-
await intake.republish({ pending: true });
|
|
167
|
-
intake.log(`join request withdrawn: ${actor}`);
|
|
168
|
-
}
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
// An Undo must NAME the Follow it revokes, and name the one we hold.
|
|
172
|
-
//
|
|
173
|
-
// The follow id is the ONLY thing binding an Undo to the follower. LDN
|
|
174
|
-
// bodies carry no signature, and unlike every other inbound type this path
|
|
175
|
-
// dereferences nothing, so there is no origin to disagree. Matching works
|
|
176
|
-
// because the id was chosen by their server and delivered in a Follow we
|
|
177
|
-
// accepted: we publish the followers collection, but never the ids.
|
|
178
|
-
//
|
|
179
|
-
// Which means a record with NO id cannot be matched at all — and the
|
|
180
|
-
// carve-out that used to let those through turned "we cannot tell" into
|
|
181
|
-
// "anyone may evict". reconcileFollowers writes exactly such records when a
|
|
182
|
-
// restored machine recovers its followers from the pod, so after a restore
|
|
183
|
-
// every follower could be removed by one unauthenticated POST, permanently:
|
|
184
|
-
// dropFollower leaves a mark and the next reconcile will not bring them
|
|
185
|
-
// back, their server recorded no unfollow so it never resends, and neither
|
|
186
|
-
// side has anything to notice.
|
|
187
|
-
//
|
|
188
|
-
// Unmatchable is refused now. The cost is a follower who really did leave
|
|
189
|
-
// staying on the list until the operator ejects them, which is the right way
|
|
190
|
-
// round: `eject` is one command, and the alternative was silent, permanent,
|
|
191
|
-
// and available to anyone.
|
|
192
|
-
// A gateway-verified Undo carries the sender's proof, so it is honored on
|
|
193
|
-
// its own — the followId match below exists only because an UNVERIFIED Undo
|
|
194
|
-
// is otherwise unbindable. A verified one needs no such crutch.
|
|
195
|
-
if (trusted) {
|
|
196
|
-
dropFollower(contacts, actor, 'undo-follow');
|
|
197
|
-
intake.store.setContacts(contacts);
|
|
198
|
-
await intake.republish({ followers: true });
|
|
199
|
-
intake.log(`unfollowed by ${actor} (gateway-verified)`);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (!rec.followId) {
|
|
203
|
-
intake.log(`Undo from ${actor} cannot be matched — this follower was `
|
|
204
|
-
+ `${rec.recovered ? 'recovered from the pod' : 'recorded before follow ids were kept'}, `
|
|
205
|
-
+ `so its follow id is unknown. Ignored; \`fedipod eject ${actor}\` if they did leave.`);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
if (undoneId !== rec.followId) {
|
|
209
|
-
intake.log(`Undo from ${actor} does not name the follow we hold `
|
|
210
|
-
+ `(revokes ${undoneId || 'nothing'}, current is ${rec.followId}) — ignored`);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
dropFollower(contacts, actor, 'undo-follow');
|
|
214
|
-
intake.store.setContacts(contacts);
|
|
215
|
-
await intake.republish({ followers: true });
|
|
216
|
-
intake.log(`unfollowed by ${actor}`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export async function onCreate(intake, activity, actor, { trusted = false } = {}) {
|
|
220
|
-
const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
221
|
-
if (!objectId) return 'Create without object id';
|
|
222
|
-
if (intake.store.isBlocked(objectId)) return `blocked domain (${objectId})`;
|
|
223
|
-
if (!intake.sameIdentity(objectId, actor)) return `object/actor identity mismatch (${objectId})`;
|
|
224
|
-
// The delivered copy is untrusted for CONTENT, but its addressing is
|
|
225
|
-
// enough to decide whether to bother fetching the origin's copy.
|
|
226
|
-
const envelope = typeof activity.object === 'object' ? { ...activity, ...activity.object } : activity;
|
|
227
|
-
if (!intake.concernsUs(envelope, actor)) return `not addressed to us (${objectId})`;
|
|
228
|
-
// The check onAnnounce has had all along. A re-delivered Create — a remote
|
|
229
|
-
// retry, a group fan-out, or our own sweep seeing an item whose DELETE was
|
|
230
|
-
// refused — cost a fresh signed GET to the origin and rewrote the private
|
|
231
|
-
// RDF note every time, because addStatus only dedupes AFTER the deref.
|
|
232
|
-
// Gated around the INGEST alone: a group must still reach amplify below,
|
|
233
|
-
// which is separately idempotent on announcedAt.
|
|
234
|
-
// Only a status that came from an INGEST counts as already done. TagFeed
|
|
235
|
-
// writes a bare `kind:'tag'` row straight into the index — no pod RDF note,
|
|
236
|
-
// no mention notification, no replies-collection entry — so treating that
|
|
237
|
-
// as ingested loses all three when the same note is then delivered to us.
|
|
238
|
-
const ingested = intake.store.getStatuses()
|
|
239
|
-
.some(x => x.noteId === objectId && (x.kind === 'timeline' || x.kind === 'mention'));
|
|
240
|
-
if (!ingested) {
|
|
241
|
-
// A verified delivery that carries the note inline is read from that
|
|
242
|
-
// copy: the door checked the sender's signature over these very bytes,
|
|
243
|
-
// and the receipt vouches for this actor. That is the only way a direct
|
|
244
|
-
// or followers-only post can ever be read — it lives in the sender's
|
|
245
|
-
// owner-only container, and fetching it from there answers 401 to
|
|
246
|
-
// everyone. An unverified delivery still fetches, as it always did.
|
|
247
|
-
const inline = trusted && typeof activity.object === 'object' && activity.object?.id === objectId
|
|
248
|
-
? activity.object : null;
|
|
249
|
-
const rejected = await intake.ingestNote(objectId, actor, { inline });
|
|
250
|
-
if (rejected) return rejected;
|
|
251
|
-
}
|
|
252
|
-
// A group carries its members' posts onward. Only reached from Create, so an
|
|
253
|
-
// inbound Announce is never re-announced. The activity is passed through
|
|
254
|
-
// untouched — FEP-1b12 wants the original wrapped, not a summary of it.
|
|
255
|
-
if (intake.config.kind === 'group') await intake.amplify(objectId, { activity });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// A boost: ingest the boosted note when the booster is someone we follow —
|
|
259
|
-
// that's what following means, their boosts widen the timeline. Anything
|
|
260
|
-
// else is unsolicited and only logged.
|
|
261
|
-
// A group we follow announces a Delete: the carrier moderating away a post
|
|
262
|
-
// it carried. Honored only within what the carry itself established — the
|
|
263
|
-
// announcer is a group we follow AND the post reached us via that same
|
|
264
|
-
// group — so no new party is trusted and nothing is dereferenced. Our own
|
|
265
|
-
// posts are never removed by anyone's moderation.
|
|
266
|
-
export async function onAnnouncedDelete(intake, actor, del) {
|
|
267
|
-
const followed = intake.store.getContacts().following.some(f => f.actor === actor && f.accepted);
|
|
268
|
-
if (!followed) { intake.log(`announced Delete from unfollowed ${actor} — ignored`); return; }
|
|
269
|
-
const targetId = typeof del.object === 'string' ? del.object : del.object?.id;
|
|
270
|
-
if (!targetId) return 'announced Delete without an object';
|
|
271
|
-
const s = intake.store.getStatuses().find(x => x.noteId === targetId);
|
|
272
|
-
if (!s || s.kind === 'post' || s.via !== actor) return;
|
|
273
|
-
await intake.forget(s);
|
|
274
|
-
intake.log(`moderated away by ${actor}: ${targetId}`);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export async function onAnnounce(intake, activity, actor, objectId) {
|
|
278
|
-
if (!objectId) return 'Announce without object id';
|
|
279
|
-
const followed = intake.store.getContacts().following.some(f => f.actor === actor && f.accepted);
|
|
280
|
-
if (!followed) { intake.log(`Announce from unfollowed ${actor} — ignored`); return; }
|
|
281
|
-
if (intake.store.isBlocked(objectId)) return `blocked domain (${objectId})`;
|
|
282
|
-
const existing = intake.store.getStatuses().find(s => s.noteId === objectId);
|
|
283
|
-
if (existing) {
|
|
284
|
-
// Known, but possibly as a lesser kind — a stranger's mention, a tag or
|
|
285
|
-
// search mirror — none of which the home timeline shows. A carry from
|
|
286
|
-
// someone we follow is exactly what promotes it there.
|
|
287
|
-
if (!['timeline', 'post'].includes(existing.kind)) {
|
|
288
|
-
intake.store.updateStatus(objectId, { kind: 'timeline', via: actor });
|
|
289
|
-
intake.log(`promoted to timeline (carried by ${actor}): ${objectId}`);
|
|
290
|
-
}
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
return intake.ingestNote(objectId, actor, { via: actor });
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Undo{Like} and Undo{Announce}: the sender unsaying a favourite or a boost.
|
|
297
|
-
// The Like it unsays was recorded on the sender's word alone, so its undoing
|
|
298
|
-
// is taken the same way: the notification it left goes. A boost unsaid by
|
|
299
|
-
// someone we follow takes the post they carried off the timeline — the carry
|
|
300
|
-
// was accepted on that follow, and the retraction is honoured on it.
|
|
301
|
-
export async function onUndoReaction(intake, inner, actor, { trusted = false } = {}) {
|
|
302
|
-
const innerActor = typeof inner.actor === 'string' ? inner.actor : inner.actor?.id;
|
|
303
|
-
if (innerActor && innerActor !== actor) return; // one cannot unsay another's
|
|
304
|
-
const wrapped = inner.object;
|
|
305
|
-
const target = (wrapped && typeof wrapped === 'object'
|
|
306
|
-
&& (wrapped.type === 'Create' || wrapped.type === 'Update')) ? wrapped.object : wrapped;
|
|
307
|
-
const objectId = typeof target === 'string' ? target : target?.id;
|
|
308
|
-
if (!objectId) return;
|
|
309
|
-
if (objectId.startsWith(intake.urls.notes)) {
|
|
310
|
-
// An emoji taken back: the one reaction it names, by that sender.
|
|
311
|
-
const reaction = reactionOf(inner);
|
|
312
|
-
if (reaction) {
|
|
313
|
-
const gone = intake.store.removeNotifications(n => n.type === 'reaction' && n.actor === actor
|
|
314
|
-
&& n.noteId === objectId && n.emoji === reaction.emoji);
|
|
315
|
-
if (gone) intake.log(`reaction ${reaction.emoji} withdrawn by ${actor} on ${objectId}`);
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
if (inner.type === 'EmojiReact') return;
|
|
319
|
-
const type = inner.type === 'Like' ? 'favourite' : 'reblog';
|
|
320
|
-
const gone = intake.store.removeNotifications(n => n.type === type && n.actor === actor && n.noteId === objectId);
|
|
321
|
-
if (gone) intake.log(`${inner.type} withdrawn by ${actor} on ${objectId}`);
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
if (inner.type !== 'Announce') return;
|
|
325
|
-
const followed = trusted || intake.store.getContacts().following.some(f => f.actor === actor && f.accepted);
|
|
326
|
-
if (!followed) return;
|
|
327
|
-
const s = intake.store.getStatuses().find(x => x.noteId === objectId);
|
|
328
|
-
if (!s || s.kind === 'post' || s.via !== actor) return;
|
|
329
|
-
await intake.forget(s);
|
|
330
|
-
intake.log(`boost withdrawn by ${actor}: ${objectId} left the timeline`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Block: the sender wants nothing more to do with this account. Mastodon
|
|
334
|
-
// drops the follow both ways on receipt, and so does this — on a delivery
|
|
335
|
-
// the door verified. A Block carries nothing to check and names nothing we
|
|
336
|
-
// hold, so one taken from the public-append inbox unverified would let
|
|
337
|
-
// anybody evict any follower, the hole the Undo handler closes with the
|
|
338
|
-
// follow id. Unverified: logged, nothing else.
|
|
339
|
-
export async function onBlock(intake, activity, actor, { trusted = false } = {}) {
|
|
340
|
-
const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
341
|
-
if (objectId !== intake.urls.actor) return;
|
|
342
|
-
if (!trusted) { intake.log(`Block from ${actor} arrived unverified — ignored`); return; }
|
|
343
|
-
const contacts = intake.store.getContacts();
|
|
344
|
-
const wasFollower = contacts.followers.some(f => f.actor === actor);
|
|
345
|
-
const wasFollowing = contacts.following.some(f => f.actor === actor);
|
|
346
|
-
if (wasFollower) dropFollower(contacts, actor, 'blocked-us');
|
|
347
|
-
contacts.following = contacts.following.filter(f => f.actor !== actor);
|
|
348
|
-
intake.store.setContacts(contacts);
|
|
349
|
-
const reqs = intake.store.getRequests();
|
|
350
|
-
const left = reqs.filter(r => r.actor !== actor);
|
|
351
|
-
if (left.length !== reqs.length) intake.store.setRequests(left);
|
|
352
|
-
await intake.republish({ followers: wasFollower, following: wasFollowing, pending: true });
|
|
353
|
-
const what = [wasFollower && 'their follow of us', wasFollowing && 'our follow of them'].filter(Boolean).join(' and ');
|
|
354
|
-
intake.log(`blocked by ${actor}: ${what || 'no relationship'} dropped`);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Mastodon sends these constantly; ignoring them left deleted posts standing
|
|
358
|
-
// for good. Two guards, because a forged Delete would otherwise erase anyone's
|
|
359
|
-
// content: it must come from the object's own origin, and the object must
|
|
360
|
-
// really be gone there. An origin we cannot reach is a retry, never a delete.
|
|
361
|
-
export async function onDelete(intake, activity, actor) {
|
|
362
|
-
const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
363
|
-
if (!objectId) return 'Delete without object id';
|
|
364
|
-
if (!intake.sameIdentity(objectId, actor)) return `Delete crosses identities (${objectId})`;
|
|
365
|
-
// `objectId === actor` is NOT evidence we care: it is true of EVERY account
|
|
366
|
-
// deletion, and Mastodon broadcasts those constantly. Taking it as known
|
|
367
|
-
// meant a signed dereference to a stranger's server for each one.
|
|
368
|
-
if (!intake.known(objectId)) return; // nothing of ours to remove
|
|
369
|
-
const gone = await intake.isGone(objectId);
|
|
370
|
-
if (gone === null) throw new Error(`cannot confirm ${objectId} is gone — will retry`);
|
|
371
|
-
if (!gone) return `Delete for something still published (${objectId})`;
|
|
372
|
-
|
|
373
|
-
if (objectId === actor) { // the account itself
|
|
374
|
-
const contacts = intake.store.getContacts();
|
|
375
|
-
dropFollower(contacts, actor, 'account-deleted');
|
|
376
|
-
contacts.following = contacts.following.filter(f => f.actor !== actor);
|
|
377
|
-
intake.store.setContacts(contacts);
|
|
378
|
-
// One publish for the lot. Each forget() used to run its own
|
|
379
|
-
// unrecordOutbox, and each of those republished the outbox — so a group
|
|
380
|
-
// that had carried M of this actor's posts paid M full page sweeps for a
|
|
381
|
-
// single inbox item. The Undo deliveries stay per-Announce, because each
|
|
382
|
-
// Announce needs its own; only the pod write is collected.
|
|
383
|
-
const retracted = [];
|
|
384
|
-
for (const s of intake.store.getStatuses().filter(s => s.actor === actor)) {
|
|
385
|
-
await intake.forget(s, { collect: retracted });
|
|
386
|
-
}
|
|
387
|
-
if (retracted.length) {
|
|
388
|
-
const gone = new Set(retracted.map(u => u.object.id));
|
|
389
|
-
await intake.publisher.unrecordOutbox(i => gone.has(i?.id),
|
|
390
|
-
{ record: (was) => (intake.config.private ? null : retracted.filter(u => was.includes(u.object.id))) });
|
|
391
|
-
}
|
|
392
|
-
// Both: an account deletion drops them from followers AND following.
|
|
393
|
-
await intake.republish({ followers: true, following: true });
|
|
394
|
-
intake.log(`account deleted upstream: ${actor}`);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const s = intake.store.getStatuses().find(x => x.noteId === objectId);
|
|
398
|
-
if (s) await intake.forget(s);
|
|
399
|
-
if (s && intake.onCarriedGone) {
|
|
400
|
-
await intake.onCarriedGone({ noteId: objectId, actor }).catch(e => intake.log(`carried gone ${objectId}: ${e.message}`));
|
|
401
|
-
}
|
|
402
|
-
intake.log(`deleted upstream: ${objectId}`);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// An edited post, or a changed profile. Verified the only way we can: by
|
|
406
|
-
// refetching at the origin and believing that, not the delivered copy.
|
|
407
|
-
export async function onUpdate(intake, activity, actor) {
|
|
408
|
-
const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
409
|
-
if (!objectId) return 'Update without object id';
|
|
410
|
-
if (!intake.sameIdentity(objectId, actor)) return `Update crosses identities (${objectId})`;
|
|
411
|
-
if (objectId === actor) { // display name, avatar, bio
|
|
412
|
-
// The same guard onDelete has, for the same reason: the inbox is
|
|
413
|
-
// public-Append, so without it anyone can name any host and make us spend
|
|
414
|
-
// a signed GET on it — and because the failure below THROWS rather than
|
|
415
|
-
// returning a rejection, one planted item buys five of them, plus five
|
|
416
|
-
// pod reads and the head of the inbox held for five sweeps.
|
|
417
|
-
if (!intake.known(actor)) return; // nothing of ours to update
|
|
418
|
-
const doc = await intake.fetchAP(actor);
|
|
419
|
-
if (!doc) throw new Error(`cannot refetch ${actor} — will retry`);
|
|
420
|
-
intake.store.cacheActor(actor, doc); // fetchAP caches Persons; Groups too
|
|
421
|
-
intake.log(`profile updated: ${actor}`);
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
const s = intake.store.getStatuses().find(x => x.noteId === objectId);
|
|
425
|
-
if (!s) return; // not one we hold
|
|
426
|
-
const note = await intake.fetchAP(objectId);
|
|
427
|
-
if (!note) throw new Error(`cannot refetch ${objectId} — will retry`);
|
|
428
|
-
if (note.id !== objectId || !isContentType(note.type)) return `object not verifiable content (${objectId}, ${note.type})`;
|
|
429
|
-
const { attachmentsOf, titledContent } = await import('../wire.mjs');
|
|
430
|
-
const content = titledContent(note);
|
|
431
|
-
const attachments = attachmentsOf(note);
|
|
432
|
-
const freshPoll = pollOf(note);
|
|
433
|
-
const freshEmojis = emojisOf(note);
|
|
434
|
-
// A quote's authorization arrives on an Update, once the quoted author
|
|
435
|
-
// has answered: the quote is checked again, and the policy follows the edit.
|
|
436
|
-
const quote = quoteOf(note);
|
|
437
|
-
const quoteState = quote ? await intake.quoteStateOf(note, quote) : null;
|
|
438
|
-
const quotePolicy = quotePolicyOf(note, s.actor);
|
|
439
|
-
intake.store.updateStatus(objectId, {
|
|
440
|
-
content, ...(attachments.length ? { attachments } : {}),
|
|
441
|
-
emojis: freshEmojis.length ? freshEmojis : undefined,
|
|
442
|
-
quote: quote || undefined, quoteState: quoteState || undefined,
|
|
443
|
-
quotePolicy: quotePolicy || undefined,
|
|
444
|
-
// The edit's own stamp when the note carries one; tallies and the
|
|
445
|
-
// content warning follow the edit too. A poll refresh keeps our vote.
|
|
446
|
-
editedAt: note.updated || new Date().toISOString(),
|
|
447
|
-
spoiler: note.summary ? String(note.summary).replace(/<[^>]*>/g, '') : undefined,
|
|
448
|
-
...(freshPoll ? {
|
|
449
|
-
poll: { ...freshPoll, voted: !!s.poll?.voted, ownVotes: s.poll?.ownVotes || [] },
|
|
450
|
-
} : {}),
|
|
451
|
-
});
|
|
452
|
-
if (intake.onCarriedEdit) {
|
|
453
|
-
await intake.onCarriedEdit({ noteId: objectId, actor, note }).catch(e => intake.log(`carried edit ${objectId}: ${e.message}`));
|
|
454
|
-
}
|
|
455
|
-
intake.log(`edited upstream: ${objectId}`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// The other answer to a Follow, and it was dropped on the floor. Their server
|
|
459
|
-
// has recorded that we do not follow them; ours went on saying we did, and
|
|
460
|
-
// published it — so the two disagreed permanently, and a retry would never
|
|
461
|
-
// come because as far as they are concerned the question was answered.
|
|
462
|
-
//
|
|
463
|
-
// It has to answer the Follow we actually SENT. Only the type was checked, so
|
|
464
|
-
// one Append per account you follow — from anyone, naming no particular
|
|
465
|
-
// follow — severed every one of them at once, and silently: their server
|
|
466
|
-
// never hears about it, so nothing ever retries and nothing looks wrong until
|
|
467
|
-
// the timeline goes quiet. `followActivity` is stored by followActor
|
|
468
|
-
// (lib/social.mjs) for exactly this kind of comparison.
|
|
469
|
-
export async function onReject(intake, activity, actor, { trusted = false } = {}) {
|
|
470
|
-
const asked = quoteAsked(intake, activity);
|
|
471
|
-
if (asked) return onQuoteAnswered(intake, asked, activity, actor, 'Reject');
|
|
472
|
-
if (activity.object?.type && activity.object.type !== 'Follow') return;
|
|
473
|
-
const contacts = intake.store.getContacts();
|
|
474
|
-
const rec = contacts.following.find(f => f.actor === actor);
|
|
475
|
-
if (!rec) return; // nothing of ours to undo
|
|
476
|
-
const named = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
477
|
-
const ours = rec.followActivity?.id;
|
|
478
|
-
// A gateway receipt bound to this actor is the other way to believe it —
|
|
479
|
-
// the door checked a signature and said whose (see receiptVouchesFor).
|
|
480
|
-
if (!trusted) {
|
|
481
|
-
if (!ours) {
|
|
482
|
-
intake.log(`Reject from ${actor}: no follow id on record to match it against — ignored`);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
if (named !== ours) {
|
|
486
|
-
intake.log(`Reject from ${actor} answers ${named || 'nothing'}, not the follow we sent — ignored`);
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
contacts.following = contacts.following.filter(f => f.actor !== actor);
|
|
491
|
-
intake.store.setContacts(contacts);
|
|
492
|
-
await intake.republish({ following: true, pending: true });
|
|
493
|
-
intake.log(`follow rejected by ${actor}`);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Someone we follow has moved. Their server will stop delivering from the old
|
|
497
|
-
// actor, so without this we keep an entry that can never produce another post
|
|
498
|
-
// and never learn where they went. The new account is not followed
|
|
499
|
-
// automatically — that is a Follow only the owner should send — but it is
|
|
500
|
-
// recorded and raised, so it can be acted on.
|
|
501
|
-
export async function onMove(intake, activity, actor) {
|
|
502
|
-
const target = typeof activity.target === 'string' ? activity.target : activity.target?.id;
|
|
503
|
-
if (!target) return 'Move without a target';
|
|
504
|
-
const contacts = intake.store.getContacts();
|
|
505
|
-
const rec = contacts.following.find(f => f.actor === actor);
|
|
506
|
-
if (!rec) return; // not someone we follow
|
|
507
|
-
// Believed only if the actor we follow says so at its OWN origin: a Move is
|
|
508
|
-
// otherwise a redirect anyone could Append.
|
|
509
|
-
const doc = await intake.fetchAP(actor);
|
|
510
|
-
if (!doc) throw new Error(`cannot confirm ${actor} moved — will retry`);
|
|
511
|
-
const movedTo = typeof doc.movedTo === 'string' ? doc.movedTo : doc.movedTo?.id;
|
|
512
|
-
if (movedTo !== target) return `Move not corroborated by ${actor} (says ${movedTo || 'nothing'})`;
|
|
513
|
-
rec.movedTo = target;
|
|
514
|
-
intake.store.setContacts(contacts);
|
|
515
|
-
intake.store.addNotification({ type: 'move', actor, target });
|
|
516
|
-
intake.log(`${actor} moved to ${target} — follow the new account to keep seeing them`);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
export async function onAccept(intake, activity, actor, { trusted = false } = {}) {
|
|
520
|
-
const asked = quoteAsked(intake, activity);
|
|
521
|
-
if (asked) return onQuoteAnswered(intake, asked, activity, actor, 'Accept');
|
|
522
|
-
const contacts = intake.store.getContacts();
|
|
523
|
-
const rec = contacts.following.find(f => f.actor === actor);
|
|
524
|
-
// It has to answer the Follow we actually sent. followActor stores that
|
|
525
|
-
// activity for the later Undo, so the id is here to compare against; without
|
|
526
|
-
// the check any Accept from an actor we happen to follow flips the flag,
|
|
527
|
-
// including one answering a Follow we never made.
|
|
528
|
-
const named = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
529
|
-
const ours = rec?.followActivity?.id;
|
|
530
|
-
// `named &&` used to be part of this, so an Accept naming NOTHING sailed
|
|
531
|
-
// past — which is the easy one to send, and it marks a request to a locked
|
|
532
|
-
// account as accepted when it is still sitting in their queue.
|
|
533
|
-
if (ours && named !== ours && !trusted) {
|
|
534
|
-
intake.log(`Accept from ${actor} answers ${named || 'nothing'}, not the follow we sent — ignored`);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
if (rec && !rec.accepted) {
|
|
538
|
-
rec.accepted = true;
|
|
539
|
-
intake.store.setContacts(contacts);
|
|
540
|
-
await intake.republish({ following: true, pending: true });
|
|
541
|
-
intake.log(`follow accepted by ${actor}`);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// --- quotes (FEP-044f) ---
|
|
546
|
-
|
|
547
|
-
// The post of ours whose quote request this Accept or Reject answers, if any.
|
|
548
|
-
// Matched on the request id we sent, which only the quoted author was given.
|
|
549
|
-
function quoteAsked(intake, activity) {
|
|
550
|
-
const named = idOf(activity.object);
|
|
551
|
-
if (!named) return null;
|
|
552
|
-
return intake.store.getStatuses().find(x => x.kind === 'post' && x.quoteRequest?.id === named) || null;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// The quoted author's answer. A yes carries their authorization, a document
|
|
556
|
-
// at their origin naming our post and theirs — fetched and compared, not
|
|
557
|
-
// believed from the envelope; then the note says so to everyone who has it.
|
|
558
|
-
// A no takes the quote off the note.
|
|
559
|
-
async function onQuoteAnswered(intake, s, activity, actor, type) {
|
|
560
|
-
if (actor !== s.quoteRequest?.actor) {
|
|
561
|
-
intake.log(`${type} of our quote request from ${actor}, who is not ${s.quoteRequest?.actor} — ignored`);
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
if (type === 'Reject') {
|
|
565
|
-
if (s.quoteState === 'rejected') return;
|
|
566
|
-
await intake.publisher.withdrawQuote(s);
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
if (s.quoteState === 'accepted') return;
|
|
570
|
-
const resultId = idOf(activity.result);
|
|
571
|
-
if (!resultId || !intake.sameIdentity(resultId, actor)) return `Accept of a quote request carries no authorization at ${actor}`;
|
|
572
|
-
const auth = await intake.fetchAP(resultId);
|
|
573
|
-
if (!auth) throw new Error(`cannot fetch quote authorization ${resultId} — will retry`);
|
|
574
|
-
const ok = auth.id === resultId && auth.type === 'QuoteAuthorization'
|
|
575
|
-
&& [].concat(auth.attributedTo || []).map(idOf).includes(actor)
|
|
576
|
-
&& idOf(auth.interactingObject) === s.noteId && idOf(auth.interactionTarget) === s.quote;
|
|
577
|
-
if (!ok) return `quote authorization ${resultId} does not say ${s.noteId} may quote ${s.quote}`;
|
|
578
|
-
await intake.publisher.authorizeQuote(s, resultId);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Someone asks to quote one of our posts. Their post is read at its origin,
|
|
582
|
-
// like every note that arrives — it has to be theirs, and has to quote ours.
|
|
583
|
-
// A post the world can read may be quoted by anyone; a followers-only or
|
|
584
|
-
// direct one by nobody. The answer is an Accept carrying our authorization,
|
|
585
|
-
// written beside the post, or a Reject. The quoting post is then kept the
|
|
586
|
-
// way a mention of ours is, under the one notification a quote raises.
|
|
587
|
-
export async function onQuoteRequest(intake, activity, actor) {
|
|
588
|
-
const objectId = idOf(activity.object);
|
|
589
|
-
const instrumentId = idOf(activity.instrument);
|
|
590
|
-
if (!objectId || !instrumentId) return 'QuoteRequest without object or instrument';
|
|
591
|
-
// A followers-only post lives in the private container; a follower may
|
|
592
|
-
// know its id and ask. It is ours, and the answer below is no.
|
|
593
|
-
const ours = objectId.startsWith(intake.urls.notes)
|
|
594
|
-
|| (intake.urls.privateNotes && objectId.startsWith(intake.urls.privateNotes));
|
|
595
|
-
if (!ours) {
|
|
596
|
-
intake.log(`QuoteRequest from ${actor} names ${objectId}, not a post of ours — ignored`);
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
if (!intake.sameIdentity(instrumentId, actor)) return `QuoteRequest instrument/actor identity mismatch (${instrumentId})`;
|
|
600
|
-
const s = intake.store.getStatuses().find(x => x.noteId === objectId && x.kind === 'post');
|
|
601
|
-
if (!s) return `QuoteRequest for a post we do not hold (${objectId})`;
|
|
602
|
-
// Not retried on failure: the instrument is a URL a stranger chose.
|
|
603
|
-
const note = await intake.fetchAP(instrumentId);
|
|
604
|
-
if (!note) return `quote request: instrument fetch failed (${instrumentId})`;
|
|
605
|
-
if (note.id !== instrumentId || !isContentType(note.type)) return `quote request: instrument is not verifiable content (${instrumentId})`;
|
|
606
|
-
if (authorOf(note, actor) !== actor) return `quote request: ${instrumentId} is not by ${actor}`;
|
|
607
|
-
if (quoteOf(note) !== objectId) return `quote request: ${instrumentId} does not quote ${objectId}`;
|
|
608
|
-
const doc = await intake.fetchAP(actor);
|
|
609
|
-
const inbox = doc?.endpoints?.sharedInbox || doc?.inbox;
|
|
610
|
-
if (!inbox) return `quote request: no inbox for ${actor}`;
|
|
611
|
-
const { quoteAnswerActivity } = await import('../wire-quotes.mjs');
|
|
612
|
-
const open = !s.visibility || s.visibility === 'public' || s.visibility === 'unlisted';
|
|
613
|
-
if (!open) {
|
|
614
|
-
await intake.deliverer.deliver(inbox,
|
|
615
|
-
quoteAnswerActivity({ urls: intake.urls, type: 'Reject', request: activity, to: actor, serial: intake.serial++ }));
|
|
616
|
-
intake.log(`quote refused: ${instrumentId} asked to quote ${objectId}, which is not public`);
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
const authorization = await intake.publisher.grantQuote(objectId, instrumentId);
|
|
620
|
-
await intake.deliverer.deliver(inbox,
|
|
621
|
-
quoteAnswerActivity({ urls: intake.urls, type: 'Accept', request: activity, result: authorization, to: actor, serial: intake.serial++ }));
|
|
622
|
-
const held = intake.store.getStatuses().find(x => x.noteId === instrumentId);
|
|
623
|
-
if (!held) {
|
|
624
|
-
// The origin's copy, already fetched above.
|
|
625
|
-
const rejected = await intake.ingestNote(instrumentId, actor, { inline: note });
|
|
626
|
-
if (rejected) intake.log(`quote of ${objectId} by ${actor}: ${rejected}`);
|
|
627
|
-
} else if (held.quoteState !== 'accepted') {
|
|
628
|
-
intake.store.updateStatus(instrumentId, { quote: objectId, quoteState: 'accepted' });
|
|
629
|
-
}
|
|
630
|
-
intake.store.addNotification({ type: 'quote', actor, noteId: instrumentId });
|
|
631
|
-
intake.log(`quote allowed: ${instrumentId} quotes ${objectId}`);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// What can be said about a note's quote. The quoted post is ours: allowed if
|
|
635
|
-
// we granted it. Elsewhere: allowed when the note carries an authorization
|
|
636
|
-
// that, fetched from the quoted post's own origin, names this note and that
|
|
637
|
-
// one. Otherwise pending — the honest word for a quote nobody has vouched for.
|
|
638
|
-
export async function quoteStateOf(intake, note, quote) {
|
|
639
|
-
if (quote.startsWith(intake.urls.notes)) {
|
|
640
|
-
const target = intake.store.getStatuses().find(x => x.noteId === quote);
|
|
641
|
-
return (target?.quotedBy || []).some(q => q.note === note.id) ? 'accepted' : 'pending';
|
|
642
|
-
}
|
|
643
|
-
const authId = idOf(note.quoteAuthorization);
|
|
644
|
-
if (!authId || !intake.sameIdentity(authId, quote)) return 'pending';
|
|
645
|
-
const auth = await intake.fetchAP(authId).catch(() => null);
|
|
646
|
-
const ok = auth && auth.id === authId && auth.type === 'QuoteAuthorization'
|
|
647
|
-
&& idOf(auth.interactingObject) === note.id && idOf(auth.interactionTarget) === quote;
|
|
648
|
-
return ok ? 'accepted' : 'pending';
|
|
649
|
-
}
|