fedipod-server 0.21.0 → 0.28.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 (332) hide show
  1. package/README.md +5 -4
  2. package/dist/claims.d.ts +3 -1
  3. package/dist/claims.js +20 -7
  4. package/dist/components/context.jsonld +6 -0
  5. package/dist/handler.d.ts +29 -1
  6. package/dist/handler.js +89 -4
  7. package/dist/handler.jsonld +36 -1
  8. package/lib/client/c2s.mjs +100 -3
  9. package/lib/client/masto/accounts.mjs +44 -4
  10. package/lib/client/masto/body.mjs +90 -3
  11. package/lib/client/masto/index.mjs +5 -1
  12. package/lib/client/masto/instance.mjs +11 -3
  13. package/lib/client/masto/media.mjs +3 -57
  14. package/lib/client/masto/oauth.mjs +66 -9
  15. package/lib/client/masto/render.mjs +62 -5
  16. package/lib/client/masto/statuses.mjs +25 -1
  17. package/lib/client/masto/timelines.mjs +128 -6
  18. package/lib/connections/tagfeed.mjs +42 -5
  19. package/lib/core/as2.mjs +36 -5
  20. package/lib/core/contexts/index.mjs +14 -0
  21. package/lib/core/contexts/litepub-0.1.json +60 -0
  22. package/lib/core/contexts/map.json +3 -1
  23. package/lib/core/contexts/quotes.json +18 -0
  24. package/lib/core/graphview.mjs +41 -3
  25. package/lib/core/intake/activities.mjs +136 -2
  26. package/lib/core/intake/activity.mjs +79 -1
  27. package/lib/core/intake/group.mjs +37 -3
  28. package/lib/core/intake/index.mjs +32 -2
  29. package/lib/core/intake/notes.mjs +60 -3
  30. package/lib/core/lease.mjs +12 -2
  31. package/lib/core/profile-page.mjs +104 -0
  32. package/lib/core/publisher/collections.mjs +6 -1
  33. package/lib/core/publisher/index.mjs +35 -17
  34. package/lib/core/publisher/notes.mjs +104 -3
  35. package/lib/core/shapes/activitystreams.ttl +28 -0
  36. package/lib/core/shapes/index.mjs +26 -1
  37. package/lib/core/wire-quotes.mjs +71 -0
  38. package/lib/core/wire.mjs +59 -103
  39. package/lib/device/admin/routes/lifecycle.mjs +9 -0
  40. package/lib/device/admin/static.mjs +4 -1
  41. package/lib/device/admin/surface.mjs +26 -2
  42. package/lib/device/certs.mjs +18 -2
  43. package/lib/device/cli/commands/setup.mjs +6 -2
  44. package/lib/device/cli/commands/state.mjs +3 -1
  45. package/lib/device/gateway-move.mjs +58 -0
  46. package/lib/device/migrate.mjs +3 -1
  47. package/lib/device/setup.mjs +67 -7
  48. package/lib/gateway/front-core.mjs +307 -36
  49. package/lib/gateway/gateway-core.mjs +15 -6
  50. package/lib/gateway/notices.mjs +84 -0
  51. package/lib/gateway/quiet.mjs +184 -0
  52. package/lib/pod/root.mjs +6 -3
  53. package/lib/pod/state.mjs +8 -7
  54. package/lib/pod/transport.mjs +5 -1
  55. package/lib/session/README.md +123 -0
  56. package/lib/session/demo.html +94 -0
  57. package/lib/session/fedi-account.mjs +346 -0
  58. package/lib/session/fedi-login.mjs +123 -0
  59. package/lib/session/oidc-session.mjs +233 -0
  60. package/lib/session/package.json +41 -0
  61. package/package.json +1 -1
  62. package/run-agent.mjs +28 -0
  63. package/vendor/gate.cjs +6 -3
  64. package/web/admin/bar.css +66 -0
  65. package/web/admin/bar.js +38 -0
  66. package/web/admin/client/index.html +31 -3
  67. package/web/admin/client-pick.js +79 -0
  68. package/web/admin/gateway.js +22 -0
  69. package/web/admin/index.html +54 -13
  70. package/web/admin/notices-bar.js +90 -0
  71. package/web/admin/oauth-signin.mjs +37 -0
  72. package/web/admin/record.js +13 -1
  73. package/web/admin/setup/index.html +31 -3
  74. package/web/admin/tokens.css +15 -3
  75. package/web/admin/upkeep.js +4 -5
  76. package/web/app/README.md +14 -10
  77. package/web/app/admin-facade.mjs +51 -12
  78. package/web/app/agent.mjs +157 -19
  79. package/web/app/authorize_interaction.html +56 -0
  80. package/web/app/authorize_interaction.js +69 -0
  81. package/web/app/boot.mjs +233 -95
  82. package/web/app/dist/boot.js +477 -569
  83. package/web/app/dist/boot.js.map +4 -4
  84. package/web/app/dist/sw.js +1861 -532
  85. package/web/app/dist/sw.js.map +4 -4
  86. package/web/app/gateway-move.mjs +60 -0
  87. package/web/app/index.html +40 -49
  88. package/web/app/keys-browser.mjs +12 -15
  89. package/web/app/keystore.mjs +19 -36
  90. package/web/app/oidc-session.mjs +5 -188
  91. package/web/app/signup.mjs +172 -132
  92. package/web/app/sw-src.mjs +1 -1
  93. package/web/app/update.js +66 -0
  94. package/web/front/admin.html +5 -3
  95. package/web/front/new-account.html +2 -2
  96. package/web/front/notices.html +69 -0
  97. package/web/front/notices.js +107 -0
  98. package/web/front/run.html +15 -8
  99. package/web/front/run.js +124 -36
  100. package/web/icons/forum.svg +11 -0
  101. package/web/icons/gateway.svg +14 -0
  102. package/web/icons/local.svg +11 -0
  103. package/web/app/index.html~ +0 -152
  104. package/web/app/site/_headers +0 -9
  105. package/web/app/site/_redirects +0 -22
  106. package/web/app/site/admin/actors.js +0 -145
  107. package/web/app/site/admin/bar.css +0 -60
  108. package/web/app/site/admin/bar.js +0 -61
  109. package/web/app/site/admin/client/client.js +0 -66
  110. package/web/app/site/admin/client/index.html +0 -43
  111. package/web/app/site/admin/common.js +0 -23
  112. package/web/app/site/admin/connections.js +0 -112
  113. package/web/app/site/admin/gateway.js +0 -111
  114. package/web/app/site/admin/group.js +0 -258
  115. package/web/app/site/admin/index.html +0 -564
  116. package/web/app/site/admin/record.js +0 -378
  117. package/web/app/site/admin/setup/index.html +0 -215
  118. package/web/app/site/admin/setup/setup.js +0 -326
  119. package/web/app/site/admin/tokens.css +0 -56
  120. package/web/app/site/admin/upkeep.js +0 -178
  121. package/web/app/site/admin/window.css +0 -55
  122. package/web/app/site/admin/window.js +0 -136
  123. package/web/app/site/app/404.html +0 -40
  124. package/web/app/site/app/_headers +0 -2
  125. package/web/app/site/app/apple-touch-icon.png +0 -0
  126. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +0 -2
  127. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +0 -1
  128. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +0 -183
  129. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
  130. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  131. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +0 -2
  132. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +0 -1
  133. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  134. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  135. package/web/app/site/app/assets/chunk-aKtaBQYM.js +0 -1
  136. package/web/app/site/app/assets/compose-CFEx7wb4.js +0 -2
  137. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +0 -1
  138. package/web/app/site/app/assets/compose-ZJN5xSb4.js +0 -35
  139. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +0 -1
  140. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +0 -52
  141. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +0 -1
  142. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +0 -2
  143. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +0 -1
  144. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  145. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  146. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  147. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  148. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +0 -1
  149. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +0 -1
  150. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
  151. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +0 -1
  152. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
  153. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
  154. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
  155. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
  156. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
  157. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
  158. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
  159. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
  160. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +0 -1
  161. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +0 -1
  162. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
  163. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +0 -1
  164. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
  165. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +0 -1
  166. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
  167. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
  168. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +0 -1
  169. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
  170. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +0 -1
  171. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
  172. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
  173. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
  174. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
  175. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
  176. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +0 -1
  177. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +0 -1
  178. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +0 -1
  179. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +0 -1
  180. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +0 -1
  181. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
  182. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +0 -1
  183. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
  184. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +0 -1
  185. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +0 -1
  186. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +0 -1
  187. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
  188. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
  189. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +0 -1
  190. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +0 -1
  191. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
  192. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
  193. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +0 -1
  194. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +0 -1
  195. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +0 -1
  196. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
  197. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +0 -1
  198. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +0 -1
  199. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +0 -1
  200. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
  201. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
  202. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
  203. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +0 -1
  204. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +0 -1
  205. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +0 -1
  206. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +0 -1
  207. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +0 -1
  208. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
  209. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
  210. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +0 -1
  211. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +0 -1
  212. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +0 -1
  213. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +0 -1
  214. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +0 -1
  215. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +0 -1
  216. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +0 -1
  217. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +0 -1
  218. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
  219. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +0 -1
  220. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +0 -1
  221. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +0 -1
  222. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
  223. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +0 -1
  224. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +0 -1
  225. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
  226. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
  227. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
  228. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +0 -1
  229. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +0 -1
  230. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
  231. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +0 -1
  232. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +0 -1
  233. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +0 -1
  234. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +0 -1
  235. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +0 -1
  236. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +0 -1
  237. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +0 -1
  238. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +0 -1
  239. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +0 -1
  240. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
  241. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +0 -1
  242. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
  243. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
  244. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
  245. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +0 -1
  246. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
  247. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +0 -1
  248. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +0 -1
  249. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
  250. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +0 -1
  251. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
  252. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +0 -1
  253. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
  254. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +0 -1
  255. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +0 -1
  256. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
  257. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +0 -1
  258. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +0 -1
  259. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
  260. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +0 -1
  261. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +0 -1
  262. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +0 -1
  263. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
  264. package/web/app/site/app/assets/instances-BNPptnn-.json +0 -333
  265. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +0 -1
  266. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +0 -1
  267. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
  268. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +0 -1
  269. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +0 -1
  270. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +0 -1
  271. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +0 -1
  272. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +0 -1
  273. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +0 -1
  274. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +0 -1
  275. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +0 -1
  276. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +0 -1
  277. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +0 -1
  278. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +0 -1
  279. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +0 -1
  280. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +0 -1
  281. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +0 -1
  282. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
  283. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +0 -1
  284. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
  285. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +0 -1
  286. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +0 -1
  287. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
  288. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +0 -1
  289. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +0 -1
  290. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
  291. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +0 -1
  292. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +0 -1
  293. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +0 -1
  294. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +0 -1
  295. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +0 -1
  296. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +0 -1
  297. package/web/app/site/app/assets/main-BdqNbG-a.js +0 -26
  298. package/web/app/site/app/assets/main-BdqNbG-a.js.map +0 -1
  299. package/web/app/site/app/assets/mock-home-DGhR__He.js +0 -2
  300. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +0 -1
  301. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  302. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  303. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  304. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +0 -2
  305. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +0 -1
  306. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +0 -20
  307. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +0 -1
  308. package/web/app/site/app/assets/style-B1iLPTPR.css +0 -2
  309. package/web/app/site/app/assets/temml-DuyIQMei.js +0 -97
  310. package/web/app/site/app/assets/temml-DuyIQMei.js.map +0 -1
  311. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +0 -2
  312. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
  313. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  314. package/web/app/site/app/assets/year-in-posts-DJv_star.js +0 -3
  315. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +0 -1
  316. package/web/app/site/app/compose/index.html +0 -20
  317. package/web/app/site/app/favicon.ico +0 -0
  318. package/web/app/site/app/index.html +0 -73
  319. package/web/app/site/app/logo-192.png +0 -0
  320. package/web/app/site/app/logo-512.png +0 -0
  321. package/web/app/site/app/logo-badge-72.png +0 -0
  322. package/web/app/site/app/logo-maskable-512.png +0 -0
  323. package/web/app/site/app/logo-monochrome-512.png +0 -0
  324. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  325. package/web/app/site/app/manifest.webmanifest +0 -1
  326. package/web/app/site/app/og-image-2.jpg +0 -0
  327. package/web/app/site/app/og-image.png +0 -0
  328. package/web/app/site/app/robots.txt +0 -2
  329. package/web/app/site/app/version.json +0 -1
  330. package/web/app/site/boot.js +0 -34277
  331. package/web/app/site/index.html +0 -251
  332. package/web/app/site/sw.js +0 -71331
