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
package/lib/core/wire.mjs
DELETED
|
@@ -1,925 +0,0 @@
|
|
|
1
|
-
// wire.mjs — builders for the opaque AS2/JSON wire documents the remote pod
|
|
2
|
-
// serves (see claude/plans/ap-pod-mapping.md, "Remote pod layout"). These are
|
|
3
|
-
// protocol documents, not our RDF.
|
|
4
|
-
|
|
5
|
-
export const AS_CTX = 'https://www.w3.org/ns/activitystreams';
|
|
6
|
-
// Lemmy's own context, held locally (lib/core/contexts/join-lemmy.json). It
|
|
7
|
-
// names the three terms a Lemmy reader looks for on a community and its
|
|
8
|
-
// posts: postingRestrictedToMods, stickied, commentsEnabled.
|
|
9
|
-
export const LEMMY_CTX = 'https://join-lemmy.org/context.json';
|
|
10
|
-
export const SEC_CTX = 'https://w3id.org/security/v1';
|
|
11
|
-
export const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
|
|
12
|
-
|
|
13
|
-
// The pod LAYOUT — which container each document lives in — moved into the pod
|
|
14
|
-
// library (lib/pod/urls.mjs), which knows nothing about FediPod and requires
|
|
15
|
-
// the container root to be stated rather than defaulted. What stays here is
|
|
16
|
-
// FediPod's own answer to "which root", and the handle rule, both of which read
|
|
17
|
-
// this application's config shape.
|
|
18
|
-
import { apUrls as podUrls } from '../pod/urls.mjs';
|
|
19
|
-
|
|
20
|
-
export { webfingerHost } from '../pod/urls.mjs';
|
|
21
|
-
|
|
22
|
-
// The container the Node agent has always used. Older than the rename to
|
|
23
|
-
// FediPod and kept because it is what every existing Node-side pod actually
|
|
24
|
-
// contains; the browser build states `fedipod/` on its own configs. A config
|
|
25
|
-
// that names a root is always believed — this is only the answer for one that
|
|
26
|
-
// does not.
|
|
27
|
-
// Every build writes its data into a `fedipod/` container in the pod. This is
|
|
28
|
-
// the answer for a config that names no root; a config that names one is always
|
|
29
|
-
// believed. (`activitypods-js/` was an earlier name, now abandoned.)
|
|
30
|
-
export const DEFAULT_ROOT = 'fedipod/';
|
|
31
|
-
|
|
32
|
-
// The handle the fediverse sees: a fronted identity's name is the front's.
|
|
33
|
-
export function publicHandle(config) {
|
|
34
|
-
return config?.gateway?.frontActor?.match(/\/u\/([^/]+)\/ap\/actor\/?$/)?.[1]
|
|
35
|
-
|| config?.handle || null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The AP URLs for an identity, with FediPod's default root applied.
|
|
40
|
-
*
|
|
41
|
-
* The library underneath refuses to guess a root. Every caller here goes
|
|
42
|
-
* through this wrapper so there is exactly ONE answer to "which container",
|
|
43
|
-
* which is the thing that was wrong: the browser agent decided `fedipod/` for
|
|
44
|
-
* its own urls while the publisher, handed a config with no `root`, decided
|
|
45
|
-
* `activitypods-js/` — putting an identity's state in one container and its
|
|
46
|
-
* published documents in another.
|
|
47
|
-
*/
|
|
48
|
-
export function apUrls(remotePod, root, opts = {}) {
|
|
49
|
-
return podUrls(remotePod, root || DEFAULT_ROOT, opts);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// RFC 6415 host-meta: several fediverse implementations discover WebFinger
|
|
53
|
-
// through this LRDD template rather than hitting /.well-known/webfinger
|
|
54
|
-
// directly.
|
|
55
|
-
export function hostMeta(base) {
|
|
56
|
-
return `<?xml version="1.0" encoding="UTF-8"?>\n<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">\n <Link rel="lrdd" template="${base}.well-known/webfinger?resource={uri}"/>\n</XRD>\n`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function jrd({ handle, host, actor, aliases = [], page = null }) {
|
|
60
|
-
return {
|
|
61
|
-
subject: `acct:${handle}@${host}`,
|
|
62
|
-
...(aliases.length ? { aliases } : {}),
|
|
63
|
-
links: [
|
|
64
|
-
{ rel: 'self', type: 'application/activity+json', href: actor },
|
|
65
|
-
...(page ? [{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: page }] : []),
|
|
66
|
-
],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// `kind: 'group'` publishes a Group rather than a Person, which is what makes
|
|
71
|
-
// Mastodon and Lemmy treat the actor as a community you join.
|
|
72
|
-
// Whether a follow will be held for the operator instead of accepted on
|
|
73
|
-
// arrival. A person's follows wait unless `autoAcceptFollows` says otherwise,
|
|
74
|
-
// because a delivery on its own proves nothing about who sent it; a group is
|
|
75
|
-
// open unless its operator asked to see joins. The actor document has to say
|
|
76
|
-
// the same thing, or a remote server shows "Following" for a follow that is
|
|
77
|
-
// in fact still waiting, and nothing ever tells it otherwise.
|
|
78
|
-
export function followsNeedApproval(config = {}) {
|
|
79
|
-
return !!config.approveJoins || (config.kind !== 'group' && !config.autoAcceptFollows);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export const assertionKeyId = (urls) => urls.actor + '#ed25519-key';
|
|
83
|
-
|
|
84
|
-
export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null, movedTo = null, kind = 'person',
|
|
85
|
-
approveJoins = false, summary = null, icon = null, image = null, fields = [],
|
|
86
|
-
webId = null, aliases = [], moderators = null, pendingFollowers = null, pendingFollowing = null, liked = null,
|
|
87
|
-
ownerOutbox = null,
|
|
88
|
-
blocked = null, inbox = null, outbox = null, oauthAuthorize = null, oauthToken = null,
|
|
89
|
-
postingRestrictedToMods = null }) {
|
|
90
|
-
// manuallyApprovesFollowers is NOT in the base AS2 context, so it is declared
|
|
91
|
-
// inline exactly as Mastodon declares it — and only when we actually use it.
|
|
92
|
-
// It is what makes a client show "Request to follow" rather than "Follow" and
|
|
93
|
-
// then sit in Requested with no explanation.
|
|
94
|
-
// PropertyValue is the same case: schema.org's, declared the way Mastodon
|
|
95
|
-
// declares it, and only when there are fields to carry. `image` (the banner)
|
|
96
|
-
// and `attachment` are both plain AS2 and need no declaration.
|
|
97
|
-
const context = [AS_CTX, SEC_CTX];
|
|
98
|
-
// `featured` is Mastodon's term for the pinned-posts collection, declared
|
|
99
|
-
// inline exactly as Mastodon declares it.
|
|
100
|
-
context.push({ toot: 'http://joinmastodon.org/ns#', featured: { '@id': 'toot:featured', '@type': '@id' } });
|
|
101
|
-
if (approveJoins) context.push({ manuallyApprovesFollowers: 'as:manuallyApprovesFollowers' });
|
|
102
|
-
// FEP-8b32. The Multikey is a SECOND key beside publicKey/#main-key, not a
|
|
103
|
-
// replacement: HTTP signatures stay RSA, and only the proof is Ed25519.
|
|
104
|
-
// `assertionMethod` comes from the DID context and the key's own terms from
|
|
105
|
-
// the Multikey one; the proof's terms travel with the proof, not here.
|
|
106
|
-
if (assertionKey) context.push('https://www.w3.org/ns/did/v1',
|
|
107
|
-
'https://w3id.org/security/multikey/v1');
|
|
108
|
-
// FEP-4ccd and FEP-c648 terms, declared exactly as those FEPs declare them,
|
|
109
|
-
// and only when the collections are actually published.
|
|
110
|
-
if (pendingFollowers || pendingFollowing) {
|
|
111
|
-
context.push({
|
|
112
|
-
pdg: 'https://purl.archive.org/socialweb/pending#',
|
|
113
|
-
pendingFollowers: { '@id': 'pdg:pendingFollowers', '@type': '@id' },
|
|
114
|
-
pendingFollowing: { '@id': 'pdg:pendingFollowing', '@type': '@id' },
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
if (blocked) {
|
|
118
|
-
context.push({
|
|
119
|
-
bl: 'https://purl.archive.org/socialweb/blocked#',
|
|
120
|
-
blocked: { '@id': 'bl:blocked', '@type': '@id' },
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
if (ownerOutbox) {
|
|
124
|
-
context.push({
|
|
125
|
-
fedipod: 'https://fedipod.net/ns#',
|
|
126
|
-
ownerOutbox: { '@id': 'fedipod:ownerOutbox', '@type': '@id' },
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
// The actor's other identities: the account's WebID, plus any aliases —
|
|
130
|
-
// old accounts elsewhere whose servers check for their own URL here before
|
|
131
|
-
// they will send a Move. Declared inline exactly as Mastodon declares it.
|
|
132
|
-
if (webId || aliases.length) context.push({ alsoKnownAs: { '@id': 'as:alsoKnownAs', '@type': '@id' } });
|
|
133
|
-
// A community says whether only its moderators may open posts in it, in
|
|
134
|
-
// Lemmy's term, declared through Lemmy's context. Groups only.
|
|
135
|
-
if (kind === 'group' && postingRestrictedToMods !== null) context.push(LEMMY_CTX);
|
|
136
|
-
if (fields.length) {
|
|
137
|
-
context.push({ schema: 'http://schema.org#', PropertyValue: 'schema:PropertyValue', value: 'schema:value' });
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
'@context': context,
|
|
141
|
-
id: urls.actor,
|
|
142
|
-
// A group is a community; an application is a service that speaks for a
|
|
143
|
-
// site rather than a person — the actor a forum publishes for itself.
|
|
144
|
-
type: kind === 'group' ? 'Group' : kind === 'application' ? 'Application' : 'Person',
|
|
145
|
-
...(approveJoins ? { manuallyApprovesFollowers: true } : {}),
|
|
146
|
-
...(assertionKey ? { assertionMethod: [{
|
|
147
|
-
id: assertionKeyId(urls), type: 'Multikey', controller: urls.actor,
|
|
148
|
-
publicKeyMultibase: assertionKey,
|
|
149
|
-
}] } : {}),
|
|
150
|
-
...(movedTo ? { movedTo } : {}),
|
|
151
|
-
...(webId || aliases.length ? { alsoKnownAs: [...(webId ? [webId] : []), ...aliases] } : {}),
|
|
152
|
-
...(kind === 'group' && postingRestrictedToMods !== null ? { postingRestrictedToMods: !!postingRestrictedToMods } : {}),
|
|
153
|
-
preferredUsername: handle,
|
|
154
|
-
name: name || handle,
|
|
155
|
-
// The bio, and the avatar. For a group, `summary` is where what the group
|
|
156
|
-
// is for goes — there is nowhere else for it.
|
|
157
|
-
...(summary ? { summary: contentHtml(summary) } : {}),
|
|
158
|
-
...(icon ? { icon: { type: 'Image', url: icon } } : {}),
|
|
159
|
-
// The banner behind the avatar, and the labelled rows a client shows under
|
|
160
|
-
// the bio — what Mastodon's profile editor calls Header picture and Extra
|
|
161
|
-
// fields. Both are what every other server publishes for the same thing.
|
|
162
|
-
...(image ? { image: { type: 'Image', url: image } } : {}),
|
|
163
|
-
// Escaped, not run through contentHtml: a field value is one inline row,
|
|
164
|
-
// and wrapping it in <p> renders as a paragraph inside a table cell.
|
|
165
|
-
...(fields.length ? {
|
|
166
|
-
attachment: fields.map(f => ({
|
|
167
|
-
type: 'PropertyValue',
|
|
168
|
-
name: String(f.name).replace(/[&<>]/g, c => HTML_ESCAPES[c]),
|
|
169
|
-
value: String(f.value).replace(/[&<>]/g, c => HTML_ESCAPES[c]),
|
|
170
|
-
})),
|
|
171
|
-
} : {}),
|
|
172
|
-
// The human page. Remote servers send profile clicks here, so it must be
|
|
173
|
-
// something a person can read — the pod root is not.
|
|
174
|
-
url: urls.profileHtml,
|
|
175
|
-
// Normally the pod inbox (which buffers with nothing running). When an
|
|
176
|
-
// inbox gateway is advertised, deliveries go to it instead — it verifies
|
|
177
|
-
// and forwards into this same pod inbox for the drain.
|
|
178
|
-
inbox: inbox || urls.inbox,
|
|
179
|
-
// Every Mastodon actor publishes one. With a single actor per pod ours is
|
|
180
|
-
// just the inbox, but its absence is the non-standard thing.
|
|
181
|
-
//
|
|
182
|
-
// The two OAuth entries are how a client-to-server client finds its way in
|
|
183
|
-
// without being told anything by hand (ActivityPub 4.1). They appear only
|
|
184
|
-
// where this identity's client surface answers on an address a stranger
|
|
185
|
-
// can reach, which is why the caller supplies them rather than this
|
|
186
|
-
// building them from the pod URL.
|
|
187
|
-
endpoints: {
|
|
188
|
-
sharedInbox: inbox || urls.inbox,
|
|
189
|
-
...(oauthAuthorize ? { oauthAuthorizationEndpoint: oauthAuthorize } : {}),
|
|
190
|
-
...(oauthToken ? { oauthTokenEndpoint: oauthToken } : {}),
|
|
191
|
-
},
|
|
192
|
-
// Where a client sends what this actor writes, which the protocol says is
|
|
193
|
-
// this same address. The pod holds the collection and answers reads of it;
|
|
194
|
-
// a client-to-server write has to reach the agent, and the pod cannot take
|
|
195
|
-
// one. So where the agent is reachable it is named here, and a read of it
|
|
196
|
-
// is sent straight on to the pod's own document.
|
|
197
|
-
outbox: outbox || urls.outbox,
|
|
198
|
-
// The pinned posts, as the collection other servers read when they show
|
|
199
|
-
// this profile. Mastodon's term, declared the way Mastodon declares it.
|
|
200
|
-
featured: urls.featured,
|
|
201
|
-
followers: urls.followers,
|
|
202
|
-
following: urls.following,
|
|
203
|
-
// What this actor has liked (§5.5). Its owner's to read, like the pending
|
|
204
|
-
// lists below.
|
|
205
|
-
...(liked ? { liked } : {}),
|
|
206
|
-
// Every message this actor produced, as its owner reads it (§5.1), at an
|
|
207
|
-
// address the owner's own credential reaches.
|
|
208
|
-
...(ownerOutbox ? { ownerOutbox } : {}),
|
|
209
|
-
// FEP-1b12: attributedTo names the moderators collection; recipients
|
|
210
|
-
// validate a group's announced moderation against it.
|
|
211
|
-
...(moderators ? { attributedTo: moderators } : {}),
|
|
212
|
-
...(pendingFollowers ? { pendingFollowers } : {}),
|
|
213
|
-
...(pendingFollowing ? { pendingFollowing } : {}),
|
|
214
|
-
...(blocked ? { blocked } : {}),
|
|
215
|
-
publicKey: { id: urls.actor + '#main-key', owner: urls.actor, publicKeyPem },
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// The human half of the actor: a page a browser can open, published beside
|
|
220
|
-
// the actor document. It says who this is and how to follow — the handle to
|
|
221
|
-
// paste, and the remote-follow control that hands the visitor to their own
|
|
222
|
-
// server's follow screen.
|
|
223
|
-
export { profilePageHtml } from './profile-page.mjs';
|
|
224
|
-
|
|
225
|
-
// Retirement. An abandoned pod keeps accepting deliveries into a container
|
|
226
|
-
// nobody will ever drain, and no remote server has any way to learn that. A
|
|
227
|
-
// Delete of the actor is the signal to drop us; the Tombstone left in its place
|
|
228
|
-
// answers anyone who dereferences the actor afterwards.
|
|
229
|
-
export function deleteActorActivity(urls, stamp) {
|
|
230
|
-
return {
|
|
231
|
-
'@context': AS_CTX,
|
|
232
|
-
id: `${urls.actor}#delete-${stamp}`,
|
|
233
|
-
type: 'Delete',
|
|
234
|
-
actor: urls.actor,
|
|
235
|
-
object: urls.actor,
|
|
236
|
-
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Move is the fediverse's "this account lives elsewhere now": remote servers
|
|
241
|
-
// stop delivering here and migrate their followers to the target, while the old
|
|
242
|
-
// handle keeps resolving. movedTo on the actor is the durable half of it.
|
|
243
|
-
export function moveActivity(urls, target, stamp) {
|
|
244
|
-
return {
|
|
245
|
-
'@context': AS_CTX,
|
|
246
|
-
id: `${urls.actor}#move-${stamp}`,
|
|
247
|
-
type: 'Move',
|
|
248
|
-
actor: urls.actor,
|
|
249
|
-
object: urls.actor,
|
|
250
|
-
target,
|
|
251
|
-
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export function tombstoneDoc(urls, deletedAt, kind = 'person') {
|
|
256
|
-
return {
|
|
257
|
-
'@context': AS_CTX,
|
|
258
|
-
id: urls.actor,
|
|
259
|
-
type: 'Tombstone',
|
|
260
|
-
formerType: kind === 'group' ? 'Group' : 'Person',
|
|
261
|
-
deleted: deletedAt,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// A deleted note is left as a Tombstone at its own URL — kept fetchable — so a
|
|
266
|
-
// peer that dereferences it learns the post was DELETED rather than getting a
|
|
267
|
-
// bare 404 it cannot tell from "never existed" (§7.4).
|
|
268
|
-
export function noteTombstone(noteId, deletedAt, formerType = 'Note') {
|
|
269
|
-
return {
|
|
270
|
-
'@context': AS_CTX,
|
|
271
|
-
id: noteId,
|
|
272
|
-
type: 'Tombstone',
|
|
273
|
-
formerType,
|
|
274
|
-
deleted: deletedAt,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// A note's replies live in a document of their own, so a server that was never
|
|
279
|
-
// delivered a reply can still discover it by dereferencing this.
|
|
280
|
-
export function repliesId(noteId) { return noteId + '-replies'; }
|
|
281
|
-
|
|
282
|
-
export function collection(id, items) {
|
|
283
|
-
return {
|
|
284
|
-
'@context': AS_CTX,
|
|
285
|
-
id, type: 'Collection',
|
|
286
|
-
totalItems: items.length,
|
|
287
|
-
items,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function orderedCollection(id, items) {
|
|
292
|
-
return {
|
|
293
|
-
'@context': AS_CTX,
|
|
294
|
-
id, type: 'OrderedCollection',
|
|
295
|
-
totalItems: items.length,
|
|
296
|
-
orderedItems: items,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// A PAGED outbox, the shape Mastodon serves.
|
|
301
|
-
//
|
|
302
|
-
// One document holding every activity ever posted has to be rewritten whole on
|
|
303
|
-
// every post, so the cost of saying something grows with everything you have
|
|
304
|
-
// already said. Pages fix that without dropping anything: a new post rewrites
|
|
305
|
-
// the newest page and a head that is four lines long, and every sealed page is
|
|
306
|
-
// written once and never touched again.
|
|
307
|
-
//
|
|
308
|
-
// Pages are numbered from the OLDEST end, and that is the load-bearing part.
|
|
309
|
-
// Number them from the newest and every page boundary shifts each time you
|
|
310
|
-
// post, which is the problem again with more requests.
|
|
311
|
-
export const OUTBOX_PAGE_SIZE = 20;
|
|
312
|
-
export const outboxPageId = (outboxId, n) => `${outboxId}-${n}`;
|
|
313
|
-
|
|
314
|
-
// How many pages `total` activities occupy. Always at least one, so an empty
|
|
315
|
-
// outbox still has somewhere to point and a reader always has a `first`.
|
|
316
|
-
export const outboxPageCount = (total) => Math.max(1, Math.ceil(total / OUTBOX_PAGE_SIZE));
|
|
317
|
-
|
|
318
|
-
// `first` is the NEWEST page and `next` walks backwards in time, which is what
|
|
319
|
-
// a client following an outbox expects.
|
|
320
|
-
// `pageCount` is passed by anyone using outboxPaging, because once a page can
|
|
321
|
-
// be SHORT the count is no longer ceil(total/20) — deriving it there would put
|
|
322
|
-
// `first` below the newest page and hide everything above it.
|
|
323
|
-
export function outboxHead(id, total, pageCount = outboxPageCount(total)) {
|
|
324
|
-
const pages = Math.max(1, pageCount);
|
|
325
|
-
return {
|
|
326
|
-
'@context': AS_CTX,
|
|
327
|
-
id, type: 'OrderedCollection',
|
|
328
|
-
totalItems: total,
|
|
329
|
-
first: outboxPageId(id, pages),
|
|
330
|
-
last: outboxPageId(id, 1),
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// `items` is this page's slice, newest-first within the page. `next` points at
|
|
335
|
-
// the older page; a sealed page never needs rewriting, so it carries no `prev`
|
|
336
|
-
// — that would have to be written when the page AFTER it is created.
|
|
337
|
-
export function outboxPage(outboxId, n, items) {
|
|
338
|
-
return {
|
|
339
|
-
'@context': AS_CTX,
|
|
340
|
-
id: outboxPageId(outboxId, n),
|
|
341
|
-
type: 'OrderedCollectionPage',
|
|
342
|
-
partOf: outboxId,
|
|
343
|
-
orderedItems: items.map(outboxWireItem),
|
|
344
|
-
...(n > 1 ? { next: outboxPageId(outboxId, n - 1) } : {}),
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// The slices, oldest page first. `outbox` arrives newest-first (recordOutbox
|
|
349
|
-
// unshifts), and paging has to be anchored at the oldest end or appending one
|
|
350
|
-
// activity moves every boundary.
|
|
351
|
-
//
|
|
352
|
-
// Anchoring at the oldest end is right for appends and wrong for REMOVALS,
|
|
353
|
-
// which is why nothing calls this to publish any more — see outboxPaging.
|
|
354
|
-
export function outboxPages(outbox) {
|
|
355
|
-
const chron = [...outbox].reverse();
|
|
356
|
-
const out = [];
|
|
357
|
-
for (let i = 0; i < Math.max(chron.length, 1); i += OUTBOX_PAGE_SIZE) {
|
|
358
|
-
out.push(chron.slice(i, i + OUTBOX_PAGE_SIZE).reverse()); // newest-first within the page
|
|
359
|
-
}
|
|
360
|
-
return out;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
export const outboxItemId = (i) => (typeof i === 'string' ? i : i?.id || null);
|
|
364
|
-
|
|
365
|
-
// The outbox lists activities. A post is recorded by its object id, which is
|
|
366
|
-
// what every local reader keys on, and published as the Create beside it — a
|
|
367
|
-
// document of its own. An Announce is recorded and published whole.
|
|
368
|
-
export const outboxWireItem = (i) => (typeof i === 'string' ? createActivityId(i) : i);
|
|
369
|
-
export const outboxLocalItem = (i) =>
|
|
370
|
-
(typeof i === 'string' && i.endsWith('-create') ? i.slice(0, -'-create'.length) : i);
|
|
371
|
-
|
|
372
|
-
// Assign entries to pages, and KEEP the assignment.
|
|
373
|
-
//
|
|
374
|
-
// Deriving the pages from position — which is what outboxPages does — makes an
|
|
375
|
-
// append cheap, because only the newest page moves. It makes a REMOVAL cost the
|
|
376
|
-
// whole history: every entry after the hole shifts back one, so every page from
|
|
377
|
-
// there to the newest gets different contents, a different digest and a PUT.
|
|
378
|
-
// Taking one old post down rewrote all of them, and an inbound Delete{actor}
|
|
379
|
-
// paid that once per post the actor had.
|
|
380
|
-
//
|
|
381
|
-
// Holding the assignment instead means a removal touches the single page that
|
|
382
|
-
// held it and leaves that page one item short. A short sealed page is legal
|
|
383
|
-
// AS2 — the newest page is already partial — and `totalItems` on the head is
|
|
384
|
-
// still the true count.
|
|
385
|
-
//
|
|
386
|
-
// `index` is the assignment we last published, oldest page first, ids only.
|
|
387
|
-
// Ids it no longer contains have been removed; ids it does not know are new and
|
|
388
|
-
// extend the newest page, spilling into fresh ones. An absent index (a first
|
|
389
|
-
// publish, or a machine that lost its state) re-derives the same boundaries
|
|
390
|
-
// outboxPages would have given, so upgrading rewrites nothing.
|
|
391
|
-
// The stable-paging core, shared by the outbox and the followers collection.
|
|
392
|
-
// `order` is the ids to place, in the order new ones should fill pages; `byId`
|
|
393
|
-
// maps each id back to its item. Ids the `index` no longer contains are dropped
|
|
394
|
-
// (a removal leaves that page one short); ids not in the index are appended to
|
|
395
|
-
// the newest page. A middle hole stays a hole — never a re-slice.
|
|
396
|
-
export function pageItems({ order, byId, index = [], pageSize }) {
|
|
397
|
-
const next = (Array.isArray(index) ? index : [])
|
|
398
|
-
.map(ids => (Array.isArray(ids) ? ids.filter(id => byId.has(id)) : []));
|
|
399
|
-
const placed = new Set(next.flat());
|
|
400
|
-
for (const id of order) {
|
|
401
|
-
if (id == null || placed.has(id)) continue;
|
|
402
|
-
placed.add(id);
|
|
403
|
-
const last = next[next.length - 1];
|
|
404
|
-
if (!last || last.length >= pageSize) next.push([id]);
|
|
405
|
-
else last.push(id);
|
|
406
|
-
}
|
|
407
|
-
// A trailing empty page would leave `first`/`last` pointing at nothing; a
|
|
408
|
-
// middle hole is the short page this exists to keep.
|
|
409
|
-
while (next.length > 1 && !next[next.length - 1].length) next.pop();
|
|
410
|
-
if (!next.length) next.push([]);
|
|
411
|
-
return { index: next, pages: next.map(ids => ids.map(id => byId.get(id))) };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
export function outboxPaging(outbox, index = []) {
|
|
415
|
-
const byId = new Map();
|
|
416
|
-
for (const it of outbox) {
|
|
417
|
-
const id = outboxItemId(it);
|
|
418
|
-
if (id && !byId.has(id)) byId.set(id, it);
|
|
419
|
-
}
|
|
420
|
-
// Fill pages oldest-first (outbox.json is newest-first), then reverse each
|
|
421
|
-
// page back to newest-first for display.
|
|
422
|
-
const order = [...outbox].reverse().map(outboxItemId);
|
|
423
|
-
const { index: idx, pages } = pageItems({ order, byId, index, pageSize: OUTBOX_PAGE_SIZE });
|
|
424
|
-
return { index: idx, pages: pages.map(p => [...p].reverse()) };
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// ── followers, paged the same way ────────────────────────────────────────
|
|
428
|
-
// A followers collection is a set of actor IRIs. Each IRI is its own id, so
|
|
429
|
-
// there is nothing to dereference to page it, and no within-page order to keep.
|
|
430
|
-
export const FOLLOWERS_PAGE_SIZE = 20;
|
|
431
|
-
export const followersPageId = (id, n) => `${id}-${n}`;
|
|
432
|
-
export const followersPageCount = (total) => Math.max(1, Math.ceil(total / FOLLOWERS_PAGE_SIZE));
|
|
433
|
-
|
|
434
|
-
// Newest first, as every OrderedCollection is (ActivityPub §5): `first` is the
|
|
435
|
-
// page the newest names were added to and `next` walks back in time, and each
|
|
436
|
-
// page lists its names newest first. The pages themselves are still filled
|
|
437
|
-
// from the oldest end, so a new name rewrites one page.
|
|
438
|
-
export function followersHead(id, total, pageCount = followersPageCount(total)) {
|
|
439
|
-
const pages = Math.max(1, pageCount);
|
|
440
|
-
return {
|
|
441
|
-
'@context': AS_CTX,
|
|
442
|
-
id, type: 'OrderedCollection',
|
|
443
|
-
totalItems: total,
|
|
444
|
-
first: followersPageId(id, pages),
|
|
445
|
-
last: followersPageId(id, 1),
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
export function followersPage(id, n, items, pageCount) { // eslint-disable-line no-unused-vars
|
|
450
|
-
return {
|
|
451
|
-
'@context': AS_CTX,
|
|
452
|
-
id: followersPageId(id, n),
|
|
453
|
-
type: 'OrderedCollectionPage',
|
|
454
|
-
partOf: id,
|
|
455
|
-
orderedItems: [...items].reverse(),
|
|
456
|
-
...(n > 1 ? { next: followersPageId(id, n - 1) } : {}),
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
export function followersPaging(actors, index = []) {
|
|
461
|
-
const list = actors.filter(a => typeof a === 'string');
|
|
462
|
-
const byId = new Map();
|
|
463
|
-
for (const a of list) if (!byId.has(a)) byId.set(a, a);
|
|
464
|
-
return pageItems({ order: list, byId, index, pageSize: FOLLOWERS_PAGE_SIZE });
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// NodeInfo (the fediverse's server self-description): a root .well-known
|
|
468
|
-
// pointer plus the document it points at. Both are static, so the pod can
|
|
469
|
-
// serve them exactly like webfinger.
|
|
470
|
-
export function nodeinfoPointer(docHref) {
|
|
471
|
-
return {
|
|
472
|
-
links: [{ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', href: docHref }],
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
export function nodeinfoDoc({ version, localPosts = 0 }) {
|
|
477
|
-
return {
|
|
478
|
-
version: '2.0',
|
|
479
|
-
// "hometown", not "fedipod": clients built on megalodon (Whalebird,
|
|
480
|
-
// Fedistar) refuse any software name outside their list, and hometown —
|
|
481
|
-
// a small Mastodon fork — is the least false of the accepted names for
|
|
482
|
-
// what this API serves.
|
|
483
|
-
software: { name: 'hometown', version },
|
|
484
|
-
protocols: ['activitypub'],
|
|
485
|
-
services: { inbound: [], outbound: [] },
|
|
486
|
-
openRegistrations: false,
|
|
487
|
-
usage: { users: { total: 1, activeMonth: 1, activeHalfyear: 1 }, localPosts },
|
|
488
|
-
metadata: {},
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Federated post bodies are attacker-controlled HTML that clients render —
|
|
493
|
-
// and Mastodon clients (Phanpy included) trust the server's content and
|
|
494
|
-
// innerHTML it, because Mastodon sanitizes server-side. That makes THIS the
|
|
495
|
-
// only line of defence, so it runs on a real HTML parser (sanitize-html →
|
|
496
|
-
// htmlparser2) rather than pattern-matching: allowlisted tags and
|
|
497
|
-
// attributes, safe URL schemes, and script/style dropped with their text.
|
|
498
|
-
import sanitize from 'sanitize-html';
|
|
499
|
-
|
|
500
|
-
const ALLOWED_TAGS = ['p', 'br', 'a', 'span', 'em', 'strong', 'b', 'i', 'u', 'del',
|
|
501
|
-
'code', 'pre', 'blockquote', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4'];
|
|
502
|
-
const ALLOWED_ATTRS = ['href', 'rel', 'class', 'lang', 'title'];
|
|
503
|
-
|
|
504
|
-
export function sanitizeHtml(html) {
|
|
505
|
-
if (!html) return '';
|
|
506
|
-
return sanitize(String(html), {
|
|
507
|
-
allowedTags: ALLOWED_TAGS,
|
|
508
|
-
allowedAttributes: { '*': ALLOWED_ATTRS },
|
|
509
|
-
allowedSchemes: ['http', 'https', 'mailto'],
|
|
510
|
-
allowedSchemesAppliedToAttributes: ['href'],
|
|
511
|
-
allowProtocolRelative: false,
|
|
512
|
-
disallowedTagsMode: 'discard',
|
|
513
|
-
nonTextTags: ['script', 'style', 'textarea', 'noscript'], // drop their text too
|
|
514
|
-
transformTags: {
|
|
515
|
-
a: sanitize.simpleTransform('a', { rel: 'nofollow noopener noreferrer' }),
|
|
516
|
-
},
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// A Lemmy post, a blog article, a PeerTube video and a Bookwyrm review each
|
|
521
|
-
// carry their headline in `name` and their body in `content`. Reading content
|
|
522
|
-
// alone loses the headline, and a link post is often nothing else — so keep it
|
|
523
|
-
// as the first line, unless the body already opens with it.
|
|
524
|
-
export function titledContent(note) {
|
|
525
|
-
const html = sanitizeHtml(note?.content);
|
|
526
|
-
const types = Array.isArray(note?.type) ? note.type : [note?.type];
|
|
527
|
-
const title = typeof note?.name === 'string' ? note.name.trim() : '';
|
|
528
|
-
if (!title || types.includes('Note')) return html;
|
|
529
|
-
const opening = html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
530
|
-
if (opening.startsWith(title.toLowerCase())) return html;
|
|
531
|
-
const esc = String(title).replace(/[&<>]/g, c => HTML_ESCAPES[c]);
|
|
532
|
-
return `<p><strong>${esc}</strong></p>${html}`;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Normalize a wire Note's attachment list to { url, mediaType, description }.
|
|
536
|
-
// Capped: this list comes from somebody else's document and lands in a
|
|
537
|
-
// statuses.json row, which is rewritten whole on every change. Twenty is more
|
|
538
|
-
// than any client shows and more than any real post carries.
|
|
539
|
-
const MAX_ATTACHMENTS = 20;
|
|
540
|
-
const MAX_ATTACHMENT_URL = 2048;
|
|
541
|
-
// http(s) only: an attachment URL becomes a client's <img src> or <video src>,
|
|
542
|
-
// and the store has guarded avatars this way all along (safeUrl).
|
|
543
|
-
const attachmentUrl = (u) => {
|
|
544
|
-
if (!u) return null;
|
|
545
|
-
const s = String(u).slice(0, MAX_ATTACHMENT_URL);
|
|
546
|
-
try {
|
|
547
|
-
const p = new URL(s);
|
|
548
|
-
return (p.protocol === 'https:' || p.protocol === 'http:') ? s : null;
|
|
549
|
-
} catch { return null; }
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
export function attachmentsOf(note) {
|
|
553
|
-
const list = Array.isArray(note?.attachment) ? note.attachment : note?.attachment ? [note.attachment] : [];
|
|
554
|
-
// A bare Link carries `href` and no `url` — Lemmy's link posts arrive that
|
|
555
|
-
// way, and reading only `url` dropped the link the post was about.
|
|
556
|
-
return list.slice(0, MAX_ATTACHMENTS).map(a => ({
|
|
557
|
-
url: typeof a?.url === 'string' ? a.url : a?.url?.href || (typeof a?.href === 'string' ? a.href : undefined),
|
|
558
|
-
mediaType: String(a?.mediaType || '').slice(0, 128),
|
|
559
|
-
...(a?.name ? { description: String(a.name).slice(0, 1500) } : {}),
|
|
560
|
-
})).map(a => ({ ...a, url: attachmentUrl(a.url) })).filter(a => a.url);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>' };
|
|
564
|
-
// user@host in the text, which is what a Mention tag has to agree with.
|
|
565
|
-
export const MENTION_RE = /@([A-Za-z0-9_.-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,}(?::\d+)?)/g;
|
|
566
|
-
|
|
567
|
-
export function mentionsIn(text) {
|
|
568
|
-
return [...new Set(String(text).match(MENTION_RE) || [])].map(m => m.slice(1));
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// #word in the text: letters, digits and underscores with at least one
|
|
572
|
-
// letter, not glued to a word or a path (so a URL's fragment is not a tag).
|
|
573
|
-
export const HASHTAG_RE = /(^|[^\/\w)])#([\p{L}\p{N}_]*[\p{L}_][\p{L}\p{N}_]*)/gu;
|
|
574
|
-
|
|
575
|
-
export function hashtagsIn(text) {
|
|
576
|
-
const seen = new Map();
|
|
577
|
-
for (const m of String(text).matchAll(HASHTAG_RE)) {
|
|
578
|
-
const key = m[2].toLowerCase();
|
|
579
|
-
if (!seen.has(key)) seen.set(key, m[2]);
|
|
580
|
-
}
|
|
581
|
-
return [...seen.values()];
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Where a tag's link points: this actor's own page, asked for the tag. A
|
|
585
|
-
// Hashtag's href must be a URL; the name is what servers file it under.
|
|
586
|
-
export function tagHref(urls, name) {
|
|
587
|
-
return urls.profileHtml + '?tag=' + encodeURIComponent(String(name).toLowerCase());
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Escape first, then linkify: escaping cannot introduce an @handle, and the
|
|
591
|
-
// anchors must survive it. A mention nobody could resolve stays plain text.
|
|
592
|
-
export function contentHtml(text, mentions = [], hashtags = []) {
|
|
593
|
-
let html = String(text).replace(/[&<>]/g, c => HTML_ESCAPES[c]);
|
|
594
|
-
if (hashtags.length) {
|
|
595
|
-
const byName = new Map(hashtags.filter(h => h?.name && h?.href).map(h => [h.name.toLowerCase(), h.href]));
|
|
596
|
-
html = html.replace(HASHTAG_RE, (all, lead, name) => {
|
|
597
|
-
const href = byName.get(name.toLowerCase());
|
|
598
|
-
if (!href) return all;
|
|
599
|
-
return `${lead}<a href="${String(href).replace(/[&<>"]/g, c => (HTML_ESCAPES[c] || '"'))}" class="mention hashtag" rel="tag">#<span>${name}</span></a>`;
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
for (const m of mentions) {
|
|
603
|
-
if (!m?.handle || !m?.actor) continue;
|
|
604
|
-
// The anchor is for PEOPLE — the mentioned actor's advertised page when it
|
|
605
|
-
// has one; a browser landing on a raw actor document reads nothing. The
|
|
606
|
-
// mention TAG keeps the actor id, which is what servers match on.
|
|
607
|
-
const href = String(m.page || m.actor).replace(/[&<>"]/g, c => (HTML_ESCAPES[c] || '"'));
|
|
608
|
-
html = html.split('@' + m.handle).join(
|
|
609
|
-
`<a href="${href}" class="u-url mention">@${m.handle.split('@')[0]}</a>`);
|
|
610
|
-
}
|
|
611
|
-
return '<p>' + html.replace(/\n+/g, '</p><p>') + '</p>';
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// Mastodon's four shapes. Public and unlisted are world-readable; private
|
|
615
|
-
// (followers-only) and direct carry no Public address at all and belong in
|
|
616
|
-
// the owner-only container. Mastodon addresses mentions in cc (to, for a
|
|
617
|
-
// direct post) and will not notify anyone it does not find there or in the
|
|
618
|
-
// tags.
|
|
619
|
-
export function addressing(urls, visibility, who = []) {
|
|
620
|
-
return {
|
|
621
|
-
public: { to: [PUBLIC], cc: [urls.followers, ...who] },
|
|
622
|
-
unlisted: { to: [urls.followers], cc: [PUBLIC, ...who] },
|
|
623
|
-
private: { to: [urls.followers], cc: who },
|
|
624
|
-
direct: { to: who, cc: [] },
|
|
625
|
-
}[visibility] || { to: [PUBLIC], cc: [urls.followers, ...who] };
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// The quote terms, declared exactly as Mastodon declares them on a post:
|
|
629
|
-
// FEP-044f's `quote` and `quoteAuthorization`, and the two older spellings
|
|
630
|
-
// (Fedibird's quoteUri, Misskey's _misskey_quote) that older servers read.
|
|
631
|
-
export const QUOTE_CTX = {
|
|
632
|
-
quote: { '@id': 'https://w3id.org/fep/044f#quote', '@type': '@id' },
|
|
633
|
-
quoteUri: { '@id': 'http://fedibird.com/ns#quoteUri', '@type': '@id' },
|
|
634
|
-
_misskey_quote: { '@id': 'https://misskey-hub.net/ns#_misskey_quote', '@type': '@id' },
|
|
635
|
-
quoteAuthorization: { '@id': 'https://w3id.org/fep/044f#quoteAuthorization', '@type': '@id' },
|
|
636
|
-
};
|
|
637
|
-
// The interaction policy terms, GoToSocial's, declared as Mastodon declares them.
|
|
638
|
-
export const POLICY_CTX = {
|
|
639
|
-
gts: 'https://gotosocial.org/ns#',
|
|
640
|
-
interactionPolicy: { '@id': 'gts:interactionPolicy', '@type': '@id' },
|
|
641
|
-
canQuote: { '@id': 'gts:canQuote', '@type': '@id' },
|
|
642
|
-
automaticApproval: { '@id': 'gts:automaticApproval', '@type': '@id' },
|
|
643
|
-
manualApproval: { '@id': 'gts:manualApproval', '@type': '@id' },
|
|
644
|
-
};
|
|
645
|
-
|
|
646
|
-
// Who may quote a post: anyone, for a post the world can read; nobody, for a
|
|
647
|
-
// followers-only or direct one. Said on every note, because a server that
|
|
648
|
-
// finds no policy offers no quote button.
|
|
649
|
-
export function quotePolicy(visibility) {
|
|
650
|
-
const open = visibility === 'public' || visibility === 'unlisted';
|
|
651
|
-
return { canQuote: { automaticApproval: open ? [PUBLIC] : [], manualApproval: [] } };
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// `also`: actors addressed without being mentioned — the parent's author on
|
|
655
|
-
// a reply. In cc but not in tag, so their server receives the post and does
|
|
656
|
-
// not notify them. A direct post names its audience in the text alone.
|
|
657
|
-
// `quote`: the post this one quotes, `{ id, authorization }` — the
|
|
658
|
-
// authorization is the quoted author's QuoteAuthorization, once granted.
|
|
659
|
-
export function noteDoc({ urls, slug, content, published, inReplyTo, attachments, mentions = [],
|
|
660
|
-
visibility = 'public', summary = null, sensitive = false, updated = null, container = null, also = [],
|
|
661
|
-
quote = null }) {
|
|
662
|
-
const id = (container || urls.notes) + slug;
|
|
663
|
-
const who = mentions.map(m => m.actor);
|
|
664
|
-
const extra = visibility === 'direct' ? [] : also.filter(a => a && !who.includes(a));
|
|
665
|
-
const addressed = addressing(urls, visibility, [...who, ...extra]);
|
|
666
|
-
const hashtags = hashtagsIn(content).map(name => ({ name, href: tagHref(urls, name) }));
|
|
667
|
-
const note = {
|
|
668
|
-
'@context': [AS_CTX, { ...POLICY_CTX, ...(quote?.id ? QUOTE_CTX : {}) }],
|
|
669
|
-
id, type: 'Note',
|
|
670
|
-
attributedTo: urls.actor,
|
|
671
|
-
content: contentHtml(content, mentions, hashtags),
|
|
672
|
-
published,
|
|
673
|
-
to: addressed.to,
|
|
674
|
-
cc: addressed.cc,
|
|
675
|
-
replies: repliesId(id),
|
|
676
|
-
interactionPolicy: quotePolicy(visibility),
|
|
677
|
-
};
|
|
678
|
-
if (quote?.id) {
|
|
679
|
-
// All three spellings name the same post, as Mastodon writes them.
|
|
680
|
-
note.quote = quote.id;
|
|
681
|
-
note.quoteUri = quote.id;
|
|
682
|
-
note._misskey_quote = quote.id;
|
|
683
|
-
if (quote.authorization) note.quoteAuthorization = quote.authorization;
|
|
684
|
-
}
|
|
685
|
-
if (summary) note.summary = summary; // the content warning
|
|
686
|
-
// A content warning is what a receiver hides behind; media marked
|
|
687
|
-
// sensitive without one is hidden the same way.
|
|
688
|
-
if (summary || sensitive) note.sensitive = true;
|
|
689
|
-
if (updated) note.updated = updated; // an edit's own stamp
|
|
690
|
-
const tag = [
|
|
691
|
-
...mentions.map(m => ({ type: 'Mention', href: m.actor, name: '@' + m.handle })),
|
|
692
|
-
...hashtags.map(h => ({ type: 'Hashtag', href: h.href, name: '#' + h.name })),
|
|
693
|
-
];
|
|
694
|
-
if (tag.length) note.tag = tag;
|
|
695
|
-
if (inReplyTo) note.inReplyTo = inReplyTo;
|
|
696
|
-
if (attachments?.length) {
|
|
697
|
-
note.attachment = attachments.map(a => ({
|
|
698
|
-
type: 'Document', mediaType: a.mediaType, url: a.url,
|
|
699
|
-
...(a.description ? { name: a.description } : {}),
|
|
700
|
-
}));
|
|
701
|
-
}
|
|
702
|
-
return note;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// A poll is a note that asks something: the same addressing, the same
|
|
706
|
-
// mentions, the same replies collection, with the choices in `oneOf` (pick
|
|
707
|
-
// one) or `anyOf` (pick several). Each choice carries the count as the
|
|
708
|
-
// totalItems of its own replies collection, which is where every server that
|
|
709
|
-
// shows a poll reads it from.
|
|
710
|
-
//
|
|
711
|
-
// `endTime` is when voting stops and `closed` is the stamp saying it has —
|
|
712
|
-
// both plain AS2. `votersCount` is Mastodon's: PEOPLE rather than answers,
|
|
713
|
-
// which differ only once a poll takes several answers each. It is not in the
|
|
714
|
-
// base context, so it is declared inline exactly as Mastodon declares it, and
|
|
715
|
-
// only when we actually carry it.
|
|
716
|
-
export function questionDoc({ options = [], multiple = false, endTime = null,
|
|
717
|
-
closed = null, votersCount = null, ...rest }) {
|
|
718
|
-
const note = noteDoc(rest);
|
|
719
|
-
const choices = options.map(o => ({
|
|
720
|
-
type: 'Note',
|
|
721
|
-
name: String(o.title),
|
|
722
|
-
replies: { type: 'Collection', totalItems: Number(o.votes) || 0 },
|
|
723
|
-
}));
|
|
724
|
-
const question = {
|
|
725
|
-
...note,
|
|
726
|
-
type: 'Question',
|
|
727
|
-
[multiple ? 'anyOf' : 'oneOf']: choices,
|
|
728
|
-
};
|
|
729
|
-
if (endTime) question.endTime = endTime;
|
|
730
|
-
if (closed) question.closed = closed;
|
|
731
|
-
if (votersCount !== null && votersCount !== undefined) {
|
|
732
|
-
question['@context'] = [...[].concat(note['@context']), {
|
|
733
|
-
toot: 'http://joinmastodon.org/ns#',
|
|
734
|
-
votersCount: {
|
|
735
|
-
'@id': 'toot:votersCount',
|
|
736
|
-
'@type': 'http://www.w3.org/2001/XMLSchema#nonNegativeInteger',
|
|
737
|
-
},
|
|
738
|
-
}];
|
|
739
|
-
question.votersCount = votersCount;
|
|
740
|
-
}
|
|
741
|
-
return question;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// The id is a document of its own, not `note.id + '#create'`. A group wraps this
|
|
745
|
-
// whole activity in its Announce (FEP-1b12), and the receiver resolves it by
|
|
746
|
-
// dereferencing this id — a fragment would just serve the Note back under a
|
|
747
|
-
// different id, which Mastodon rejects.
|
|
748
|
-
export function createActivityId(noteId) { return noteId + '-create'; }
|
|
749
|
-
|
|
750
|
-
export function createActivity(note, urls) {
|
|
751
|
-
return {
|
|
752
|
-
'@context': AS_CTX,
|
|
753
|
-
id: createActivityId(note.id),
|
|
754
|
-
type: 'Create',
|
|
755
|
-
actor: urls.actor,
|
|
756
|
-
published: note.published,
|
|
757
|
-
to: note.to, cc: note.cc,
|
|
758
|
-
object: note,
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
export function acceptActivity({ urls, followActivity, serial }) {
|
|
763
|
-
return {
|
|
764
|
-
'@context': AS_CTX,
|
|
765
|
-
id: urls.actor + '#accept-' + serial,
|
|
766
|
-
type: 'Accept',
|
|
767
|
-
actor: urls.actor,
|
|
768
|
-
object: followActivity,
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// The other half of Accept: what a group sends to end a following it will no
|
|
773
|
-
// longer carry. Without it an ejected member's server never learns, and keeps
|
|
774
|
-
// receiving everything the group announces.
|
|
775
|
-
export function rejectActivity({ urls, followActivity, serial }) {
|
|
776
|
-
return {
|
|
777
|
-
'@context': AS_CTX,
|
|
778
|
-
id: urls.actor + '#reject-' + serial,
|
|
779
|
-
type: 'Reject',
|
|
780
|
-
actor: urls.actor,
|
|
781
|
-
object: followActivity,
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
export function followActivity({ urls, targetActor, serial }) {
|
|
786
|
-
return {
|
|
787
|
-
'@context': AS_CTX,
|
|
788
|
-
id: urls.actor + '#follow-' + serial,
|
|
789
|
-
type: 'Follow',
|
|
790
|
-
actor: urls.actor,
|
|
791
|
-
object: targetActor,
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Addressed to the author of what is liked, the one it is delivered to.
|
|
796
|
-
export function likeActivity({ urls, noteId, serial, to = null }) {
|
|
797
|
-
return {
|
|
798
|
-
'@context': AS_CTX,
|
|
799
|
-
id: urls.actor + '#like-' + serial,
|
|
800
|
-
type: 'Like',
|
|
801
|
-
actor: urls.actor,
|
|
802
|
-
...(to ? { to: [to] } : {}),
|
|
803
|
-
object: noteId,
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// `object` is a note URL for a personal boost — what Mastodon renders — or the
|
|
808
|
-
// whole received activity when a group carries a member's post, which is what
|
|
809
|
-
// FEP-1b12 requires and what lets a follower see the author's own activity
|
|
810
|
-
// rather than the group's summary of it. Preserved exactly as delivered.
|
|
811
|
-
export function announceActivity({ urls, object, serial, published = new Date().toISOString(),
|
|
812
|
-
audience = null }) {
|
|
813
|
-
return {
|
|
814
|
-
'@context': AS_CTX,
|
|
815
|
-
id: announceActivityId(urls, serial),
|
|
816
|
-
type: 'Announce',
|
|
817
|
-
actor: urls.actor,
|
|
818
|
-
published,
|
|
819
|
-
to: [PUBLIC], cc: [urls.followers],
|
|
820
|
-
// FEP-1b12: a group's carry names the group as the audience.
|
|
821
|
-
...(audience ? { audience } : {}),
|
|
822
|
-
object,
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// Tell followers the actor document changed. Nothing obliges a server to fetch
|
|
827
|
-
// an actor it already holds — Mastodon shows its cached copy, follower count
|
|
828
|
-
// included, until something makes it look again — so a profile edit is
|
|
829
|
-
// invisible to everyone until this goes out.
|
|
830
|
-
//
|
|
831
|
-
// The actor's own @context is hoisted onto the activity and the embedded copy
|
|
832
|
-
// loses its: the document declares `schema:` when it carries PropertyValue
|
|
833
|
-
// attachments, and an object nested under a plainer context would lose them.
|
|
834
|
-
export function updateActorActivity({ urls, actor, serial, published = new Date().toISOString() }) {
|
|
835
|
-
const { '@context': ctx, ...object } = actor;
|
|
836
|
-
return {
|
|
837
|
-
'@context': ctx || AS_CTX,
|
|
838
|
-
id: urls.actor + '#update-' + serial,
|
|
839
|
-
type: 'Update',
|
|
840
|
-
actor: urls.actor,
|
|
841
|
-
published,
|
|
842
|
-
to: [PUBLIC], cc: [urls.followers],
|
|
843
|
-
object,
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// An edit: same Note id, `updated` stamped, so receivers replace their copy
|
|
848
|
-
// and show it as edited. The activity id carries the stamp — each edit is a
|
|
849
|
-
// new activity, and a replay of an old one is already seen.
|
|
850
|
-
// `serial` names the activity when the note is not being edited — a quote
|
|
851
|
-
// authorization arriving on it changes what the note says about its quote,
|
|
852
|
-
// not what it says.
|
|
853
|
-
// An edit's Update is named by when the edit was made, so whoever made it can
|
|
854
|
-
// say its id before it exists; a restatement that is not an edit has no stamp.
|
|
855
|
-
// Each is a document of its own beside the post, so it can be fetched by its
|
|
856
|
-
// id (§3.1) once it is on the public record; a fragment of another document
|
|
857
|
-
// could not be.
|
|
858
|
-
export const updateActivityId = (noteId, updated) => noteId + '-update-' + String(updated).replace(/[^0-9TZ]/g, '');
|
|
859
|
-
export const deleteActivityId = (noteId) => noteId + '-delete';
|
|
860
|
-
export const announceActivityId = (urls, serial) => urls.notes + 'announce-' + serial;
|
|
861
|
-
export const undoActivityId = (urls, serial) => urls.notes + 'undo-' + serial;
|
|
862
|
-
|
|
863
|
-
export function updateActivity(note, urls, { serial = null } = {}) {
|
|
864
|
-
return {
|
|
865
|
-
'@context': AS_CTX,
|
|
866
|
-
id: note.updated ? updateActivityId(note.id, note.updated) : note.id + '-update-q' + (serial ?? Date.now()),
|
|
867
|
-
type: 'Update',
|
|
868
|
-
actor: urls.actor,
|
|
869
|
-
to: note.to, cc: note.cc,
|
|
870
|
-
object: note,
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// Addressed as the post was, so a followers-only or direct post's deletion
|
|
875
|
-
// reaches the people who had it and is not announced to anyone else.
|
|
876
|
-
export function deleteActivity({ urls, noteId, to = [PUBLIC], cc = null }) {
|
|
877
|
-
return {
|
|
878
|
-
'@context': AS_CTX,
|
|
879
|
-
id: deleteActivityId(noteId),
|
|
880
|
-
type: 'Delete',
|
|
881
|
-
actor: urls.actor,
|
|
882
|
-
to,
|
|
883
|
-
...(cc?.length ? { cc } : {}),
|
|
884
|
-
object: { id: noteId, type: 'Tombstone' },
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// Addressed as what it takes back was: an Undo goes to whoever had that.
|
|
889
|
-
export function undoActivity({ urls, activity, serial }) {
|
|
890
|
-
const to = activity?.to ?? (activity?.type === 'Follow' && typeof activity.object === 'string' ? [activity.object] : null);
|
|
891
|
-
return {
|
|
892
|
-
'@context': AS_CTX,
|
|
893
|
-
id: undoActivityId(urls, serial),
|
|
894
|
-
type: 'Undo',
|
|
895
|
-
actor: urls.actor,
|
|
896
|
-
...(to ? { to } : {}),
|
|
897
|
-
...(activity?.cc ? { cc: activity.cc } : {}),
|
|
898
|
-
object: activity,
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
// A group's ban (FEP-1b12 moderation). Only ever announced BY a group —
|
|
903
|
-
// a person's blocks are never put on the wire.
|
|
904
|
-
export function blockActivity({ urls, targetActor, serial }) {
|
|
905
|
-
return {
|
|
906
|
-
'@context': AS_CTX,
|
|
907
|
-
id: urls.actor + '#block-' + serial,
|
|
908
|
-
type: 'Block',
|
|
909
|
-
actor: urls.actor,
|
|
910
|
-
object: targetActor,
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// Add/Remove on one of this actor's own collections — the moderator roster
|
|
915
|
-
// is the one that changes hands (FEP-1b12).
|
|
916
|
-
export function addRemoveActivity({ urls, type, object, target, serial }) {
|
|
917
|
-
return {
|
|
918
|
-
'@context': AS_CTX,
|
|
919
|
-
id: urls.actor + '#' + type.toLowerCase() + '-' + serial,
|
|
920
|
-
type,
|
|
921
|
-
actor: urls.actor,
|
|
922
|
-
object,
|
|
923
|
-
target,
|
|
924
|
-
};
|
|
925
|
-
}
|