fedipod-server 0.28.1 → 0.30.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 +38 -34
- package/config/server.json +22 -0
- package/dist/claims.js +3 -2
- package/dist/directory.d.ts +2 -0
- package/dist/door.d.ts +4 -0
- package/dist/door.js +31 -0
- package/dist/fedipod.d.ts +16 -0
- package/dist/fedipod.js +13 -0
- package/dist/front.d.ts +3 -0
- package/dist/front.js +94 -0
- package/dist/handler.d.ts +39 -101
- package/dist/handler.js +86 -617
- package/dist/handler.jsonld +6 -30
- package/dist/identities.d.ts +62 -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/store-pod.js +37 -0
- package/package.json +10 -18
- package/lib/client/c2s.mjs +0 -582
- package/lib/client/localapi.mjs +0 -87
- package/lib/client/masto/accounts.mjs +0 -309
- 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 -371
- package/lib/core/graphview.mjs +0 -327
- package/lib/core/intake/activities.mjs +0 -633
- package/lib/core/intake/activity.mjs +0 -318
- package/lib/core/intake/channel.mjs +0 -144
- package/lib/core/intake/group.mjs +0 -256
- package/lib/core/intake/index.mjs +0 -701
- package/lib/core/intake/notes.mjs +0 -360
- 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/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 -237
- package/lib/core/publisher/index.mjs +0 -500
- package/lib/core/publisher/notes.mjs +0 -453
- package/lib/core/publisher/questions.mjs +0 -239
- 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 -531
- package/lib/core/storage.mjs +0 -176
- package/lib/core/store.mjs +0 -568
- package/lib/core/wire-quotes.mjs +0 -71
- package/lib/core/wire.mjs +0 -889
- 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 -412
- 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 -447
- 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 -459
- 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 -970
- package/lib/gateway/gateway-core.mjs +0 -165
- 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/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/media.mjs +0 -27
- package/lib/pod/notes.mjs +0 -80
- 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 -525
- package/lib/pod/urls.mjs +0 -85
- package/lib/server/embed.mjs +0 -423
- package/lib/session/README.md +0 -137
- package/lib/session/demo.html +0 -94
- package/lib/session/fedi-account.mjs +0 -408
- 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 -897
- 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 -145
- 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 -603
- package/web/admin/notices-bar.js +0 -90
- package/web/admin/oauth-signin.mjs +0 -37
- package/web/admin/record.js +0 -390
- package/web/admin/setup/index.html +0 -241
- package/web/admin/setup/setup.js +0 -326
- 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 -663
- 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 -576
- package/web/app/deliver-relay.mjs +0 -129
- package/web/app/dist/boot.js +0 -34218
- package/web/app/dist/boot.js.map +0 -7
- package/web/app/dist/sw.js +0 -72810
- 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 -242
- 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 -318
- 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 -58
- package/web/front/run.js +0 -195
- 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,132 +0,0 @@
|
|
|
1
|
-
// shapes/index.mjs — validating an ActivityStreams document against the shapes
|
|
2
|
-
// beside this file.
|
|
3
|
-
//
|
|
4
|
-
// A shape says what a kind of document IS. Checking one against a document we
|
|
5
|
-
// were sent is a different question from whether we trust it: a forged actor,
|
|
6
|
-
// a receipt that does not bind, a reply naming a note we never wrote are each
|
|
7
|
-
// guarded elsewhere, and none of those guards is replaced here.
|
|
8
|
-
//
|
|
9
|
-
// NOTHING HERE REJECTS ANYTHING. A failure is recorded and the activity is
|
|
10
|
-
// handled exactly as it would have been. Every fediverse implementation writes
|
|
11
|
-
// slightly different documents, and a shape strict enough to catch something
|
|
12
|
-
// real is strict enough to reject somebody's quirk; which one cannot be known
|
|
13
|
-
// in advance, so it is learned from the record rather than guessed at. A shape
|
|
14
|
-
// earns enforcement by not firing.
|
|
15
|
-
|
|
16
|
-
import { Parser } from 'n3';
|
|
17
|
-
import Environment from '@rdfjs/environment';
|
|
18
|
-
import DataFactory from '@rdfjs/data-model/Factory.js';
|
|
19
|
-
import DatasetFactory from '@rdfjs/dataset/Factory.js';
|
|
20
|
-
import NamespaceFactory from '@rdfjs/namespace/Factory.js';
|
|
21
|
-
import TermSetFactory from '@rdfjs/term-set/Factory.js';
|
|
22
|
-
import TermMapFactory from '@rdfjs/term-map/Factory.js';
|
|
23
|
-
import ClownfaceFactory from 'clownface/Factory.js';
|
|
24
|
-
import SHACLValidator from 'rdf-validate-shacl';
|
|
25
|
-
import { SHAPES_TTL } from './shapes-text.mjs';
|
|
26
|
-
|
|
27
|
-
// The validator needs a full RDF/JS environment, not a bare factory. These
|
|
28
|
-
// pieces are what rdf-validate-shacl itself depends on, composed here rather
|
|
29
|
-
// than pulling in a packaged environment that reads files — this has to run in
|
|
30
|
-
// a service worker too.
|
|
31
|
-
const rdf = new Environment([DataFactory, DatasetFactory, NamespaceFactory, TermSetFactory, TermMapFactory, ClownfaceFactory]);
|
|
32
|
-
|
|
33
|
-
// The shapes document's own URL is the base, so `<#Create>` resolves to a
|
|
34
|
-
// fragment of it and no namespace is claimed.
|
|
35
|
-
const SHAPES_BASE = 'https://fedipod.invalid/shapes';
|
|
36
|
-
|
|
37
|
-
let validator;
|
|
38
|
-
let brokenReported = false;
|
|
39
|
-
|
|
40
|
-
/** The validator, built once. Reading and parsing the shapes is not per-document work. */
|
|
41
|
-
function shacl() {
|
|
42
|
-
if (!validator) {
|
|
43
|
-
const quads = new Parser({ baseIRI: SHAPES_BASE, format: 'text/turtle' }).parse(SHAPES_TTL);
|
|
44
|
-
validator = new SHACLValidator(rdf.dataset(quads.map(asTerm)), { factory: rdf });
|
|
45
|
-
}
|
|
46
|
-
return validator;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** jsonld and n3 each mint their own terms; the validator's environment must mint the ones it sees. */
|
|
50
|
-
function asTerm(q) {
|
|
51
|
-
const term = (t) => {
|
|
52
|
-
if (t.termType === 'NamedNode') return rdf.namedNode(t.value);
|
|
53
|
-
if (t.termType === 'BlankNode') return rdf.blankNode(String(t.value).replace(/^_:/u, ''));
|
|
54
|
-
if (t.termType === 'Literal') {
|
|
55
|
-
return rdf.literal(t.value, t.language || (t.datatype ? rdf.namedNode(t.datatype.value) : undefined));
|
|
56
|
-
}
|
|
57
|
-
return rdf.defaultGraph();
|
|
58
|
-
};
|
|
59
|
-
return rdf.quad(term(q.subject), term(q.predicate), term(q.object));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* What to call the property that did not fit.
|
|
64
|
-
*
|
|
65
|
-
* A single-predicate path names itself: the IRI, shortened. A sequence path is
|
|
66
|
-
* a list, and its head is a blank node that names nothing — so the failing
|
|
67
|
-
* property shape is asked for its own `sh:name` instead.
|
|
68
|
-
*/
|
|
69
|
-
const SH_NAME = 'http://www.w3.org/ns/shacl#name';
|
|
70
|
-
function pathName(r) {
|
|
71
|
-
const direct = r.path?.value ?? null;
|
|
72
|
-
if (direct && /[#/]/u.test(direct)) return direct;
|
|
73
|
-
const shape = r.sourceShape;
|
|
74
|
-
if (shape) {
|
|
75
|
-
for (const q of shacl().$shapes?.dataset ?? []) {
|
|
76
|
-
if (q.subject.equals(shape) && q.predicate.value === SH_NAME) return q.object.value;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return direct;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Check a document's graph against the shapes.
|
|
84
|
-
*
|
|
85
|
-
* `quads` is what `parseAS2` produced. Returns null when the document
|
|
86
|
-
* conforms or when there was no graph to check, and otherwise a short record
|
|
87
|
-
* of what did not fit — the failing node, the property, and the shape's own
|
|
88
|
-
* words for it.
|
|
89
|
-
*
|
|
90
|
-
* Never throws. A validator that cannot run is not a reason to drop mail — but
|
|
91
|
-
* it is said once, because a checker that silently passes everything is the
|
|
92
|
-
* same silence this change exists to remove.
|
|
93
|
-
*/
|
|
94
|
-
export async function checkShapes(quads) {
|
|
95
|
-
if (!Array.isArray(quads) || quads.length === 0) return null;
|
|
96
|
-
let report;
|
|
97
|
-
try {
|
|
98
|
-
report = await shacl().validate(rdf.dataset(quads.map(asTerm)));
|
|
99
|
-
} catch (e) {
|
|
100
|
-
// A validator that cannot run must not read as "everything conforms" —
|
|
101
|
-
// that is the same silence this whole change exists to remove. Said once,
|
|
102
|
-
// then quiet, because a broken validator would otherwise say it per item.
|
|
103
|
-
if (!brokenReported) {
|
|
104
|
-
brokenReported = true;
|
|
105
|
-
return { conforms: false, broken: true, results: [{ focus: null, path: null, message: `shape validation is not running: ${e.message}`, shape: null }] };
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
if (report.conforms) return null;
|
|
110
|
-
return {
|
|
111
|
-
conforms: false,
|
|
112
|
-
results: report.results.slice(0, 10).map((r) => ({
|
|
113
|
-
focus: r.focusNode?.value ?? null,
|
|
114
|
-
// A SEQUENCE path — `( as:object as:closed )` — is an RDF list, so its
|
|
115
|
-
// node is blank and `.value` is a generated label like "n3-13". Reported
|
|
116
|
-
// as that, the record named nothing a reader could act on. The shape's
|
|
117
|
-
// own `sh:name` is what it is called, so that is used when the path
|
|
118
|
-
// cannot name itself.
|
|
119
|
-
path: pathName(r),
|
|
120
|
-
message: r.message?.[0]?.value ?? String(r.sourceConstraintComponent?.value ?? 'does not fit the shape'),
|
|
121
|
-
shape: r.sourceShape?.value ?? null,
|
|
122
|
-
})),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** The failure as one line, for a log or a dead-letter record. */
|
|
127
|
-
export function describeShapeFailure(failure) {
|
|
128
|
-
if (!failure) return null;
|
|
129
|
-
return failure.results
|
|
130
|
-
.map((r) => `${r.path ? r.path.replace(/^.*[#/]/u, '') : 'node'}: ${r.message}`)
|
|
131
|
-
.join('; ');
|
|
132
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// shapes-text.mjs — the shapes document, as text.
|
|
2
|
-
//
|
|
3
|
-
// The shapes are authored as Turtle in activitystreams.ttl, which is the one
|
|
4
|
-
// source of truth and opens in any RDF tool. Reading it differs by where this
|
|
5
|
-
// runs: Node reads the file, and the browser bundle swaps this module for a
|
|
6
|
-
// shim that imports the same .ttl through esbuild's text loader.
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
-
|
|
11
|
-
export const SHAPES_TTL = fs.readFileSync(
|
|
12
|
-
path.join(path.dirname(fileURLToPath(import.meta.url)), 'activitystreams.ttl'), 'utf8',
|
|
13
|
-
);
|
package/lib/core/social.mjs
DELETED
|
@@ -1,531 +0,0 @@
|
|
|
1
|
-
// social.mjs — outbound social actions (follow, like, announce, delete)
|
|
2
|
-
// shared by the admin API and the Mastodon-API facade. Each action delivers
|
|
3
|
-
// the activity and mirrors state into the store; the store keeps the
|
|
4
|
-
// activity a later Undo must name.
|
|
5
|
-
|
|
6
|
-
import crypto from 'node:crypto';
|
|
7
|
-
import * as wire from './wire.mjs';
|
|
8
|
-
import * as podNotes from '../pod/notes.mjs';
|
|
9
|
-
import { dropFollower } from './store.mjs';
|
|
10
|
-
|
|
11
|
-
// WebFinger, ours rather than fedify's — v2 dropped lookupWebFinger.
|
|
12
|
-
//
|
|
13
|
-
// Better here anyway: the host comes out of a handle, and a handle can arrive
|
|
14
|
-
// in a MENTION inside a post somebody else wrote, so it is remote input and
|
|
15
|
-
// belongs behind the same SSRF guard as every other outbound request. fedify's
|
|
16
|
-
// lookup did not go through ours. https only, because a handle resolved over
|
|
17
|
-
// plaintext is a handle anyone on the path can answer for.
|
|
18
|
-
//
|
|
19
|
-
// `fetchImpl`, when given, makes the request instead of safeFetch: the agent's
|
|
20
|
-
// own remote read, which in the browser build is the relay — a server-side
|
|
21
|
-
// fetch, so the lookup does not depend on the far pod host answering a
|
|
22
|
-
// browser's cross-origin request. Every other remote read there goes the
|
|
23
|
-
// same way; WebFinger went direct and was the one lookup that could fail
|
|
24
|
-
// while the rest worked.
|
|
25
|
-
export async function lookupWebFinger(acct, { fetchImpl = null } = {}) {
|
|
26
|
-
const clean = String(acct || '').replace(/^acct:/, '');
|
|
27
|
-
const at = clean.lastIndexOf('@');
|
|
28
|
-
if (at < 1) return null;
|
|
29
|
-
const [user, host] = [clean.slice(0, at), clean.slice(at + 1)];
|
|
30
|
-
if (!host || /[/\\?#]/.test(host)) return null;
|
|
31
|
-
const { safeFetch, readCapped } = await import('../shared/safefetch.mjs');
|
|
32
|
-
const url = `https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${clean}`)}`;
|
|
33
|
-
const headers = { accept: 'application/jrd+json, application/json' };
|
|
34
|
-
const res = await (fetchImpl ? fetchImpl(url, { headers }) : safeFetch(url, { headers })).catch(() => null);
|
|
35
|
-
if (!res || res.status >= 400) return null;
|
|
36
|
-
let jrd;
|
|
37
|
-
try { jrd = JSON.parse(await readCapped(res, 256 * 1024)); } catch { return null; }
|
|
38
|
-
// The JRD says who it is about. Nothing checked it, so a host answering every
|
|
39
|
-
// query with one account's document made `@anyone@that.host` resolve to that
|
|
40
|
-
// account — and it was then stored under the handle that was asked for, not
|
|
41
|
-
// the one it turned out to be.
|
|
42
|
-
const subject = String(jrd?.subject || '').replace(/^acct:/, '').toLowerCase();
|
|
43
|
-
if (subject && subject !== clean.toLowerCase()) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
return jrd;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const selfLink = (jrd) => jrd?.links?.find(l => l.rel === 'self' &&
|
|
50
|
-
/application\/(activity\+json|ld\+json)/.test(l.type || ''));
|
|
51
|
-
|
|
52
|
-
// A WebFinger server can name an actor on ANY host, and until this ran we
|
|
53
|
-
// believed it. The handle is remote input — it arrives in the Mention tags of a
|
|
54
|
-
// note somebody else wrote — so evil.example could answer for `@x@evil.example`
|
|
55
|
-
// with a real, busy actor elsewhere, and a reply that carried that mention was
|
|
56
|
-
// addressed, tagged and DELIVERED to a third party who had nothing to do with
|
|
57
|
-
// the thread, over our signature.
|
|
58
|
-
//
|
|
59
|
-
// The host cannot simply be required to match: `@user@example.com` served by
|
|
60
|
-
// mastodon.example is the ordinary delegated setup and the whole point of
|
|
61
|
-
// WebFinger. So ask the ACTOR's own host who it says that actor is, and require
|
|
62
|
-
// it to come back to the same document. A server may name anyone; it cannot
|
|
63
|
-
// make that person's server agree. This is the round trip Mastodon does.
|
|
64
|
-
//
|
|
65
|
-
// Only when the two hosts differ — same-host is self-consistent by
|
|
66
|
-
// construction, so the common case costs no extra request.
|
|
67
|
-
export async function confirmDelegation(asked, doc, lookup = lookupWebFinger) {
|
|
68
|
-
let home;
|
|
69
|
-
try { home = new URL(doc.id).host.toLowerCase(); }
|
|
70
|
-
catch { throw new Error(`actor id is not a URL (${doc.id})`); }
|
|
71
|
-
if (home === asked.toLowerCase()) return;
|
|
72
|
-
const user = doc.preferredUsername;
|
|
73
|
-
if (!user) {
|
|
74
|
-
throw new Error(`${asked} points at ${doc.id} on another host, which names no `
|
|
75
|
-
+ 'username to confirm it by');
|
|
76
|
-
}
|
|
77
|
-
const back = await lookup(`acct:${user}@${home}`);
|
|
78
|
-
if (selfLink(back)?.href !== doc.id) {
|
|
79
|
-
throw new Error(`${asked} claims ${doc.id}, but ${home} does not agree that is `
|
|
80
|
-
+ `@${user}@${home} — refusing to take one host's word for another's actor`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// acct:user@host → verified actor doc (cached by fetchAP), or throws.
|
|
85
|
-
export async function resolveHandle(agent, handle) {
|
|
86
|
-
const clean = String(handle || '').replace(/^@/, '');
|
|
87
|
-
if (!/^[^@]+@[^@]+$/.test(clean)) throw new Error('handle must look like user@host');
|
|
88
|
-
if (agent.store.isBlocked('https://' + clean.split('@')[1] + '/')) throw new Error('domain is blocked');
|
|
89
|
-
// Through the agent's own remote read where it has one — the relay, in the
|
|
90
|
-
// browser build — and directly otherwise.
|
|
91
|
-
const remote = agent.intake?.deliverer?.signedFetch
|
|
92
|
-
? (u, i) => agent.intake.deliverer.signedFetch(u, i) : null;
|
|
93
|
-
const lookup = (a) => lookupWebFinger(a, { fetchImpl: remote });
|
|
94
|
-
const jrd = await lookup('acct:' + clean);
|
|
95
|
-
const self = selfLink(jrd);
|
|
96
|
-
if (!self?.href) throw new Error(`webfinger found no actor for ${clean}`);
|
|
97
|
-
const doc = await agent.intake.fetchAP(self.href);
|
|
98
|
-
if (!doc?.id) throw new Error(`actor document unusable for ${clean}`);
|
|
99
|
-
if (agent.store.isBlocked(doc.id)) throw new Error('actor is blocked');
|
|
100
|
-
await confirmDelegation(clean.slice(clean.lastIndexOf('@') + 1), doc, lookup);
|
|
101
|
-
// Counts live in the collections, not the actor document, so a client asking
|
|
102
|
-
// "who is this" would otherwise be told everyone has none. Two GETs, and only
|
|
103
|
-
// when someone asked by name — never on the drain path.
|
|
104
|
-
await cacheCounts(agent, doc);
|
|
105
|
-
return doc;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// totalItems off the followers/following collections, stored beside the cached
|
|
109
|
-
// actor. Best effort: a collection that will not answer leaves the count unset
|
|
110
|
-
// rather than reported as zero, which is a different claim.
|
|
111
|
-
async function cacheCounts(agent, doc) {
|
|
112
|
-
const total = async (url) => {
|
|
113
|
-
if (!url) return null;
|
|
114
|
-
try {
|
|
115
|
-
const res = await agent.intake.fetchAP(url);
|
|
116
|
-
return Number.isInteger(res?.totalItems) ? res.totalItems : null;
|
|
117
|
-
} catch { return null; }
|
|
118
|
-
};
|
|
119
|
-
const [followers, following] = await Promise.all([total(doc.followers), total(doc.following)]);
|
|
120
|
-
if (followers === null && following === null) return;
|
|
121
|
-
const actors = agent.store.getActors();
|
|
122
|
-
if (!actors[doc.id]) return;
|
|
123
|
-
actors[doc.id] = { ...actors[doc.id], counts: { followers, following } };
|
|
124
|
-
agent.store.write('actors.json', actors);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// `doc` skips the fetch when the caller already resolved the actor; a bulk
|
|
128
|
-
// import passes publish:false and republishes the collections once at the end.
|
|
129
|
-
export async function followActor(agent, actorUrl, { publish = true, doc: fetched = null } = {}) {
|
|
130
|
-
const doc = fetched || await agent.intake.fetchAP(actorUrl);
|
|
131
|
-
if (!doc?.inbox) throw new Error(`actor document unusable (${actorUrl})`);
|
|
132
|
-
if (agent.store.isBlocked(doc.id)) throw new Error('actor is blocked');
|
|
133
|
-
const contacts = agent.store.getContacts();
|
|
134
|
-
let rec = contacts.following.find(f => f.actor === doc.id);
|
|
135
|
-
if (!rec) {
|
|
136
|
-
rec = { actor: doc.id, inbox: doc.inbox, accepted: false };
|
|
137
|
-
contacts.following.push(rec);
|
|
138
|
-
}
|
|
139
|
-
const follow = wire.followActivity({ urls: agent.publisher.urls, targetActor: doc.id, serial: Date.now() });
|
|
140
|
-
rec.followActivity = follow; // kept for a later Undo
|
|
141
|
-
agent.store.setContacts(contacts);
|
|
142
|
-
await agent.deliverer.deliver(doc.inbox, follow);
|
|
143
|
-
if (publish) await agent.publisher.publishCollections({ following: true, pending: true });
|
|
144
|
-
return doc;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export async function followHandle(agent, handle) {
|
|
148
|
-
const doc = await resolveHandle(agent, handle);
|
|
149
|
-
await followActor(agent, doc.id);
|
|
150
|
-
const clean = String(handle || '').replace(/^@/, '');
|
|
151
|
-
const contacts = agent.store.getContacts();
|
|
152
|
-
const rec = contacts.following.find(f => f.actor === doc.id);
|
|
153
|
-
if (rec && !rec.handle) { rec.handle = clean; agent.store.setContacts(contacts); }
|
|
154
|
-
return { ok: true, actor: doc.id };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function unfollowActor(agent, actor) {
|
|
158
|
-
const contacts = agent.store.getContacts();
|
|
159
|
-
const rec = contacts.following.find(f => f.actor === actor);
|
|
160
|
-
if (!rec) throw new Error('not following that actor');
|
|
161
|
-
if (rec.followActivity) {
|
|
162
|
-
await agent.deliverer.deliver(rec.inbox,
|
|
163
|
-
wire.undoActivity({ urls: agent.publisher.urls, activity: rec.followActivity, serial: Date.now() }));
|
|
164
|
-
}
|
|
165
|
-
contacts.following = contacts.following.filter(f => f.actor !== actor);
|
|
166
|
-
agent.store.setContacts(contacts);
|
|
167
|
-
await agent.publisher.publishCollections({ following: true, pending: true });
|
|
168
|
-
return { ok: true };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export async function favourite(agent, s) {
|
|
172
|
-
if (s.favourited) return s;
|
|
173
|
-
const doc = await agent.intake.fetchAP(s.actor);
|
|
174
|
-
if (!doc?.inbox) throw new Error('author inbox unavailable');
|
|
175
|
-
const act = wire.likeActivity({ urls: agent.publisher.urls, noteId: s.noteId, serial: Date.now() });
|
|
176
|
-
await agent.deliverer.deliver(doc.endpoints?.sharedInbox || doc.inbox, act);
|
|
177
|
-
return agent.store.updateStatus(s.noteId, { favourited: true, likeActivity: act });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export async function unfavourite(agent, s) {
|
|
181
|
-
if (s.likeActivity) {
|
|
182
|
-
const doc = await agent.intake.fetchAP(s.actor).catch(() => null);
|
|
183
|
-
if (doc?.inbox) {
|
|
184
|
-
await agent.deliverer.deliver(doc.endpoints?.sharedInbox || doc.inbox,
|
|
185
|
-
wire.undoActivity({ urls: agent.publisher.urls, activity: s.likeActivity, serial: Date.now() }));
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return agent.store.updateStatus(s.noteId, { favourited: false, likeActivity: undefined });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Announce goes to our followers (that's what boosting is) and to the author.
|
|
192
|
-
export async function reblog(agent, s) {
|
|
193
|
-
if (s.reblogged) return s;
|
|
194
|
-
const { urls } = agent.publisher;
|
|
195
|
-
const act = wire.announceActivity({ urls, object: s.noteId, serial: Date.now() });
|
|
196
|
-
const inboxes = new Set(agent.store.getContacts().followers
|
|
197
|
-
.map(f => f.sharedInbox || f.inbox).filter(Boolean));
|
|
198
|
-
if (s.actor !== urls.actor) {
|
|
199
|
-
const doc = await agent.intake.fetchAP(s.actor).catch(() => null);
|
|
200
|
-
if (doc?.inbox) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
|
|
201
|
-
}
|
|
202
|
-
await agent.deliverer.deliverToAll([...inboxes], act);
|
|
203
|
-
// Mark it boosted before recording it: a failed outbox write costs one
|
|
204
|
-
// missing entry, a failed status write would let a retry announce twice.
|
|
205
|
-
const updated = agent.store.updateStatus(s.noteId, { reblogged: true, announceActivity: act });
|
|
206
|
-
await agent.publisher.recordOutbox(act);
|
|
207
|
-
if (await agent.store.commit?.() === false) agent.log(`boost sent but its timeline row was refused: ${s.noteId}`);
|
|
208
|
-
return updated;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export async function unreblog(agent, s) {
|
|
212
|
-
if (s.announceActivity) {
|
|
213
|
-
const undo = wire.undoActivity({ urls: agent.publisher.urls, activity: s.announceActivity, serial: Date.now() });
|
|
214
|
-
const inboxes = new Set(agent.store.getContacts().followers
|
|
215
|
-
.map(f => f.sharedInbox || f.inbox).filter(Boolean));
|
|
216
|
-
const doc = await agent.intake.fetchAP(s.actor).catch(() => null);
|
|
217
|
-
if (doc?.inbox && s.actor !== agent.publisher.urls.actor) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
|
|
218
|
-
await agent.deliverer.deliverToAll([...inboxes], undo);
|
|
219
|
-
await agent.publisher.unrecordOutbox(i => i?.id === s.announceActivity.id);
|
|
220
|
-
}
|
|
221
|
-
return agent.store.updateStatus(s.noteId, { reblogged: false, announceActivity: undefined });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Group: end a following we will no longer carry. Forgetting them locally is
|
|
225
|
-
// not enough — without the Reject their server keeps the relationship and they
|
|
226
|
-
// keep receiving everything the group announces. Muting them too, because an
|
|
227
|
-
// ejection anyone can undo by re-following is not an ejection; a re-follow is
|
|
228
|
-
// still auto-accepted, but nothing of theirs gets carried.
|
|
229
|
-
export async function ejectFollower(agent, actor) {
|
|
230
|
-
const { urls } = agent.publisher;
|
|
231
|
-
const contacts = agent.store.getContacts();
|
|
232
|
-
const rec = contacts.followers.find(f => f.actor === actor);
|
|
233
|
-
if (!rec) throw new Error('not a member');
|
|
234
|
-
dropFollower(contacts, actor, 'ejected');
|
|
235
|
-
agent.store.setContacts(contacts);
|
|
236
|
-
const muted = agent.store.getMuted();
|
|
237
|
-
muted.actors = [...new Set([...muted.actors, actor])];
|
|
238
|
-
agent.store.setMuted(muted);
|
|
239
|
-
await agent.publisher.publishCollections({ followers: true });
|
|
240
|
-
// The ban is broadcast to the membership (FEP-1b12), so their servers can
|
|
241
|
-
// mirror it. Never sent to the ejected member — they get the Reject below.
|
|
242
|
-
await announceModeration(agent,
|
|
243
|
-
wire.blockActivity({ urls, targetActor: actor, serial: Date.now() }))
|
|
244
|
-
.catch(e => agent.log?.(`moderation announce failed: ${e.message}`));
|
|
245
|
-
// A Bluesky-side member's departure sticks by blocking the DID.
|
|
246
|
-
if (rec.bsky) {
|
|
247
|
-
await agent.bskygroup?.blockDid(rec.bsky.did)
|
|
248
|
-
.catch(e => agent.log?.(`bluesky block failed for ${rec.bsky.handle}: ${e.message}`));
|
|
249
|
-
return { ok: true, actor, told: true };
|
|
250
|
-
}
|
|
251
|
-
if (rec.inbox) {
|
|
252
|
-
await agent.deliverer.deliver(rec.inbox, wire.rejectActivity({
|
|
253
|
-
urls,
|
|
254
|
-
followActivity: { id: rec.followId, type: 'Follow', actor, object: urls.actor },
|
|
255
|
-
serial: Date.now(),
|
|
256
|
-
}));
|
|
257
|
-
}
|
|
258
|
-
return { ok: true, actor, told: !!rec.inbox };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Group: answer a held join request. Admitting is the Accept onFollow would
|
|
262
|
-
// have sent immediately had the group not been gated.
|
|
263
|
-
export async function admitRequest(agent, actor, { publish = true } = {}) {
|
|
264
|
-
const reqs = agent.store.getRequests();
|
|
265
|
-
const req = reqs.find(r => r.actor === actor);
|
|
266
|
-
if (!req) throw new Error('no such request');
|
|
267
|
-
const contacts = agent.store.getContacts();
|
|
268
|
-
if (!contacts.followers.some(f => f.actor === actor)) {
|
|
269
|
-
// Admitting a bridged Bluesky member retires their Bluesky-only record,
|
|
270
|
-
// the same way an ungated group's follow does: one person, one membership.
|
|
271
|
-
const bridgedDid = /^https:\/\/bsky\.brid\.gy\/ap\/(did:[^/]+)$/.exec(actor)?.[1];
|
|
272
|
-
if (bridgedDid) contacts.followers = contacts.followers.filter(f => f.bsky?.did !== bridgedDid);
|
|
273
|
-
contacts.followers.push(req.bsky
|
|
274
|
-
? { actor, bsky: req.bsky }
|
|
275
|
-
: { actor, inbox: req.inbox, sharedInbox: req.sharedInbox, followId: req.activity?.id });
|
|
276
|
-
agent.store.setContacts(contacts);
|
|
277
|
-
agent.store.addNotification({ type: 'follow', actor, ...(req.bsky ? { bsky: true } : {}) });
|
|
278
|
-
}
|
|
279
|
-
agent.store.setRequests(reqs.filter(r => r.actor !== actor));
|
|
280
|
-
// A Bluesky-only member has no inbox and no Accept to receive; the follow
|
|
281
|
-
// already stands on their side. The one-time bridge nudge goes out instead.
|
|
282
|
-
if (req.bsky) {
|
|
283
|
-
await agent.bskygroup?.nudgeOnce(contacts.followers.find(f => f.actor === actor));
|
|
284
|
-
return { ok: true, actor };
|
|
285
|
-
}
|
|
286
|
-
// A bulk admit passes publish:false and republishes once for the lot.
|
|
287
|
-
if (publish) await agent.publisher.publishCollections({ followers: true, pending: true });
|
|
288
|
-
await agent.deliverer.deliver(req.inbox, wire.acceptActivity({
|
|
289
|
-
urls: agent.publisher.urls, followActivity: req.activity, serial: Date.now(),
|
|
290
|
-
}));
|
|
291
|
-
return { ok: true, actor };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Refusing is not a ban: they may ask again, and the queue is where you would
|
|
295
|
-
// see that. Eject is the sticky one, because it mutes.
|
|
296
|
-
export async function refuseRequest(agent, actor) {
|
|
297
|
-
const reqs = agent.store.getRequests();
|
|
298
|
-
const req = reqs.find(r => r.actor === actor);
|
|
299
|
-
if (!req) throw new Error('no such request');
|
|
300
|
-
agent.store.setRequests(reqs.filter(r => r.actor !== actor));
|
|
301
|
-
await agent.publisher?.publishCollections({ pending: true }).catch(() => {});
|
|
302
|
-
// No Reject can reach a Bluesky-only requester, so the refusal is enforced
|
|
303
|
-
// where it can be: their DID is blocked.
|
|
304
|
-
if (req.bsky) {
|
|
305
|
-
await agent.bskygroup?.blockDid(req.bsky.did)
|
|
306
|
-
.catch(e => agent.log?.(`bluesky block failed for ${req.bsky.handle}: ${e.message}`));
|
|
307
|
-
return { ok: true, actor };
|
|
308
|
-
}
|
|
309
|
-
await agent.deliverer.deliver(req.inbox, wire.rejectActivity({
|
|
310
|
-
urls: agent.publisher.urls, followActivity: req.activity, serial: Date.now(),
|
|
311
|
-
}));
|
|
312
|
-
return { ok: true, actor };
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Group: unsay an Announce. The work is in Intake because an upstream Delete
|
|
316
|
-
// has to do the same thing, and both must use the recipient set amplify used.
|
|
317
|
-
export async function retractAnnouncement(agent, noteId) {
|
|
318
|
-
return agent.intake.retract(noteId);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Remove an own note everywhere: Delete→followers, remote note + outbox,
|
|
322
|
-
// local pod RDF, statuses mirror.
|
|
323
|
-
//
|
|
324
|
-
// Two of the three pod deletes have to land, and both are checked. While the
|
|
325
|
-
// note document answers, anyone holding its id can still read a post its author
|
|
326
|
-
// deleted — and the `-create` beside it EMBEDS the whole note, so leaving that
|
|
327
|
-
// one behind publishes the same content under another name. A failure keeps the
|
|
328
|
-
// post here and says so, rather than reporting a deletion that did not happen:
|
|
329
|
-
// removing it locally would leave nothing to try again with, and nothing to see
|
|
330
|
-
// that it is still up.
|
|
331
|
-
// A vote is the fediverse convention: one bare Note per chosen option, whose
|
|
332
|
-
// name IS the option, inReplyTo the question, addressed to the question's
|
|
333
|
-
// author alone. The documents are stored for dereference but never listed —
|
|
334
|
-
// not in the outbox, not in the local index.
|
|
335
|
-
export async function votePoll(agent, s, choices) {
|
|
336
|
-
const { urls } = agent.publisher;
|
|
337
|
-
const poll = s.poll || {};
|
|
338
|
-
const opts = poll.options || [];
|
|
339
|
-
if (poll.closed || (poll.expiresAt && Date.parse(poll.expiresAt) < Date.now())) {
|
|
340
|
-
return { ok: false, error: 'this poll has closed' };
|
|
341
|
-
}
|
|
342
|
-
if (poll.voted) return { ok: false, error: 'already voted' };
|
|
343
|
-
// Your own poll is the one you are counting, not one you are answering.
|
|
344
|
-
if (s.actor === urls.actor) return { ok: false, error: 'this is your own poll' };
|
|
345
|
-
const picks = [...new Set(choices)].filter(i => i >= 0 && i < opts.length);
|
|
346
|
-
if (!picks.length) return { ok: false, error: 'invalid choice' };
|
|
347
|
-
if (!poll.multiple && picks.length > 1) return { ok: false, error: 'this poll takes one choice' };
|
|
348
|
-
const author = await agent.intake.fetchAP(s.actor).catch(() => null);
|
|
349
|
-
const inbox = author?.endpoints?.sharedInbox || author?.inbox;
|
|
350
|
-
if (!inbox) return { ok: false, error: 'could not reach the poll author' };
|
|
351
|
-
const published = new Date().toISOString();
|
|
352
|
-
for (const i of picks) {
|
|
353
|
-
const id = urls.notes + published.slice(0, 10) + '-vote-' + crypto.randomBytes(4).toString('hex');
|
|
354
|
-
const note = {
|
|
355
|
-
'@context': wire.AS_CTX, id, type: 'Note',
|
|
356
|
-
attributedTo: urls.actor, name: opts[i].title,
|
|
357
|
-
inReplyTo: s.noteId, published, to: [s.actor],
|
|
358
|
-
};
|
|
359
|
-
const create = {
|
|
360
|
-
'@context': wire.AS_CTX, id: wire.createActivityId(id), type: 'Create',
|
|
361
|
-
actor: urls.actor, published, to: [s.actor], object: note,
|
|
362
|
-
};
|
|
363
|
-
await podNotes.write(agent.remote, id, note);
|
|
364
|
-
await podNotes.writeCreate(agent.remote, create.id, create);
|
|
365
|
-
await agent.deliverer.deliverToAll([inbox], create);
|
|
366
|
-
}
|
|
367
|
-
agent.store.updateStatus(s.noteId, {
|
|
368
|
-
poll: {
|
|
369
|
-
...poll, voted: true, ownVotes: picks,
|
|
370
|
-
options: opts.map((o, j) => (picks.includes(j) ? { ...o, votes: (o.votes || 0) + 1 } : o)),
|
|
371
|
-
},
|
|
372
|
-
});
|
|
373
|
-
return { ok: true };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
export async function deleteNote(agent, s) {
|
|
377
|
-
const { urls } = agent.publisher;
|
|
378
|
-
const inboxes = agent.store.getContacts().followers
|
|
379
|
-
.map(f => f.sharedInbox || f.inbox).filter(Boolean);
|
|
380
|
-
await agent.deliverer.deliverToAll(inboxes, wire.deleteActivity({ urls, noteId: s.noteId }));
|
|
381
|
-
|
|
382
|
-
// The note is REPLACED with a Tombstone rather than removed, so a peer that
|
|
383
|
-
// dereferences it learns the post was deleted instead of a bare 404 (§7.4).
|
|
384
|
-
// The Tombstone must stay readable; the container above it is owner-only, so
|
|
385
|
-
// the ACL cannot be inherited. The Create activity is the withdrawn action,
|
|
386
|
-
// not the object, so it is deleted outright.
|
|
387
|
-
const stuck = [];
|
|
388
|
-
const deletedAt = new Date().toISOString();
|
|
389
|
-
try {
|
|
390
|
-
await podNotes.writeTombstone(agent.remote, s.noteId, wire.noteTombstone(s.noteId, deletedAt));
|
|
391
|
-
} catch { stuck.push(s.noteId); }
|
|
392
|
-
if (!await podNotes.dropCreate(agent.remote, wire.createActivityId(s.noteId))) {
|
|
393
|
-
stuck.push(wire.createActivityId(s.noteId));
|
|
394
|
-
}
|
|
395
|
-
if (stuck.length) {
|
|
396
|
-
agent.log?.(`delete ${s.noteId}: the pod kept ${stuck.length} document(s) — the post is STILL PUBLISHED`);
|
|
397
|
-
return {
|
|
398
|
-
ok: false, stillPublished: stuck,
|
|
399
|
-
error: 'the pod would not remove the post — it is still published, and has been kept here so you can try again',
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
// Empty, and referenced by nothing once the note is gone.
|
|
403
|
-
await podNotes.dropReplies(agent.remote, wire.repliesId(s.noteId));
|
|
404
|
-
|
|
405
|
-
// The Bluesky mirror goes with the post. Best-effort: a mirror the far side
|
|
406
|
-
// already lost is not a reason to keep the post here.
|
|
407
|
-
if (s.atproto?.uri && agent.atproto?.connected()) {
|
|
408
|
-
await agent.atproto.deleteCrossPost(s.atproto.uri)
|
|
409
|
-
.then(() => agent.log?.(`bluesky mirror deleted: ${s.atproto.uri}`))
|
|
410
|
-
.catch(e => agent.log?.(`bluesky mirror not deleted (${e.message}): ${s.atproto.uri}`));
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// The note's outbox entry, and our own boost of it if there was one.
|
|
414
|
-
await agent.publisher.unrecordOutbox(i => i === s.noteId || (i?.id && i.id === s.announceActivity?.id));
|
|
415
|
-
agent.store.removeStatus(s.noteId);
|
|
416
|
-
return { ok: true };
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// FEP-1b12: a GROUP's moderation is announced to its followers, so their
|
|
420
|
-
// servers can mirror the action. Group moderation only — a person's blocks
|
|
421
|
-
// never go on the wire (that is also the spec's own SHOULD NOT), which is why
|
|
422
|
-
// the guard lives here rather than in every caller. Never delivered to the
|
|
423
|
-
// moderated party; the announce goes to the membership.
|
|
424
|
-
export async function announceModeration(agent, activity) {
|
|
425
|
-
if (agent.store.getConfig()?.kind !== 'group') return null;
|
|
426
|
-
const { urls } = agent.publisher;
|
|
427
|
-
const act = wire.announceActivity({ urls, object: activity, serial: Date.now(), audience: urls.actor });
|
|
428
|
-
const inboxes = [...new Set(agent.store.getContacts().followers
|
|
429
|
-
.map(f => f.sharedInbox || f.inbox).filter(Boolean))];
|
|
430
|
-
await agent.deliverer.deliverToAll(inboxes, act);
|
|
431
|
-
await agent.publisher.recordOutbox(act);
|
|
432
|
-
return act;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// A block is state, not speech: the list changes and the follow ends, and
|
|
436
|
-
// nothing is delivered TO THE BLOCKED PARTY (§6.10). Intake already refuses a
|
|
437
|
-
// blocked author on arrival. A group additionally announces the ban to its
|
|
438
|
-
// members (FEP-1b12) — announceModeration keeps a person's blocks silent.
|
|
439
|
-
export async function blockActor(agent, actorUrl) {
|
|
440
|
-
if (!/^https?:\/\/\S+$/.test(String(actorUrl || ''))) throw new Error('actor must be a URL');
|
|
441
|
-
const b = agent.store.getBlocklist();
|
|
442
|
-
if (!b.actors.includes(actorUrl)) b.actors.push(actorUrl);
|
|
443
|
-
agent.store.setBlocklist(b);
|
|
444
|
-
await unfollowActor(agent, actorUrl).catch(() => {}); // not following is fine
|
|
445
|
-
await agent.publisher?.publishCollections({ blocked: true }).catch(() => {});
|
|
446
|
-
await announceModeration(agent,
|
|
447
|
-
wire.blockActivity({ urls: agent.publisher.urls, targetActor: actorUrl, serial: Date.now() }))
|
|
448
|
-
.catch(e => agent.log?.(`moderation announce failed: ${e.message}`));
|
|
449
|
-
return { ok: true, actor: actorUrl };
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
export async function unblockActor(agent, actorUrl) {
|
|
453
|
-
const b = agent.store.getBlocklist();
|
|
454
|
-
b.actors = b.actors.filter(a => a !== actorUrl);
|
|
455
|
-
agent.store.setBlocklist(b);
|
|
456
|
-
await agent.publisher?.publishCollections({ blocked: true }).catch(() => {});
|
|
457
|
-
// The unban is announced as the Undo of the ban, the ban re-stated inside it
|
|
458
|
-
// — its id was a fragment nobody stored, so receivers match on the shape.
|
|
459
|
-
await announceModeration(agent, wire.undoActivity({
|
|
460
|
-
urls: agent.publisher.urls,
|
|
461
|
-
activity: { type: 'Block', actor: agent.publisher.urls.actor, object: actorUrl },
|
|
462
|
-
serial: Date.now(),
|
|
463
|
-
})).catch(e => agent.log?.(`moderation announce failed: ${e.message}`));
|
|
464
|
-
return { ok: true, actor: actorUrl };
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Apply (or refuse) one queued moderation request — an action a LISTED
|
|
468
|
-
// moderator asked for over federation. Deliveries carry nothing that proves
|
|
469
|
-
// the sender, which is exactly why these wait in a queue for the operator
|
|
470
|
-
// instead of running on arrival; applying one is the operator vouching for it.
|
|
471
|
-
export async function applyModeration(agent, entry) {
|
|
472
|
-
const { urls } = agent.publisher;
|
|
473
|
-
const target = typeof entry.activity?.object === 'string'
|
|
474
|
-
? entry.activity.object : entry.activity?.object?.id;
|
|
475
|
-
switch (entry.type) {
|
|
476
|
-
case 'Block': {
|
|
477
|
-
if (!target) throw new Error('the queued Block names nobody');
|
|
478
|
-
// A member is ejected (which mutes and announces the ban); a stranger is
|
|
479
|
-
// simply blocked. Either way the ban is broadcast once.
|
|
480
|
-
const member = agent.store.getContacts().followers.some(f => f.actor === target);
|
|
481
|
-
return member ? ejectFollower(agent, target) : blockActor(agent, target);
|
|
482
|
-
}
|
|
483
|
-
case 'Undo': {
|
|
484
|
-
const inner = entry.activity?.object;
|
|
485
|
-
const unbanned = typeof inner === 'object' && inner?.type === 'Block'
|
|
486
|
-
? (typeof inner.object === 'string' ? inner.object : inner.object?.id) : null;
|
|
487
|
-
if (!unbanned) throw new Error('the queued Undo does not name a Block');
|
|
488
|
-
const muted = agent.store.getMuted();
|
|
489
|
-
muted.actors = muted.actors.filter(a => a !== unbanned);
|
|
490
|
-
agent.store.setMuted(muted);
|
|
491
|
-
return unblockActor(agent, unbanned);
|
|
492
|
-
}
|
|
493
|
-
case 'Delete': {
|
|
494
|
-
if (!target) throw new Error('the queued Delete names nothing');
|
|
495
|
-
// Removing a post from the group is unsaying its carry.
|
|
496
|
-
return agent.intake.retract(target);
|
|
497
|
-
}
|
|
498
|
-
case 'Add':
|
|
499
|
-
case 'Remove': {
|
|
500
|
-
if (!target) throw new Error('the queued roster change names nobody');
|
|
501
|
-
const cfg = { ...agent.store.getConfig() };
|
|
502
|
-
const prev = cfg.moderators || [];
|
|
503
|
-
cfg.moderators = entry.type === 'Add'
|
|
504
|
-
? [...new Set([...prev, target])]
|
|
505
|
-
: prev.filter(m => m !== target);
|
|
506
|
-
agent.store.setConfig(cfg);
|
|
507
|
-
if (agent.publisher) agent.publisher.config.moderators = cfg.moderators;
|
|
508
|
-
await agent.publisher.publishProfile();
|
|
509
|
-
await announceModeration(agent, wire.addRemoveActivity({
|
|
510
|
-
urls, type: entry.type, object: target, target: urls.moderators, serial: Date.now(),
|
|
511
|
-
})).catch(e => agent.log?.(`moderation announce failed: ${e.message}`));
|
|
512
|
-
return { ok: true, moderators: cfg.moderators };
|
|
513
|
-
}
|
|
514
|
-
default: throw new Error(`no way to apply a queued ${entry.type}`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Pin or unpin an own post. Republishing the featured collection is what makes
|
|
519
|
-
// the pin real to anyone else — the actor document names that collection, and
|
|
520
|
-
// a visitor's server reads it, so the profile is republished too (it skips
|
|
521
|
-
// itself when nothing changed).
|
|
522
|
-
export async function pinStatus(agent, s, pinned) {
|
|
523
|
-
if (s.actor !== agent.publisher.urls.actor) throw new Error('not your status');
|
|
524
|
-
const updated = agent.store.updateStatus(s.noteId, { pinned: !!pinned });
|
|
525
|
-
await agent.publisher.publishFeatured().catch(e => agent.log?.(`featured: ${e.message}`));
|
|
526
|
-
if (agent.publisher.publishProfilePage) {
|
|
527
|
-
await agent.publisher.publishProfilePage().catch(e => agent.log?.(`profile page: ${e.message}`));
|
|
528
|
-
}
|
|
529
|
-
agent.publisher.publishProfile?.().catch(() => {});
|
|
530
|
-
return updated || s;
|
|
531
|
-
}
|