ap.ml 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612
  1. (*
  2. * _ _ ____ _
  3. * _| || |_/ ___| ___ _ __ _ __ ___ | |
  4. * |_ .. _\___ \ / _ \ '_ \| '_ \ / _ \| |
  5. * |_ _|___) | __/ |_) | |_) | (_) |_|
  6. * |_||_| |____/ \___| .__/| .__/ \___/(_)
  7. * |_| |_|
  8. *
  9. * Personal Social Ap.
  10. *
  11. * Copyright (C) The #Seppo contributors. All rights reserved.
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU General Public License as published by
  15. * the Free Software Foundation, either version 3 of the License, or
  16. * (at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. *)
  26. let seppo_cgi' = Cfg.seppo_cgi
  27. let apub = "activitypub/"
  28. let proj = apub ^ "actor.jsa" (* the public actor profile *)
  29. let prox = apub ^ "actor.xml" (* the public actor profile *)
  30. let content_length_max = 10 * 1024
  31. let ( let* ) = Result.bind
  32. let ( >>= ) = Result.bind
  33. let to_result none = Option.to_result ~none
  34. let chain a b =
  35. let f a = Ok (a, b) in
  36. Result.bind a f
  37. let write oc (j : Ezjsonm.t) =
  38. Ezjsonm.to_channel ~minify:false oc j;
  39. Ok ""
  40. let writev oc (j : Ezjsonm.value) =
  41. Ezjsonm.value_to_channel ~minify:false oc j;
  42. Ok ""
  43. let json_from_file fn =
  44. let ic = open_in_gen [ Open_rdonly; Open_binary ] 0 fn in
  45. let j = Ezjsonm.value_from_channel ic in
  46. close_in ic;
  47. Ok j
  48. (** X509.Public_key from PEM. *)
  49. module PubKeyPem = struct
  50. let of_pem s =
  51. s
  52. |> Cstruct.of_string
  53. |> X509.Public_key.decode_pem
  54. let target = apub ^ "id_rsa.pub.pem"
  55. let pk_pem = "app/etc/id_rsa.priv.pem"
  56. let pk_rule : Make.t = {
  57. target = pk_pem;
  58. prerequisites = [];
  59. fresh = Make.Missing;
  60. command = fun _ _ _ ->
  61. File.out_channel_replace (fun oc ->
  62. Logr.debug (fun m -> m "create private key pem.");
  63. (* https://discuss.ocaml.org/t/tls-signature-with-opam-tls/9399/3?u=mro
  64. * $ openssl genrsa -out app/etc/id_rsa.priv.pem 2048
  65. *)
  66. try
  67. `RSA
  68. |> X509.Private_key.generate ~bits:2048
  69. |> X509.Private_key.encode_pem
  70. |> Cstruct.to_bytes
  71. |> output_bytes oc;
  72. Ok ""
  73. with _ ->
  74. Logr.err (fun m -> m "%s couldn't create pk" E.e1010);
  75. Error "couldn't create pk")
  76. }
  77. let rule : Make.t = {
  78. target;
  79. prerequisites = [ pk_pem ];
  80. fresh = Make.Outdated;
  81. command = fun _pre _ r ->
  82. File.out_channel_replace (fun oc ->
  83. Logr.debug (fun m -> m "create public key pem." );
  84. match r.prerequisites with
  85. | [ fn_priv ] -> (
  86. assert (fn_priv = pk_pem);
  87. match
  88. fn_priv
  89. |> File.to_string
  90. |> Cstruct.of_string
  91. |> X509.Private_key.decode_pem
  92. with
  93. | Ok (`RSA _ as key) ->
  94. key
  95. |> X509.Private_key.public
  96. |> X509.Public_key.encode_pem
  97. |> Cstruct.to_bytes
  98. |> output_bytes oc;
  99. Ok ""
  100. | Ok _ ->
  101. Logr.err (fun m -> m "%s %s" E.e1032 "wrong key flavour, must be RSA.");
  102. Error "wrong key flavour, must be RSA."
  103. | Error (`Msg mm) ->
  104. Logr.err (fun m -> m "%s %s" E.e1033 mm);
  105. Error mm
  106. )
  107. | l ->
  108. Error
  109. (Printf.sprintf
  110. "rule must have exactly one dependency, not %d"
  111. (List.length l)))
  112. }
  113. let rulez = pk_rule :: rule :: []
  114. let make pre =
  115. Make.make ~pre rulez target
  116. let private_of_pem_data pem_data =
  117. match pem_data
  118. |> X509.Private_key.decode_pem with
  119. | Ok ((`RSA _) as pk) -> Ok pk
  120. | Ok _ -> Error "key must be RSA"
  121. | Error (`Msg e) -> Error e
  122. (** load a private key pem from a file *)
  123. let private_of_pem fn =
  124. fn
  125. |> File.to_bytes
  126. |> Cstruct.of_bytes
  127. |> private_of_pem_data
  128. (** RSA SHA256 sign data with pk.
  129. returns
  130. algorithm,signature
  131. with algorithm currently being fixed to rsa-sha256.
  132. See https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#autoid-38
  133. *)
  134. let sign pk (data : Cstruct.t) : (string * Cstruct.t) =
  135. (* Logr.debug (fun m -> m "PubKeyPem.sign"); *)
  136. (*
  137. * https://discuss.ocaml.org/t/tls-signature-with-opam-tls/9399/9?u=mro
  138. * https://mirleft.github.io/ocaml-x509/doc/x509/X509/Private_key/#cryptographic-sign-operation
  139. *)
  140. (Http.Signature.RSA_SHA256.name, Http.Signature.RSA_SHA256.sign pk (`Message data)
  141. |> Result.get_ok)
  142. (** https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#autoid-38
  143. *)
  144. let verify ~algo ~inbox ~key ~signature data =
  145. let data = `Message data
  146. and _ = inbox in
  147. match algo with
  148. | "hs2019" -> (* https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#autoid-38 *)
  149. (match Http.Signature.HS2019.verify
  150. ~signature
  151. key
  152. data with
  153. | Error (`Msg "bad signature") ->
  154. (* gotosocial and unnamed other AP implementations seem to use `SHA256 and `RSA_PKCS1
  155. while
  156. https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#autoid-38
  157. and
  158. https://datatracker.ietf.org/doc/id/draft-richanna-http-message-signatures-00.html#name-hs2019
  159. as I understand them recommend `SHA512 and `RSA_PSS. *)
  160. (match Http.Signature.RSA_SHA256.verify
  161. ~signature
  162. key
  163. data with
  164. | Ok _ as o ->
  165. Logr.info (fun m -> m "%s.%s another dadaist http signature" "Ap.PubKeyPem" "verify");
  166. o
  167. | x -> x)
  168. | x -> x)
  169. | "rsa-sha256" ->
  170. Http.Signature.RSA_SHA256.verify
  171. ~signature
  172. key
  173. data
  174. | a ->
  175. Error (`Msg (Printf.sprintf "unknown algorithm: '%s'" a))
  176. (** not key related *)
  177. let digest_base64 s =
  178. Logr.debug (fun m -> m "%s.%s %s" "Ap.PubKeyPem" "digest" "SHA-256");
  179. "SHA-256=" ^ (s
  180. |> Cstruct.of_string
  181. |> Mirage_crypto.Hash.SHA256.digest
  182. |> Cstruct.to_string
  183. |> Base64.encode_exn)
  184. let digest_base64' s =
  185. Some (digest_base64 s)
  186. end
  187. module Actor = struct
  188. let http_get ?(key = None) u =
  189. Logr.debug (fun m -> m "%s.%s %a" "Ap.Actor" "http_get" Uri.pp u);
  190. let%lwt p = u |> Http.get_jsonv ~key Result.ok in
  191. (match p with
  192. | Error _ as e -> e
  193. | Ok (r,j) ->
  194. match r.status with
  195. | #Cohttp.Code.success_status ->
  196. let mape (e : Ezjsonm.value Decoders__Error.t) =
  197. let s = e |> Decoders_ezjsonm.Decode.string_of_error in
  198. Logr.err (fun m -> m "%s %s.%s failed to decode actor %a:\n%s" E.e1002 "Ap.Actor" "http_get" Uri.pp u s);
  199. s in
  200. j
  201. |> As2_vocab.Decode.person
  202. |> Result.map_error mape
  203. | _sta -> Format.asprintf "HTTP %a %a" Http.pp_status r.status Uri.pp u
  204. |> Result.error)
  205. |> Lwt.return
  206. end
  207. let sep n = `Data ("\n" ^ String.make (n*2) ' ')
  208. (** A person actor object. https://www.w3.org/TR/activitypub/#actor-objects *)
  209. module Person = struct
  210. (** generate my key-id from my actor id. *)
  211. let my_key_id me =
  212. Uri.with_fragment me (Some "main-key")
  213. let empty = ({
  214. id = Uri.empty;
  215. inbox = Uri.empty;
  216. outbox = Uri.empty;
  217. followers = None;
  218. following = None;
  219. attachment = [];
  220. discoverable = false;
  221. generator = None;
  222. icon = [];
  223. image = None;
  224. manually_approves_followers= true;
  225. name = None;
  226. name_map = [];
  227. preferred_username = None;
  228. preferred_username_map = [];
  229. public_key = {
  230. id = Uri.empty;
  231. owner = None;
  232. pem = "";
  233. signatureAlgorithm = None;
  234. };
  235. published = None;
  236. summary = None;
  237. summary_map = [];
  238. url = [];
  239. } : As2_vocab.Types.person)
  240. let prsn _pubdate (pem, ((pro : Cfg.Profile.t), (Auth.Uid uid, _base))) =
  241. let Rfc4287.Rfc4646 la = pro.language in
  242. let actor = Uri.make ~path:proj () in
  243. let path u = u |> Http.reso ~base:actor in
  244. ({
  245. id = actor;
  246. inbox = Uri.make ~path:("../" ^ seppo_cgi' ^ "/" ^ apub ^ "inbox.jsa") () |> path;
  247. outbox = Uri.make ~path:"outbox/index.jsa" () |> path;
  248. followers = Some (Uri.make ~path:"subscribers/index.jsa" () |> path);
  249. following = Some (Uri.make ~path:"subscribed_to/index.jsa" () |> path);
  250. attachment = [];
  251. discoverable = true;
  252. generator = Some {href=St.seppo_u; name=(Some St.seppo_c); name_map=[]; rel=None };
  253. icon = [ (Uri.make ~path:"../me-avatar.jpg" () |> path) ];
  254. image = Some (Uri.make ~path:"../me-banner.jpg" () |> path);
  255. manually_approves_followers= false;
  256. name = Some pro.title;
  257. name_map = [];
  258. preferred_username = Some uid;
  259. preferred_username_map = [];
  260. public_key = {
  261. id = actor |> my_key_id;
  262. owner = Some actor; (* add this deprecated property to make mastodon happy *)
  263. pem;
  264. signatureAlgorithm = Some "https://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; (* from hubzilla, e.g. https://im.allmendenetz.de/channel/minetest *)
  265. };
  266. published = None;
  267. summary = Some pro.bio;
  268. summary_map = [(la,pro.bio)];
  269. url = [ Uri.make ~path:"../" () |> path ];
  270. } : As2_vocab.Types.person)
  271. module Json = struct
  272. let decode j =
  273. j
  274. |> As2_vocab.Decode.person
  275. |> Result.map_error (fun _ -> "@TODO aua json")
  276. let encode _pubdate (pem, ((pro : Cfg.Profile.t), (uid, base))) =
  277. let Rfc4287.Rfc4646 l = pro.language in
  278. let lang = Some l in
  279. prsn _pubdate (pem, (pro, (uid, base)))
  280. |> As2_vocab.Encode.person ~base ~lang
  281. |> Result.ok
  282. end
  283. let x2txt v =
  284. Markup.(v
  285. |> string
  286. |> parse_html
  287. |> signals
  288. (* |> filter_map (function
  289. | `Text _ as t -> Some t
  290. | `Start_element ((_,"p"), _) -> Some (`Text ["\n<p>&#0x10;\n"])
  291. | `Start_element ((_,"br"), _) -> Some (`Text ["\n<br>\n"])
  292. | _ -> None)
  293. |> write_html
  294. *)
  295. |> text
  296. |> to_string)
  297. let x2txt' v =
  298. Option.bind v (fun x -> Some (x |> x2txt))
  299. let flatten (p : As2_vocab.Types.person) =
  300. {p with
  301. summary = x2txt' p.summary;
  302. attachment = List.fold_left (fun init (e : As2_vocab.Types.property_value) ->
  303. ({e with value = x2txt e.value}) :: init) [] p.attachment}
  304. let target = proj
  305. let rule : Make.t =
  306. {
  307. target;
  308. prerequisites = [
  309. Auth.fn;
  310. Cfg.Base.fn;
  311. Cfg.Profile.fn;
  312. PubKeyPem.target;
  313. ];
  314. fresh = Make.Outdated;
  315. command = fun pre _ _ ->
  316. File.out_channel_replace (fun oc ->
  317. let now = Ptime_clock.now () in
  318. Cfg.Base.(fn |> from_file)
  319. >>= chain Auth.(fn |> uid_from_file)
  320. >>= chain Cfg.Profile.(fn |> from_file)
  321. >>= chain (PubKeyPem.make pre >>= File.cat)
  322. >>= Json.encode now
  323. >>= writev oc)
  324. }
  325. let rulez = rule :: PubKeyPem.rulez
  326. let make pre = Make.make ~pre rulez target
  327. let from_file fn =
  328. fn
  329. |> json_from_file
  330. >>= Json.decode
  331. module Rdf = struct
  332. let encode' ~base ~lang ({ id; name; name_map; url; inbox; outbox;
  333. preferred_username; preferred_username_map; summary; summary_map;
  334. manually_approves_followers;
  335. discoverable; generator; followers; following;
  336. public_key; published; attachment; icon; image}: As2_vocab.Types.person) : _ Xmlm.frag =
  337. let ns_as = As2_vocab.Constants.ActivityStreams.ns_as ^ "#"
  338. and ns_ldp = "http://www.w3.org/ns/ldp#"
  339. and ns_rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  340. and ns_schema = "http://schema.org#"
  341. (* and ns_sec = As2_vocab.Constants.ActivityStreams.ns_sec ^ "#" *)
  342. and ns_toot = "http://joinmastodon.org/ns#"
  343. and ns_xsd = "http://www.w3.org/2001/XMLSchema#" in
  344. let txt ?(lang = None) ?(datatype = None) ns tn (s : string) =
  345. let att = [] in
  346. let att = match lang with
  347. | Some v -> ((Xmlm.ns_xml, "lang"), v) :: att
  348. | None -> att in
  349. let att = match datatype with
  350. | Some v -> ((ns_rdf, "datatype"), v) :: att
  351. | None -> att in
  352. `El (((ns, tn), att), [`Data s]) in
  353. let uri ns tn u = `El (((ns, tn), [ ((ns_rdf, "resource"), u |> Http.reso ~base |> Uri.to_string) ]), []) in
  354. let txt' ns tn none s' = s' |> Option.fold ~none ~some:(fun n -> txt ns tn n :: sep 2 :: none) in
  355. let link_tbd ns tn none s' = s' |> Option.fold ~none ~some:(fun (_ : As2_vocab.Types.link) ->
  356. `El (((ns, tn), []), [ (* @TODO *) ])
  357. :: sep 2 :: none) in
  358. let bool' ns tn none s' = s' |> Option.fold ~none ~some:(fun n -> txt ~datatype:(Some (ns_xsd ^ "boolean")) ns tn (if n then "true" else "false") :: sep 2 :: none) in
  359. let rfc3339' ns tn none s'=s'|> Option.fold ~none ~some:(fun n -> txt ~datatype:(Some (ns_xsd ^ "dateTime")) ns tn (n |> Ptime.to_rfc3339) :: sep 2 :: none) in
  360. let uri' ns tn none s' = s' |> Option.fold ~none ~some:(fun n -> uri ns tn n :: sep 2 :: none) in
  361. let img' _n tn none (u' : Uri.t option) = u' |> Option.fold ~none ~some:(fun u ->
  362. `El (((ns_as, tn), []),
  363. sep 3
  364. :: `El (((ns_as, "Image"), []),
  365. sep 4
  366. :: uri ns_as "url" u
  367. :: [])
  368. :: []) :: sep 2 :: none
  369. ) in
  370. let img'' _n tn none (u' : Uri.t list) = img' _n tn none (List.nth_opt u' 0) in
  371. let lang = lang |> Option.value ~default:"und" in
  372. Logr.debug (fun m -> m "%s.%s %a %s" "Ap.Person.RDF" "encode" Uri.pp base lang);
  373. let _ = public_key in
  374. let f_map name init (lang,value) = txt ~lang:(Some lang) ns_as name value :: sep 3 :: init in
  375. let f_uri name init value = uri ns_as name value :: sep 2 :: init in
  376. let f_att init ({name; name_map; value; value_map} : As2_vocab.Types.property_value) =
  377. let _ = name_map and _ = value_map in (* TODO *)
  378. let sub = sep 4
  379. :: txt ns_as "name" name
  380. :: sep 4
  381. :: txt ns_schema "value" value
  382. :: [] in
  383. let sub = name_map |> List.fold_left (f_map "name") sub in
  384. let sub = value_map |> List.fold_left (f_map "value") sub in
  385. `El (((ns_as, "attachment"), []),
  386. sep 3
  387. :: `El (((ns_schema, "PropertyValue"), []), sub)
  388. :: []) :: sep 2 :: init in
  389. let chi = [] in
  390. let chi = Some outbox |> uri' ns_as "outbox" chi in
  391. let chi = Some inbox |> uri' ns_ldp "inbox" chi in
  392. let chi = followers |> uri' ns_as "followers" chi in
  393. let chi = following |> uri' ns_as "following" chi in
  394. let chi = attachment |> List.fold_left f_att chi in
  395. let chi = image |> img' ns_as "image" chi in
  396. let chi = icon |> img'' ns_as "icon" chi in
  397. let chi = summary |> txt' ns_as "summary" chi in
  398. let chi = summary_map |> List.fold_left (f_map "summary") chi in
  399. let chi = url |> List.fold_left (f_uri "url") chi in
  400. let chi = name |> txt' ns_as "name" chi in
  401. let chi = name_map |> List.fold_left (f_map "name") chi in
  402. let chi = generator |> link_tbd ns_as "generator" chi in
  403. let chi = Some discoverable |> bool' ns_toot "discoverable" chi in
  404. let chi = Some manually_approves_followers |> bool' ns_as "manuallyApprovesFollowers" chi in
  405. let chi = published |> rfc3339' ns_as "published" chi in
  406. let chi = preferred_username |> txt' ns_as "preferredUsername" chi in
  407. let chi = preferred_username_map |> List.fold_left (f_map "preferredUsername") chi in
  408. let chi = Some id |> uri' ns_as "id" chi in
  409. let chi = sep 2 :: chi in
  410. `El (((ns_as, "Person"), [
  411. ((Xmlm.ns_xmlns, "as"), ns_as);
  412. ((Xmlm.ns_xmlns, "ldp"), ns_ldp);
  413. ((Xmlm.ns_xmlns, "schema"), ns_schema);
  414. (* ((Xmlm.ns_xmlns, "sec"), ns_sec); *)
  415. ((Xmlm.ns_xmlns, "toot"), ns_toot);
  416. (* needs to be inline vebose ((Xmlm.ns_xmlns, "xsd"), ns_xsd); *)
  417. ((ns_rdf, "about"), "");
  418. ((Xmlm.ns_xml, "lang"), lang);
  419. ]), chi)
  420. (* Alternatively may want to take a Ap.Feder.t *)
  421. let encode ?(token = None) ?(is_in_subscribers = None) ?(am_subscribed_to = None) ?(blocked = None) ~base ~lang pe : _ Xmlm.frag =
  422. let open Xml in
  423. let txt ?(datatype = None) ns tn (s : string) =
  424. `El (((ns, tn), match datatype with
  425. | Some ty -> [((ns_rdf, "datatype"), ty)]
  426. | None -> []), [`Data s]) in
  427. let txt' ns tn none s' = s' |> Option.fold ~none ~some:(fun n -> txt ns tn n :: sep 2 :: none) in
  428. let noyes' ns tn none s' = s' |> Option.fold ~none ~some:(fun n -> txt ns tn (n |> As2.No_p_yes.to_string) :: sep 2 :: none) in
  429. `El (((ns_rdf, "RDF"), [
  430. ((Xmlm.ns_xmlns, "rdf"), ns_rdf);
  431. ((Xmlm.ns_xmlns, "seppo"), ns_seppo);
  432. ((Xmlm.ns_xml,"base"),base |> Uri.to_string);
  433. ]),
  434. sep 1 ::
  435. `El (((ns_rdf, "Description"), [ (ns_rdf, "about"), "" ]),
  436. sep 2 ::
  437. txt' ns_seppo "token" [] token @
  438. noyes' ns_seppo "is_subscriber" [] is_in_subscribers @
  439. noyes' ns_seppo "am_subscribed_to" [] am_subscribed_to @
  440. noyes' ns_seppo "is_blocked" [] blocked
  441. )
  442. :: sep 1
  443. :: encode' ~base ~lang pe
  444. :: [])
  445. end
  446. end
  447. (* Xml subset of the profle page. *)
  448. module PersonX = struct
  449. let xml_ pubdate (pem, (pro, (uid, base))) =
  450. let Rfc4287.Rfc4646 lang = (pro : Cfg.Profile.t).language in
  451. Person.prsn pubdate (pem, (pro, (uid, base)))
  452. |> Person.Rdf.encode ~base ~lang:(Some lang)
  453. |> Result.ok
  454. let target = prox
  455. let rule = {Person.rule
  456. with target;
  457. command = fun pre _ _ ->
  458. File.out_channel_replace (fun oc ->
  459. let now = Ptime_clock.now () in
  460. let writex oc x =
  461. let xsl = Some "../themes/current/actor.xsl" in
  462. Xml.to_chan ~xsl x oc;
  463. Ok "" in
  464. Cfg.Base.(fn |> from_file)
  465. >>= chain Auth.(fn |> uid_from_file)
  466. >>= chain Cfg.Profile.(fn |> from_file)
  467. >>= chain (PubKeyPem.make pre >>= File.cat)
  468. >>= xml_ now
  469. >>= writex oc) }
  470. let rulez = rule :: PubKeyPem.rulez
  471. let make pre = Make.make ~pre rulez target
  472. end
  473. (**
  474. * https://www.w3.org/TR/activitystreams-core/
  475. * https://www.w3.org/TR/activitystreams-core/#media-type
  476. *)
  477. let send ?(success = `OK) ~key (f_ok : Cohttp.Response.t * string -> unit) to_ msg =
  478. let body = msg |> Ezjsonm.value_to_string in
  479. let signed_headers body = PubKeyPem.(Http.signed_headers key (digest_base64' body) to_) in
  480. let headers = signed_headers body in
  481. let headers = Http.H.add' headers Http.H.ct_jlda in
  482. let headers = Http.H.add' headers Http.H.acc_app_jlda in
  483. (* TODO queue it and re-try in case of failure *)
  484. let%lwt r = Http.post ~headers body to_ in
  485. (match r with
  486. | Ok (res,body') ->
  487. let%lwt body' = body' |> Cohttp_lwt.Body.to_string in
  488. (match res.status with
  489. | #Cohttp.Code.success_status ->
  490. Logr.debug (fun m -> m "%s.%s %a\n%a\n\n%s" "Ap" "send" Uri.pp to_ Cohttp.Response.pp_hum res body');
  491. f_ok (res, body');
  492. Ok (success, [Http.H.ct_plain], Cgi.Response.body "ok")
  493. | sta ->
  494. Logr.warn (fun m -> m "%s.%s %a\n%a\n\n%s" "Ap" "send" Uri.pp to_ Cohttp.Response.pp_hum res body');
  495. Http.s502 ~body:(sta |> Cohttp.Code.string_of_status |> (Cgi.Response.body ~ee:E.e1039))
  496. ) |> Lwt.return
  497. | Error e ->
  498. Logr.warn (fun m -> m "%s.%s <- %s %a\n%s" "Ap" "send" "post" Uri.pp to_ e);
  499. Http.s500 |> Lwt.return)
  500. let snd_reject
  501. ~uuid
  502. ~base
  503. ~key
  504. me
  505. (siac : As2_vocab.Types.person)
  506. (j : Ezjsonm.value) =
  507. Logr.warn(fun m -> m "%s.%s %a %a" "Ap" "snd_reject" Uuidm.pp uuid Uri.pp siac.inbox);
  508. assert (not (me |> Uri.equal siac.id));
  509. let reject me id =
  510. `O [("@context", `String As2_vocab.Constants.ActivityStreams.ns_as);
  511. ("type", `String "Reject");
  512. ("actor", `String (me |> Http.reso ~base |> Uri.to_string));
  513. ("object", `String (id |> Uri.to_string))]
  514. in
  515. let id = match j with
  516. | `O (_ :: ("id", `String id) :: _) -> id |> Uri.of_string
  517. | _ -> Uri.empty in
  518. id
  519. |> reject me
  520. |> send ~success:`Unprocessable_entity ~key
  521. (fun _ -> Logr.info (fun m -> m "%s.%s Reject %a due to fallthrough to %a" "Ap" "snd_reject" Uri.pp id Uri.pp siac.inbox))
  522. siac.inbox
  523. (** re-used for following as well (there using block, too) *)
  524. module Followers = struct
  525. (** follower tri-state *)
  526. module State = struct
  527. (** Tri-state *)
  528. type t =
  529. | Pending
  530. | Accepted
  531. | Blocked
  532. let of_string = function
  533. | "pending" -> Some Pending
  534. | "accepted" -> Some Accepted
  535. | "blocked" -> Some Blocked
  536. | _ -> None
  537. let to_string = function
  538. | Pending -> "pending"
  539. | Accepted -> "accepted"
  540. | Blocked -> "blocked"
  541. let predicate ?(invert = false) (s : t) =
  542. let r = match s with
  543. | Pending
  544. | Accepted -> true
  545. | Blocked -> false in
  546. if invert
  547. then not r
  548. else r
  549. (** Rich follower state info:
  550. state, timestamp, actor id, name, rfc7565, inbox
  551. *)
  552. type t' = t * Ptime.t * Uri.t * string option * Rfc7565.t option * Uri.t option
  553. let ibox (_,_,ibox,_,_,_ : t') : Uri.t = ibox
  554. (** input to fold_left *)
  555. let ibox' f a (k,v) = f a (k,v |> ibox)
  556. let of_actor tnow st (siac : As2_vocab.Types.person) : t' =
  557. let us = match Uri.host siac.id, siac.preferred_username with
  558. | None,_
  559. | _,None -> None
  560. | Some domain, Some local -> Some Rfc7565.(make ~local ~domain ()) in
  561. (st,tnow,siac.inbox,siac.name,us,List.nth_opt siac.icon 0)
  562. let decode = function
  563. | Csexp.(List [Atom "1"; Atom s; Atom t0; Atom inbox; Atom name; Atom rfc7565; Atom avatar]) ->
  564. Option.bind
  565. (s |> of_string)
  566. (fun s ->
  567. match t0 |> Ptime.of_rfc3339 with
  568. | Ok (t,_,_) ->
  569. let inbox = inbox |> Uri.of_string
  570. and rfc7565 = rfc7565 |> Rfc7565.of_string |> Result.to_option
  571. and avatar = avatar |> Uri.of_string in
  572. let r : t' = (s,t,inbox,Some name,rfc7565,Some avatar) in
  573. Some r
  574. | _ -> None )
  575. (* legacy: *)
  576. (* assume the preferred_username is @ attached to the inbox *)
  577. | Csexp.(List [Atom s; Atom t0; Atom inbox]) ->
  578. Option.bind
  579. (s |> of_string)
  580. (fun s ->
  581. match t0 |> Ptime.of_rfc3339 with
  582. | Ok (t,_,_) ->
  583. let inbox = inbox |> Uri.of_string in
  584. let us = Option.bind
  585. (inbox |> Uri.user)
  586. (fun local -> Some Rfc7565.(make ~local ~domain:(inbox |> Uri.host_with_default ~default:"-") ())) in
  587. let r : t' = (s,t,Uri.with_userinfo inbox None,inbox |> Uri.user,us,None) in
  588. Some r
  589. | _ -> None)
  590. | _ -> None
  591. let decode' = function
  592. | Ok s -> s |> decode
  593. | _ -> None
  594. let encode ((state,t,inbox,name,(us : Rfc7565.t option) ,avatar) : t') =
  595. (* attach the preferred_username to the inbox *)
  596. let state = state |> to_string in
  597. let t0 = t |> Ptime.to_rfc3339 in
  598. let inbox = inbox |> Uri.to_string in
  599. let name = name |> Option.value ~default:"" in
  600. let avatar = avatar
  601. |> Option.value ~default:Uri.empty
  602. |> Uri.to_string in
  603. let rfc7565 = Option.bind us
  604. (fun l -> Some (l |> Rfc7565.to_string))
  605. |> Option.value ~default:"" in
  606. Csexp.(List [Atom "1"; Atom state; Atom t0; Atom inbox; Atom name; Atom rfc7565; Atom avatar])
  607. let is_accepted = function
  608. | None -> As2.No_p_yes.No
  609. | Some (Accepted,_,_,_,_,_) -> As2.No_p_yes.Yes
  610. | Some (Blocked ,_,_,_,_,_) -> As2.No_p_yes.No
  611. | Some (Pending ,_,_,_,_,_) -> As2.No_p_yes.Pending
  612. let is_blocked = function
  613. | None -> As2.No_p_yes.No
  614. | Some (Accepted,_,_,_,_,_) -> As2.No_p_yes.No
  615. | Some (Blocked ,_,_,_,_,_) -> As2.No_p_yes.Yes
  616. | Some (Pending ,_,_,_,_,_) -> As2.No_p_yes.No
  617. end
  618. let fold_left (fkt : 'a -> (Uri.t * State.t') -> 'a) =
  619. let kv f a (k,v) = f a
  620. (k |> Bytes.to_string |> Uri.of_string
  621. ,v |> Bytes.to_string |> Csexp.parse_string |> State.decode') in
  622. let opt f a = function
  623. | (k,None) -> Logr.warn (fun m -> m "%s.%s ignored actor %a" "Ap.Followers" "fold_left" Uri.pp k);
  624. a
  625. | (k,Some v) -> f a (k,v) in
  626. (* caveat, this folding really looks reverse: *)
  627. fkt |> opt |> kv |> Mapcdb.fold_left
  628. let cdb = Mapcdb.Cdb "app/var/db/subscribers.cdb"
  629. let find
  630. ?(cdb = cdb)
  631. id : State.t' option =
  632. assert (id |> Uri.user |> Option.is_none);
  633. let ke = id |> Uri.to_string in
  634. Option.bind
  635. (Mapcdb.find_string_opt ke cdb)
  636. (fun s -> s |> Csexp.parse_string |> State.decode')
  637. let update ?(cdb = cdb) id v =
  638. assert (id |> Uri.user |> Option.is_none);
  639. Mapcdb.update_string (id |> Uri.to_string) (v |> State.encode |> Csexp.to_string) cdb
  640. (** remove from cdb *)
  641. let remove ?(cdb = cdb) id =
  642. assert (id |> Uri.user |> Option.is_none);
  643. Mapcdb.remove_string (id |> Uri.to_string) cdb
  644. let is_in_subscribers ?(cdb = cdb) id =
  645. assert (id |> Uri.user |> Option.is_none);
  646. id
  647. |> find ~cdb
  648. |> State.is_accepted
  649. (** https://www.rfc-editor.org/rfc/rfc4287#section-4.1.1 *)
  650. module Atom = struct
  651. (** create all from oldest to newest and return newest file name. *)
  652. let of_cdb
  653. ?(cdb = cdb)
  654. ?(predicate = State.predicate ~invert:false)
  655. ~base
  656. ~title
  657. ~xsl
  658. ~rel
  659. ?(page_size = 50)
  660. dir =
  661. Logr.debug (fun m -> m "%s.%s %s" "Ap.Followers.Atom" "of_cdb" dir);
  662. let predicate (s,_,_,_,_,_ : State.t') = s |> predicate in
  663. (** write one page of a paged xml feed *)
  664. let flush_page_xml ~is_last (u,p,i) =
  665. let _ = is_last
  666. and _ : (Uri.t * State.t') list = u in
  667. assert (0 <= p);
  668. assert (dir |> St.is_suffix ~affix:"/");
  669. let fn = Printf.sprintf "%s%d.xml" dir p in
  670. Logr.debug (fun m -> m "%s.%s %s" "Ap.Followers.Atom" "of_cdb.flush" dir);
  671. assert (u |> List.length = i);
  672. let open Xml in
  673. let mk_rel rel i =
  674. let path,title = match rel with
  675. | Rfc4287.Link.(Rel (Single "first")) ->
  676. assert (i == -1);
  677. ".",Some "last"
  678. | _ ->
  679. assert (i >= 0);
  680. Printf.sprintf "%d.xml" i,
  681. Some (Printf.sprintf "%i" (i+1))
  682. and rel = Some rel in
  683. Rfc4287.Link.(Uri.make ~path () |> make ~rel ~title |> to_atom)
  684. in
  685. let self = mk_rel Rfc4287.Link.self p in
  686. let first = mk_rel Rfc4287.Link.first (-1) in
  687. let last = mk_rel Rfc4287.Link.last 0 in
  688. let prev = mk_rel Rfc4287.Link.prev (succ p) in
  689. let add_next i l = match i with
  690. | 0 -> l
  691. | i -> sep 1 :: mk_rel Rfc4287.Link.next (pred i) :: l in
  692. let id_s = Printf.sprintf "%i.xml" p in
  693. let xml : _ Xmlm.frag =
  694. `El (((ns_a, "feed"), [
  695. ((Xmlm.ns_xmlns, "xmlns"), ns_a);
  696. ((Xmlm.ns_xml, "base"), base |> Uri.to_string);
  697. ]),
  698. sep 1
  699. :: `El (((ns_a,"title"), []), [`Data title]) :: sep 1
  700. :: `El (((ns_a,"id"), []), [`Data id_s ])
  701. :: sep 1 :: self
  702. :: sep 1 :: first
  703. :: sep 1 :: last
  704. :: sep 1 :: prev
  705. :: (u
  706. |> List.rev
  707. |> List.fold_left
  708. (fun init (href,(_,_,_,title,us,_unused_icon)) ->
  709. let href = Uri.with_userinfo href None in
  710. let rfc7565 = Option.bind us
  711. (fun us -> Some (us |> Rfc7565.to_string)) in
  712. sep 1
  713. :: Rfc4287.Link.(make ~rel ~title ~rfc7565 href |> to_atom)
  714. :: init)
  715. [`Data "\n"]
  716. |> add_next p) )
  717. in
  718. fn |> File.out_channel_replace (Xml.to_chan ~xsl xml);
  719. Ok fn in
  720. (** fold a filtered list cdb into paged xml files *)
  721. fold_left (fun (l,p,i as init) (href,st as k) ->
  722. if st |> predicate
  723. then (
  724. Logr.debug (fun m -> m "%s.%s %a" "Ap.Followers.Atom" "of_cdb.fold_left" Uri.pp href);
  725. let i = succ i in
  726. if i > page_size
  727. then
  728. let _ = (l,p,i-1) |> flush_page_xml ~is_last:false in
  729. k :: [],p+1,1
  730. else
  731. k :: l,p,i)
  732. else
  733. init)
  734. ([],0,0) cdb
  735. |> flush_page_xml ~is_last:true
  736. let dir = apub ^ "subscribers/"
  737. let target = dir ^ "index.xml"
  738. let rule : Make.t = {
  739. target;
  740. prerequisites = PersonX.rule.target
  741. :: (cdb |> (fun (Mapcdb.Cdb v) -> v))
  742. :: [];
  743. fresh = Make.Outdated;
  744. command = fun _pre _ _ _ ->
  745. let* base = Cfg.Base.(from_file fn) in
  746. of_cdb
  747. ~cdb
  748. ~base
  749. ~title:"📣 Subscribers"
  750. ~xsl:(Rfc4287.xsl "subscribers.xsl" target)
  751. ~rel:(Some Rfc4287.Link.subscribers)
  752. ~page_size:50
  753. dir
  754. }
  755. let make = Make.make [rule]
  756. end
  757. (** https://www.w3.org/TR/activitypub/#followers *)
  758. module Json = struct
  759. let to_page ~is_last (i : int) (fs : Uri.t list) : Uri.t As2_vocab.Types.collection_page =
  760. let p i =
  761. let path = i |> Printf.sprintf "%d.jsa" in
  762. Uri.make ~path () in
  763. let self = p i in
  764. let next = if i > 0
  765. then Some (p (pred i))
  766. else None in
  767. let prev = if not is_last
  768. then Some (p (succ i))
  769. else None in
  770. {
  771. id = self;
  772. current = Some self;
  773. first = None;
  774. is_ordered = true;
  775. items = fs;
  776. last = Some (p 0);
  777. next;
  778. part_of = Some (Uri.make ~path:"index.jsa" ());
  779. prev;
  780. total_items= None;
  781. }
  782. (** write one page of an https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection *)
  783. let to_page_json ~base _prefix ~is_last (i : int) (ids : Uri.t list) =
  784. to_page ~is_last i ids
  785. |> As2_vocab.Encode.(collection_page ~base (uri ~base))
  786. (** dehydrate into https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection
  787. and https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollectionpage
  788. dst afterwards contains an
  789. index.jsa
  790. index-0.jsa
  791. ...
  792. index-n.jsa
  793. *)
  794. let flush_page_json ~base ~oc prefix ~is_last (tot,pa,lst,_) =
  795. let fn j = j |> Printf.sprintf "%d.jsa" in
  796. Logr.debug (fun m -> m "%s.%s lst#%d" "Ap.Followers" "flush_page" (lst |> List.length));
  797. let js = lst |> List.rev |> to_page_json ~base prefix ~is_last pa in
  798. (prefix ^ (fn pa)) |> File.out_channel_replace (fun ch -> Ezjsonm.value_to_channel ~minify:false ch js);
  799. (if is_last
  800. then
  801. let p i =
  802. let path = fn i in
  803. Uri.make ~path () in
  804. let c : Uri.t As2_vocab.Types.collection =
  805. { id = Uri.make ~path:"index.jsa" ();
  806. current = None;
  807. first = Some (p pa);
  808. is_ordered = true;
  809. items = Some [];
  810. last = Some (p 0);
  811. total_items = Some tot;
  812. } in
  813. c
  814. |> As2_vocab.Encode.(collection ~base (uri ~base))
  815. |> Ezjsonm.value_to_channel ~minify:false oc)
  816. (** paging logic *)
  817. let fold2pages pagesize flush_page (tot,pa,lst,i) id =
  818. Logr.debug (fun m -> m "%s.%s %a" "Ap.Followers" "fold2pages" Uri.pp id );
  819. if i >= pagesize
  820. then (
  821. flush_page ~is_last:false (tot,pa,lst,i);
  822. (tot |> succ,pa |> succ,id :: [],0)
  823. ) else
  824. (tot |> succ,pa,id :: lst,i |> succ)
  825. (** dehydrate the cdb (e.g. followers list) into the current directory
  826. uses fold2pages & flush_page_json
  827. *)
  828. let coll_of_cdb
  829. ~base
  830. ~oc
  831. ?(pagesize = 100)
  832. ?(predicate = State.predicate ~invert:false)
  833. prefix cdb =
  834. assert (0 < pagesize && pagesize < 10_001);
  835. (* Logr.debug (fun m -> m "%s.%s %d %a" "Ap.Followers" "cdb2coll" pagesize Uri.pp base ); *)
  836. let base = Http.reso ~base (Uri.make ~path:prefix ()) in
  837. let* res = fold_left (fun a (k,(s,_,_,_,_,_)) ->
  838. match a with
  839. | Error _ as e ->
  840. Logr.err (fun m -> m "%s %s.%s foohoo" E.e1008 "Ap.Followers" "coll_of_cdb");
  841. e
  842. | Ok ctx ->
  843. Ok (if s |> predicate
  844. then k |> fold2pages pagesize (flush_page_json ~base ~oc prefix) ctx
  845. else (
  846. Logr.debug (fun m -> m "%s.%s ignored %a" "Ap.Followers" "coll_of_cdb.fold_left" Uri.pp k);
  847. ctx) (* just go on *) )
  848. ) (Ok (0,0,[],0)) cdb in
  849. flush_page_json ~base prefix ~oc ~is_last:true res;
  850. Ok (prefix ^ "index.jsa")
  851. let dir = apub ^ "subscribers/"
  852. let target = dir ^ "index.jsa"
  853. let rule = {Atom.rule
  854. with
  855. target;
  856. prerequisites = Person.rule.target
  857. :: (cdb |> (fun (Mapcdb.Cdb v) -> v))
  858. :: [];
  859. command = fun _pre _ _ ->
  860. File.out_channel_replace (fun oc ->
  861. let* base = Cfg.Base.(from_file fn) in
  862. coll_of_cdb ~base ~oc dir cdb)
  863. }
  864. let make = Make.make [rule]
  865. end
  866. let span_follow = 92 * 24 * 60 * 60 |> Ptime.Span.of_int_s
  867. (* notify the follower (uri) and do the local effect *)
  868. let snd_accept
  869. ?(tnow = Ptime_clock.now ())
  870. ~uuid
  871. ~base
  872. ~key
  873. ?(cdb = cdb)
  874. me
  875. (siac : As2_vocab.Types.person)
  876. (fo : As2_vocab.Types.follow) =
  877. Logr.warn(fun m -> m "%s.%s %a %a" "Ap.Followers" "snd_accept" Uri.pp fo.actor Uuidm.pp uuid);
  878. assert (not (me |> Uri.equal fo.actor));
  879. let end_time = Ptime.(span_follow |> add_span tnow) in
  880. assert (fo.actor |> Uri.user |> Option.is_none);
  881. let side_ok _ =
  882. let _ = State.of_actor tnow Accepted siac
  883. |> update ~cdb fo.actor
  884. in
  885. let _ = Make.make [Json.rule] Json.target in
  886. let _ = Atom.(make target) in
  887. () in
  888. match Option.bind
  889. (let ke = fo.actor |> Uri.to_string in
  890. Mapcdb.find_string_opt ke cdb)
  891. (fun s -> s |> Csexp.parse_string |> State.decode') with
  892. | None ->
  893. (* Immediately accept *)
  894. let msg = ({
  895. id = fo.id;
  896. actor = me;
  897. obj = fo;
  898. published = Some tnow;
  899. end_time;
  900. } : As2_vocab.Types.follow As2_vocab.Types.accept)
  901. |> As2_vocab.Encode.(accept (follow ~base)) ~base in
  902. send ~key side_ok siac.inbox msg
  903. | Some (Accepted,tnow,_,_,_,_)
  904. | Some (Pending,tnow,_,_,_,_) ->
  905. let msg = ({
  906. id = fo.id;
  907. actor = me;
  908. obj = fo;
  909. published = Some tnow;
  910. end_time;
  911. } : As2_vocab.Types.follow As2_vocab.Types.accept)
  912. |> As2_vocab.Encode.(accept (follow ~base)) ~base in
  913. send ~key side_ok siac.inbox msg
  914. | Some (Blocked,_,_tnow,_,_,_) -> Lwt.return Http.s403
  915. (* do the local effect *)
  916. let snd_accept_undo
  917. ?(tnow = Ptime_clock.now ())
  918. ?(cdb = cdb)
  919. ~uuid
  920. ~base
  921. ~key
  922. me
  923. (siac : As2_vocab.Types.person)
  924. (ufo : As2_vocab.Types.follow As2_vocab.Types.undo) =
  925. Logr.warn(fun m -> m "%s.%s %a %a" "Ap.Follower" "snd_accept_undo" Uri.pp ufo.obj.actor Uuidm.pp uuid);
  926. assert (not (me |> Uri.equal ufo.actor));
  927. assert (ufo.actor |> Uri.equal ufo.obj.actor );
  928. assert (ufo.actor |> Uri.equal siac.id);
  929. let _ = remove ~cdb ufo.actor in
  930. let _ = Json.(make target) in
  931. let _ = Atom.(make target) in
  932. let side_ok _ = () (* noop *) in
  933. ({
  934. id = ufo.id;
  935. actor = me;
  936. obj = ufo;
  937. published = Some tnow;
  938. end_time = None;
  939. } : As2_vocab.Types.follow As2_vocab.Types.undo As2_vocab.Types.accept)
  940. |> As2_vocab.Encode.(accept ~base (undo ~base (follow ~base)))
  941. |> send ~key side_ok siac.inbox
  942. end
  943. (** Logic for https://www.w3.org/TR/activitypub/#following *)
  944. module Following = struct
  945. let n = "subscribed_to"
  946. let cdb = Mapcdb.Cdb ("app/var/db/" ^ n ^ ".cdb")
  947. let find ?(cdb = cdb) = Followers.find ~cdb
  948. let remove ?(cdb = cdb) = Followers.remove ~cdb
  949. let update ?(cdb = cdb) = Followers.update ~cdb
  950. (** lists whom I subscribed to *)
  951. module Subscribed_to = struct
  952. let dir = apub ^ n ^ "/"
  953. (** Mostly delegates to Followers.Atom.of_cdb *)
  954. module Atom = struct
  955. let target = dir ^ "index.xml"
  956. let rule : Make.t = {
  957. target;
  958. prerequisites = PersonX.rule.target
  959. :: (cdb |> (fun (Mapcdb.Cdb v) -> v))
  960. :: [];
  961. fresh = Make.Outdated;
  962. command = fun _pre _ _ _ ->
  963. let* base = Cfg.Base.(from_file fn) in
  964. Followers.Atom.of_cdb
  965. ~cdb
  966. ~base
  967. ~title:"👂 Subscribed to"
  968. ~xsl:(Rfc4287.xsl "subscribed_to.xsl" target)
  969. ~rel:(Some Rfc4287.Link.subscribed_to)
  970. ~page_size:50 dir
  971. }
  972. end
  973. (** Mostly delegates to Followers.Json.coll_of_cdb *)
  974. module Json = struct
  975. let target = dir ^ "index.jsa"
  976. let rule : Make.t = {
  977. target;
  978. prerequisites = Person.rule.target
  979. :: (cdb |> (fun (Mapcdb.Cdb v) -> v))
  980. :: [];
  981. fresh = Make.Outdated;
  982. command = fun _pre _ _ ->
  983. File.out_channel_replace (fun oc ->
  984. let* base = Cfg.Base.(from_file fn) in
  985. Followers.Json.coll_of_cdb ~base ~oc dir cdb)
  986. }
  987. end
  988. end
  989. let am_subscribed_to ?(cdb = cdb) id =
  990. assert (id |> Uri.user |> Option.is_none);
  991. id
  992. |> find ~cdb
  993. |> Followers.State.is_accepted
  994. (** lists whom I block *)
  995. module Blocked = struct
  996. let dir = apub ^ "blocked" ^ "/"
  997. (** Mostly delegates to Followers.Atom.of_cdb *)
  998. module Atom = struct
  999. let target = dir ^ "index.xml"
  1000. let rule : Make.t = {
  1001. target;
  1002. prerequisites = PersonX.rule.target
  1003. :: (cdb |> (fun (Mapcdb.Cdb v) -> v))
  1004. :: [];
  1005. fresh = Make.Outdated;
  1006. command = fun _pre _ _ _ ->
  1007. let* base = Cfg.Base.(from_file fn) in
  1008. Followers.Atom.of_cdb
  1009. ~cdb
  1010. ~predicate:Followers.State.(predicate ~invert:true)
  1011. ~base
  1012. ~title:"🤐 Blocked"
  1013. ~xsl:(Rfc4287.xsl "blocked.xsl" target)
  1014. ~rel:(Some Rfc4287.Link.blocked)
  1015. ~page_size:50 dir
  1016. }
  1017. end
  1018. (** Mostly delegates to Followers.Json.coll_of_cdb *)
  1019. module Json = struct
  1020. let target = dir ^ "index.jsa"
  1021. let rule : Make.t = {
  1022. target;
  1023. prerequisites = Person.rule.target
  1024. :: (cdb |> (fun (Mapcdb.Cdb v) -> v))
  1025. :: [];
  1026. fresh = Make.Outdated;
  1027. command = fun _pre _ _ ->
  1028. File.out_channel_replace (fun oc ->
  1029. let* base = Cfg.Base.(from_file fn) in
  1030. Followers.Json.coll_of_cdb
  1031. ~predicate:Followers.State.(predicate ~invert:true)
  1032. ~base ~oc dir cdb)
  1033. }
  1034. end
  1035. end
  1036. let is_blocked ?(cdb = cdb) id =
  1037. assert (id |> Uri.user |> Option.is_none);
  1038. id
  1039. |> find ~cdb
  1040. |> Followers.State.is_blocked
  1041. let make ?(tnow = Ptime_clock.now ()) ~me ~inbox reac : As2_vocab.Activitypub.Types.follow =
  1042. assert (not (me |> Uri.equal reac));
  1043. let _ = inbox
  1044. and end_time = Ptime.(Followers.span_follow |> add_span tnow) in
  1045. {
  1046. id = Uri.with_fragment me (Some "subscribe");
  1047. actor = me;
  1048. cc = [];
  1049. end_time;
  1050. object_ = reac;
  1051. state = None;
  1052. to_ = [];
  1053. }
  1054. let undo ~me (o : As2_vocab.Types.follow) : As2_vocab.Types.follow As2_vocab.Types.undo =
  1055. assert (not (me |> Uri.equal o.object_));
  1056. assert (me |> Uri.equal o.actor );
  1057. {
  1058. id = Uri.with_fragment o.id (Some "subscribe#undo");
  1059. actor = me;
  1060. obj = o;
  1061. published= None;
  1062. }
  1063. let rcv_accept
  1064. ?(tnow = Ptime_clock.now ())
  1065. ?(subscribed_to = cdb)
  1066. ~uuid
  1067. ~base
  1068. me
  1069. (siac : As2_vocab.Types.person)
  1070. (fo : As2_vocab.Types.follow) =
  1071. Logr.debug (fun m -> m "%s.%s %a %a" "Ap.Following" "rcv_accept" Uuidm.pp uuid Uri.pp fo.object_);
  1072. assert (siac.id |> Uri.equal fo.object_);
  1073. assert (not (me |> Uri.equal siac.id));
  1074. (* assert (me |> Uri.equal fo.actor);
  1075. assert (not (fo.actor |> Uri.equal fo.object_)); *)
  1076. Logr.warn (fun m -> m "%s.%s TODO only take those that I expect" "Ap.Following" "accept");
  1077. let _ = fo.end_time in
  1078. let _ = base in
  1079. let _ = Followers.State.(of_actor tnow Accepted siac)
  1080. |> update ~cdb:subscribed_to siac.id in
  1081. let _ = Subscribed_to.Json.(Make.make [rule] target) in
  1082. let _ = Subscribed_to.Atom.(Make.make [rule] target) in
  1083. Ok (`Created, [Http.H.ct_plain], Cgi.Response.body "created")
  1084. |> Lwt.return
  1085. end
  1086. let rcv_reject
  1087. ?(tnow = Ptime_clock.now ())
  1088. ~uuid
  1089. ~base
  1090. (siac : As2_vocab.Types.person)
  1091. o =
  1092. Logr.debug (fun m -> m "%s.%s %a %a" "Ap" "rcv_reject" Uri.pp siac.id Uuidm.pp uuid);
  1093. let _ = tnow
  1094. and _ = base
  1095. in
  1096. (match o with
  1097. | `Follow (fo : As2_vocab.Types.follow) ->
  1098. Logr.info (fun m -> m "%s.%s Follow request rejected by %a" "Ap" "rcv_reject" Uri.pp fo.object_);
  1099. let _ = Following.remove fo.object_ in
  1100. let _ = Following.Subscribed_to.Json.(Make.make [rule] target) in
  1101. let _ = Following.Subscribed_to.Atom.(Make.make [rule] target) in
  1102. (* @TODO: add a notification to the timeline? *)
  1103. Ok (`OK, [Http.H.ct_plain], Cgi.Response.body "ok")
  1104. | _ ->
  1105. Logr.err (fun m -> m "%s.%s TODO" "Ap" "rcv_reject");
  1106. Http.s501)
  1107. |> Lwt.return
  1108. module Note = struct
  1109. let empty = ({
  1110. id = Uri.empty;
  1111. agent = None;
  1112. attachment = [];
  1113. attributed_to = Uri.empty;
  1114. cc = [];
  1115. content_map = [];
  1116. in_reply_to = [];
  1117. reaction_inbox = None;
  1118. media_type = (Some Http.Mime.text_html); (* https://www.w3.org/TR/activitystreams-vocabulary/#dfn-mediatype *)
  1119. published = None;
  1120. sensitive = false;
  1121. source = None;
  1122. summary_map = [];
  1123. tags = [];
  1124. to_ = [];
  1125. url = [];
  1126. } : As2_vocab.Types.note)
  1127. let actor_from_author _author =
  1128. Uri.make ~path:proj ()
  1129. let followers actor =
  1130. Uri.make ~path:"subscribers/index.jsa" () |> Http.reso ~base:actor
  1131. let of_rfc4287
  1132. ?(to_ = [As2_vocab.Constants.ActivityStreams.public])
  1133. (e : Rfc4287.Entry.t)
  1134. : As2_vocab.Types.note =
  1135. Logr.debug (fun m -> m "%s.%s %a" "Ap.Note" "of_rfc4287" Uri.pp e.id);
  1136. let tag init (lbl,term,base) =
  1137. let ty = `Hashtag in
  1138. let open Rfc4287.Category in
  1139. let Label (Single name) = lbl
  1140. and Term (Single term) = term in
  1141. let path = term ^ "/" in
  1142. let href = Uri.make ~path () |> Http.reso ~base in
  1143. let ta : As2_vocab.Types.tag = {ty; name; href} in
  1144. ta :: init
  1145. in
  1146. let id = e.id in
  1147. let actor = actor_from_author e.author in
  1148. let cc = [actor |> followers] in
  1149. let Rfc3339.T published = e.published in
  1150. let published = match published |> Ptime.of_rfc3339 with
  1151. | Ok (t,_,_) -> Some t
  1152. | _ -> None in
  1153. let tags = e.categories |> List.fold_left tag [] in
  1154. let Rfc4287.Rfc4646 lang = e.lang in
  1155. let summary_map = [lang,e.title] in
  1156. let content_map = [lang,e.content] in
  1157. let url = e.links |> List.fold_left (
  1158. (* sift, use those without a rel *)
  1159. fun i (l : Rfc4287.Link.t) ->
  1160. match l.rel with
  1161. | None -> l.href :: i
  1162. | Some _ -> i) [] in
  1163. {empty with
  1164. id;
  1165. content_map;
  1166. attributed_to = actor;
  1167. cc;
  1168. media_type = Some Http.Mime.text_plain;
  1169. published;
  1170. summary_map;
  1171. tags;
  1172. to_;
  1173. url;
  1174. }
  1175. let to_rfc4287 ~tz ~now (n : As2_vocab.Types.note) : Rfc4287.Entry.t =
  1176. let _ = tz
  1177. and _ = now in
  1178. Logr.debug (fun m -> m "%s.%s %a" "Ap.Note" "to_rfc4287" Uri.pp n.id);
  1179. let published = n.published |> Option.value ~default:now |> Rfc3339.of_ptime ~tz
  1180. and author = {Rfc4287.Person.empty with
  1181. name = (match n.attributed_to |> Uri.user with
  1182. | None -> n.attributed_to |> Uri.to_string
  1183. | Some u -> u );
  1184. uri = Some n.attributed_to} in
  1185. let a (s,_,_) = s in
  1186. let (lang,cont) = n.content_map |> List.hd in
  1187. let sum = try let _,s = n.summary_map |> List.hd in
  1188. Some s
  1189. with Failure _ -> None in
  1190. let links = match n.reaction_inbox with
  1191. | None -> []
  1192. | Some ib -> [Rfc4287.Link.(make ~rel:(Some inbox) ib )]
  1193. in
  1194. {Rfc4287.Entry.empty with
  1195. id = n.id;
  1196. author;
  1197. lang = Rfc4287.Rfc4646 lang;
  1198. title = sum |> Option.value ~default:"" |> Html.to_plain |> a;
  1199. content = cont |> Html.to_plain |> a;
  1200. published;
  1201. links;
  1202. updated = published;
  1203. in_reply_to = n.in_reply_to |> List.map Rfc4287.Inreplyto.make;
  1204. }
  1205. (** Not implemented yet *)
  1206. let plain_to_html s : string =
  1207. (* care about :
  1208. * - newlines
  1209. * - urls
  1210. * - tags
  1211. * - mentions
  1212. *)
  1213. s
  1214. let html_to_plain _s =
  1215. failwith "not implemented yet."
  1216. let sensitive_marker = "⚠️"
  1217. (** Turn text/plain to text/html, add set id as self url
  1218. Mastodon interprets summary as content warning indicator. . *)
  1219. let diluviate (n : As2_vocab.Types.note) =
  1220. let sensitive,summary_map = n.summary_map |> List.fold_left (fun (sen,suma) (l,txt) ->
  1221. let sen = sen || (txt |> Astring.String.is_prefix ~affix:sensitive_marker) in
  1222. let html = txt |> plain_to_html in
  1223. sen,(l,html) :: suma)
  1224. (n.sensitive,[]) in
  1225. (* add all urls before the content (in each language) *)
  1226. let ur = n.url |> List.fold_left (fun i u ->
  1227. let s = u |> Uri.to_string in
  1228. Printf.sprintf "%s<a href='%s'>%s</a><br/>\n" i s s) "" in
  1229. let content_map = n.content_map |> List.fold_left (fun init (l,co) ->
  1230. (* if not warning, fetch summary of content language *)
  1231. let su = match sensitive with
  1232. | true -> ""
  1233. | false -> match summary_map |> List.assoc_opt l with
  1234. | None -> ""
  1235. | Some su -> su ^ "<br/>\n" in
  1236. let txt = su
  1237. ^ ur
  1238. ^ (if su |> String.equal "" && ur |> String.equal ""
  1239. then ""
  1240. else "<br/>\n")
  1241. ^ (co |> plain_to_html) in
  1242. (l,txt) :: init) []
  1243. in
  1244. {n with
  1245. content_map;
  1246. sensitive;
  1247. summary_map = if sensitive then summary_map else [];
  1248. url = [n.id] }
  1249. (** https://www.w3.org/TR/activitypub/#create-activity-outbox *)
  1250. module Create = struct
  1251. let make (obj : As2_vocab.Types.note) : As2_vocab.Types.note As2_vocab.Types.create =
  1252. let frag = match obj.id |> Uri.fragment with
  1253. | None -> Some "Create"
  1254. | Some f -> Some (f ^ "/Create") in
  1255. {
  1256. id = frag |> Uri.with_fragment obj.id;
  1257. actor = obj.attributed_to;
  1258. published = obj.published;
  1259. to_ = obj.to_;
  1260. cc = obj.cc;
  1261. direct_message = false;
  1262. obj = obj; (* {obj with to_ = []; cc = []}; *)
  1263. }
  1264. (** turn an Atom entry into an ActivityPub (Mastodon) Create Note activity. *)
  1265. let to_json ~base n =
  1266. let lang = As2_vocab.Constants.ActivityStreams.und in
  1267. n
  1268. |> of_rfc4287
  1269. |> diluviate
  1270. |> make
  1271. |> As2_vocab.Encode.(create ~base ~lang (note ~base))
  1272. end
  1273. (** Rather use a tombstone? https://www.w3.org/TR/activitypub/#delete-activity-outbox *)
  1274. module Delete = struct
  1275. let make (obj : As2_vocab.Types.note) : As2_vocab.Types.note As2_vocab.Types.delete =
  1276. let frag = match obj.id |> Uri.fragment with
  1277. | None -> Some "Delete"
  1278. | Some f -> Some (f ^ "/Delete") in
  1279. {
  1280. id = frag |> Uri.with_fragment obj.id;
  1281. actor = obj.attributed_to;
  1282. published = obj.published; (* rather use tnow *)
  1283. obj = obj;
  1284. }
  1285. let to_json ~base n =
  1286. n
  1287. |> of_rfc4287
  1288. |> make
  1289. |> As2_vocab.Encode.(delete ~base (note ~base))
  1290. end
  1291. let _5381_63 = 5381 |> Optint.Int63.of_int
  1292. (* http://cr.yp.to/cdb/cdb.txt *)
  1293. let hash63_gen len f_get : Optint.Int63.t =
  1294. let mask = Optint.Int63.max_int
  1295. and ( +. ) = Optint.Int63.add
  1296. and ( << ) = Optint.Int63.shift_left
  1297. and ( ^ ) = Optint.Int63.logxor
  1298. and ( land ) = Optint.Int63.logand in
  1299. let rec fkt (idx : int) (h : Optint.Int63.t) =
  1300. if idx = len
  1301. then h
  1302. else
  1303. let c = idx |> f_get |> Char.code |> Optint.Int63.of_int in
  1304. (((h << 5) +. h) ^ c) land mask
  1305. |> fkt (succ idx)
  1306. in
  1307. fkt 0 _5381_63
  1308. let hash63_str dat : Optint.Int63.t =
  1309. hash63_gen (String.length dat) (String.get dat)
  1310. let uhash ?(off = 0) ?(buf = Bytes.make (Optint.Int63.encoded_size) (Char.chr 0)) u =
  1311. u
  1312. |> Uri.to_string
  1313. |> hash63_str
  1314. |> Optint.Int63.encode buf ~off;
  1315. buf
  1316. |> Bytes.to_string
  1317. |> Base64.encode_string ~pad:false ~alphabet:Base64.uri_safe_alphabet
  1318. let ibc_dir = "app/var/cache/inbox/"
  1319. (** not just Note *)
  1320. let to_file ~msg_id ~prefix ~dir json =
  1321. let fn = msg_id
  1322. |> uhash
  1323. |> Printf.sprintf "%s%s.json" prefix in
  1324. let tmp = dir ^ "tmp/" ^ fn in
  1325. (dir ^ "new/" ^ fn) |> File.out_channel_create ~tmp
  1326. (fun oc ->
  1327. json
  1328. |> Ezjsonm.value_to_channel oc)
  1329. let do_cache
  1330. ?(tnow = Ptime_clock.now ())
  1331. ?(dir = ibc_dir)
  1332. ~(base : Uri.t)
  1333. (a : As2_vocab.Types.note As2_vocab.Types.create) =
  1334. let _ = tnow in
  1335. Logr.debug (fun m -> m "%s.%s TODO %a" "Ap.Note" "do_cache" Uri.pp a.id);
  1336. assert (a.actor |> Uri.user |> Option.is_some);
  1337. assert (a.obj.attributed_to |> Uri.user |> Option.is_some);
  1338. a
  1339. |> As2_vocab.Encode.(create ~base (note ~base))
  1340. |> to_file ~msg_id:a.id ~prefix:"note-" ~dir
  1341. let do_cache'
  1342. ?(tnow = Ptime_clock.now ())
  1343. ?(dir = ibc_dir)
  1344. ~(base : Uri.t)
  1345. (a : As2_vocab.Types.note As2_vocab.Types.update) =
  1346. let _ = tnow in
  1347. Logr.debug (fun m -> m "%s.%s TODO %a" "Ap.Note" "do_cache'" Uri.pp a.id);
  1348. assert (a.actor |> Uri.user |> Option.is_some);
  1349. assert (a.obj.attributed_to |> Uri.user |> Option.is_some);
  1350. a
  1351. |> As2_vocab.Encode.(update ~base (note ~base))
  1352. |> to_file ~msg_id:a.id ~prefix:"note-" ~dir
  1353. let rcv_create
  1354. ?(tnow = Ptime_clock.now ())
  1355. ~uuid
  1356. ~(base : Uri.t)
  1357. (siac : As2_vocab.Types.person)
  1358. (a : As2_vocab.Types.note As2_vocab.Types.create) : Cgi.Response.t' Lwt.t =
  1359. Logr.debug (fun m -> m "%s.%s %a %a" "Ap.Note" "rcv_create" Uri.pp a.obj.attributed_to Uuidm.pp uuid);
  1360. assert (a.actor |> Uri.equal siac.id);
  1361. assert (a.actor |> Uri.equal a.obj.attributed_to);
  1362. let actor = siac.preferred_username |> Uri.with_userinfo a.actor in
  1363. let attributed_to = siac.preferred_username |> Uri.with_userinfo a.obj.attributed_to in
  1364. let a = {a with actor} in
  1365. let a = {a with obj = {a.obj with attributed_to}} in
  1366. let _ = do_cache ~tnow ~base a in
  1367. Ok (`Created, [Http.H.ct_plain], Cgi.Response.body "create")
  1368. |> Lwt.return
  1369. let rcv_update
  1370. ?(tnow = Ptime_clock.now ())
  1371. ~uuid
  1372. ~(base : Uri.t)
  1373. (siac : As2_vocab.Types.person)
  1374. (a : As2_vocab.Types.note As2_vocab.Types.update) : Cgi.Response.t' Lwt.t =
  1375. Logr.debug (fun m -> m "%s.%s %a %a" "Ap.Note" "rcv_update" Uri.pp a.obj.attributed_to Uuidm.pp uuid);
  1376. assert (a.actor |> Uri.equal siac.id);
  1377. assert (a.actor |> Uri.equal a.obj.attributed_to);
  1378. let actor = siac.preferred_username |> Uri.with_userinfo a.actor in
  1379. let attributed_to = siac.preferred_username |> Uri.with_userinfo a.obj.attributed_to in
  1380. let a = {a with actor} in
  1381. let a = {a with obj = {a.obj with attributed_to}} in
  1382. let _ = do_cache' ~tnow ~base a in
  1383. Ok (`Created, [Http.H.ct_plain], Cgi.Response.body "update")
  1384. |> Lwt.return
  1385. end
  1386. module Like = struct
  1387. let do_cache
  1388. ?(tnow = Ptime_clock.now ())
  1389. ?(dir = Note.ibc_dir)
  1390. ~(base : Uri.t)
  1391. (a : As2_vocab.Types.like) =
  1392. let _ = tnow in
  1393. Logr.debug (fun m -> m "%s.%s TODO %a" "Ap.Like" "do_cache" Uri.pp a.id);
  1394. assert (a.actor |> Uri.user |> Option.is_some);
  1395. a
  1396. |> As2_vocab.Encode.like ~base
  1397. |> Note.to_file ~msg_id:a.id ~prefix:"like-" ~dir
  1398. let do_cache'
  1399. ?(tnow = Ptime_clock.now ())
  1400. ?(dir = Note.ibc_dir)
  1401. ~(base : Uri.t)
  1402. (a : As2_vocab.Types.like As2_vocab.Types.undo) =
  1403. let _ = tnow in
  1404. Logr.debug (fun m -> m "%s.%s TODO %a" "Ap.Like" "do_cache'" Uri.pp a.id);
  1405. assert (a.actor |> Uri.user |> Option.is_some);
  1406. a
  1407. |> As2_vocab.Encode.(undo ~base (like ~base))
  1408. |> Note.to_file ~msg_id:a.id ~prefix:"like-" ~dir
  1409. let rcv_like
  1410. ?(tnow = Ptime_clock.now ())
  1411. ~uuid
  1412. ~(base : Uri.t)
  1413. (siac : As2_vocab.Types.person)
  1414. (a : As2_vocab.Types.like) : Cgi.Response.t' Lwt.t =
  1415. Logr.debug (fun m -> m "%s.%s %a %a" "Ap.Like" "rcv_like" Uri.pp a.actor Uuidm.pp uuid);
  1416. assert (a.actor |> Uri.equal siac.id);
  1417. let actor = Uri.with_userinfo a.actor siac.preferred_username in
  1418. let a = {a with actor} in
  1419. let _ = do_cache ~tnow ~base a in
  1420. Ok (`Created, [Http.H.ct_plain], Cgi.Response.body "like")
  1421. |> Lwt.return
  1422. let rcv_like_undo
  1423. ?(tnow = Ptime_clock.now ())
  1424. ~uuid
  1425. ~(base : Uri.t)
  1426. (siac : As2_vocab.Types.person)
  1427. (a : As2_vocab.Types.like As2_vocab.Types.undo) : Cgi.Response.t' Lwt.t =
  1428. Logr.debug (fun m -> m "%s.%s %a %a" "Ap.Like" "rcv_like_undo" Uri.pp a.actor Uuidm.pp uuid);
  1429. assert (a.actor |> Uri.equal siac.id);
  1430. let actor = Uri.with_userinfo a.actor siac.preferred_username in
  1431. let a = {a with actor} in
  1432. let _ = do_cache' ~tnow ~base a in
  1433. Ok (`Created, [Http.H.ct_plain], Cgi.Response.body "like")
  1434. |> Lwt.return
  1435. end
  1436. module Announce = struct
  1437. let do_cache
  1438. ?(tnow = Ptime_clock.now ())
  1439. ?(dir = Note.ibc_dir)
  1440. ~base
  1441. (a : As2_vocab.Types.announce) =
  1442. let _ = tnow in
  1443. Logr.debug (fun m -> m "%s.%s TODO %a" "Ap.Announce" "do_cache" Uri.pp a.id);
  1444. assert (a.actor |> Uri.user |> Option.is_some);
  1445. a
  1446. |> As2_vocab.Encode.announce ~base
  1447. |> Note.to_file ~msg_id:a.id ~prefix:"anno-" ~dir
  1448. let do_cache'
  1449. ?(tnow = Ptime_clock.now ())
  1450. ?(dir = Note.ibc_dir)
  1451. ~base
  1452. (a : As2_vocab.Types.announce As2_vocab.Types.undo) =
  1453. let _ = tnow in
  1454. Logr.debug (fun m -> m "%s.%s TODO %a" "Ap.Announce" "do_cache'" Uri.pp a.id);
  1455. assert (a.actor |> Uri.user |> Option.is_some);
  1456. a
  1457. |> As2_vocab.Encode.(undo ~base (announce ~base))
  1458. |> Note.to_file ~msg_id:a.id ~prefix:"anno-" ~dir
  1459. let rcv_announce
  1460. ?(tnow = Ptime_clock.now ())
  1461. ~uuid
  1462. ~base
  1463. (siac : As2_vocab.Types.person)
  1464. (a : As2_vocab.Types.announce) : Cgi.Response.t' Lwt.t =
  1465. Logr.debug (fun m -> m "%s.%s %a %a" "Ap.Announce" "rcv_announce" Uri.pp a.actor Uuidm.pp uuid);
  1466. assert (a.actor |> Uri.equal siac.id);
  1467. let actor = Uri.with_userinfo a.actor siac.preferred_username in
  1468. {a with actor} |> do_cache ~tnow ~base;
  1469. Ok (`Created, [Http.H.ct_plain], Cgi.Response.body "announce")
  1470. |> Lwt.return
  1471. let rcv_announce_undo
  1472. ?(tnow = Ptime_clock.now ())
  1473. ~uuid
  1474. ~(base : Uri.t)
  1475. (siac : As2_vocab.Types.person)
  1476. (a : As2_vocab.Types.announce As2_vocab.Types.undo) : Cgi.Response.t' Lwt.t =
  1477. Logr.debug (fun m -> m "%s.%s %a %a" "Ap.Announce" "rcv_announce_undo" Uri.pp a.actor Uuidm.pp uuid);
  1478. assert (a.actor |> Uri.equal siac.id);
  1479. let actor = Uri.with_userinfo a.actor siac.preferred_username in
  1480. {a with actor} |> do_cache' ~tnow ~base;
  1481. Ok (`Created, [Http.H.ct_plain], Cgi.Response.body "announce")
  1482. |> Lwt.return
  1483. end