ap.ml 58 KB

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