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
@@ -0,0 +1,104 @@
1
+ // profile-page.mjs — the human page a browser opens for an actor: who the
2
+ // account is, its fields, when it joined, what it pinned, and a remote-follow
3
+ // control. The actor document is for servers; this is the address you hand
4
+ // to a person. The agent writes it beside the actor (publisher
5
+ // publishProfilePage) only when its content changed.
6
+
7
+ const HTML_ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
8
+
9
+ // `fields` are the profile's name/value rows, `joined` an ISO date, `pinned`
10
+ // the pinned posts as { content (sanitized HTML), published, url }.
11
+ export function profilePageHtml({ name, address, summary = null, icon = null, image = null,
12
+ fields = [], joined = null, pinned = [], kind = 'person' }) {
13
+ const esc = (s) => String(s ?? '').replace(/[&<>"]/g, c => ({ ...HTML_ESCAPES, '"': '&quot;' }[c]));
14
+ const what = kind === 'group' ? 'a group' : 'an account';
15
+ const month = (iso) => {
16
+ const d = new Date(iso);
17
+ return Number.isNaN(d.getTime()) ? null
18
+ : d.toLocaleDateString('en', { month: 'long', year: 'numeric', timeZone: 'UTC' });
19
+ };
20
+ const day = (iso) => {
21
+ const d = new Date(iso);
22
+ return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10);
23
+ };
24
+ const fieldRows = fields.filter(f => f?.name && f?.value !== undefined && f?.value !== null && String(f.value) !== '');
25
+ const joinedText = joined ? month(joined) : null;
26
+ const pins = pinned.filter(x => x && typeof x.content === 'string');
27
+ return `<!doctype html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="utf-8">
31
+ <meta name="viewport" content="width=device-width, initial-scale=1">
32
+ <title>${esc(name)} (${esc(address)})</title>
33
+ <style>
34
+ :root { color-scheme: light dark; }
35
+ body { font: 112.5%/1.5 system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
36
+ img.header { width: 100%; max-height: 12rem; object-fit: cover; border-radius: 1rem; }
37
+ img.avatar { width: 6rem; height: 6rem; border-radius: 1rem; object-fit: cover; margin-top: 1rem; }
38
+ h1 { margin: .5rem 0 0; }
39
+ .address { font-size: 1.1rem; user-select: all; }
40
+ dl.fields { display: grid; grid-template-columns: max-content 1fr; gap: .4rem 1rem; margin: 1.5rem 0; }
41
+ dl.fields dt { font-weight: 600; }
42
+ dl.fields dd { margin: 0; overflow-wrap: anywhere; }
43
+ .joined { margin: 1rem 0; }
44
+ section.pinned h2 { font-size: 1.1rem; margin: 2rem 0 .5rem; }
45
+ article.pin { border: 1px solid #8884; border-radius: 1rem; padding: 1rem 1.2rem; margin: 1rem 0; }
46
+ article.pin p:last-child { margin-bottom: 0; }
47
+ article.pin .when { font-size: 1rem; }
48
+ form { margin-top: 2rem; }
49
+ label { display: block; margin-bottom: .3rem; }
50
+ input { font: inherit; padding: .5rem; width: 14rem; max-width: 100%; }
51
+ button { font: inherit; padding: .5rem 1rem; }
52
+ .hint { color: #666; font-size: 1rem; }
53
+ .err { color: #b00020; }
54
+ .err:empty { display: none; }
55
+ @media (prefers-color-scheme: dark) {
56
+ .hint { color: #aaa; }
57
+ .err { color: #ff8a80; }
58
+ }
59
+ </style>
60
+ </head>
61
+ <body>
62
+ <main>
63
+ ${image ? `<img class="header" src="${esc(image)}" alt="">` : ''}
64
+ ${icon ? `<img class="avatar" src="${esc(icon)}" alt="">` : ''}
65
+ <h1>${esc(name)}</h1>
66
+ <p class="address">${esc(address)}</p>
67
+ ${summary ? `<div>${summary}</div>` : ''}
68
+ ${fieldRows.length ? `<dl class="fields">
69
+ ${fieldRows.map(f => `<dt>${esc(f.name)}</dt><dd>${esc(f.value)}</dd>`).join('\n')}
70
+ </dl>` : ''}
71
+ ${joinedText ? `<p class="joined">Joined ${esc(joinedText)}</p>` : ''}
72
+ ${pins.length ? `<section class="pinned">
73
+ <h2>Pinned</h2>
74
+ ${pins.map(x => `<article class="pin">
75
+ ${x.content}
76
+ <p class="when">${x.url ? `<a href="${esc(x.url)}">${esc(day(x.published))}</a>` : esc(day(x.published))}</p>
77
+ </article>`).join('\n')}
78
+ </section>` : ''}
79
+ <p>This is ${what} on the Fediverse. To follow it, paste the address above
80
+ into the search box of Mastodon or any Fediverse app — or use the form.</p>
81
+ <form id="follow">
82
+ <label for="server">your server</label>
83
+ <input id="server" type="text" placeholder="mastodon.social" autocomplete="off"
84
+ required aria-describedby="follow-err">
85
+ <button type="submit">Follow</button>
86
+ <p class="err" id="follow-err" role="alert"></p>
87
+ </form>
88
+ <p class="hint">The form sends you to your own server's follow screen.</p>
89
+ </main>
90
+ <script>
91
+ document.getElementById('follow').addEventListener('submit', (ev) => {
92
+ ev.preventDefault();
93
+ const s = document.getElementById('server').value.trim().replace(/^https?:\\/\\//, '').replace(/\\/.*$/, '');
94
+ const err = document.getElementById('follow-err');
95
+ const addr = document.querySelector('.address').textContent.trim();
96
+ if (!s) { err.textContent = 'Enter your server, e.g. mastodon.social'; return; }
97
+ err.textContent = '';
98
+ location.href = 'https://' + s + '/authorize_interaction?uri=' + encodeURIComponent(addr);
99
+ });
100
+ </script>
101
+ </body>
102
+ </html>
103
+ `;
104
+ }
@@ -15,7 +15,7 @@ import * as podPrivate from '../../pod/private.mjs';
15
15
  // still gets everything, so a missed call site degrades to the old cost rather
16
16
  // than silently publishing nothing.
17
17
  export const ALL_COLLECTIONS = { followers: true, following: true, outbox: true, acls: true,
18
- pending: true, blocked: true };
18
+ pending: true, blocked: true, featured: true };
19
19
 
