fedipod-server 0.29.0 → 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 +11 -6
- 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 +36 -101
- package/dist/handler.js +85 -645
- package/dist/handler.jsonld +2 -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/package.json +10 -18
- package/lib/client/c2s.mjs +0 -636
- package/lib/client/localapi.mjs +0 -87
- package/lib/client/masto/accounts.mjs +0 -310
- package/lib/client/masto/body.mjs +0 -156
- package/lib/client/masto/index.mjs +0 -206
- package/lib/client/masto/instance.mjs +0 -112
- package/lib/client/masto/media.mjs +0 -79
- package/lib/client/masto/oauth.mjs +0 -656
- package/lib/client/masto/render.mjs +0 -518
- package/lib/client/masto/statuses.mjs +0 -372
- package/lib/client/masto/timelines.mjs +0 -438
- package/lib/client/oidc-auth.mjs +0 -62
- package/lib/client/streaming.mjs +0 -154
- package/lib/client/webpush.mjs +0 -98
- package/lib/connections/acctfeed.mjs +0 -257
- package/lib/connections/atproto.mjs +0 -309
- package/lib/connections/bskyfeed.mjs +0 -243
- package/lib/connections/bskygroup.mjs +0 -162
- package/lib/connections/fediacct.mjs +0 -253
- package/lib/connections/import.mjs +0 -357
- package/lib/connections/tagfeed.mjs +0 -212
- package/lib/connections/vault.mjs +0 -114
- package/lib/core/as2.mjs +0 -206
- package/lib/core/contexts/activitystreams.json +0 -379
- package/lib/core/contexts/anno.json +0 -126
- package/lib/core/contexts/did-v1.json +0 -57
- package/lib/core/contexts/fep-5711.json +0 -36
- package/lib/core/contexts/gotosocial.json +0 -86
- package/lib/core/contexts/identity-v1.json +0 -152
- package/lib/core/contexts/index.mjs +0 -63
- package/lib/core/contexts/join-lemmy.json +0 -33
- package/lib/core/contexts/joinmastodon.json +0 -28
- package/lib/core/contexts/litepub-0.1.json +0 -60
- package/lib/core/contexts/map.json +0 -19
- package/lib/core/contexts/miscellany.json +0 -19
- package/lib/core/contexts/quotes.json +0 -18
- package/lib/core/contexts/schemaorg.json +0 -8845
- package/lib/core/contexts/security-data-integrity-v1.json +0 -78
- package/lib/core/contexts/security-data-integrity-v2.json +0 -81
- package/lib/core/contexts/security-multikey-v1.json +0 -35
- package/lib/core/contexts/security-v1.json +0 -74
- package/lib/core/contexts/webfinger.json +0 -10
- package/lib/core/deliver.mjs +0 -387
- package/lib/core/graphview.mjs +0 -327
- package/lib/core/intake/activities.mjs +0 -649
- package/lib/core/intake/activity.mjs +0 -318
- package/lib/core/intake/channel.mjs +0 -144
- package/lib/core/intake/group.mjs +0 -258
- package/lib/core/intake/index.mjs +0 -732
- package/lib/core/intake/notes.mjs +0 -367
- package/lib/core/intake/verify.mjs +0 -142
- package/lib/core/keys.mjs +0 -153
- package/lib/core/lease.mjs +0 -206
- package/lib/core/place.mjs +0 -82
- package/lib/core/polls.mjs +0 -105
- package/lib/core/profile-page.mjs +0 -104
- package/lib/core/proof.mjs +0 -79
- package/lib/core/publisher/collections.mjs +0 -272
- package/lib/core/publisher/index.mjs +0 -522
- package/lib/core/publisher/notes.mjs +0 -519
- package/lib/core/publisher/own.mjs +0 -143
- package/lib/core/publisher/questions.mjs +0 -241
- package/lib/core/publisher/restore.mjs +0 -224
- package/lib/core/shapes/activitystreams.ttl +0 -157
- package/lib/core/shapes/index.mjs +0 -132
- package/lib/core/shapes/shapes-text.mjs +0 -13
- package/lib/core/social.mjs +0 -561
- package/lib/core/storage.mjs +0 -176
- package/lib/core/store.mjs +0 -578
- package/lib/core/wire-quotes.mjs +0 -71
- package/lib/core/wire.mjs +0 -925
- package/lib/device/account.mjs +0 -87
- package/lib/device/admin/index.mjs +0 -13
- package/lib/device/admin/origins.mjs +0 -35
- package/lib/device/admin/routes/connections.mjs +0 -144
- package/lib/device/admin/routes/gateway.mjs +0 -199
- package/lib/device/admin/routes/lifecycle.mjs +0 -200
- package/lib/device/admin/routes/owner.mjs +0 -322
- package/lib/device/admin/routes/setup.mjs +0 -428
- package/lib/device/admin/routes/social.mjs +0 -188
- package/lib/device/admin/server.mjs +0 -95
- package/lib/device/admin/static.mjs +0 -247
- package/lib/device/admin/surface.mjs +0 -315
- package/lib/device/certs.mjs +0 -281
- package/lib/device/cli/commands/account.mjs +0 -586
- package/lib/device/cli/commands/run.mjs +0 -278
- package/lib/device/cli/commands/service.mjs +0 -221
- package/lib/device/cli/commands/setup.mjs +0 -489
- package/lib/device/cli/commands/state.mjs +0 -561
- package/lib/device/cli/context.mjs +0 -288
- package/lib/device/export-collections.mjs +0 -161
- package/lib/device/gateway-move.mjs +0 -58
- package/lib/device/home.mjs +0 -161
- package/lib/device/migrate.mjs +0 -115
- package/lib/device/ports.mjs +0 -27
- package/lib/device/remote.mjs +0 -54
- package/lib/device/setup.mjs +0 -496
- package/lib/device/update.mjs +0 -96
- package/lib/gateway/caches.mjs +0 -36
- package/lib/gateway/directory.mjs +0 -90
- package/lib/gateway/front-core.mjs +0 -966
- package/lib/gateway/gateway-core.mjs +0 -244
- package/lib/gateway/headers.mjs +0 -49
- package/lib/gateway/httpsig.mjs +0 -159
- package/lib/gateway/notices.mjs +0 -84
- package/lib/gateway/quiet.mjs +0 -189
- package/lib/gateway/relay-extras.mjs +0 -89
- package/lib/gateway/token-claims.mjs +0 -16
- package/lib/pod/README.md +0 -61
- package/lib/pod/actor.mjs +0 -96
- package/lib/pod/collection.mjs +0 -77
- package/lib/pod/containers.mjs +0 -140
- package/lib/pod/discovery.mjs +0 -46
- package/lib/pod/featured.mjs +0 -18
- package/lib/pod/followers.mjs +0 -24
- package/lib/pod/following.mjs +0 -10
- package/lib/pod/http.mjs +0 -74
- package/lib/pod/inbox.mjs +0 -166
- package/lib/pod/links.mjs +0 -35
- package/lib/pod/location.mjs +0 -52
- package/lib/pod/media.mjs +0 -27
- package/lib/pod/notes.mjs +0 -78
- package/lib/pod/notifications.mjs +0 -89
- package/lib/pod/outbox.mjs +0 -18
- package/lib/pod/package.json +0 -19
- package/lib/pod/policy.mjs +0 -58
- package/lib/pod/private.mjs +0 -19
- package/lib/pod/root.mjs +0 -104
- package/lib/pod/state.mjs +0 -40
- package/lib/pod/transport.mjs +0 -674
- package/lib/pod/type-index.mjs +0 -101
- package/lib/pod/urls.mjs +0 -91
- package/lib/server/embed.mjs +0 -424
- package/lib/session/README.md +0 -137
- package/lib/session/demo.html +0 -94
- package/lib/session/fedi-account.mjs +0 -417
- package/lib/session/fedi-login.mjs +0 -123
- package/lib/session/oidc-session.mjs +0 -233
- package/lib/session/package.json +0 -49
- package/lib/shared/guard.mjs +0 -213
- package/lib/shared/links.mjs +0 -9
- package/lib/shared/safefetch.mjs +0 -235
- package/lib/shared/ua.mjs +0 -29
- package/phanpy/dist/404.html +0 -40
- package/phanpy/dist/_headers +0 -2
- package/phanpy/dist/apple-touch-icon.png +0 -0
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js +0 -2
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +0 -1
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +0 -183
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
- package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +0 -2
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +0 -1
- package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
- package/phanpy/dist/assets/chunk-aKtaBQYM.js +0 -1
- package/phanpy/dist/assets/compose-CFEx7wb4.js +0 -2
- package/phanpy/dist/assets/compose-CFEx7wb4.js.map +0 -1
- package/phanpy/dist/assets/compose-ZJN5xSb4.js +0 -35
- package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +0 -1
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js +0 -52
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +0 -1
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js +0 -2
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +0 -1
- package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +0 -1
- package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +0 -1
- package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
- package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +0 -1
- package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
- package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
- package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
- package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
- package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
- package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
- package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
- package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
- package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +0 -1
- package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +0 -1
- package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
- package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +0 -1
- package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
- package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +0 -1
- package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
- package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
- package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +0 -1
- package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
- package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +0 -1
- package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
- package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
- package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
- package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
- package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
- package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +0 -1
- package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +0 -1
- package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +0 -1
- package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +0 -1
- package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +0 -1
- package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
- package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +0 -1
- package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
- package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +0 -1
- package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +0 -1
- package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +0 -1
- package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
- package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
- package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +0 -1
- package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +0 -1
- package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
- package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
- package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +0 -1
- package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +0 -1
- package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +0 -1
- package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
- package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +0 -1
- package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +0 -1
- package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +0 -1
- package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
- package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
- package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
- package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +0 -1
- package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +0 -1
- package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +0 -1
- package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +0 -1
- package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +0 -1
- package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
- package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
- package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +0 -1
- package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +0 -1
- package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +0 -1
- package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +0 -1
- package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +0 -1
- package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +0 -1
- package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +0 -1
- package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +0 -1
- package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
- package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +0 -1
- package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +0 -1
- package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +0 -1
- package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
- package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +0 -1
- package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +0 -1
- package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
- package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
- package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
- package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +0 -1
- package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +0 -1
- package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
- package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +0 -1
- package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +0 -1
- package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +0 -1
- package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +0 -1
- package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +0 -1
- package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +0 -1
- package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +0 -1
- package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +0 -1
- package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +0 -1
- package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
- package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +0 -1
- package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
- package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
- package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
- package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +0 -1
- package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
- package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +0 -1
- package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +0 -1
- package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
- package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +0 -1
- package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
- package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +0 -1
- package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
- package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +0 -1
- package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +0 -1
- package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
- package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +0 -1
- package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +0 -1
- package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
- package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +0 -1
- package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +0 -1
- package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +0 -1
- package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
- package/phanpy/dist/assets/instances-BNPptnn-.json +0 -333
- package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +0 -1
- package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
- package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +0 -1
- package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +0 -1
- package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +0 -1
- package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +0 -1
- package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +0 -1
- package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +0 -1
- package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +0 -1
- package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +0 -1
- package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +0 -1
- package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +0 -1
- package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +0 -1
- package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +0 -1
- package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
- package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
- package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +0 -1
- package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
- package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +0 -1
- package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +0 -1
- package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
- package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/tok-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +0 -1
- package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +0 -1
- package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/main-BdqNbG-a.js +0 -26
- package/phanpy/dist/assets/main-BdqNbG-a.js.map +0 -1
- package/phanpy/dist/assets/mock-home-DGhR__He.js +0 -2
- package/phanpy/dist/assets/mock-home-DGhR__He.js.map +0 -1
- package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
- package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +0 -2
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +0 -1
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +0 -20
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +0 -1
- package/phanpy/dist/assets/style-B1iLPTPR.css +0 -2
- package/phanpy/dist/assets/temml-DuyIQMei.js +0 -97
- package/phanpy/dist/assets/temml-DuyIQMei.js.map +0 -1
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +0 -2
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
- package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
- package/phanpy/dist/assets/year-in-posts-DJv_star.js +0 -3
- package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +0 -1
- package/phanpy/dist/compose/index.html +0 -20
- package/phanpy/dist/favicon.ico +0 -0
- package/phanpy/dist/index.html +0 -73
- package/phanpy/dist/logo-192.png +0 -0
- package/phanpy/dist/logo-512.png +0 -0
- package/phanpy/dist/logo-badge-72.png +0 -0
- package/phanpy/dist/logo-maskable-512.png +0 -0
- package/phanpy/dist/logo-monochrome-512.png +0 -0
- package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
- package/phanpy/dist/manifest.webmanifest +0 -1
- package/phanpy/dist/og-image-2.jpg +0 -0
- package/phanpy/dist/og-image.png +0 -0
- package/phanpy/dist/robots.txt +0 -2
- package/phanpy/dist/sw.js +0 -2
- package/phanpy/dist/sw.js.map +0 -1
- package/phanpy/dist/version.json +0 -1
- package/run-agent.mjs +0 -899
- package/vendor/gate.cjs +0 -138
- package/vendor/idp-grant.cjs +0 -394
- package/vendor/jfetch.cjs +0 -16
- package/web/admin/actors.js +0 -147
- package/web/admin/bar.css +0 -126
- package/web/admin/bar.js +0 -99
- package/web/admin/client/client.js +0 -66
- package/web/admin/client/index.html +0 -69
- package/web/admin/client-pick.js +0 -79
- package/web/admin/common.js +0 -23
- package/web/admin/connections.js +0 -112
- package/web/admin/gateway.js +0 -133
- package/web/admin/group.js +0 -258
- package/web/admin/index.html +0 -612
- package/web/admin/notices-bar.js +0 -90
- package/web/admin/oauth-signin.mjs +0 -37
- package/web/admin/record.js +0 -393
- package/web/admin/setup/index.html +0 -257
- package/web/admin/setup/setup.js +0 -339
- package/web/admin/tokens.css +0 -68
- package/web/admin/upkeep.js +0 -177
- package/web/admin/window.css +0 -55
- package/web/admin/window.js +0 -136
- package/web/app/README.md +0 -81
- package/web/app/admin-facade.mjs +0 -588
- package/web/app/agent.mjs +0 -672
- package/web/app/atproto-browser.mjs +0 -86
- package/web/app/authorize_interaction.html +0 -56
- package/web/app/authorize_interaction.js +0 -69
- package/web/app/boot.mjs +0 -616
- package/web/app/deliver-relay.mjs +0 -129
- package/web/app/dist/boot.js +0 -34562
- package/web/app/dist/boot.js.map +0 -7
- package/web/app/dist/sw.js +0 -73456
- package/web/app/dist/sw.js.map +0 -7
- package/web/app/fediacct-browser.mjs +0 -146
- package/web/app/gateway-move.mjs +0 -60
- package/web/app/idb-kv.mjs +0 -35
- package/web/app/index.html +0 -258
- package/web/app/keys-browser.mjs +0 -71
- package/web/app/keystore.mjs +0 -82
- package/web/app/oidc-session.mjs +0 -6
- package/web/app/pod-auth.mjs +0 -248
- package/web/app/pod-remote.mjs +0 -84
- package/web/app/shims/_globals.mjs +0 -2
- package/web/app/shims/fedify-sig.mjs +0 -77
- package/web/app/shims/node-crypto.mjs +0 -113
- package/web/app/shims/node-fs.mjs +0 -17
- package/web/app/shims/node-path.mjs +0 -19
- package/web/app/shims/node-url.mjs +0 -7
- package/web/app/shims/prelude.js +0 -58
- package/web/app/shims/safefetch.mjs +0 -58
- package/web/app/shims/shapes-text.mjs +0 -8
- package/web/app/shims/web-push.mjs +0 -14
- package/web/app/signup.mjs +0 -357
- package/web/app/sw-src.mjs +0 -220
- package/web/app/update.js +0 -70
- package/web/front/admin.html +0 -55
- package/web/front/admin.js +0 -89
- package/web/front/install.sh +0 -69
- package/web/front/new-account.html +0 -35
- package/web/front/new-account.js +0 -13
- package/web/front/notices.html +0 -69
- package/web/front/notices.js +0 -107
- package/web/front/run.html +0 -64
- package/web/front/run.js +0 -221
- package/web/front/solid-oidc-client.js +0 -6
- package/web/icons/forum.svg +0 -11
- package/web/icons/gateway.svg +0 -14
- package/web/icons/local.svg +0 -11
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
// timelines.mjs — what a client reads by the page: the home and public
|
|
2
|
-
// timelines, hashtags and the tag feed, lists and their timelines, the v2
|
|
3
|
-
// filters, conversations, notifications, and search.
|
|
4
|
-
|
|
5
|
-
import crypto from 'node:crypto';
|
|
6
|
-
import * as social from '../../core/social.mjs';
|
|
7
|
-
import { sanitizeHtml } from '../../core/wire.mjs';
|
|
8
|
-
import { authorOf } from '../../core/intake/index.mjs';
|
|
9
|
-
import { readBody } from './body.mjs';
|
|
10
|
-
|
|
11
|
-
export async function handle(api, ctx) {
|
|
12
|
-
const { req, res, pathname, url, send } = ctx; // eslint-disable-line no-unused-vars
|
|
13
|
-
|
|
14
|
-
// home + public = everything known (follows, boosts, tag feed, own);
|
|
15
|
-
// public?local=true = own posts; trends/statuses = the same activity (a
|
|
16
|
-
// single-actor instance has no firehose — what it knows IS its public
|
|
17
|
-
// face). Sorted by publish time so tag-feed backfill interleaves.
|
|
18
|
-
if (pathname === '/api/v1/timelines/home' || pathname === '/api/v1/timelines/public'
|
|
19
|
-
|| pathname === '/api/v1/trends/statuses') {
|
|
20
|
-
const localOnly = pathname === '/api/v1/timelines/public' && url.searchParams.get('local') === 'true';
|
|
21
|
-
// The UNFILTERED list, both as the source and as the reply-count corpus:
|
|
22
|
-
// a reply can be a mention, and mentions are filtered out of timelines
|
|
23
|
-
// below — counting against the filtered set would undercount them.
|
|
24
|
-
const all = api.store.getStatuses();
|
|
25
|
-
const muted = new Set(api.store.getMuted().actors);
|
|
26
|
-
let items = all
|
|
27
|
-
// search ingests and strangers' mentions stay out of the timelines
|
|
28
|
-
// (mentions remain in notifications and /api/v1/statuses); direct
|
|
29
|
-
// posts belong to the conversations view, not a timeline; and the
|
|
30
|
-
// local/public view shows only what is actually public-facing.
|
|
31
|
-
.filter(s => s.kind !== 'remote' && s.kind !== 'mention')
|
|
32
|
-
.filter(s => !s.direct && s.visibility !== 'direct')
|
|
33
|
-
.filter(s => !muted.has(s.actor) && !muted.has(s.via))
|
|
34
|
-
.filter(s => !localOnly || (s.kind === 'post' && s.visibility !== 'private'))
|
|
35
|
-
.sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
|
|
36
|
-
const { items: page, headers } = api.page(items, url);
|
|
37
|
-
return send(200, page.map(s => api.statusOrBoost(s, { all })), headers);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// --- hashtags: the client's Followed Hashtags surface drives the tag feed ---
|
|
41
|
-
// followed_tags is what the feed currently pulls; follow/unfollow add and
|
|
42
|
-
// remove a hashtag (unfollow the last and the topical feed goes quiet); the
|
|
43
|
-
// tag timeline is the mirrored notes carrying that tag.
|
|
44
|
-
if (pathname === '/api/v1/followed_tags') {
|
|
45
|
-
const tags = api.agent.tagfeed?.config().tags || [];
|
|
46
|
-
return send(200, tags.map(t => api.tagObject(t, true, req)));
|
|
47
|
-
}
|
|
48
|
-
const mTagFollow = pathname.match(/^\/api\/v1\/tags\/([^/]+)\/(follow|unfollow)$/);
|
|
49
|
-
if (mTagFollow && req.method === 'POST') {
|
|
50
|
-
const name = decodeURIComponent(mTagFollow[1]).replace(/^#/, '').toLowerCase();
|
|
51
|
-
const follow = mTagFollow[2] === 'follow';
|
|
52
|
-
const tf = api.agent.tagfeed;
|
|
53
|
-
if (tf) {
|
|
54
|
-
const cur = tf.config().tags;
|
|
55
|
-
tf.setConfig({ tags: follow ? [...new Set([...cur, name])] : cur.filter(t => t !== name) });
|
|
56
|
-
}
|
|
57
|
-
return send(200, api.tagObject(name, follow, req));
|
|
58
|
-
}
|
|
59
|
-
const mTagGet = pathname.match(/^\/api\/v1\/tags\/([^/]+)$/);
|
|
60
|
-
if (mTagGet && req.method === 'GET') {
|
|
61
|
-
const name = decodeURIComponent(mTagGet[1]).replace(/^#/, '').toLowerCase();
|
|
62
|
-
const following = (api.agent.tagfeed?.config().tags || []).includes(name);
|
|
63
|
-
return send(200, api.tagObject(name, following, req));
|
|
64
|
-
}
|
|
65
|
-
// --- trending tags: counted off the notes already mirrored ---
|
|
66
|
-
//
|
|
67
|
-
// A single-actor instance has no firehose to measure, so "trending" here can
|
|
68
|
-
// only mean what the tag feed has actually brought in — which is the honest
|
|
69
|
-
// answer, and the one that makes a client's Explore surface show something
|
|
70
|
-
// instead of an empty list. `/api/v1/trends` is Mastodon's older name for the
|
|
71
|
-
// same thing.
|
|
72
|
-
if (pathname === '/api/v1/trends/tags' || pathname === '/api/v1/trends') {
|
|
73
|
-
const DAY = 86400000;
|
|
74
|
-
const today = Math.floor(Date.now() / DAY) * DAY;
|
|
75
|
-
// tag -> day -> { uses, accounts }. Accounts are counted distinctly, which
|
|
76
|
-
// is what the number means in Mastodon: one person posting nine times is
|
|
77
|
-
// nine uses by one account, and a client shows the second number.
|
|
78
|
-
const seen = new Map();
|
|
79
|
-
for (const st of api.store.getStatuses()) {
|
|
80
|
-
if (st.kind !== 'tag' || !st.tag) continue;
|
|
81
|
-
const at = Date.parse(st.published || '');
|
|
82
|
-
if (!Number.isFinite(at) || at < today - 6 * DAY) continue;
|
|
83
|
-
const day = Math.floor(at / DAY) * DAY;
|
|
84
|
-
if (!seen.has(st.tag)) seen.set(st.tag, new Map());
|
|
85
|
-
const days = seen.get(st.tag);
|
|
86
|
-
if (!days.has(day)) days.set(day, { uses: 0, accounts: new Set() });
|
|
87
|
-
const rec = days.get(day);
|
|
88
|
-
rec.uses++;
|
|
89
|
-
if (st.actor) rec.accounts.add(st.actor);
|
|
90
|
-
}
|
|
91
|
-
const followed = new Set(api.agent.tagfeed?.config().tags || []);
|
|
92
|
-
const ranked = [...seen.entries()].map(([name, days]) => {
|
|
93
|
-
// Seven entries, newest first, gaps filled with zeroes — a client draws
|
|
94
|
-
// the sparkline straight off this and a short array draws a short line.
|
|
95
|
-
const history = [];
|
|
96
|
-
for (let i = 0; i < 7; i++) {
|
|
97
|
-
const rec = days.get(today - i * DAY);
|
|
98
|
-
history.push({ day: String(Math.floor((today - i * DAY) / 1000)),
|
|
99
|
-
uses: String(rec ? rec.uses : 0), accounts: String(rec ? rec.accounts.size : 0) });
|
|
100
|
-
}
|
|
101
|
-
return { total: history.reduce((n, h) => n + Number(h.uses), 0),
|
|
102
|
-
tag: api.tagObject(name, followed.has(name), req, history) };
|
|
103
|
-
}).sort((a, b) => b.total - a.total);
|
|
104
|
-
const limit = Math.min(Math.max(Number(url.searchParams.get('limit')) || 10, 1), 40);
|
|
105
|
-
return send(200, ranked.slice(0, limit).map(x => x.tag));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const mTagTl = pathname.match(/^\/api\/v1\/timelines\/tag\/([^/]+)$/);
|
|
109
|
-
if (mTagTl && req.method === 'GET') {
|
|
110
|
-
const name = decodeURIComponent(mTagTl[1]).replace(/^#/, '').toLowerCase();
|
|
111
|
-
const all = api.store.getStatuses();
|
|
112
|
-
const items = all.filter(s => s.kind === 'tag' && s.tag === name)
|
|
113
|
-
.sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
|
|
114
|
-
const { items: page, headers } = api.page(items, url);
|
|
115
|
-
return send(200, page.map(s => api.statusOrBoost(s, { all })), headers);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Lists, filters and scheduled posts are the client's own arrangements —
|
|
119
|
-
// kept in local state, nothing federates.
|
|
120
|
-
const listJson = (l) => ({ id: l.id, title: l.title, replies_policy: l.repliesPolicy || 'list', exclusive: false });
|
|
121
|
-
if (pathname === '/api/v1/lists' && req.method === 'GET') {
|
|
122
|
-
return send(200, api.store.getLists().map(listJson));
|
|
123
|
-
}
|
|
124
|
-
if (pathname === '/api/v1/lists' && req.method === 'POST') {
|
|
125
|
-
const body = await readBody(req);
|
|
126
|
-
const title = String(body.title || '').trim();
|
|
127
|
-
if (!title) return send(422, { error: 'a title is required' });
|
|
128
|
-
const lists = api.store.getLists();
|
|
129
|
-
const l = { id: crypto.randomBytes(8).toString('hex'), title, repliesPolicy: body.replies_policy || 'list', members: [] };
|
|
130
|
-
lists.push(l);
|
|
131
|
-
api.store.setLists(lists);
|
|
132
|
-
return send(200, listJson(l));
|
|
133
|
-
}
|
|
134
|
-
const mList = /^\/api\/v1\/lists\/([a-f0-9]+)$/.exec(pathname);
|
|
135
|
-
if (mList) {
|
|
136
|
-
const lists = api.store.getLists();
|
|
137
|
-
const l = lists.find(x => x.id === mList[1]);
|
|
138
|
-
if (!l) return send(404, { error: 'Record not found' });
|
|
139
|
-
if (req.method === 'DELETE') {
|
|
140
|
-
api.store.setLists(lists.filter(x => x.id !== mList[1]));
|
|
141
|
-
return send(200, {});
|
|
142
|
-
}
|
|
143
|
-
if (req.method === 'PUT') {
|
|
144
|
-
const body = await readBody(req);
|
|
145
|
-
if (body.title) l.title = String(body.title);
|
|
146
|
-
if (body.replies_policy) l.repliesPolicy = body.replies_policy;
|
|
147
|
-
api.store.setLists(lists);
|
|
148
|
-
}
|
|
149
|
-
return send(200, listJson(l));
|
|
150
|
-
}
|
|
151
|
-
const mListAcc = /^\/api\/v1\/lists\/([a-f0-9]+)\/accounts$/.exec(pathname);
|
|
152
|
-
if (mListAcc) {
|
|
153
|
-
const lists = api.store.getLists();
|
|
154
|
-
const l = lists.find(x => x.id === mListAcc[1]);
|
|
155
|
-
if (!l) return send(404, { error: 'Record not found' });
|
|
156
|
-
if (req.method === 'GET') return send(200, (l.members || []).map(a => api.account(a)));
|
|
157
|
-
const body = await readBody(req).catch(() => ({}));
|
|
158
|
-
const ids = [].concat(body.account_ids || body['account_ids[]'] || url.searchParams.getAll('account_ids[]')).filter(Boolean);
|
|
159
|
-
const actors = ids.map(id => api.store.urlFor(id)).filter(Boolean);
|
|
160
|
-
if (req.method === 'POST') l.members = [...new Set([...(l.members || []), ...actors])];
|
|
161
|
-
if (req.method === 'DELETE') l.members = (l.members || []).filter(a => !actors.includes(a));
|
|
162
|
-
api.store.setLists(lists);
|
|
163
|
-
return send(200, {});
|
|
164
|
-
}
|
|
165
|
-
const mListTl = /^\/api\/v1\/timelines\/list\/([a-f0-9]+)$/.exec(pathname);
|
|
166
|
-
if (mListTl) {
|
|
167
|
-
const l = api.store.getLists().find(x => x.id === mListTl[1]);
|
|
168
|
-
if (!l) return send(404, { error: 'Record not found' });
|
|
169
|
-
const members = new Set(l.members || []);
|
|
170
|
-
const all = api.store.getStatuses();
|
|
171
|
-
const items = all.filter(s => members.has(s.actor) || members.has(s.via))
|
|
172
|
-
.sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
|
|
173
|
-
const { items: page, headers } = api.page(items, url);
|
|
174
|
-
return send(200, page.map(s => api.statusOrBoost(s, { all })), headers);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// v2 filters: stored, served, and APPLIED — every status carries what it
|
|
178
|
-
// matched in `filtered` (see filtersFor), because no client matches
|
|
179
|
-
// keywords for itself.
|
|
180
|
-
const filterJson = (f) => ({
|
|
181
|
-
id: f.id, title: f.title, context: f.context || ['home'],
|
|
182
|
-
expires_at: f.expiresAt || null, filter_action: f.action || 'warn',
|
|
183
|
-
keywords: (f.keywords || []).map((k, i) => ({ id: `${f.id}-${i}`, keyword: k.keyword, whole_word: !!k.wholeWord })),
|
|
184
|
-
statuses: [],
|
|
185
|
-
});
|
|
186
|
-
const keywordsOf = (attrs) => [].concat(attrs || [])
|
|
187
|
-
.filter(k => k?.keyword && !(k._destroy === true || k._destroy === 'true'))
|
|
188
|
-
.map(k => ({ keyword: String(k.keyword), wholeWord: k.whole_word === true || k.whole_word === 'true' }));
|
|
189
|
-
if (pathname === '/api/v2/filters' && req.method === 'GET') {
|
|
190
|
-
return send(200, api.store.getFilters().map(filterJson));
|
|
191
|
-
}
|
|
192
|
-
if (pathname === '/api/v2/filters' && req.method === 'POST') {
|
|
193
|
-
const body = await readBody(req);
|
|
194
|
-
const title = String(body.title || '').trim();
|
|
195
|
-
if (!title) return send(422, { error: 'a title is required' });
|
|
196
|
-
const filters = api.store.getFilters();
|
|
197
|
-
const f = {
|
|
198
|
-
id: crypto.randomBytes(8).toString('hex'), title,
|
|
199
|
-
context: [].concat(body.context || ['home']),
|
|
200
|
-
action: body.filter_action || 'warn', expiresAt: null,
|
|
201
|
-
keywords: keywordsOf(body.keywords_attributes),
|
|
202
|
-
};
|
|
203
|
-
filters.push(f);
|
|
204
|
-
api.store.setFilters(filters);
|
|
205
|
-
return send(200, filterJson(f));
|
|
206
|
-
}
|
|
207
|
-
const mFilter = /^\/api\/v2\/filters\/([a-f0-9]+)$/.exec(pathname);
|
|
208
|
-
if (mFilter) {
|
|
209
|
-
const filters = api.store.getFilters();
|
|
210
|
-
const f = filters.find(x => x.id === mFilter[1]);
|
|
211
|
-
if (!f) return send(404, { error: 'Record not found' });
|
|
212
|
-
if (req.method === 'DELETE') {
|
|
213
|
-
api.store.setFilters(filters.filter(x => x.id !== mFilter[1]));
|
|
214
|
-
return send(200, {});
|
|
215
|
-
}
|
|
216
|
-
if (req.method === 'PUT') {
|
|
217
|
-
const body = await readBody(req);
|
|
218
|
-
if (body.title) f.title = String(body.title);
|
|
219
|
-
if (body.context) f.context = [].concat(body.context);
|
|
220
|
-
if (body.filter_action) f.action = body.filter_action;
|
|
221
|
-
if (body.keywords_attributes) f.keywords = keywordsOf(body.keywords_attributes);
|
|
222
|
-
api.store.setFilters(filters);
|
|
223
|
-
}
|
|
224
|
-
return send(200, filterJson(f));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Conversations: direct posts, grouped by who is in them.
|
|
228
|
-
if (pathname === '/api/v1/conversations' && req.method === 'GET') {
|
|
229
|
-
const all = api.store.getStatuses();
|
|
230
|
-
const me = api.urls.actor;
|
|
231
|
-
const convos = new Map();
|
|
232
|
-
for (const s of all) {
|
|
233
|
-
if (!(s.direct || s.visibility === 'direct')) continue;
|
|
234
|
-
const others = [...new Set([
|
|
235
|
-
...(s.actor !== me ? [s.actor] : []),
|
|
236
|
-
...((s.mentions || []).map(m => m.href).filter(a => a && a !== me)),
|
|
237
|
-
])];
|
|
238
|
-
const key = others.sort().join(' ') || me;
|
|
239
|
-
const c = convos.get(key) || { accounts: new Set(), last: s };
|
|
240
|
-
for (const o of others) c.accounts.add(o);
|
|
241
|
-
if (String(s.published || '') > String(c.last.published || '')) c.last = s;
|
|
242
|
-
convos.set(key, c);
|
|
243
|
-
}
|
|
244
|
-
const items = [...convos.entries()]
|
|
245
|
-
.sort((a, b) => String(b[1].last.published || '').localeCompare(String(a[1].last.published || '')));
|
|
246
|
-
return send(200, items.map(([key, c]) => ({
|
|
247
|
-
id: api.store.idFor('conversation:' + key),
|
|
248
|
-
unread: false,
|
|
249
|
-
accounts: [...(c.accounts.size ? c.accounts : [me])].map(a => api.account(a)),
|
|
250
|
-
last_status: api.status(c.last, { all }),
|
|
251
|
-
})));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// The direct timeline: every direct post, newest first, paged like the rest.
|
|
255
|
-
if (pathname === '/api/v1/timelines/direct' && req.method === 'GET') {
|
|
256
|
-
const all = api.store.getStatuses();
|
|
257
|
-
const items = all.filter(s => s.direct || s.visibility === 'direct');
|
|
258
|
-
const { items: page, headers } = api.page(items, url);
|
|
259
|
-
return send(200, page.map(s => api.status(s, { all })), headers);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// One notification by id — what a push notification tap opens — and the
|
|
263
|
-
// two ways of being rid of them.
|
|
264
|
-
if (pathname === '/api/v1/notifications/clear' && req.method === 'POST') {
|
|
265
|
-
api.store.removeNotifications(() => true);
|
|
266
|
-
return send(200, {});
|
|
267
|
-
}
|
|
268
|
-
const mNotif = /^\/api\/v1\/notifications\/([A-Za-z0-9_-]+)(\/dismiss)?$/.exec(pathname);
|
|
269
|
-
if (mNotif) {
|
|
270
|
-
const n = api.store.getNotifications().find(x => x.id === mNotif[1]);
|
|
271
|
-
if (!n) return send(404, { error: 'Record not found' });
|
|
272
|
-
if (mNotif[2] && req.method === 'POST') { api.store.removeNotifications(x => x.id === n.id); return send(200, {}); }
|
|
273
|
-
if (!mNotif[2] && req.method === 'GET') return send(200, api.notification(n));
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Grouped notifications, the shape a client reads from API version 2 on.
|
|
277
|
-
// One group per notification: the client does its own gathering of
|
|
278
|
-
// favourites and boosts on a post, and every field it joins on — the
|
|
279
|
-
// account and status ids — is here.
|
|
280
|
-
const mGroup = /^\/api\/v2\/notifications\/([A-Za-z0-9_-]+)(\/accounts|\/dismiss)?$/.exec(pathname);
|
|
281
|
-
if (mGroup) {
|
|
282
|
-
const n = api.store.getNotifications().find(x => x.id === mGroup[1]);
|
|
283
|
-
if (!n) return send(404, { error: 'Record not found' });
|
|
284
|
-
if (mGroup[2] === '/dismiss' && req.method === 'POST') { api.store.removeNotifications(x => x.id === n.id); return send(200, {}); }
|
|
285
|
-
if (mGroup[2] === '/accounts' && req.method === 'GET') return send(200, [api.account(n.actor)]);
|
|
286
|
-
if (!mGroup[2] && req.method === 'GET') return send(200, groupedNotifications(api, [n]));
|
|
287
|
-
}
|
|
288
|
-
if (pathname === '/api/v2/notifications') {
|
|
289
|
-
const { page, headers } = notificationPage(api, ctx);
|
|
290
|
-
return send(200, groupedNotifications(api, page), headers);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (pathname === '/api/v1/notifications') {
|
|
294
|
-
const { page, headers } = notificationPage(api, ctx);
|
|
295
|
-
return send(200, page.map((n) => api.notification(n)), headers);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Search: @user@host → webfinger resolve; URL → actor or note ingest;
|
|
299
|
-
// plain text → local mirror + actor-cache scan.
|
|
300
|
-
if (pathname === '/api/v2/search' || pathname === '/api/v1/search') {
|
|
301
|
-
return searchHandler(api, ctx);
|
|
302
|
-
}
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function groupedNotifications(api, page) {
|
|
307
|
-
const accounts = new Map();
|
|
308
|
-
const statuses = new Map();
|
|
309
|
-
const groups = page.map((n) => {
|
|
310
|
-
const acct = api.account(n.actor);
|
|
311
|
-
accounts.set(acct.id, acct);
|
|
312
|
-
const s = n.noteId && api.store.getStatuses().find(x => x.noteId === n.noteId);
|
|
313
|
-
let statusId = null;
|
|
314
|
-
if (s) { const j = api.status(s); statuses.set(j.id, j); statusId = j.id; }
|
|
315
|
-
return {
|
|
316
|
-
group_key: n.id, notifications_count: 1, type: api.notificationType(n.type),
|
|
317
|
-
most_recent_notification_id: n.id, page_min_id: n.id, page_max_id: n.id,
|
|
318
|
-
latest_page_notification_at: n.at, sample_account_ids: [acct.id],
|
|
319
|
-
...(statusId ? { status_id: statusId } : {}),
|
|
320
|
-
...(n.type === 'reaction' ? { emoji: n.emoji, ...(n.url ? { emoji_url: n.url } : {}) } : {}),
|
|
321
|
-
};
|
|
322
|
-
});
|
|
323
|
-
return { accounts: [...accounts.values()], statuses: [...statuses.values()], notification_groups: groups };
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// The slice of notifications a request asks for, and the Link header that
|
|
327
|
-
// pages it. Shared by the flat list and the grouped one.
|
|
328
|
-
function notificationPage(api, ctx) {
|
|
329
|
-
const { req, pathname, url } = ctx;
|
|
330
|
-
{
|
|
331
|
-
const limit = Math.min(Number(url.searchParams.get('limit')) || 30, 60);
|
|
332
|
-
const q = url.searchParams;
|
|
333
|
-
// Newest first, which is what every id rule below assumes.
|
|
334
|
-
let items = api.store.getNotifications();
|
|
335
|
-
|
|
336
|
-
// Which KINDS. A client that asks for mentions and is handed favourites
|
|
337
|
-
// and boosts renders them as mentions — Phanpy's Mentions column did
|
|
338
|
-
// exactly that, and re-pilled on every poll. Both spellings: clients send
|
|
339
|
-
// `types[]=mention` and some send bare `types=mention`.
|
|
340
|
-
const listParam = (name) => {
|
|
341
|
-
const all = [...q.getAll(`${name}[]`), ...q.getAll(name)]
|
|
342
|
-
.flatMap((v) => String(v).split(',')).map((v) => v.trim()).filter(Boolean);
|
|
343
|
-
return all.length ? new Set(all) : null;
|
|
344
|
-
};
|
|
345
|
-
const want = listParam('types');
|
|
346
|
-
const skip = listParam('exclude_types');
|
|
347
|
-
// Mastodon spells the type with an underscore; ours is stored with a
|
|
348
|
-
// hyphen, and notification() translates on the way out — so match on what
|
|
349
|
-
// the client would have been given, not on what is on disk.
|
|
350
|
-
const shown = (n) => api.notificationType(n.type);
|
|
351
|
-
if (want) items = items.filter((n) => want.has(shown(n)));
|
|
352
|
-
if (skip) items = items.filter((n) => !skip.has(shown(n)));
|
|
353
|
-
|
|
354
|
-
// Which SLICE. `max_id` walks backwards into history; `since_id` and
|
|
355
|
-
// `min_id` both mean "newer than this", which is how a client asks
|
|
356
|
-
// "anything since I last looked?". Ignoring them meant every poll
|
|
357
|
-
// returned the whole list, so the client always saw unread items and the
|
|
358
|
-
// bell never went out.
|
|
359
|
-
const cut = (id, keepNewer) => {
|
|
360
|
-
const i = items.findIndex((n) => n.id === id);
|
|
361
|
-
if (i < 0) return;
|
|
362
|
-
items = keepNewer ? items.slice(0, i) : items.slice(i + 1);
|
|
363
|
-
};
|
|
364
|
-
const maxId = q.get('max_id');
|
|
365
|
-
if (maxId) cut(maxId, false);
|
|
366
|
-
const sinceId = q.get('since_id') || q.get('min_id');
|
|
367
|
-
if (sinceId) cut(sinceId, true);
|
|
368
|
-
// min_id asks for the ones IMMEDIATELY newer, i.e. the oldest end of
|
|
369
|
-
// what is newer; since_id asks for the newest. Only the window differs.
|
|
370
|
-
const page = q.get('min_id') && !q.get('since_id')
|
|
371
|
-
? items.slice(Math.max(0, items.length - limit))
|
|
372
|
-
: items.slice(0, limit);
|
|
373
|
-
|
|
374
|
-
// A client pages by following these rather than by guessing ids.
|
|
375
|
-
if (page.length) {
|
|
376
|
-
const base = `${api.scheme || (req.socket?.encrypted ? 'https' : 'http')}://${req.headers.host}${api.mount || ''}${pathname}`;
|
|
377
|
-
const link = (params) => {
|
|
378
|
-
const u = new URL(base);
|
|
379
|
-
for (const [k, v] of q) if (k !== 'max_id' && k !== 'since_id' && k !== 'min_id') u.searchParams.append(k, v);
|
|
380
|
-
for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
|
|
381
|
-
return u.href;
|
|
382
|
-
};
|
|
383
|
-
return { page, headers: {
|
|
384
|
-
link: `<${link({ max_id: page[page.length - 1].id })}>; rel="next", `
|
|
385
|
-
+ `<${link({ min_id: page[0].id })}>; rel="prev"`,
|
|
386
|
-
} };
|
|
387
|
-
}
|
|
388
|
-
return { page: [], headers: undefined };
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Search: @user@host → webfinger resolve; URL → actor or note ingest;
|
|
393
|
-
// plain text → local mirror + actor-cache scan.
|
|
394
|
-
async function searchHandler(api, ctx) {
|
|
395
|
-
const { req, res, pathname, url, send } = ctx; // eslint-disable-line no-unused-vars
|
|
396
|
-
{
|
|
397
|
-
const q = String(url.searchParams.get('q') || '').trim();
|
|
398
|
-
const type = url.searchParams.get('type');
|
|
399
|
-
const out = { accounts: [], statuses: [], hashtags: [] };
|
|
400
|
-
const asHandle = /^@?[^@\s]+@[^@\s.]+\.[^@\s]+$/.test(q);
|
|
401
|
-
if (asHandle && type !== 'statuses') {
|
|
402
|
-
try {
|
|
403
|
-
const doc = await social.resolveHandle(api.agent, q);
|
|
404
|
-
out.accounts.push(api.account(doc.id));
|
|
405
|
-
} catch (e) { api.log(`search resolve ${q}: ${e.message}`); }
|
|
406
|
-
} else if (/^https?:\/\//.test(q)) {
|
|
407
|
-
const doc = await api.agent.intake.fetchAP(q).catch(() => null);
|
|
408
|
-
if (doc?.type === 'Person' && doc.id) out.accounts.push(api.account(doc.id));
|
|
409
|
-
else if (doc?.type === 'Note' && doc.id) {
|
|
410
|
-
let s = api.store.getStatuses().find(x => x.noteId === doc.id);
|
|
411
|
-
// Third site of the same rule: the note's own origin has to vouch for
|
|
412
|
-
// the author before we store one. The owner chose the URL, but the
|
|
413
|
-
// document at it still names whoever it likes.
|
|
414
|
-
const author = authorOf(doc);
|
|
415
|
-
if (!s && author) {
|
|
416
|
-
s = {
|
|
417
|
-
noteId: doc.id, actor: author, content: sanitizeHtml(doc.content),
|
|
418
|
-
published: doc.published, inReplyTo: doc.inReplyTo, kind: 'remote',
|
|
419
|
-
};
|
|
420
|
-
api.store.addStatus(s);
|
|
421
|
-
}
|
|
422
|
-
if (s) out.statuses.push(api.status(s));
|
|
423
|
-
}
|
|
424
|
-
} else if (q) {
|
|
425
|
-
const needle = q.toLowerCase();
|
|
426
|
-
if (type !== 'accounts') {
|
|
427
|
-
const all = api.store.getStatuses();
|
|
428
|
-
out.statuses = all
|
|
429
|
-
.filter(s => (s.content || '').toLowerCase().includes(needle)).slice(0, 20)
|
|
430
|
-
.map(s => api.status(s, { all }));
|
|
431
|
-
}
|
|
432
|
-
if (type !== 'statuses') out.accounts = await api.accountSearch(q);
|
|
433
|
-
}
|
|
434
|
-
return send(200, out);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
return false;
|
|
438
|
-
}
|
package/lib/client/oidc-auth.mjs
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// oidc-auth.mjs — who may drive the C2S surface. Two proofs are accepted:
|
|
2
|
-
// the Mastodon facade's bearer (minted on this machine, so its holder is the
|
|
3
|
-
// operator by construction), or a Solid-OIDC token with its DPoP proof,
|
|
4
|
-
// which Solid-OIDC requires, verified by the same library the wider
|
|
5
|
-
// Solid world uses. The token alone is not enough: the WebID it names must
|
|
6
|
-
// be THIS identity's owner, an authorization step the token does not carry.
|
|
7
|
-
//
|
|
8
|
-
// The verifier is injected so offline tests stub it, and wrapped so the
|
|
9
|
-
// library (CJS, older jose) can be replaced without touching any caller.
|
|
10
|
-
|
|
11
|
-
export function makeC2sAuth({ agent, masto = null, verifier = null, log = () => {}, scheme = null, mount = '' }) {
|
|
12
|
-
let verify = verifier;
|
|
13
|
-
const loadVerifier = async () => {
|
|
14
|
-
if (!verify) {
|
|
15
|
-
const { createSolidTokenVerifier } = await import('@solid/access-token-verifier');
|
|
16
|
-
verify = createSolidTokenVerifier();
|
|
17
|
-
}
|
|
18
|
-
return verify;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
return async function authenticate(req, pathname) {
|
|
22
|
-
if (masto?.authed(req)) {
|
|
23
|
-
return { ok: true, webid: agent.remote?.webId || null, via: 'bearer' };
|
|
24
|
-
}
|
|
25
|
-
if (!req.headers.authorization) {
|
|
26
|
-
return { ok: false, status: 401, error: 'authentication required: a Solid-OIDC token (DPoP) or this agent\'s own bearer' };
|
|
27
|
-
}
|
|
28
|
-
// Solid-OIDC binds the token to a key and has the client prove it holds
|
|
29
|
-
// that key on every request. A token shown without its proof is a token
|
|
30
|
-
// anyone who saw it could show.
|
|
31
|
-
if (!req.headers.dpop) {
|
|
32
|
-
return { ok: false, status: 401, error: 'a Solid-OIDC token must come with its DPoP proof',
|
|
33
|
-
headers: { 'www-authenticate': 'DPoP' } };
|
|
34
|
-
}
|
|
35
|
-
let webid;
|
|
36
|
-
try {
|
|
37
|
-
const v = await loadVerifier();
|
|
38
|
-
// The URL the client signed its proof over. The Host header already
|
|
39
|
-
// passed the Authorities firewall, so whichever alias the client used
|
|
40
|
-
// (localhost, 127.0.0.1, the named origin) is one this agent answers on;
|
|
41
|
-
// the scheme is whichever listener the request arrived on. `pathname` is
|
|
42
|
-
// relative to this identity's mount, so a suffix pod folds the mount back
|
|
43
|
-
// in — the client signed over the full path it actually requested.
|
|
44
|
-
const htu = `${scheme ? scheme.replace(/:$/u, '') : req.socket?.encrypted ? 'https' : 'http'
|
|
45
|
-
}://${req.headers.host}${mount}${pathname}`;
|
|
46
|
-
({ webid } = await v(
|
|
47
|
-
req.headers.authorization,
|
|
48
|
-
{ header: req.headers.dpop, method: req.method, url: htu },
|
|
49
|
-
));
|
|
50
|
-
} catch (e) {
|
|
51
|
-
log(`c2s auth: token rejected — ${e.message}`);
|
|
52
|
-
return { ok: false, status: 401, error: `token rejected: ${e.message}` };
|
|
53
|
-
}
|
|
54
|
-
const owner = agent.remote?.webId;
|
|
55
|
-
if (!owner || webid !== owner) {
|
|
56
|
-
// Verified is not authorized: a valid token from ANY WebID must not
|
|
57
|
-
// post as this actor. (The gap jg10's outbox leaves open.)
|
|
58
|
-
return { ok: false, status: 403, error: 'authenticated, but this outbox belongs to its owner alone' };
|
|
59
|
-
}
|
|
60
|
-
return { ok: true, webid, via: 'oidc' };
|
|
61
|
-
};
|
|
62
|
-
}
|
package/lib/client/streaming.mjs
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// streaming.mjs — the Mastodon streaming API over a hand-rolled WebSocket
|
|
2
|
-
// server (send-mostly; we answer pings and ignore everything else a client
|
|
3
|
-
// frames at us). No dependencies — the handshake is one SHA-1 and text
|
|
4
|
-
// frames are trivial to emit. Clients connect to
|
|
5
|
-
// ws://host/api/v1/streaming?access_token=…&stream=user
|
|
6
|
-
// and receive {stream, event, payload} messages for new statuses and
|
|
7
|
-
// notifications the moment the agent learns of them.
|
|
8
|
-
|
|
9
|
-
import crypto from 'node:crypto';
|
|
10
|
-
|
|
11
|
-
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
12
|
-
|
|
13
|
-
// One identity's live feed is a handful of that owner's clients, so these are
|
|
14
|
-
// generous. Without them a single token could hold every file descriptor open,
|
|
15
|
-
// and one client that stops reading would grow its write buffer forever.
|
|
16
|
-
const MAX_SOCKETS = 20; // matches the client-token cap
|
|
17
|
-
const MAX_BUFFERED_BYTES = 1024 * 1024; // a consumer this far behind is gone
|
|
18
|
-
|
|
19
|
-
import { checkRequest } from '../shared/guard.mjs';
|
|
20
|
-
|
|
21
|
-
export class Streaming {
|
|
22
|
-
constructor({ masto, log = console.log, allowed = null, gate = null, gateOptional = false }) {
|
|
23
|
-
this.masto = masto;
|
|
24
|
-
this.log = log;
|
|
25
|
-
this.allowed = allowed;
|
|
26
|
-
this.gate = gate;
|
|
27
|
-
// A pod-hosted identity is a public instance: a phone app arrives with its
|
|
28
|
-
// bearer and no way to send the operator's header, so there the bearer is
|
|
29
|
-
// the credential. Locally the gate stands in front of the socket as well.
|
|
30
|
-
this.gateOptional = gateOptional;
|
|
31
|
-
this.sockets = new Set(); // entries: { send(string), close() }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Whether a client may open the live feed, asked of the upgrade request
|
|
35
|
-
// alone. Shared with the pod-server front, which completes the handshake
|
|
36
|
-
// before any of this runs and so must answer by closing rather than refusing.
|
|
37
|
-
authorizeUpgrade(req, url) {
|
|
38
|
-
const proto = String(req.headers['sec-websocket-protocol'] || '').split(',')[0].trim();
|
|
39
|
-
const token = url.searchParams.get('access_token') || proto;
|
|
40
|
-
const hasToken = this.masto.tokens().includes(token);
|
|
41
|
-
// WebSockets are exempt from CORS. A live bearer is the credential — a
|
|
42
|
-
// web client on its own origin connects the way it does to any Mastodon
|
|
43
|
-
// server — so with one, only the Host check stands (it is what stops DNS
|
|
44
|
-
// rebinding); without one the full Host/Origin firewall applies.
|
|
45
|
-
const bad = this.allowed ? checkRequest(req, this.allowed, { ignoreOrigin: hasToken }) : null;
|
|
46
|
-
if (bad) return `streaming refused: ${bad}`;
|
|
47
|
-
// A gate token covers every other route; an upgrade that skipped it was a
|
|
48
|
-
// hole in a control the operator had switched on. A client bearer is the
|
|
49
|
-
// other way in — a phone app has no way to send the operator's header.
|
|
50
|
-
if (this.gate && !this.gate.upgradeOk(req) && !(this.gateOptional && hasToken)) {
|
|
51
|
-
return 'streaming refused: no gate token';
|
|
52
|
-
}
|
|
53
|
-
if (!hasToken) return 'streaming refused: not a live client token';
|
|
54
|
-
if (this.sockets.size >= MAX_SOCKETS) return 'streaming refused: too many live connections';
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Adopt a WebSocket somebody else finished the handshake for — the pod
|
|
59
|
-
// server owns the socket, and hands over an object that already frames.
|
|
60
|
-
adopt(ws) {
|
|
61
|
-
const entry = {
|
|
62
|
-
send: (str) => {
|
|
63
|
-
// A consumer that stops reading must not keep costing memory.
|
|
64
|
-
if ((ws.bufferedAmount ?? 0) > MAX_BUFFERED_BYTES) {
|
|
65
|
-
this.sockets.delete(entry);
|
|
66
|
-
try { ws.close(); } catch { /* already gone */ }
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
ws.send(str);
|
|
70
|
-
},
|
|
71
|
-
close: () => ws.close(),
|
|
72
|
-
};
|
|
73
|
-
this.sockets.add(entry);
|
|
74
|
-
const drop = () => this.sockets.delete(entry);
|
|
75
|
-
ws.on('close', drop);
|
|
76
|
-
ws.on('error', drop);
|
|
77
|
-
return entry;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Let go of every client: the server is stopping, not the connection failing.
|
|
81
|
-
stop() {
|
|
82
|
-
for (const s of this.sockets) { try { s.close(); } catch { /* already gone */ } }
|
|
83
|
-
this.sockets.clear();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Attach the upgrade handler to an http.Server.
|
|
87
|
-
attach(server) {
|
|
88
|
-
server.on('upgrade', (req, socket) => {
|
|
89
|
-
const url = new URL(req.url, 'http://localhost');
|
|
90
|
-
if (!url.pathname.startsWith('/api/v1/streaming')) { socket.destroy(); return; }
|
|
91
|
-
// WebSockets are exempt from CORS, so the Host/Origin firewall is the
|
|
92
|
-
// only thing standing between a visited page and the live feed.
|
|
93
|
-
const refusal = this.authorizeUpgrade(req, url);
|
|
94
|
-
if (refusal) {
|
|
95
|
-
this.log(refusal);
|
|
96
|
-
socket.write(refusal.includes('refused:') && !refusal.includes('token')
|
|
97
|
-
? 'HTTP/1.1 403 Forbidden\r\n\r\n'
|
|
98
|
-
: 'HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
99
|
-
socket.destroy();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const proto = String(req.headers['sec-websocket-protocol'] || '').split(',')[0].trim();
|
|
103
|
-
const accept = crypto.createHash('sha1')
|
|
104
|
-
.update(req.headers['sec-websocket-key'] + WS_MAGIC).digest('base64');
|
|
105
|
-
socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n'
|
|
106
|
-
+ `Sec-WebSocket-Accept: ${accept}\r\n`
|
|
107
|
-
+ (proto ? `Sec-WebSocket-Protocol: ${proto}\r\n` : '') + '\r\n');
|
|
108
|
-
socket.setNoDelay(true);
|
|
109
|
-
const entry = {
|
|
110
|
-
send: (str) => {
|
|
111
|
-
// A consumer that stops reading must not keep costing memory.
|
|
112
|
-
if (socket.writableLength > MAX_BUFFERED_BYTES) {
|
|
113
|
-
this.sockets.delete(entry);
|
|
114
|
-
try { socket.destroy(); } catch { /* already gone */ }
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
socket.write(this.frame(str));
|
|
118
|
-
},
|
|
119
|
-
close: () => { try { socket.end(); } catch { /* already gone */ } },
|
|
120
|
-
};
|
|
121
|
-
this.sockets.add(entry);
|
|
122
|
-
const drop = () => this.sockets.delete(entry);
|
|
123
|
-
socket.on('close', drop);
|
|
124
|
-
socket.on('error', drop);
|
|
125
|
-
socket.on('data', (buf) => {
|
|
126
|
-
if (buf.length && (buf[0] & 0x0f) === 0x9) { // ping → pong
|
|
127
|
-
try { socket.write(Buffer.from([0x8a, 0x00])); } catch { drop(); }
|
|
128
|
-
}
|
|
129
|
-
if (buf.length && (buf[0] & 0x0f) === 0x8) { drop(); try { socket.end(); } catch {} } // close
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Server→client unmasked text frame.
|
|
135
|
-
frame(str) {
|
|
136
|
-
const payload = Buffer.from(str);
|
|
137
|
-
let header;
|
|
138
|
-
if (payload.length < 126) header = Buffer.from([0x81, payload.length]);
|
|
139
|
-
else if (payload.length < 65536) {
|
|
140
|
-
header = Buffer.alloc(4); header[0] = 0x81; header[1] = 126; header.writeUInt16BE(payload.length, 2);
|
|
141
|
-
} else {
|
|
142
|
-
header = Buffer.alloc(10); header[0] = 0x81; header[1] = 127; header.writeBigUInt64BE(BigInt(payload.length), 2);
|
|
143
|
-
}
|
|
144
|
-
return Buffer.concat([header, payload]);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
broadcast(event, payloadObj, streams = ['user']) {
|
|
148
|
-
if (!this.sockets.size) return;
|
|
149
|
-
const message = JSON.stringify({ stream: streams, event, payload: JSON.stringify(payloadObj) });
|
|
150
|
-
for (const s of this.sockets) {
|
|
151
|
-
try { s.send(message); } catch { this.sockets.delete(s); }
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|