@@ -3,7 +3,9 @@
3
3
  // rejection reason or nothing.
4
4
 
5
5
  import { dropFollower } from '../store.mjs';
6
- import { emojisOf, pollOf, isContentType, trimActivity } from './activity.mjs';
6
+ import { emojisOf, pollOf, isContentType, trimActivity, reactionOf, quoteOf, quotePolicyOf, authorOf } from './activity.mjs';
7
+
8
+ const idOf = (v) => (typeof v === 'string' ? v : v?.id);
7
9
 
8
10
  // §7.6 Add / §7.9 Remove. The side effect would be to add or remove the object
9
11
  // to/from the collection named in `target` — but only a collection we own AND
@@ -104,7 +106,7 @@ export async function onFollow(intake, activity, actor, { trusted = false } = {}
104
106
 
105
107
  export async function onUndo(intake, activity, actor, { trusted = false } = {}) {
106
108
  const inner = activity.object && typeof activity.object === 'object' ? activity.object : null;
107
- if (inner?.type === 'Like' || inner?.type === 'Announce') return onUndoReaction(intake, inner, actor, { trusted });
109
+ if (inner?.type === 'Like' || inner?.type === 'Announce' || inner?.type === 'EmojiReact') return onUndoReaction(intake, inner, actor, { trusted });
108
110
  // AS2 allows `object` to be a bare IRI, and that IRI is exactly the Follow
109
111
  // id we stored. Reading `.type` off a string gives undefined, so the whole
110
112
  // Undo was dropped — silently, since handle() reads that as handled, so no
@@ -290,6 +292,15 @@ export async function onUndoReaction(intake, inner, actor, { trusted = false } =
290
292
  const objectId = typeof target === 'string' ? target : target?.id;
291
293
  if (!objectId) return;
292
294
  if (objectId.startsWith(intake.urls.notes)) {
295
+ // An emoji taken back: the one reaction it names, by that sender.
296
+ const reaction = reactionOf(inner);
297
+ if (reaction) {
298
+ const gone = intake.store.removeNotifications(n => n.type === 'reaction' && n.actor === actor
299
+ && n.noteId === objectId && n.emoji === reaction.emoji);
300
+ if (gone) intake.log(`reaction ${reaction.emoji} withdrawn by ${actor} on ${objectId}`);
301
+ return;
302
+ }
303
+ if (inner.type === 'EmojiReact') return;
293
304
  const type = inner.type === 'Like' ? 'favourite' : 'reblog';
294
305
  const gone = intake.store.removeNotifications(n => n.type === type && n.actor === actor && n.noteId === objectId);
295
306
  if (gone) intake.log(`${inner.type} withdrawn by ${actor} on ${objectId}`);
@@ -369,6 +380,9 @@ export async function onDelete(intake, activity, actor) {
369
380
  }
370
381
  const s = intake.store.getStatuses().find(x => x.noteId === objectId);
371
382
  if (s) await intake.forget(s);
383
+ if (s && intake.onCarriedGone) {
384
+ await intake.onCarriedGone({ noteId: objectId, actor }).catch(e => intake.log(`carried gone ${objectId}: ${e.message}`));
385
+ }
372
386
  intake.log(`deleted upstream: ${objectId}`);
373
387
  }
374
388
 
@@ -401,9 +415,16 @@ export async function onUpdate(intake, activity, actor) {
401
415
  const attachments = attachmentsOf(note);
402
416
  const freshPoll = pollOf(note);
403
417
  const freshEmojis = emojisOf(note);
418
+ // A quote's authorization arrives on an Update, once the quoted author
419
+ // has answered: the quote is checked again, and the policy follows the edit.
420
+ const quote = quoteOf(note);
421
+ const quoteState = quote ? await intake.quoteStateOf(note, quote) : null;
422
+ const quotePolicy = quotePolicyOf(note, s.actor);
404
423
  intake.store.updateStatus(objectId, {
405
424
  content, ...(attachments.length ? { attachments } : {}),
406
425
  emojis: freshEmojis.length ? freshEmojis : undefined,
426
+ quote: quote || undefined, quoteState: quoteState || undefined,
427
+ quotePolicy: quotePolicy || undefined,
407
428
  // The edit's own stamp when the note carries one; tallies and the
408
429
  // content warning follow the edit too. A poll refresh keeps our vote.
409
430
  editedAt: note.updated || new Date().toISOString(),
@@ -412,6 +433,9 @@ export async function onUpdate(intake, activity, actor) {
412
433
  poll: { ...freshPoll, voted: !!s.poll?.voted, ownVotes: s.poll?.ownVotes || [] },
413
434
  } : {}),
414
435
  });
436
+ if (intake.onCarriedEdit) {
437
+ await intake.onCarriedEdit({ noteId: objectId, actor, note }).catch(e => intake.log(`carried edit ${objectId}: ${e.message}`));
438
+ }
415
439
  intake.log(`edited upstream: ${objectId}`);
416
440
  }
417
441
 
@@ -427,6 +451,8 @@ export async function onUpdate(intake, activity, actor) {
427
451
  // the timeline goes quiet. `followActivity` is stored by followActor
428
452
  // (lib/social.mjs) for exactly this kind of comparison.
429
453
  export async function onReject(intake, activity, actor, { trusted = false } = {}) {
454
+ const asked = quoteAsked(intake, activity);
455
+ if (asked) return onQuoteAnswered(intake, asked, activity, actor, 'Reject');
430
456
  if (activity.object?.type && activity.object.type !== 'Follow') return;
431
457
  const contacts = intake.store.getContacts();
432
458
  const rec = contacts.following.find(f => f.actor === actor);
@@ -475,6 +501,8 @@ export async function onMove(intake, activity, actor) {
475
501
  }
476
502
 
477
503
  export async function onAccept(intake, activity, actor, { trusted = false } = {}) {
504
+ const asked = quoteAsked(intake, activity);
505
+ if (asked) return onQuoteAnswered(intake, asked, activity, actor, 'Accept');
478
506
  const contacts = intake.store.getContacts();
479
507
  const rec = contacts.following.find(f => f.actor === actor);
480
508
  // It has to answer the Follow we actually sent. followActor stores that
@@ -497,3 +525,109 @@ export async function onAccept(intake, activity, actor, { trusted = false } = {}
497
525
  intake.log(`follow accepted by ${actor}`);
498
526
  }
499
527
  }
528
+
529
+ // --- quotes (FEP-044f) ---
530
+
531
+ // The post of ours whose quote request this Accept or Reject answers, if any.
532
+ // Matched on the request id we sent, which only the quoted author was given.
533
+ function quoteAsked(intake, activity) {
534
+ const named = idOf(activity.object);
535
+ if (!named) return null;
536
+ return intake.store.getStatuses().find(x => x.kind === 'post' && x.quoteRequest?.id === named) || null;
537
+ }
538
+
539
+ // The quoted author's answer. A yes carries their authorization, a document
540
+ // at their origin naming our post and theirs — fetched and compared, not
541
+ // believed from the envelope; then the note says so to everyone who has it.
542
+ // A no takes the quote off the note.
543
+ async function onQuoteAnswered(intake, s, activity, actor, type) {
544
+ if (actor !== s.quoteRequest?.actor) {
545
+ intake.log(`${type} of our quote request from ${actor}, who is not ${s.quoteRequest?.actor} — ignored`);
546
+ return;
547
+ }
548
+ if (type === 'Reject') {
549
+ if (s.quoteState === 'rejected') return;
550
+ await intake.publisher.withdrawQuote(s);
551
+ return;
552
+ }
553
+ if (s.quoteState === 'accepted') return;
554
+ const resultId = idOf(activity.result);
555
+ if (!resultId || !intake.sameIdentity(resultId, actor)) return `Accept of a quote request carries no authorization at ${actor}`;
556
+ const auth = await intake.fetchAP(resultId);
557
+ if (!auth) throw new Error(`cannot fetch quote authorization ${resultId} — will retry`);
558
+ const ok = auth.id === resultId && auth.type === 'QuoteAuthorization'
559
+ && [].concat(auth.attributedTo || []).map(idOf).includes(actor)
560
+ && idOf(auth.interactingObject) === s.noteId && idOf(auth.interactionTarget) === s.quote;
561
+ if (!ok) return `quote authorization ${resultId} does not say ${s.noteId} may quote ${s.quote}`;
562
+ await intake.publisher.authorizeQuote(s, resultId);
563
+ }
564
+
565
+ // Someone asks to quote one of our posts. Their post is read at its origin,
566
+ // like every note that arrives — it has to be theirs, and has to quote ours.
567
+ // A post the world can read may be quoted by anyone; a followers-only or
568
+ // direct one by nobody. The answer is an Accept carrying our authorization,
569
+ // written beside the post, or a Reject. The quoting post is then kept the
570
+ // way a mention of ours is, under the one notification a quote raises.
571
+ export async function onQuoteRequest(intake, activity, actor) {
572
+ const objectId = idOf(activity.object);
573
+ const instrumentId = idOf(activity.instrument);
574
+ if (!objectId || !instrumentId) return 'QuoteRequest without object or instrument';
575
+ // A followers-only post lives in the private container; a follower may
576
+ // know its id and ask. It is ours, and the answer below is no.
577
+ const ours = objectId.startsWith(intake.urls.notes)
578
+ || (intake.urls.privateNotes && objectId.startsWith(intake.urls.privateNotes));
579
+ if (!ours) {
580
+ intake.log(`QuoteRequest from ${actor} names ${objectId}, not a post of ours — ignored`);
581
+ return;
582
+ }
583
+ if (!intake.sameIdentity(instrumentId, actor)) return `QuoteRequest instrument/actor identity mismatch (${instrumentId})`;
584
+ const s = intake.store.getStatuses().find(x => x.noteId === objectId && x.kind === 'post');
585
+ if (!s) return `QuoteRequest for a post we do not hold (${objectId})`;
586
+ // Not retried on failure: the instrument is a URL a stranger chose.
587
+ const note = await intake.fetchAP(instrumentId);
588
+ if (!note) return `quote request: instrument fetch failed (${instrumentId})`;
589
+ if (note.id !== instrumentId || !isContentType(note.type)) return `quote request: instrument is not verifiable content (${instrumentId})`;
590
+ if (authorOf(note, actor) !== actor) return `quote request: ${instrumentId} is not by ${actor}`;
591
+ if (quoteOf(note) !== objectId) return `quote request: ${instrumentId} does not quote ${objectId}`;
592
+ const doc = await intake.fetchAP(actor);
593
+ const inbox = doc?.endpoints?.sharedInbox || doc?.inbox;
594
+ if (!inbox) return `quote request: no inbox for ${actor}`;
595
+ const { quoteAnswerActivity } = await import('../wire-quotes.mjs');
596
+ const open = !s.visibility || s.visibility === 'public' || s.visibility === 'unlisted';
597
+ if (!open) {
598
+ await intake.deliverer.deliver(inbox,
599
+ quoteAnswerActivity({ urls: intake.urls, type: 'Reject', request: activity, to: actor, serial: intake.serial++ }));
600
+ intake.log(`quote refused: ${instrumentId} asked to quote ${objectId}, which is not public`);
601
+ return;
602
+ }
603
+ const authorization = await intake.publisher.grantQuote(objectId, instrumentId);
604
+ await intake.deliverer.deliver(inbox,
605
+ quoteAnswerActivity({ urls: intake.urls, type: 'Accept', request: activity, result: authorization, to: actor, serial: intake.serial++ }));
606
+ const held = intake.store.getStatuses().find(x => x.noteId === instrumentId);
607
+ if (!held) {
608
+ // The origin's copy, already fetched above.
609
+ const rejected = await intake.ingestNote(instrumentId, actor, { inline: note });
610
+ if (rejected) intake.log(`quote of ${objectId} by ${actor}: ${rejected}`);
611
+ } else if (held.quoteState !== 'accepted') {
612
+ intake.store.updateStatus(instrumentId, { quote: objectId, quoteState: 'accepted' });
613
+ }
614
+ intake.store.addNotification({ type: 'quote', actor, noteId: instrumentId });
615
+ intake.log(`quote allowed: ${instrumentId} quotes ${objectId}`);
616
+ }
617
+
618
+ // What can be said about a note's quote. The quoted post is ours: allowed if
619
+ // we granted it. Elsewhere: allowed when the note carries an authorization
620
+ // that, fetched from the quoted post's own origin, names this note and that
621
+ // one. Otherwise pending — the honest word for a quote nobody has vouched for.
622
+ export async function quoteStateOf(intake, note, quote) {
623
+ if (quote.startsWith(intake.urls.notes)) {
624
+ const target = intake.store.getStatuses().find(x => x.noteId === quote);
625
+ return (target?.quotedBy || []).some(q => q.note === note.id) ? 'accepted' : 'pending';
626
+ }
627
+ const authId = idOf(note.quoteAuthorization);
628
+ if (!authId || !intake.sameIdentity(authId, quote)) return 'pending';
629
+ const auth = await intake.fetchAP(authId).catch(() => null);
630
+ const ok = auth && auth.id === authId && auth.type === 'QuoteAuthorization'
631
+ && idOf(auth.interactingObject) === note.id && idOf(auth.interactionTarget) === quote;
632
+ return ok ? 'accepted' : 'pending';
633
+ }
@@ -130,7 +130,32 @@ export function trimActivity(a) {
130
130
  if (v) out[k] = String(v).slice(0, 2048);
131
131
  }
132
132
  const obj = idOf(a.object);
133
- if (obj) out.object = String(obj).slice(0, 2048);
133
+ if (obj) {
134
+ // An Update says what to change INSIDE its object — a topic's new name, or
135
+ // whether it is closed — so flattening the object to its id alone threw the
136
+ // instruction away and left a bare topic id that meant nothing. Close,
137
+ // reopen and rename all arrived at the queue saying only which topic, and
138
+ // the moderator watched them do nothing. Kept now, bounded, and only the
139
+ // two fields a moderation queue acts on: everything else is still dropped.
140
+ const asked = a.object && typeof a.object === 'object' ? a.object : null;
141
+ const said = {};
142
+ if (asked) {
143
+ if (typeof asked.name === 'string') said.name = asked.name.slice(0, 300);
144
+ // `false` is the whole of a reopen, so presence is what counts, not truth.
145
+ if ('closed' in asked) {
146
+ // A date stays a date; a boolean stays a boolean; and the STRINGS
147
+ // "true"/"false" — which is what a boolean looks like after a form, a
148
+ // template or an RDF round trip — become the booleans they mean,
149
+ // because `!!"false"` is true and would turn a reopen into a close.
150
+ const v = asked.closed;
151
+ said.closed = v === 'false' ? false : v === 'true' ? true
152
+ : typeof v === 'string' ? v.slice(0, 64) : !!v;
153
+ }
154
+ }
155
+ out.object = Object.keys(said).length
156
+ ? { id: String(obj).slice(0, 2048), ...said }
157
+ : String(obj).slice(0, 2048);
158
+ }
134
159
  // A typed object with no id still says what it was — a Block of nobody, an
135
160
  // Undo of a Follow — and that is the whole of what the queue reads.
136
161
  else if (a.object && typeof a.object === 'object' && a.object.type) {
@@ -238,3 +263,56 @@ export function authorOf(note, delivered = null) {
238
263
  // post. See sameIdentity.
239
264
  return sameIdentity(note?.id, author) ? author : null;
240
265
  }
266
+
267
+ // A reaction: an emoji somebody put on a post. Pleroma, Akkoma, Misskey and
268
+ // Sharkey send EmojiReact; Misskey also sends a Like whose `content` is the
269
+ // emoji, which is what its own users see as a reaction. Either way the
270
+ // emoji is the content — a unicode character, or `:name:` for a custom one
271
+ // whose image rides the tag list. Null when the activity is a plain Like.
272
+ export const MAX_REACTION_CHARS = 64;
273
+ export function reactionOf(activity) {
274
+ if (!activity || (activity.type !== 'EmojiReact' && activity.type !== 'Like')) return null;
275
+ const content = typeof activity.content === 'string' ? activity.content.trim() : '';
276
+ if (!content) return null;
277
+ const emoji = content.slice(0, MAX_REACTION_CHARS);
278
+ const bare = emoji.replace(/^:|:$/g, '');
279
+ const custom = emojisOf(activity).find(e => e.shortcode === bare);
280
+ return { emoji, ...(custom ? { url: custom.url } : {}) };
281
+ }
282
+
283
+ // What a note quotes, whichever way its server says it. Mastodon writes
284
+ // FEP-044f's `quote` alongside the two older spellings, Fedibird's quoteUri
285
+ // and Misskey's _misskey_quote; Pleroma writes quoteUrl; FEP-e232 puts a Link
286
+ // in the tag list with the Misskey relation. One post, one quote.
287
+ const QUOTE_REL = 'https://misskey-hub.net/ns#_misskey_quote';
288
+ export function quoteOf(note) {
289
+ const idOf = (v) => (typeof v === 'string' ? v : v?.id);
290
+ const named = [note?.quote, note?.quoteUri, note?._misskey_quote, note?.quoteUrl]
291
+ .map(v => idOf(Array.isArray(v) ? v[0] : v)).find(Boolean);
292
+ const linked = [].concat(note?.tag || []).find(t => t?.type === 'Link' && t.href
293
+ && [].concat(t.rel || []).includes(QUOTE_REL))?.href;
294
+ const q = named || linked;
295
+ return q && q !== note?.id ? httpOnly(String(q).slice(0, MAX_URL_CHARS)) : null;
296
+ }
297
+
298
+ // Who the author lets quote a post: their interaction policy's canQuote, in
299
+ // the words a client uses. The public collection is `public`; the author's
300
+ // own followers or following collections are `followers` and `following`.
301
+ // Null when the note says nothing, which is the common case on servers that
302
+ // predate the policy.
303
+ export function quotePolicyOf(note, author) {
304
+ const can = note?.interactionPolicy?.canQuote;
305
+ if (!can || typeof can !== 'object') return null;
306
+ const word = (v) => {
307
+ const id = typeof v === 'string' ? v : v?.id;
308
+ if (!id) return null;
309
+ if (id === 'https://www.w3.org/ns/activitystreams#Public' || id === 'as:Public' || id === 'Public') return 'public';
310
+ if (author && sameIdentity(id, author)) {
311
+ if (/\/followers\/?$/u.test(id)) return 'followers';
312
+ if (/\/following\/?$/u.test(id)) return 'following';
313
+ }
314
+ return null;
315
+ };
316
+ const list = (v) => [...new Set([].concat(v || []).map(word).filter(Boolean))];
317
+ return { automatic: list(can.automaticApproval), manual: list(can.manualApproval) };
318
+ }
@@ -9,6 +9,9 @@ import { trimActivity, MAX_MODQUEUE, MAX_PENDING_REVIEW, CO_MEMBER_TTL_MS, CO_ME
9
9
  // moderator's ordinary traffic (their posts, likes, follows) is not
10
10
  // moderation and takes the normal arms.
11
11
  export function isModerationAsk(intake, activity) {
12
+ // What a forum holds beyond a group's own asks: a topic removed or moved,
13
+ // a post deleted out of a topic, a report.
14
+ if (intake.isModerationAskExtra?.(activity)) return true;
12
15
  if (activity.type === 'Block') return true;
13
16
  if (activity.type === 'Undo') {
14
17
  return typeof activity.object === 'object' && activity.object?.type === 'Block';
@@ -78,14 +81,34 @@ export async function amplify(intake, noteId, { approved = false, activity = nul
78
81
  // A DM to the group, or a followers-only post it happened to receive, was
79
82
  // addressed to less than the world — carrying it would widen the author's
80
83
  // audience for them. A group only ever amplifies public posts.
81
- if (s.direct || s.nonPublic) {
84
+ //
85
+ // Except a PRIVATE group, where that reasoning runs backwards. Its members
86
+ // write to the group and to nobody else on purpose, and its membership is
87
+ // exactly the people already allowed to read what they write; refusing the
88
+ // carry there would mean a private group could only carry posts its author
89
+ // had published to the world. So a group told it is private carries what is
90
+ // addressed to it. A DM is still a DM and is still never carried.
91
+ if (s.direct || (s.nonPublic && !intake.config.private)) {
82
92
  intake.log(`not amplified — ${noteId} was not addressed publicly, and a group never widens a post's audience`);
83
93
  return;
84
94
  }
85
95
  const contacts = intake.store.getContacts();
86
96
  if (!contacts.followers.some(f => f.actor === s.actor)) {
87
- intake.log(`not amplified — ${s.actor} is not a member`);
88
- return;
97
+ // A group carries its members and nobody else. A forum may choose to
98
+ // HOLD a stranger's reply for a moderator instead of dropping it, which
99
+ // is what `replyPolicy` says; the group itself still carries nothing it
100
+ // has not been told to.
101
+ if (intake.onStranger) {
102
+ const held = await intake.onStranger({ noteId, actor: s.actor, activity }).catch(() => false);
103
+ if (held) { intake.log(`held for a moderator — ${s.actor} is not a member`); return; }
104
+ }
105
+ // The hook may have made them one — a forum whose categories are open
106
+ // treats posting into one as joining it. So ask the record again rather
107
+ // than the copy taken before the hook ran.
108
+ if (!intake.store.getContacts().followers.some(f => f.actor === s.actor)) {
109
+ intake.log(`not amplified — ${s.actor} is not a member`);
110
+ return;
111
+ }
89
112
  }
90
113
  if (intake.store.getMuted().actors.includes(s.actor)) {
91
114
  intake.log(`not amplified — ${s.actor} is muted`);
@@ -155,6 +178,17 @@ export async function amplify(intake, noteId, { approved = false, activity = nul
155
178
  await intake.publisher.recordOutbox(act);
156
179
  intake.store.setPending(intake.store.getPending().filter(p => p.noteId !== noteId));
157
180
  intake.log(`amplified ${noteId} → ${inboxes.length} inbox(es)`);
181
+ // What the group carried is what the group's conversation holds. A forum
182
+ // places the post in its topic here — after the carry, so only what the
183
+ // category vouched for is in the topic (FEP-7888 ownership). Its failure
184
+ // never undoes the carry.
185
+ if (intake.onCarried) {
186
+ // Both: what the group said (the Announce) and what its author sent. A
187
+ // forum reads the author's own activity — the name they gave the topic
188
+ // they were opening is on that one, and on no other.
189
+ await intake.onCarried({ noteId, actor: s.actor, activity: act, sent: activity })
190
+ .catch(e => intake.log(`after the carry of ${noteId}: ${e.message}`));
191
+ }
158
192
  // The same carry, shown natively to the group's Bluesky followers.
159
193
  await intake.bskyGroup?.mirrorCarry(s)
160
194
  .catch(e => intake.log(`bluesky mirror of the carry failed: ${e.message}`));
@@ -24,7 +24,7 @@
24
24
 
25
25
  import * as podInbox from '../../pod/inbox.mjs';
26
26
  import { readCapped } from '../../shared/safefetch.mjs';
27
- import { trimActivity, sameIdentity, sameOrigin, httpUrl, MAX_ITEM_BYTES, MAX_FORWARDS_PER_DRAIN } from './activity.mjs';
27
+ import { trimActivity, sameIdentity, sameOrigin, httpUrl, reactionOf, MAX_EMOJIS, MAX_ITEM_BYTES, MAX_FORWARDS_PER_DRAIN } from './activity.mjs';
28
28
  import * as channel from './channel.mjs';
29
29
  import * as verify from './verify.mjs';
30
30
  import * as group from './group.mjs';
@@ -322,6 +322,9 @@ export class Intake {
322
322
  }
323
323
  this.lastDrain = new Date().toISOString();
324
324
  this.lastDrainAtMs = Date.now();
325
+ // What a drain leaves behind may need acting on: a forum's queue of asks,
326
+ // which is filled here and emptied by whoever can vouch for them.
327
+ if (this.afterDrain) setTimeout(() => this.afterDrain(), 0);
325
328
  this._pruneAttempts();
326
329
  let all;
327
330
  try {
@@ -548,7 +551,8 @@ export class Intake {
548
551
  case 'Undo': return this.onUndo(activity, actor, { trusted });
549
552
  case 'Create': return this.onCreate(activity, actor, { trusted });
550
553
  case 'Accept': return this.onAccept(activity, actor, { trusted });
551
- case 'Like': case 'Announce': {
554
+ case 'QuoteRequest': return this.onQuoteRequest(activity, actor, { trusted });
555
+ case 'Like': case 'Announce': case 'EmojiReact': {
552
556
  // FEP-1b12: a group Announces the member's whole Create, not the note.
553
557
  // Without unwrapping we try to ingest a Create as if it were a Note and
554
558
  // dead-letter every post a group ever carries — including our own.
@@ -564,6 +568,29 @@ export class Intake {
564
568
  && (wrapped.type === 'Create' || wrapped.type === 'Update')) ? wrapped.object : wrapped;
565
569
  const objectId = typeof inner === 'string' ? inner : inner?.id;
566
570
  this.log(`${activity.type} from ${actor} on ${objectId}`);
571
+ // An emoji on one of our posts. Recorded the way a Like is — on the
572
+ // sender's word, nothing to dereference — with the emoji itself, and
573
+ // a custom emoji's image added to the post's own list so a client
574
+ // that draws reactions from there finds it. Anything else an
575
+ // EmojiReact names is not ours to notice.
576
+ const reaction = activity.type !== 'Announce' ? reactionOf(activity) : null;
577
+ if (reaction) {
578
+ if (!objectId || !objectId.startsWith(this.urls.notes)) return;
579
+ this.store.addNotification({
580
+ type: 'reaction', actor, noteId: objectId, emoji: reaction.emoji,
581
+ ...(reaction.url ? { url: reaction.url } : {}),
582
+ ...(this.known(actor) ? {} : { unverified: true }),
583
+ });
584
+ if (reaction.url) {
585
+ const s = this.store.getStatuses().find(x => x.noteId === objectId);
586
+ const shortcode = reaction.emoji.replace(/^:|:$/g, '');
587
+ if (s && !(s.emojis || []).some(e => e.shortcode === shortcode) && (s.emojis || []).length < MAX_EMOJIS) {
588
+ this.store.updateStatus(objectId, { emojis: [...(s.emojis || []), { shortcode, url: reaction.url }] });
589
+ }
590
+ }
591
+ return;
592
+ }
593
+ if (activity.type === 'EmojiReact') return;
567
594
  if (objectId && objectId.startsWith(this.urls.notes)) {
568
595
  // Nothing vouches for this actor: a Like carries no signature and,
569
596
  // unlike a Create, has no object at the sender's origin to re-read.
@@ -585,6 +612,7 @@ export class Intake {
585
612
  case 'Move': return this.onMove(activity, actor);
586
613
  case 'Add': case 'Remove': return this.onAddRemove(activity, actor);
587
614
  case 'Block': return this.onBlock(activity, actor, { trusted });
615
+ case 'Flag': if (this.onReport) return this.onReport(activity, actor, { trusted }); return;
588
616
  default: this.log(`ignored ${activity.type} from ${actor}`);
589
617
  }
590
618
  }
@@ -641,6 +669,8 @@ export class Intake {
641
669
  onCreate(...a) { return activities.onCreate(this, ...a); }
642
670
  onAnnouncedDelete(...a) { return activities.onAnnouncedDelete(this, ...a); }
643
671
  onAnnounce(...a) { return activities.onAnnounce(this, ...a); }
672
+ onQuoteRequest(...a) { return activities.onQuoteRequest(this, ...a); }
673
+ quoteStateOf(...a) { return activities.quoteStateOf(this, ...a); }
644
674
  onDelete(...a) { return activities.onDelete(this, ...a); }
645
675
  onUpdate(...a) { return activities.onUpdate(this, ...a); }
646
676
  onReject(...a) { return activities.onReject(this, ...a); }
@@ -6,7 +6,7 @@
6
6
  import * as podNotes from '../../pod/notes.mjs';
7
7
  import { PUBLIC } from '../wire.mjs';
8
8
  import * as polls from '../polls.mjs';
9
- import { httpOnly, emojisOf, pollOf, isContentType, authorOf, MAX_MENTIONS, MAX_URL_CHARS, MAX_REPLIES_RECORDED, MAX_FORWARDED, FORWARDABLE, FORWARD_TYPES } from './activity.mjs';
9
+ import { httpOnly, emojisOf, pollOf, isContentType, authorOf, quoteOf, quotePolicyOf, MAX_MENTIONS, MAX_URL_CHARS, MAX_REPLIES_RECORDED, MAX_FORWARDED, FORWARDABLE, FORWARD_TYPES } from './activity.mjs';
10
10
 
11
11
  // Does this activity/note concern us at all? Either it comes from someone
12
12
  // we follow, or it names us (to/cc, mention tag) or replies to one of our
@@ -21,6 +21,9 @@ export function concernsUs(intake, doc, actor) {
21
21
  const tagged = [].concat(doc?.tag || [])
22
22
  .some(t => t?.type === 'Mention' && (t.href === intake.urls.actor || t.name?.includes(intake.urls.actor)));
23
23
  if (tagged) return true;
24
+ // A post that quotes one of ours is about us as much as a reply is.
25
+ const quoted = quoteOf(doc);
26
+ if (quoted && quoted.startsWith(intake.urls.notes)) return true;
24
27
  const inReplyTo = typeof doc?.inReplyTo === 'string' ? doc.inReplyTo : doc?.inReplyTo?.id;
25
28
  if (!inReplyTo) return false;
26
29
  if (String(inReplyTo).startsWith(intake.urls.notes)) return true;
@@ -130,6 +133,9 @@ export async function ingestNote(intake, objectId, actor, { via, inline = null }
130
133
  // signed Announce to every member.
131
134
  const author = authorOf(note, actor);
132
135
  if (!author) return `object names an author its origin does not vouch for (${objectId})`;
136
+ // The verified document, kept for whoever acts on this note after the
137
+ // carry: a forum caches it for its readers. Only when someone asked.
138
+ intake.recentNotes?.set(note.id, note);
133
139
  // This is the check that catches a blocked actor reaching us through
134
140
  // somebody else's boost, or through a hashtag feed.
135
141
  if (intake.store.isBlocked(author)) return `blocked author (${author})`;
@@ -197,6 +203,13 @@ export async function ingestNote(intake, objectId, actor, { via, inline = null }
197
203
  .filter(t => t.name);
198
204
  const emojis = emojisOf(note);
199
205
  const poll = pollOf(note);
206
+ // What the note quotes, and what its author lets others quote. The quoted
207
+ // post is fetched and kept so the quote can be shown whole; whether the
208
+ // quote was allowed is checked, never assumed.
209
+ const quote = quoteOf(note);
210
+ const quotePolicy = quotePolicyOf(note, author);
211
+ const quoteState = quote ? await intake.quoteStateOf(note, quote) : null;
212
+ if (quote) await holdQuoted(intake, quote);
200
213
  // Explicitly addressed, but to nobody public and to no followers
201
214
  // collection: a direct message, which belongs to the conversations view
202
215
  // rather than a timeline. A note with no addressing at all is NOT direct —
@@ -220,15 +233,21 @@ export async function ingestNote(intake, objectId, actor, { via, inline = null }
220
233
  ...(emojis.length ? { emojis } : {}),
221
234
  ...(mentions.length ? { mentions } : {}),
222
235
  ...(attachments.length ? { attachments } : {}),
236
+ ...(quote ? { quote, quoteState } : {}),
237
+ ...(quotePolicy ? { quotePolicy } : {}),
223
238
  ...(via ? { via } : {}),
224
239
  });
225
240
  // A notification for anything addressed to this person: a stranger's
226
241
  // post, a reply to one of ours, a direct message, or a post that names
227
242
  // us — whoever sent it. A direct message from someone followed used to
228
- // raise nothing and sat unseen in the private mentions.
243
+ // raise nothing and sat unseen in the private mentions. A post quoting
244
+ // one of ours is its own kind of notification.
229
245
  const namesUs = mentions.some((m) => m.href === intake.urls.actor);
230
246
  const replyToOurs = !!note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes);
231
- if (!followed || replyToOurs || direct || namesUs) {
247
+ const quotesUs = !!quote && quote.startsWith(intake.urls.notes);
248
+ if (quotesUs) {
249
+ intake.store.addNotification({ type: 'quote', actor: author, noteId: note.id });
250
+ } else if (!followed || replyToOurs || direct || namesUs) {
232
251
  intake.store.addNotification({ type: 'mention', actor: author, noteId: note.id });
233
252
  }
234
253
  if (note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes)) {
@@ -238,6 +257,44 @@ export async function ingestNote(intake, objectId, actor, { via, inline = null }
238
257
  intake.log(`${kind}: ${note.id}${via ? ` (boosted by ${via})` : ''}`);
239
258
  }
240
259
 
260
+ // The post a quote shows: fetched at its origin and kept out of every
261
+ // timeline (`remote`), the way a thread's ancestors are — it is there to be
262
+ // shown inside the quoting post, not on its own. Nothing is fetched twice,
263
+ // and nothing is followed further: a quote of a quote shows one level.
264
+ export async function holdQuoted(intake, quoteId) {
265
+ if (intake.store.getStatuses().some(s => s.noteId === quoteId)) return;
266
+ if (intake.store.isBlocked(quoteId)) return;
267
+ const q = await intake.fetchAP(quoteId).catch(() => null);
268
+ if (!q || q.id !== quoteId || !isContentType(q.type)) {
269
+ intake.log(`quoted post ${quoteId} could not be read — the quote shows without it`);
270
+ return;
271
+ }
272
+ const by = authorOf(q, null);
273
+ if (!by || intake.store.isBlocked(by)) return;
274
+ const { attachmentsOf, titledContent } = await import('../wire.mjs');
275
+ const attachments = attachmentsOf(q);
276
+ const emojis = emojisOf(q);
277
+ const poll = pollOf(q);
278
+ const quotePolicy = quotePolicyOf(q, by);
279
+ const mentions = [].concat(q.tag || [])
280
+ .filter(t => t?.type === 'Mention' && t.href && t.name)
281
+ .slice(0, MAX_MENTIONS)
282
+ .map(t => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) }))
283
+ .filter(m => m.href);
284
+ intake.store.addStatus({
285
+ noteId: q.id, actor: by, content: titledContent(q),
286
+ published: q.published, inReplyTo: q.inReplyTo, kind: 'remote',
287
+ ...(q.summary ? { spoiler: String(q.summary).replace(/<[^>]*>/g, '') } : {}),
288
+ ...(q.sensitive === true ? { sensitive: true } : {}),
289
+ ...(poll ? { poll } : {}),
290
+ ...(emojis.length ? { emojis } : {}),
291
+ ...(mentions.length ? { mentions } : {}),
292
+ ...(attachments.length ? { attachments } : {}),
293
+ ...(quotePolicy ? { quotePolicy } : {}),
294
+ });
295
+ intake.log(`quoted post kept: ${q.id}`);
296
+ }
297
+
241
298
  // Drop a post we were holding. A group that carried it also unsays its own
242
299
  // Announce — forwarding the author's Delete would be signed by us and not by
243
300
  // them, which receivers are right to refuse.
@@ -21,11 +21,21 @@ const JITTER = () => 0.85 + Math.random() * 0.3;
21
21
  const UNREADABLE = Symbol('lease-unreadable');
22
22
 
23
23
  export class Lease {
24
- constructor({ url, fetchImpl, log = console.log }) {
24
+ // `id` names the HOLDER, and acquire() lets a holder reclaim its own lease
25
+ // without waiting for the TTL — so whether it is stable decides whether a
26
+ // restart is the same agent coming back or a second one arriving. A process
27
+ // that is gone for good wants a new one each time; a browser does not, since
28
+ // its agent lives in a service worker the browser is free to kill and restart
29
+ // whenever it likes. Left to the default, every one of those restarts looked
30
+ // like another device and asked the owner to take over their own account —
31
+ // which is what switching between the two clients did, because that is a
32
+ // navigation and a navigation can take the worker with it. The browser build
33
+ // passes one it keeps.
34
+ constructor({ url, fetchImpl, log = console.log, id = null }) {
25
35
  this.url = url;
26
36
  this.fetchImpl = fetchImpl;
27
37
  this.log = log;
28
- this.id = crypto.randomUUID();
38
+ this.id = id || crypto.randomUUID();
29
39
  this.stopped = false;
30
40
  this.timer = null;
31
41
  this.heldUntil = 0; // wall-clock end of the lease we last wrote