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