20
20
  // Publish the collections a change actually TOUCHED.
21
21
  //
@@ -152,6 +152,11 @@ export async function publishCollections(publisher, which = ALL_COLLECTIONS) {
152
152
  }
153
153
  if (which.pending) await publisher.publishPending();
154
154
  if (which.blocked) await publisher.publishBlocked();
155
+ // The pinned posts, written even when there are none. The actor names this
156
+ // collection, and a server showing the profile reads it whether or not
157
+ // anything was ever pinned; with no document there the pod refused every
158
+ // reader, a refusal nothing caches, and every server asked again.
159
+ if (which.featured) await publisher.publishFeatured();
155
160
  if (which.outbox) {
156
161
  const outbox = publisher.store.read('outbox.json', []);
157
162
  // Reconcile reads every published page, which is the expensive part of a
@@ -91,7 +91,11 @@ export class Publisher {
91
91
  // Every actor document this identity publishes, from one place: the
92
92
  // profile publish and a Move both write it, and a Move that rebuilt it
93
93
  // by hand dropped the gateway inbox, the outbox door and the collections.
94
- actorDocFor({ priv = false, moderators = null, inbox = null, movedTo = this.config.movedTo || null } = {}) {
94
+ // `clientOrigin` is overridden only by the agent serving a client its own
95
+ // local actor: standalone, the published document cannot name a loopback
96
+ // address, but a client already talking to that address can be told it.
97
+ actorDocFor({ priv = false, moderators = null, inbox = null, movedTo = this.config.movedTo || null,
98
+ clientOrigin = this.clientOrigin } = {}) {
95
99
  const { urls } = this;
96
100
  return wire.actorDoc({
97
101
  urls, handle: wire.publicHandle(this.config), name: this.config.name, publicKeyPem: this.publicKeyPem,
@@ -102,6 +106,9 @@ export class Publisher {
102
106
  image: this.config.image || null, fields: this.config.fields || [],
103
107
  webId: this.remote.webId || null,
104
108
  aliases: this.config.aliases || [],
109
+ // A group says whether only its moderators may open posts (Lemmy's
110
+ // term); a person's actor carries nothing of the kind.
111
+ postingRestrictedToMods: this.config.kind === 'group' ? !!this.config.postingRestrictedToMods : null,
105
112
  moderators,
106
113
  pendingFollowers: priv ? urls.pendingFollowers : null,
107
114
  pendingFollowing: priv ? urls.pendingFollowing : null,
@@ -110,11 +117,11 @@ export class Publisher {
110
117
  // The agent's own outbox endpoint, where it is reachable: a client
111
118
  // following the actor must arrive somewhere that will take a write.
112
119
  // Otherwise the Gateway's outbox door, when one is attached.
113
- outbox: this.clientOrigin ? `${this.clientOrigin}ap/outbox` : this.gatewayOutbox(),
120
+ outbox: clientOrigin ? `${clientOrigin}ap/outbox` : this.gatewayOutbox(),
114
121
  // How a client-to-server client finds the way in with nothing configured
115
122
  // by hand. Advertised only where the surface is publicly reachable.
116
- oauthAuthorize: this.clientOrigin ? `${this.clientOrigin}oauth/authorize` : null,
117
- oauthToken: this.clientOrigin ? `${this.clientOrigin}oauth/token` : null,
123
+ oauthAuthorize: clientOrigin ? `${clientOrigin}oauth/authorize` : null,
124
+ oauthToken: clientOrigin ? `${clientOrigin}oauth/token` : null,
118
125
  });
119
126
  }
120
127
 
@@ -177,7 +184,9 @@ export class Publisher {
177
184
  // actor's inbox; deliveries reach the pod inbox through it. Absent → today.
178
185
  const gw = this.config.gateway;
179
186
  const gwActive = gw && gw.url && gw.mode && gw.mode !== 'off';
180
- const actorDoc = this.actorDocFor({ priv, moderators, inbox: gwActive ? gw.url : null });
187
+ // A forum's category advertises the forum's one inbox (config.inboxUrl):
188
+ // deliveries to any category land there, and the forum routes them.
189
+ const actorDoc = this.actorDocFor({ priv, moderators, inbox: gwActive ? gw.url : (this.config.inboxUrl || null) });
181
190
  const surface = crypto.createHash('sha256').update(JSON.stringify({
182
191
  actor: actorDoc, handle: this.config.handle, host,
183
192
  quiesced: !!this.config.quiescedAt, version: AGENT_VERSION,
@@ -191,16 +200,21 @@ export class Publisher {
191
200
  return { unreachable: [], updated: 0, skipped: true };
192
201
  }
193
202
 
194
- await discovery.writeWebfinger(this.remote, urls,
195
- wire.jrd({ handle: this.config.handle, host, actor: urls.actor, page: urls.profileHtml }));
196
- await discovery.writeHostMeta(this.remote, urls, wire.hostMeta(urls.base));
203
+ // Discovery is the pod's, and a pod has one WebFinger document: a forum's
204
+ // category (config.forum names the forum) is not the pod's identity, so it
205
+ // writes none — the forum's own actor, or a Gateway, answers for it.
206
+ if (!this.config.forum) {
207
+ await discovery.writeWebfinger(this.remote, urls,
208
+ wire.jrd({ handle: this.config.handle, host, actor: urls.actor, page: urls.profileHtml }));
209
+ await discovery.writeHostMeta(this.remote, urls, wire.hostMeta(urls.base));
197
210
 
198
- const nodeinfoDocUrl = urls.home + 'ap/nodeinfo-2.0';
199
- const localPosts = this.store.getStatuses().filter(s => s.kind === 'post').length;
200
- await discovery.writeNodeinfo(this.remote, urls, {
201
- pointer: wire.nodeinfoPointer(nodeinfoDocUrl),
202
- doc: wire.nodeinfoDoc({ version: AGENT_VERSION, localPosts }),
203
- });
211
+ const nodeinfoDocUrl = urls.home + 'ap/nodeinfo-2.0';
212
+ const localPosts = this.store.getStatuses().filter(s => s.kind === 'post').length;
213
+ await discovery.writeNodeinfo(this.remote, urls, {
214
+ pointer: wire.nodeinfoPointer(nodeinfoDocUrl),
215
+ doc: wire.nodeinfoDoc({ version: AGENT_VERSION, localPosts }),
216
+ });
217
+ }
204
218
 
205
219
  const actor = actorDoc; // built above, for the digest
206
220
  await podActor.write(this.remote, urls, actor);
@@ -218,7 +232,7 @@ export class Publisher {
218
232
  // WebID → actor: the profile card lists the actor as a foaf:account.
219
233
  // Best-effort — a profile that cannot be read or edited does not stop the
220
234
  // publish, it is logged and the rest of the surface still goes up.
221
- try {
235
+ if (this.config.forum) { /* a category is not the pod owner's account */ } else try {
222
236
  const wrote = await podActor.linkInWebIdProfile(this.remote, {
223
237
  actorUrl: urls.actor,
224
238
  accountName: `@${this.config.handle}@${host}`,
@@ -304,12 +318,13 @@ export class Publisher {
304
318
  async verifyPublicSurface() {
305
319
  const { urls } = this;
306
320
  const targets = [
307
- ['webfinger', urls.webfinger],
308
- ['host-meta', urls.base + '.well-known/host-meta'],
321
+ // Discovery documents are the pod's, not a forum category's (see publishProfile).
322
+ ...(this.config.forum ? [] : [['webfinger', urls.webfinger], ['host-meta', urls.base + '.well-known/host-meta']]),
309
323
  ['actor', urls.actor],
310
324
  ['notes', urls.notes],
311
325
  ['followers', urls.followers],
312
326
  ['following', urls.following],
327
+ ['featured', urls.featured],
313
328
  ['outbox', urls.outbox],
314
329
  ];
315
330
  const unreachable = [];
@@ -468,6 +483,9 @@ export class Publisher {
468
483
  privateReady(...a) { return notes.privateReady(this, ...a); }
469
484
  _mentionsFor(...a) { return notes.mentionsFor(this, ...a); }
470
485
  publishNote(...a) { return notes.publishNote(this, ...a); }
486
+ authorizeQuote(...a) { return notes.authorizeQuote(this, ...a); }
487
+ withdrawQuote(...a) { return notes.withdrawQuote(this, ...a); }
488
+ grantQuote(...a) { return notes.grantQuote(this, ...a); }
471
489
  publishObject(...a) { return notes.publishObject(this, ...a); }
472
490
  updateNote(...a) { return notes.updateNote(this, ...a); }
473
491
 
@@ -4,6 +4,7 @@
4
4
 
5
5
  import crypto from 'node:crypto';
6
6
  import * as wire from '../wire.mjs';
7
+ import * as wireQuotes from '../wire-quotes.mjs';
7
8
  import * as containers from '../../pod/containers.mjs';
8
9
  import * as podNotes from '../../pod/notes.mjs';
9
10
 
@@ -159,7 +160,10 @@ export function rowContent(obj) {
159
160
  return `<p><a href="${esc(obj?.id || '')}">${esc(obj?.type || 'object')}</a></p>`;
160
161
  }
161
162
 
162
- export async function publishNote(publisher, content, { inReplyTo, attachments, visibility = 'public', spoilerText = null, sensitive = false, slug: wanted = null, also = [], deliverTo = [] } = {}) {
163
+ // `quote`: the post this one quotes, `{ id, actor }` — the quoted author is
164
+ // asked to allow it (FEP-044f). A quote of one of this actor's own posts is
165
+ // allowed on the spot.
166
+ export async function publishNote(publisher, content, { inReplyTo, attachments, visibility = 'public', spoilerText = null, sensitive = false, slug: wanted = null, also = [], deliverTo = [], quote = null } = {}) {
163
167
  const { urls } = publisher;
164
168
  const priv = visibility === 'private' || visibility === 'direct';
165
169
  if (priv) {
@@ -171,10 +175,16 @@ export async function publishNote(publisher, content, { inReplyTo, attachments,
171
175
  const mentions = await publisher._mentionsFor(content, inReplyTo);
172
176
  if (visibility === 'direct') assertDirectAddressed(content, mentions);
173
177
  const reply = await replyTarget(publisher, inReplyTo, mentions, visibility);
178
+ const selfQuote = quote?.id && quote.actor === urls.actor;
179
+ const quoted = quote?.id ? {
180
+ id: quote.id,
181
+ // Our own post: the authorization is minted here, before the note goes out.
182
+ authorization: selfQuote ? await grantQuote(publisher, quote.id, (priv ? urls.privateNotes : urls.notes) + slug) : null,
183
+ } : null;
174
184
  const note = wire.noteDoc({ urls, slug, content, published, inReplyTo, attachments, mentions,
175
185
  visibility, summary: spoilerText, sensitive, container: priv ? urls.privateNotes : urls.notes,
176
- also: [...also, ...(reply ? [reply.actor] : [])] });
177
- const addressedInboxes = await inboxesFor(publisher, [...also, ...deliverTo]);
186
+ also: [...also, ...(reply ? [reply.actor] : []), ...(quote?.actor && !selfQuote ? [quote.actor] : [])], quote: quoted });
187
+ const addressedInboxes = await inboxesFor(publisher, [...also, ...deliverTo, ...(quote?.actor && !selfQuote ? [quote.actor] : [])]);
178
188
 
179
189
  await podNotes.write(publisher.remote, note.id, note);
180
190
  // Empty, but present: a dangling `replies` that 404s is worse than none.
@@ -183,6 +193,9 @@ export async function publishNote(publisher, content, { inReplyTo, attachments,
183
193
  // The outbox is the PUBLIC index; a private or direct post is not in it.
184
194
  if (!priv) await publisher.recordOutbox(note.id);
185
195
 
196
+ const request = quote?.id && !selfQuote
197
+ ? wireQuotes.quoteRequestActivity({ urls, note, quoted: quote.id, quotedActor: quote.actor, serial: Date.now() })
198
+ : null;
186
199
  publisher.store.addStatus({
187
200
  noteId: note.id, actor: urls.actor, content: note.content, published, inReplyTo,
188
201
  kind: 'post', slug, text: content, visibility,
@@ -190,6 +203,12 @@ export async function publishNote(publisher, content, { inReplyTo, attachments,
190
203
  ...(note.sensitive ? { sensitive: true } : {}),
191
204
  ...(attachments?.length ? { attachments } : {}),
192
205
  ...rowTags(note),
206
+ ...(quote?.id ? {
207
+ quote: quote.id,
208
+ quoteState: selfQuote ? 'accepted' : 'pending',
209
+ ...(quoted.authorization ? { quoteAuthorization: quoted.authorization } : {}),
210
+ ...(request ? { quoteRequest: { id: request.id, actor: quote.actor } } : {}),
211
+ } : {}),
193
212
  });
194
213
  // The row is what the author's own timeline reads. Land it before answering
195
214
  // — the same PUT the debounce would send in 300 ms — so a worker killed or a
@@ -212,6 +231,13 @@ export async function publishNote(publisher, content, { inReplyTo, attachments,
212
231
  ].filter(Boolean))];
213
232
  await publisher.deliverer.deliverToAll(inboxes, create);
214
233
  publisher.log(`note published: ${note.id} → ${inboxes.length} inbox(es)`);
234
+ // The quoted author is asked after the note is out: their Accept names the
235
+ // note, so it has to exist where they will look for it.
236
+ if (request) {
237
+ const [inbox] = await inboxesFor(publisher, [quote.actor]);
238
+ if (inbox) await publisher.deliverer.deliver(inbox, request);
239
+ else publisher.log(`quote of ${quote.id}: no inbox found for ${quote.actor} — the quote stays pending`);
240
+ }
215
241
 
216
242
  // Bluesky mirror: PUBLIC posts only — unlisted, followers-only and direct
217
243
  // are never carried off the fediverse. A mirror failure never fails the
@@ -328,6 +354,7 @@ export async function updateNote(publisher, s, { content, spoilerText = null, se
328
354
  attachments: atts, mentions, visibility: s.visibility || 'public',
329
355
  summary: spoilerText, sensitive: sensitive ?? !!s.sensitive, updated, container,
330
356
  also: reply ? [reply.actor] : [],
357
+ quote: rowQuote(s),
331
358
  });
332
359
  await podNotes.write(publisher.remote, note.id, note);
333
360
  await podNotes.writeCreate(publisher.remote, wire.createActivityId(note.id), wire.createActivity(note, urls));
@@ -350,3 +377,77 @@ export async function updateNote(publisher, s, { content, spoilerText = null, se
350
377
  publisher.log(`note edited: ${note.id} → ${inboxes.length} inbox(es)`);
351
378
  return patched;
352
379
  }
380
+
381
+ // What a note's document says about its quote, read off the timeline row: the
382
+ // quoted post and, once granted, the authorization. A quote the author
383
+ // refused is no longer said.
384
+ export function rowQuote(s) {
385
+ if (!s?.quote || s.quoteState === 'rejected') return null;
386
+ return { id: s.quote, ...(s.quoteAuthorization ? { authorization: s.quoteAuthorization } : {}) };
387
+ }
388
+
389
+ // The note's document again, unchanged in what it says, changed in what it
390
+ // says about its quote. Not an edit — no `updated` stamp — so nobody shows it
391
+ // as edited; the Update goes where the Create went, plus to the quoted author.
392
+ async function restateNote(publisher, s, { quote }) {
393
+ const { urls } = publisher;
394
+ const content = s.text ?? '';
395
+ const mentions = await publisher._mentionsFor(content, s.inReplyTo);
396
+ const container = String(s.noteId).startsWith(urls.privateNotes) ? urls.privateNotes : urls.notes;
397
+ const slug = s.slug || String(s.noteId).slice(container.length);
398
+ const reply = await replyTarget(publisher, s.inReplyTo, mentions, s.visibility || 'public');
399
+ const note = wire.noteDoc({
400
+ urls, slug, content, published: s.published, inReplyTo: s.inReplyTo,
401
+ attachments: s.attachments || [], mentions, visibility: s.visibility || 'public',
402
+ summary: s.spoiler || null, sensitive: !!s.sensitive, updated: s.editedAt || null, container,
403
+ also: [...(reply ? [reply.actor] : []), ...(s.quoteRequest?.actor ? [s.quoteRequest.actor] : [])],
404
+ quote,
405
+ });
406
+ await podNotes.write(publisher.remote, note.id, note);
407
+ await podNotes.writeCreate(publisher.remote, wire.createActivityId(note.id), wire.createActivity(note, urls));
408
+ const update = wire.updateActivity(note, urls, { serial: Date.now() });
409
+ const contacts = publisher.store.getContacts();
410
+ const inboxes = [...new Set([
411
+ ...(s.visibility === 'direct' ? [] : contacts.followers.map(f => f.sharedInbox || f.inbox)),
412
+ ...mentions.map(m => m.inbox),
413
+ reply?.inbox,
414
+ ...(await inboxesFor(publisher, s.quoteRequest?.actor ? [s.quoteRequest.actor] : [])),
415
+ ].filter(Boolean))];
416
+ await publisher.deliverer.deliverToAll(inboxes, update);
417
+ return inboxes.length;
418
+ }
419
+
420
+ // The quoted author said yes: their authorization goes onto the note, and
421
+ // everyone who has the note is told.
422
+ export async function authorizeQuote(publisher, s, authorizationId) {
423
+ const patched = publisher.store.updateStatus(s.noteId, { quoteState: 'accepted', quoteAuthorization: authorizationId });
424
+ const n = await restateNote(publisher, patched || s, { quote: { id: s.quote, authorization: authorizationId } });
425
+ publisher.log(`quote allowed by ${s.quoteRequest?.actor || 'the author'}: ${s.noteId} → ${n} inbox(es)`);
426
+ return patched;
427
+ }
428
+
429
+ // The quoted author said no: the note stops naming their post.
430
+ export async function withdrawQuote(publisher, s) {
431
+ const patched = publisher.store.updateStatus(s.noteId, { quoteState: 'rejected', quoteAuthorization: undefined });
432
+ const n = await restateNote(publisher, patched || s, { quote: null });
433
+ publisher.log(`quote refused by ${s.quoteRequest?.actor || 'the author'}: ${s.noteId} → ${n} inbox(es)`);
434
+ return patched;
435
+ }
436
+
437
+ // Somebody may quote one of this actor's posts: the authorization is written
438
+ // beside the post — public-Read, like the post — and remembered on its row,
439
+ // so a receiver that checks the quote finds it and a re-asked question gets
440
+ // the same answer. Returns the authorization's id.
441
+ const MAX_QUOTED_BY = 200;
442
+ export async function grantQuote(publisher, noteId, instrumentId) {
443
+ const { urls } = publisher;
444
+ const id = wireQuotes.quoteAuthorizationId(noteId, instrumentId);
445
+ await podNotes.write(publisher.remote, id, wireQuotes.quoteAuthorizationDoc({ urls, id, instrument: instrumentId, target: noteId }));
446
+ const s = publisher.store.getStatuses().find(x => x.noteId === noteId);
447
+ if (s) {
448
+ const quotedBy = (s.quotedBy || []).filter(q => q.note !== instrumentId);
449
+ quotedBy.unshift({ note: instrumentId, authorization: id });
450
+ publisher.store.updateStatus(noteId, { quotedBy: quotedBy.slice(0, MAX_QUOTED_BY) });
451
+ }
452
+ return id;
453
+ }
@@ -127,3 +127,31 @@
127
127
  sh:minCount 1 ;
128
128
  sh:maxCount 1 ;
129
129
  ] .
130
+
131
+ <#Update>
132
+ a sh:NodeShape ;
133
+ sh:targetClass as:Update ;
134
+ sh:name "Update" ;
135
+ sh:description "A change to the object it names, carrying the parts that are now different." ;
136
+ sh:property [
137
+ sh:path as:actor ;
138
+ sh:name "actor" ;
139
+ sh:description "The one that performed the activity." ;
140
+ sh:nodeKind sh:BlankNodeOrIRI ;
141
+ sh:minCount 1 ;
142
+ sh:maxCount 1 ;
143
+ ] ;
144
+ sh:property [
145
+ sh:path as:object ;
146
+ sh:name "object" ;
147
+ sh:description "The thing the activity is about." ;
148
+ sh:minCount 1 ;
149
+ sh:maxCount 1 ;
150
+ ] ;
151
+ sh:property [
152
+ sh:path ( as:object as:closed ) ;
153
+ sh:name "closed" ;
154
+ sh:description "The time the object stopped taking anything more." ;
155
+ sh:datatype xsd:dateTime ;
156
+ sh:maxCount 1 ;
157
+ ] .
@@ -59,6 +59,26 @@ function asTerm(q) {
59
59
  return rdf.quad(term(q.subject), term(q.predicate), term(q.object));
60
60
  }
61
61
 
62
+ /**
63
+ * What to call the property that did not fit.
64
+ *
65
+ * A single-predicate path names itself: the IRI, shortened. A sequence path is
66
+ * a list, and its head is a blank node that names nothing — so the failing
67
+ * property shape is asked for its own `sh:name` instead.
68
+ */
69
+ const SH_NAME = 'http://www.w3.org/ns/shacl#name';
70
+ function pathName(r) {
71
+ const direct = r.path?.value ?? null;
72
+ if (direct && /[#/]/u.test(direct)) return direct;
73
+ const shape = r.sourceShape;
74
+ if (shape) {
75
+ for (const q of shacl().$shapes?.dataset ?? []) {
76
+ if (q.subject.equals(shape) && q.predicate.value === SH_NAME) return q.object.value;
77
+ }
78
+ }
79
+ return direct;
80
+ }
81
+
62
82
  /**
63
83
  * Check a document's graph against the shapes.
64
84
  *
@@ -91,7 +111,12 @@ export async function checkShapes(quads) {
91
111
  conforms: false,
92
112
  results: report.results.slice(0, 10).map((r) => ({
93
113
  focus: r.focusNode?.value ?? null,
94
- path: r.path?.value ?? null,
114
+ // A SEQUENCE path — `( as:object as:closed )` — is an RDF list, so its
115
+ // node is blank and `.value` is a generated label like "n3-13". Reported
116
+ // as that, the record named nothing a reader could act on. The shape's
117
+ // own `sh:name` is what it is called, so that is used when the path
118
+ // cannot name itself.
119
+ path: pathName(r),
95
120
  message: r.message?.[0]?.value ?? String(r.sourceConstraintComponent?.value ?? 'does not fit the shape'),
96
121
  shape: r.sourceShape?.value ?? null,
97
122
  })),
@@ -0,0 +1,71 @@
1
+ // wire-quotes.mjs — the documents and activities a quote is made of on the
2
+ // wire (FEP-044f): the request to the quoted author, the authorization they
3
+ // publish, and the Accept or Reject that answers. The quote terms a note
4
+ // itself carries live in wire.mjs with the note.
5
+
6
+ import crypto from 'node:crypto';
7
+ import { AS_CTX, QUOTE_CTX, POLICY_CTX } from './wire.mjs';
8
+
9
+ // The activity and object types, and the two terms an authorization names
10
+ // its parties by — GoToSocial's, declared as Mastodon declares them.
11
+ export const QUOTE_TYPES_CTX = {
12
+ QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest',
13
+ QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization',
14
+ gts: 'https://gotosocial.org/ns#',
15
+ interactingObject: { '@id': 'gts:interactingObject', '@type': '@id' },
16
+ interactionTarget: { '@id': 'gts:interactionTarget', '@type': '@id' },
17
+ };
18
+
19
+ // FEP-044f: asking the quoted post's author to allow the quote. The whole
20
+ // quoting note rides along as the instrument, so their server can see what
21
+ // is being asked without fetching it; `object` is their post.
22
+ export function quoteRequestActivity({ urls, note, quoted, quotedActor, serial }) {
23
+ const { '@context': ctx, ...instrument } = note; // eslint-disable-line no-unused-vars
24
+ return {
25
+ '@context': [AS_CTX, { ...QUOTE_CTX, ...POLICY_CTX, QuoteRequest: QUOTE_TYPES_CTX.QuoteRequest }],
26
+ id: urls.actor + '#quote-request-' + serial,
27
+ type: 'QuoteRequest',
28
+ actor: urls.actor,
29
+ to: [quotedActor],
30
+ object: quoted,
31
+ instrument,
32
+ };
33
+ }
34
+
35
+ // The authorization a quoted author publishes (FEP-044f): a document of its
36
+ // own, at the quoted post's origin, naming the quoting post and the quoted
37
+ // one. A receiver checks the quote by fetching this and comparing both.
38
+ export function quoteAuthorizationId(noteId, instrumentId) {
39
+ return noteId + '-quote-' + crypto.createHash('sha256').update(String(instrumentId)).digest('hex').slice(0, 16);
40
+ }
41
+
42
+ export function quoteAuthorizationDoc({ urls, id, instrument, target }) {
43
+ return {
44
+ '@context': [AS_CTX, QUOTE_TYPES_CTX],
45
+ id,
46
+ type: 'QuoteAuthorization',
47
+ attributedTo: urls.actor,
48
+ interactingObject: instrument,
49
+ interactionTarget: target,
50
+ };
51
+ }
52
+
53
+ // The answer to a QuoteRequest: Accept, carrying the authorization as its
54
+ // result, or Reject. The request is echoed by its id, type, actor, object
55
+ // and the instrument's id — enough for the asker to match it, and nothing a
56
+ // stranger wrote is repeated whole.
57
+ export function quoteAnswerActivity({ urls, type, request, result = null, to, serial }) {
58
+ const idOf = (v) => (typeof v === 'string' ? v : v?.id);
59
+ return {
60
+ '@context': [AS_CTX, { QuoteRequest: QUOTE_TYPES_CTX.QuoteRequest }],
61
+ id: urls.actor + '#' + type.toLowerCase() + '-' + serial,
62
+ type,
63
+ actor: urls.actor,
64
+ to: [to],
65
+ object: {
66
+ id: idOf(request), type: 'QuoteRequest', actor: idOf(request?.actor),
67
+ object: idOf(request?.object), instrument: idOf(request?.instrument),
68
+ },
69
+ ...(result ? { result } : {}),
70
+ };
71
+ }