ap.ml 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368
  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 cgi' = Cfg.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. module PubKeyPem = struct
  49. let of_pem s =
  50. match s
  51. |> Cstruct.of_string
  52. |> X509.Public_key.decode_pem with
  53. | Ok (`RSA _) as k -> k
  54. | Ok _ -> Error (`Msg "public key must be RSA")
  55. | e -> e
  56. let check (`RSA k) =
  57. Logr.warn (fun m -> m "@TODO PubKeyPem.check." );
  58. Ok (`RSA k)
  59. let target = apub ^ "id_rsa.pub.pem"
  60. let pk_pem = "app/etc/id_rsa.priv.pem"
  61. let pk_rule : Make.t = {
  62. target = pk_pem;
  63. prerequisites = [];
  64. fresh = Make.Missing;
  65. command = fun _ _ _ ->
  66. File.out_channel' (fun oc ->
  67. Logr.debug (fun m -> m "create private key pem.");
  68. (* https://discuss.ocaml.org/t/tls-signature-with-opam-tls/9399/3?u=mro
  69. * $ openssl genrsa -out app/etc/id_rsa.priv.pem 2048
  70. *)
  71. try
  72. `RSA
  73. |> X509.Private_key.generate ~bits:2048
  74. |> X509.Private_key.encode_pem
  75. |> Cstruct.to_bytes
  76. |> output_bytes oc;
  77. Ok ""
  78. with _ ->
  79. Logr.err (fun m -> m "%s couldn't create pk" E.e1010);
  80. Error "couldn't create pk")
  81. }
  82. let rule : Make.t = {
  83. target;
  84. prerequisites = [ pk_pem ];
  85. fresh = Make.Outdated;
  86. command = fun _pre _ r ->
  87. File.out_channel' (fun oc ->
  88. Logr.debug (fun m -> m "create public key pem." );
  89. match r.prerequisites with
  90. | [ fn_priv ] -> (
  91. assert (fn_priv = pk_pem);
  92. match
  93. fn_priv
  94. |> File.to_string
  95. |> Cstruct.of_string
  96. |> X509.Private_key.decode_pem
  97. with
  98. | Ok (`RSA _ as key) ->
  99. key
  100. |> X509.Private_key.public
  101. |> X509.Public_key.encode_pem
  102. |> Cstruct.to_bytes
  103. |> output_bytes oc;
  104. Ok ""
  105. | Ok _ ->
  106. Logr.err (fun m -> m "%s %s" E.e1032 "wrong key flavour, must be RSA.");
  107. Error "wrong key flavour, must be RSA."
  108. | Error (`Msg mm) ->
  109. Logr.err (fun m -> m "%s %s" E.e1033 mm);
  110. Error mm
  111. )
  112. | l ->
  113. Error
  114. (Printf.sprintf
  115. "rule must have exactly one dependency, not %d"
  116. (List.length l)))
  117. }
  118. let rulez = pk_rule :: rule :: []
  119. let make pre =
  120. Make.make ~pre rulez target
  121. let private_of_pem_data pem_data =
  122. match pem_data
  123. |> X509.Private_key.decode_pem with
  124. | Ok ((`RSA _) as pk) -> Ok pk
  125. | Ok _ -> Error "key must be RSA"
  126. | Error (`Msg e) -> Error e
  127. let private_of_pem fn_priv =
  128. fn_priv
  129. |> File.to_bytes
  130. |> Cstruct.of_bytes
  131. |> private_of_pem_data
  132. let sign pk (data : Cstruct.t) : (string * Cstruct.t) =
  133. (* Logr.debug (fun m -> m "PubKeyPem.sign"); *)
  134. (*
  135. * https://discuss.ocaml.org/t/tls-signature-with-opam-tls/9399/9?u=mro
  136. * https://mirleft.github.io/ocaml-x509/doc/x509/X509/Private_key/#cryptographic-sign-operation
  137. *)
  138. let scheme = `RSA_PKCS1 in
  139. ("rsa-sha256",
  140. X509.Private_key.sign
  141. `SHA256
  142. ~scheme
  143. pk
  144. (`Message data)
  145. |> Result.get_ok)
  146. let verify ~uuid ?(key = Uri.empty) ~algo pubkey signature data =
  147. match algo with
  148. | "rsa-sha256" -> (
  149. let scheme = `RSA_PKCS1 in
  150. match X509.Public_key.verify
  151. `SHA256
  152. ~scheme
  153. ~signature
  154. pubkey
  155. (`Message data) with
  156. | Ok _ as o ->
  157. Logr.debug (fun m -> m "%s.%s %s valid %a" "As2.PubKeyPem" "verify" algo Uri.pp key);
  158. o
  159. | Error _ as e ->
  160. Logr.debug (fun m -> m "%s.%s %a %s invalid %a\nsig: %s\ndata: {|%s|}" "As2.PubKeyPem" "verify" Uuidm.pp uuid algo Uri.pp key "..." (data |> Cstruct.to_string));
  161. e)
  162. | a -> Error (`Msg a)
  163. (* not key related *)
  164. let digest_base64 s =
  165. Logr.debug (fun m -> m "%s.%s %s" "As2.PubKeyPem" "digest" "SHA-256");
  166. "SHA-256=" ^ (s
  167. |> Cstruct.of_string
  168. |> Mirage_crypto.Hash.SHA256.digest
  169. |> Cstruct.to_string
  170. |> Base64.encode_exn)
  171. let digest_base64' s =
  172. Some (digest_base64 s)
  173. end
  174. module Actor = struct
  175. let http_get ?(key : Http.t_sign_k option = None) u =
  176. let%lwt p = u |> Http.get_jsonv' ~key Result.ok in
  177. (match p with
  178. | Error _ as e -> e
  179. | Ok (r,j) ->
  180. match r.status with
  181. | #Cohttp.Code.success_status ->
  182. let mape (e : Ezjsonm.value Decoders__Error.t) =
  183. let s = e |> Decoders_ezjsonm.Decode.string_of_error in
  184. 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);
  185. s in
  186. j
  187. |> As2_vocab.Decode.person
  188. |> Result.map_error mape
  189. | sta -> Format.asprintf "HTTP %s %a" (Cohttp.Code.string_of_status sta) Uri.pp u
  190. |> Result.error)
  191. |> Lwt.return
  192. end
  193. let sep n = `Data ("\n" ^ String.make (n*2) ' ')
  194. (* A person actor object. https://www.w3.org/TR/activitypub/#actor-objects *)
  195. module Person = struct
  196. let key_id sndr =
  197. Uri.with_fragment sndr (Some "main-key")
  198. let empty = ({
  199. id = Uri.empty;
  200. inbox = Uri.empty;
  201. outbox = Uri.empty;
  202. followers = None;
  203. following = None;
  204. attachment = [];
  205. discoverable = false;
  206. generator = None;
  207. icon = None;
  208. image = None;
  209. manually_approves_followers= true;
  210. name = None;
  211. name_map = [];
  212. preferred_username = None;
  213. preferred_username_map = [];
  214. public_key = {
  215. id = Uri.empty;
  216. owner = None;
  217. pem = "";
  218. signatureAlgorithm = None;
  219. };
  220. published = None;
  221. summary = None;
  222. summary_map = [];
  223. url = [];
  224. } : As2_vocab.Types.person)
  225. let prsn _pubdate (pem, ((pro : Cfg.Profile.t), (Auth.Uid uid, _base))) =
  226. let Rfc4287.Rfc4646 la = pro.language in
  227. let actor = Uri.make ~path:proj () in
  228. let path u = u |> Http.reso ~base:actor in
  229. ({
  230. id = actor;
  231. inbox = Uri.make ~path:("../" ^ cgi' ^ "/" ^ apub ^ "inbox.jsa") () |> path;
  232. outbox = Uri.make ~path:"outbox/index.jsa" () |> path;
  233. followers = Some (Uri.make ~path:"notify/index.jsa" () |> path);
  234. following = Some (Uri.make ~path:"subscribed/index.jsa" () |> path);
  235. attachment = [];
  236. discoverable = true;
  237. generator = Some {href=St.seppo_u; name=(Some St.seppo_c); name_map=[]; rel=None };
  238. icon = Some (Uri.make ~path:"../me-avatar.jpg" () |> path);
  239. image = Some (Uri.make ~path:"../me-banner.jpg" () |> path);
  240. manually_approves_followers= false;
  241. name = Some pro.title;
  242. name_map = [];
  243. preferred_username = Some uid;
  244. preferred_username_map = [];
  245. public_key = {
  246. id = actor |> key_id;
  247. owner = Some actor; (* add this deprecated property to make mastodon happy *)
  248. pem;
  249. signatureAlgorithm = Some "https://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; (* from hubzilla, e.g. https://im.allmendenetz.de/channel/minetest *)
  250. };
  251. published = None;
  252. summary = Some pro.bio;
  253. summary_map = [(la,pro.bio)];
  254. url = [ Uri.make ~path:"../" () |> path ];
  255. } : As2_vocab.Types.person)
  256. module Json = struct
  257. let decode j =
  258. j
  259. |> As2_vocab.Decode.person
  260. |> Result.map_error (fun _ -> "@TODO aua json")
  261. let encode _pubdate (pem, ((pro : Cfg.Profile.t), (uid, base))) =
  262. let Rfc4287.Rfc4646 l = pro.language in
  263. let context = Some l in
  264. prsn _pubdate (pem, (pro, (uid, base)))
  265. |> As2_vocab.Encode.person ~base ~context
  266. |> Result.ok
  267. end
  268. let x2txt v =
  269. Markup.(v
  270. |> string
  271. |> parse_html
  272. |> signals
  273. (* |> filter_map (function
  274. | `Text _ as t -> Some t
  275. | `Start_element ((_,"p"), _) -> Some (`Text ["\n<p>&#0x10;\n"])
  276. | `Start_element ((_,"br"), _) -> Some (`Text ["\n<br>\n"])
  277. | _ -> None)
  278. |> write_html
  279. *)
  280. |> text
  281. |> to_string)
  282. let x2txt' v =
  283. Option.bind v (fun x -> Some (x |> x2txt))
  284. let flatten (p : As2_vocab.Types.person) =
  285. {p with
  286. summary = x2txt' p.summary;
  287. attachment = List.fold_left (fun init (e : As2_vocab.Types.property_value) ->
  288. ({e with value = x2txt e.value}) :: init) [] p.attachment}
  289. let target = proj
  290. let rule : Make.t =
  291. {
  292. target;
  293. prerequisites = [
  294. Auth.fn;
  295. Cfg.Base.fn;
  296. Cfg.Profile.fn;
  297. Cfg.Profile.ban.target;
  298. Cfg.Profile.ava.target;
  299. PubKeyPem.target;
  300. ];
  301. fresh = Make.Outdated;
  302. command = fun pre _ _ ->
  303. File.out_channel' (fun oc ->
  304. let now = Ptime_clock.now () in
  305. Cfg.Base.(fn |> from_file)
  306. >>= chain Auth.(fn |> uid_from_file)
  307. >>= chain Cfg.Profile.(fn |> from_file)
  308. >>= chain (PubKeyPem.make pre >>= File.cat)
  309. >>= Json.encode now
  310. >>= writev oc)
  311. }
  312. let rulez = rule :: PubKeyPem.rulez
  313. let make pre = Make.make ~pre rulez target
  314. let from_file fn =
  315. fn
  316. |> json_from_file
  317. >>= Json.decode
  318. module Rdf = struct
  319. let encode' ~base ~context ({ id; name; name_map; url; inbox; outbox;
  320. preferred_username; preferred_username_map; summary; summary_map;
  321. manually_approves_followers;
  322. discoverable; generator; followers; following;
  323. public_key; published; attachment; icon; image}: As2_vocab.Types.person) : _ Xmlm.frag =
  324. let ns_as = As2_vocab.Constants.ActivityStreams.ns_as ^ "#"
  325. and ns_ldp = "http://www.w3.org/ns/ldp#"
  326. and ns_rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  327. and ns_schema = "http://schema.org#"
  328. (* and ns_sec = As2_vocab.Constants.ActivityStreams.ns_sec ^ "#" *)
  329. and ns_toot = "http://joinmastodon.org/ns#"
  330. and ns_xsd = "http://www.w3.org/2001/XMLSchema#" in
  331. let txt ?(lang = None) ?(datatype = None) ns tn (s : string) =
  332. let att = [] in
  333. let att = match lang with
  334. | Some v -> ((Xmlm.ns_xml, "lang"), v) :: att
  335. | None -> att in
  336. let att = match datatype with
  337. | Some v -> ((ns_rdf, "datatype"), v) :: att
  338. | None -> att in
  339. `El (((ns, tn), att), [`Data s]) in
  340. let uri ns tn u = `El (((ns, tn), [ ((ns_rdf, "resource"), u |> Http.reso ~base |> Uri.to_string) ]), []) in
  341. let txt' ns tn none s' = s' |> Option.fold ~none ~some:(fun n -> txt ns tn n :: sep 2 :: none) in
  342. let link_tbd ns tn none s' = s' |> Option.fold ~none ~some:(fun (_ : As2_vocab.Types.link) ->
  343. `El (((ns, tn), []), [ (* @TODO *) ])
  344. :: sep 2 :: none) in
  345. 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
  346. 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
  347. let uri' ns tn none s' = s' |> Option.fold ~none ~some:(fun n -> uri ns tn n :: sep 2 :: none) in
  348. let img' _n tn none (u' : Uri.t option) = u' |> Option.fold ~none ~some:(fun u ->
  349. `El (((ns_as, tn), []),
  350. sep 3
  351. :: `El (((ns_as, "Image"), []),
  352. sep 4
  353. :: uri ns_as "url" u
  354. :: [])
  355. :: []) :: sep 2 :: none
  356. ) in
  357. let context = context |> Option.value ~default:"und" in
  358. Logr.debug (fun m -> m "%s.%s %a %s" "As2.Person.RDF" "encode" Uri.pp base context);
  359. let _ = public_key in
  360. let f_map name init (lang,value) = txt ~lang:(Some lang) ns_as name value :: sep 3 :: init in
  361. let f_uri name init value = uri ns_as name value :: sep 2 :: init in
  362. let f_att init ({name; name_map; value; value_map} : As2_vocab.Types.property_value) =
  363. let _ = name_map and _ = value_map in (* TODO *)
  364. let sub = sep 4
  365. :: txt ns_as "name" name
  366. :: sep 4
  367. :: txt ns_schema "value" value
  368. :: [] in
  369. let sub = name_map |> List.fold_left (f_map "name") sub in
  370. let sub = value_map |> List.fold_left (f_map "value") sub in
  371. `El (((ns_as, "attachment"), []),
  372. sep 3
  373. :: `El (((ns_schema, "PropertyValue"), []), sub)
  374. :: []) :: sep 2 :: init in
  375. let chi = [] in
  376. let chi = Some outbox |> uri' ns_as "outbox" chi in
  377. let chi = Some inbox |> uri' ns_ldp "inbox" chi in
  378. let chi = followers |> uri' ns_as "followers" chi in
  379. let chi = following |> uri' ns_as "following" chi in
  380. let chi = attachment |> List.fold_left f_att chi in
  381. let chi = image |> img' ns_as "image" chi in
  382. let chi = icon |> img' ns_as "icon" chi in
  383. let chi = summary |> txt' ns_as "summary" chi in
  384. let chi = summary_map |> List.fold_left (f_map "summary") chi in
  385. let chi = url |> List.fold_left (f_uri "url") chi in
  386. let chi = name |> txt' ns_as "name" chi in
  387. let chi = name_map |> List.fold_left (f_map "name") chi in
  388. let chi = generator |> link_tbd ns_as "generator" chi in
  389. let chi = Some discoverable |> bool' ns_toot "discoverable" chi in
  390. let chi = Some manually_approves_followers |> bool' ns_as "manuallyApprovesFollowers" chi in
  391. let chi = published |> rfc3339' ns_as "published" chi in
  392. let chi = preferred_username |> txt' ns_as "preferredUsername" chi in
  393. let chi = preferred_username_map |> List.fold_left (f_map "preferredUsername") chi in
  394. let chi = Some id |> uri' ns_as "id" chi in
  395. let chi = sep 2 :: chi in
  396. `El (((ns_as, "Person"), [
  397. ((Xmlm.ns_xmlns, "as"), ns_as);
  398. ((Xmlm.ns_xmlns, "ldp"), ns_ldp);
  399. ((Xmlm.ns_xmlns, "schema"), ns_schema);
  400. (* ((Xmlm.ns_xmlns, "sec"), ns_sec); *)
  401. ((Xmlm.ns_xmlns, "toot"), ns_toot);
  402. (* needs to be inline vebose ((Xmlm.ns_xmlns, "xsd"), ns_xsd); *)
  403. ((ns_rdf, "about"), "");
  404. ((Xmlm.ns_xml, "lang"), context);
  405. ]), chi)
  406. (* Alternatively may want to take a Ap.Feder.t *)
  407. let encode ?(token = None) ?(notify = None) ?(subscribed = None) ?(blocked = None) ~base ~context pe : _ Xmlm.frag =
  408. let open Xml in
  409. let txt ?(datatype = None) ns tn (s : string) =
  410. `El (((ns, tn), match datatype with
  411. | Some ty -> [((ns_rdf, "datatype"), ty)]
  412. | None -> []), [`Data s]) in
  413. let txt' ns tn none s' = s' |> Option.fold ~none ~some:(fun n -> txt ns tn n :: sep 2 :: none) in
  414. 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
  415. `El (((ns_rdf, "RDF"), [
  416. ((Xmlm.ns_xmlns, "rdf"), ns_rdf);
  417. ((Xmlm.ns_xml,"base"),base |> Uri.to_string);
  418. ]),
  419. sep 1 ::
  420. `El (((ns_rdf, "Description"), [
  421. ((Xmlm.ns_xmlns, "seppo"), ns_seppo);
  422. ((ns_rdf, "about"), "");
  423. ]),
  424. sep 2 ::
  425. txt' ns_seppo "token" [] token @
  426. noyes' ns_seppo "notify" [] notify @
  427. noyes' ns_seppo "subscribed" [] subscribed @
  428. noyes' ns_seppo "blocked" [] blocked
  429. )
  430. :: sep 1
  431. :: encode' ~base ~context pe
  432. :: [])
  433. end
  434. end
  435. (* Xml subset of the profle page. *)
  436. module PersonX = struct
  437. let xml_ pubdate (pem, (pro, (uid, base))) =
  438. let Rfc4287.Rfc4646 lang = (pro : Cfg.Profile.t).language in
  439. Person.prsn pubdate (pem, (pro, (uid, base)))
  440. |> Person.Rdf.encode ~base ~context:(Some lang)
  441. |> Result.ok
  442. let target = prox
  443. let rule = {Person.rule
  444. with target;
  445. command = fun pre _ _ ->
  446. File.out_channel' (fun oc ->
  447. let now = Ptime_clock.now () in
  448. let writex oc x =
  449. let xsl = Some "../themes/current/actor.xsl" in
  450. Xml.to_chan ~xsl x oc;
  451. Ok "" in
  452. Cfg.Base.(fn |> from_file)
  453. >>= chain Auth.(fn |> uid_from_file)
  454. >>= chain Cfg.Profile.(fn |> from_file)
  455. >>= chain (PubKeyPem.make pre >>= File.cat)
  456. >>= xml_ now
  457. >>= writex oc) }
  458. let rulez = rule :: PubKeyPem.rulez
  459. let make pre = Make.make ~pre rulez target
  460. end
  461. module Activity = struct
  462. type t = T of string
  463. let ty s = match s |> String.lowercase_ascii with
  464. | "like" -> Ok (T "Like")
  465. | "dislike" -> Ok (T "Dislike")
  466. | _ -> Error ("Activity '" ^ s ^ "' not supported.")
  467. let make_like me _act _pubdate objec _remote_actor: As2_vocab.Types.like =
  468. {
  469. id = Uri.empty;
  470. actor = me;
  471. obj = objec;
  472. published= None;
  473. (* to cc *)
  474. }
  475. let digest_base64 = PubKeyPem.digest_base64
  476. let digest_base64' = PubKeyPem.digest_base64'
  477. (** e.g. https://tube.network.europa.eu/w/aTx3DYwH1km2gTEn9gKpah
  478. *
  479. * $ curl -H 'accept: application/activity+json' 'https://tube.network.europa.eu/w/aTx3DYwH1km2gTEn9gKpah'
  480. * $ curl -H 'accept: application/activity+json' 'https://tube.network.europa.eu/accounts/edps'
  481. *)
  482. let like' pk (act_type : t) post_uri (me : As2_vocab.Types.person) : (As2_vocab.Types.uri,string) result Lwt.t =
  483. let base = Uri.empty in
  484. let open Cohttp_lwt in
  485. (* we need the sender and recipient actor profiles *)
  486. (* https://github.com/roburio/http-lwt-client/blob/main/src/http_lwt_client.ml *)
  487. let post_attributed_to json =
  488. let extract3tries k0 k1 j =
  489. match Ezjsonm.find j [ k0 ] with
  490. | `String s -> Some s
  491. | `A (`String s :: _) -> Some s
  492. | `A ((`O _ as hd) :: _) -> (
  493. (* ignore 'type' *)
  494. match Ezjsonm.find hd [ k1 ] with
  495. | `String s -> Some s
  496. | _ -> None)
  497. | _ -> None
  498. in
  499. json
  500. |> extract3tries "attributedTo" "id"
  501. |> to_result (* TODO examine the http response code? *) "attribution not found"
  502. >>= fun v -> Ok (Uri.of_string v)
  503. in
  504. let%lwt p = Http.get_jsonv post_attributed_to post_uri in
  505. match p with
  506. | Error _ as e -> Lwt.return e
  507. | Ok (act_uri : Uri.t) ->
  508. let%lwt j = Http.get_jsonv Result.ok act_uri in
  509. match j >>= Person.Json.decode with
  510. | Error _ as e -> Lwt.return e
  511. | Ok pro ->
  512. let _ = Person.make "" in
  513. let date = Ptime_clock.now ()
  514. and sndr = me.id
  515. and key = me.public_key.id
  516. and rcpt = pro.id
  517. and inbx = pro.inbox in
  518. let body = make_like sndr act_type date post_uri rcpt
  519. |> As2_vocab.Encode.like ~base
  520. |> Ezjsonm.value_to_string in
  521. let headers = Http.signed_headers (key,PubKeyPem.sign pk,date) (digest_base64' body) inbx in
  522. let headers = Http.H.add' headers Http.H.ct_json in
  523. let headers = Http.H.add' headers Http.H.acc_app_jlda in
  524. Logr.info (fun m -> m "-> http POST %a" Uri.pp inbx);
  525. let%lwt p = Http.post ~headers body inbx in
  526. match p with
  527. | Error _ as e -> Lwt.return e
  528. | Ok (_resp, body) ->
  529. let%lwt b = body |> Body.to_string in
  530. Logr.debug (fun m -> m "%s" b);
  531. Lwt.return (Ok post_uri)
  532. let like pk aty uri act =
  533. aty |> ty
  534. >>= fun v -> Ok (like' pk v uri act)
  535. end
  536. (*
  537. * https://www.w3.org/TR/activitystreams-core/
  538. * https://www.w3.org/TR/activitystreams-core/#media-type
  539. *)
  540. let send ?(success = `OK) ~(key : Http.t_sign_k) (f_ok : Cohttp.Response.t * string -> unit) to_ msg =
  541. let body = msg |> Ezjsonm.value_to_string in
  542. let signed_headers body = PubKeyPem.(Http.signed_headers key (digest_base64' body) to_) in
  543. let headers = signed_headers body in
  544. let headers = Http.H.add' headers Http.H.ct_jlda in
  545. let headers = Http.H.add' headers Http.H.acc_app_jlda in
  546. (* TODO queue it and re-try in case of failure *)
  547. let%lwt r = Http.post ~headers body to_ in
  548. (match r with
  549. | Ok (res,body') ->
  550. let%lwt body' = body' |> Cohttp_lwt.Body.to_string in
  551. (match res.status with
  552. | #Cohttp.Code.success_status ->
  553. Logr.debug (fun m -> m "%s.%s %a\n%a\n\n%s" "Ap" "send" Uri.pp to_ Cohttp.Response.pp_hum res body');
  554. f_ok (res, body');
  555. Ok (success, [Http.H.ct_plain], Cgi.Response.body "ok")
  556. | _ ->
  557. Logr.warn (fun m -> m "%s.%s %a\n%a\n\n%s" "Ap" "send" Uri.pp to_ Cohttp.Response.pp_hum res body');
  558. Http.s502
  559. ) |> Lwt.return
  560. | Error e ->
  561. Logr.warn (fun m -> m "%s.%s <- %s %a\n%s" "Ap" "send" "post" Uri.pp to_ e);
  562. Http.s500 |> Lwt.return)
  563. let rcv_reject
  564. ?(tnow = Ptime_clock.now ())
  565. ~uuid
  566. ~base
  567. (siac : As2_vocab.Types.person)
  568. _ =
  569. Logr.warn(fun m -> m "%s.%s %a %a" "Ap" "rcv_reject" Uri.pp siac.id Uuidm.pp uuid);
  570. let _ = tnow
  571. and _ = base
  572. and _ = siac
  573. in
  574. Lwt.return Http.s501
  575. let snd_reject
  576. ~uuid
  577. ~base
  578. ~key
  579. me
  580. (siac : As2_vocab.Types.person)
  581. (j : Ezjsonm.value) =
  582. Logr.warn(fun m -> m "%s.%s %a %a" "Ap" "snd_reject" Uuidm.pp uuid Uri.pp siac.inbox);
  583. assert (not (me |> Uri.equal siac.id));
  584. let reject me id =
  585. `O [("@context", `String As2_vocab.Constants.ActivityStreams.ns_as);
  586. ("type", `String "Reject");
  587. ("actor", `String (me |> Http.reso ~base |> Uri.to_string));
  588. ("object", `String (id |> Uri.to_string))]
  589. in
  590. let id = match j with
  591. | `O (_ :: ("id", `String id) :: _) -> id |> Uri.of_string
  592. | _ -> Uri.empty in
  593. id
  594. |> reject me
  595. |> send ~success:`Unprocessable_entity ~key
  596. (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))
  597. siac.inbox
  598. module Followers = struct
  599. module State = struct
  600. type t =
  601. | Pending
  602. | Accepted
  603. | Blocked
  604. let of_string = function
  605. | "pending" -> Some Pending
  606. | "accepted" -> Some Accepted
  607. | "blocked" -> Some Blocked
  608. | _ -> None
  609. let to_string = function
  610. | Pending -> "pending"
  611. | Accepted -> "accepted"
  612. | Blocked -> "blocked"
  613. type t' = t * Ptime.t * Uri.t * string option * Webfinger.Client.t option * Uri.t option
  614. let ibox (_,_,ibox,_,_,_ : t') : Uri.t = ibox
  615. (* input to fold_left *)
  616. let ibox' f a (k,v) = f a (k,v |> ibox)
  617. let of_actor tnow st (siac : As2_vocab.Types.person) : t' =
  618. let us = match Uri.host siac.id, siac.preferred_username with
  619. | None,_
  620. | _,None -> None
  621. | Some hos, Some usr -> Some Webfinger.Client.(Localpart usr, Domainpart hos) in
  622. (st,tnow,siac.inbox,siac.name,us,siac.icon)
  623. let decode = function
  624. | Csexp.(List [Atom "1"; Atom s; Atom t0; Atom inbox; Atom name; Atom rfc7033; Atom avatar]) ->
  625. Option.bind
  626. (s |> of_string)
  627. (fun s ->
  628. match t0 |> Ptime.of_rfc3339 with
  629. | Ok (t,_,_) ->
  630. let inbox = inbox |> Uri.of_string
  631. and rfc7033 = rfc7033 |> Webfinger.Client.from_string |> Result.to_option
  632. and avatar = avatar |> Uri.of_string in
  633. let r : t' = (s,t,inbox,Some name,rfc7033,Some avatar) in
  634. Some r
  635. | _ -> None )
  636. (* legacy: *)
  637. (* assume the preferred_username is @ attached to the inbox *)
  638. | Csexp.(List [Atom s; Atom t0; Atom inbox]) ->
  639. Option.bind
  640. (s |> of_string)
  641. (fun s ->
  642. match t0 |> Ptime.of_rfc3339 with
  643. | Ok (t,_,_) ->
  644. let inbox = inbox |> Uri.of_string in
  645. let us = Option.bind
  646. (inbox |> Uri.user)
  647. (fun u -> Some Webfinger.Client.(Localpart u, Domainpart (inbox |> Uri.host_with_default ~default:"-"))) in
  648. let r : t' = (s,t,Uri.with_userinfo inbox None,inbox |> Uri.user,us,None) in
  649. Some r
  650. | _ -> None)
  651. | _ -> None
  652. let decode' = function
  653. | Ok s -> s |> decode
  654. | _ -> None
  655. let encode ((state,t,inbox,name,us,avatar) : t') =
  656. (* attach the preferred_username to the inbox *)
  657. let state = state |> to_string in
  658. let t0 = t |> Ptime.to_rfc3339 in
  659. let inbox = inbox |> Uri.to_string in
  660. let name = name |> Option.value ~default:"" in
  661. let avatar = avatar
  662. |> Option.value ~default:Uri.empty
  663. |> Uri.to_string in
  664. let rfc7033 = Option.bind us
  665. (fun l -> Some (l |> Webfinger.Client.to_string))
  666. |> Option.value ~default:"" in
  667. Csexp.(List [Atom "1"; Atom state; Atom t0; Atom inbox; Atom name; Atom rfc7033; Atom avatar])
  668. let to_yn ?(invert = false) (x,_,_,_,_,_ : t') : As2.No_p_yes.t option =
  669. match x,invert with
  670. | Pending ,_ -> Some As2.No_p_yes.Pending
  671. | Accepted,false
  672. | Blocked ,true -> Some As2.No_p_yes.Yes
  673. | Blocked ,false
  674. | Accepted,true -> Some As2.No_p_yes.No
  675. end
  676. let fold_left (fkt : 'a -> (Uri.t * State.t') -> 'a) =
  677. (* let _k2u f a (k,v) = f a (k |> Bytes.to_string |> Uri.of_string,v) in
  678. let _v2s f a (k,v) = f a (k,v |> Bytes.to_string |> Csexp.parse_string |> State.decode') in *)
  679. let kv f a (k,v) = f a
  680. (k |> Bytes.to_string |> Uri.of_string
  681. ,v |> Bytes.to_string |> Csexp.parse_string |> State.decode') in
  682. let opt f a = function
  683. | (k,None) -> Logr.warn (fun m -> m "%s.%s ignored actor %a" "Ap.Followers" "fold_left" Uri.pp k);
  684. a
  685. | (k,Some v) -> f a (k,v) in
  686. (* caveat, this folding really looks reverse: *)
  687. fkt |> opt |> kv |> Mapcdb.fold_left
  688. let cdb = Mapcdb.Cdb "app/var/db/notify.cdb"
  689. let find_uri
  690. ?(cdb = cdb)
  691. u : State.t' option =
  692. let ke = u |> Uri.to_string in
  693. Option.bind
  694. (Mapcdb.find_string_opt ke cdb)
  695. (fun s -> s |> Csexp.parse_string |> State.decode')
  696. let notify ?(cdb = cdb) id =
  697. match find_uri ~cdb id with
  698. | Some s -> s |> State.to_yn
  699. | None -> Some As2.No_p_yes.No
  700. module Atom = struct
  701. (* create all from oldest to newest and return newest file name. *)
  702. let of_cdb
  703. ?(cdb = cdb)
  704. ~title
  705. ~xsl
  706. ~rel
  707. ?(page_size = 50)
  708. dir =
  709. Logr.debug (fun m -> m "%s.%s" "Ap.Followers.Atom" "of_cdb");
  710. let flush _is_last (u,p,i) =
  711. let _ : (Uri.t * string option * Webfinger.Client.t option * Uri.t option) list = u in
  712. assert (0 <= p);
  713. assert (dir |> St.ends_with ~suffix:"/");
  714. let fn = Printf.sprintf "%s%d.xml" dir p in
  715. Logr.debug (fun m -> m "%s.%s %s" "Ap.Followers.Atom" "of_cdb.flush" dir);
  716. assert (u |> List.length = i);
  717. let open Xml in
  718. let mk_rel rel i =
  719. let path,title = match rel with
  720. | Rfc4287.Link.(Rel (Single "first")) ->
  721. assert (i == -1);
  722. ".",Some "last"
  723. | _ ->
  724. assert (i >= 0);
  725. Printf.sprintf "%d.xml" i,
  726. Some (Printf.sprintf "%i" (i+1))
  727. and rel = Some rel in
  728. Rfc4287.Link.(Uri.make ~path () |> make ~rel ~title |> to_atom)
  729. in
  730. let self = mk_rel Rfc4287.Link.self p in
  731. let first = mk_rel Rfc4287.Link.first (-1) in
  732. let last = mk_rel Rfc4287.Link.last 0 in
  733. let prev = mk_rel Rfc4287.Link.prev (p + 1) in
  734. let add_next i l = match i with
  735. | 0 -> l
  736. | i -> sep 1 :: mk_rel Rfc4287.Link.next (i - 1) :: l in
  737. let id_s = Printf.sprintf "%i.xml" p in
  738. let s : _ Xmlm.frag =
  739. `El (((ns_a, "feed"), [ ((Xmlm.ns_xmlns, "xmlns"), ns_a) ]),
  740. sep 1
  741. :: `El (((ns_a,"title"), []), [`Data title]) :: sep 1
  742. :: `El (((ns_a,"id"), []), [`Data id_s ])
  743. :: sep 1 :: self
  744. :: sep 1 :: first
  745. :: sep 1 :: last
  746. :: sep 1 :: prev
  747. :: (u
  748. |> List.rev
  749. |> List.fold_left
  750. (fun i (href,title,us,_unused_icon) ->
  751. let href = Uri.with_userinfo href None in
  752. let rfc7033 = Option.bind us
  753. (fun us -> Some (us |> Webfinger.Client.to_string)) in
  754. sep 1
  755. :: Rfc4287.Link.(make ~rel ~title ~rfc7033 href |> to_atom)
  756. :: i)
  757. [`Data "\n"]
  758. |> add_next p )
  759. )
  760. in
  761. let mode = [Open_binary;Open_creat;Open_trunc;Open_wronly] in
  762. File.out_channel ~mode fn (Xml.to_chan ~xsl s);
  763. Ok fn in
  764. fold_left (fun (l,p,i) (href,((_,_,_inbox,title,us,icon) : State.t')) ->
  765. Logr.debug (fun m -> m "%s.%s %a" "Ap.Followers.Atom" "of_cdb.fo" Uri.pp href);
  766. let k = (href,title,us,icon) in
  767. let i = i + 1 in
  768. if i > page_size
  769. then
  770. (let _ = (l,p,i-1) |> flush false in
  771. (k :: [],p+1,1))
  772. else
  773. (k :: l,p,i))
  774. ([],0,0) cdb
  775. |> flush true
  776. let dir = apub ^ "notify/"
  777. let target = dir ^ "index.xml"
  778. let rule : Make.t = {
  779. target;
  780. prerequisites = PersonX.rule.target
  781. :: (cdb |> (fun (Mapcdb.Cdb v) -> v))
  782. :: [];
  783. fresh = Make.Outdated;
  784. command = fun _pre _ _ _ ->
  785. of_cdb
  786. ~cdb
  787. ~title:"📣 Notify (Followers)"
  788. ~xsl:(Rfc4287.xsl "notify.xsl" target)
  789. ~rel:(Some Rfc4287.Link.notify)
  790. ~page_size:50
  791. dir
  792. }
  793. let make = Make.make [rule]
  794. end
  795. module Json = struct
  796. let to_page ~finish (i : int) (fs : Uri.t list) : Uri.t As2_vocab.Types.collection_page =
  797. let p i =
  798. let path = i |> Printf.sprintf "%d.jsa" in
  799. Uri.make ~path () in
  800. let self = p i in
  801. let next = if i > 0
  802. then Some (p (i - 1))
  803. else None in
  804. let prev = if not finish
  805. then Some (p (i + 1))
  806. else None in
  807. {
  808. id = self;
  809. current = Some self;
  810. first = None;
  811. is_ordered = true;
  812. items = fs;
  813. last = Some (p 0);
  814. next;
  815. part_of = Some (Uri.make ~path:"index.jsa" ());
  816. prev;
  817. total_items= None;
  818. }
  819. let to_page_json ~base _prefix ~finish (i : int) (ids : Uri.t list) =
  820. to_page ~finish i ids
  821. |> As2_vocab.Encode.(collection_page ~base (uri ~base))
  822. (*
  823. * dehydrate into https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection
  824. * and https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollectionpage
  825. * dst afterwards contains an
  826. * index.jsa
  827. * index-0.jsa
  828. * ...
  829. * index-n.jsa
  830. *)
  831. let flush_page ~base ~oc prefix ~finish (tot,pa,lst,_) =
  832. let fn j = j |> Printf.sprintf "%d.jsa" in
  833. Logr.debug (fun m -> m "%s.%s lst#%d" "Ap.Followers" "flush_page" (lst |> List.length));
  834. let js = lst |> List.rev |> to_page_json ~base prefix ~finish pa in
  835. let mode = [Open_binary;Open_creat;Open_trunc;Open_wronly] in
  836. File.out_channel ~mode (prefix ^ (fn pa)) (fun ch -> Ezjsonm.value_to_channel ~minify:false ch js);
  837. (if finish
  838. then
  839. let p i =
  840. let path = fn i in
  841. Uri.make ~path () in
  842. let c : Uri.t As2_vocab.Types.collection =
  843. { id = Uri.make ~path:"index.jsa" ();
  844. current = None;
  845. first = Some (p pa);
  846. is_ordered = true;
  847. items = Some [];
  848. last = Some (p 0);
  849. total_items = Some tot;
  850. } in
  851. c
  852. |> As2_vocab.Encode.(collection ~base (uri ~base))
  853. |> Ezjsonm.value_to_channel ~minify:false oc)
  854. let fold2pages pagesize flush_page (tot,pa,lst,i) id =
  855. Logr.debug (fun m -> m "%s.%s %a" "Ap.Followers" "fold2pages" Uri.pp id );
  856. if i >= pagesize
  857. then (
  858. flush_page ~finish:false (tot,pa,lst,i);
  859. (tot+1,pa+1,id :: [],0)
  860. ) else
  861. (tot+1,pa,id :: lst,i+1)
  862. (**
  863. * dehydrate the cdb (e.g. followers list) into the current directory
  864. *
  865. * uses fold2pages & flush_page
  866. *)
  867. let coll_of_cdb ~base ~oc ?(pagesize = 100) prefix cdb =
  868. assert (0 < pagesize && pagesize < 10_001);
  869. (* Logr.debug (fun m -> m "%s.%s %d %a" "Ap.Followers" "cdb2coll" pagesize Uri.pp base ); *)
  870. let base = Http.reso ~base (Uri.make ~path:prefix ()) in
  871. let* res = fold_left (fun a (k,v) ->
  872. match a with
  873. | Ok ctx ->
  874. (match v with
  875. | (State.Pending,_,_,_,_,_)
  876. | (State.Blocked,_,_,_,_,_) ->
  877. Logr.debug (fun m -> m "%s.%s ignored %a" "Ap.Followers" "cdb2coll.fold_left" Uri.pp k);
  878. Ok ctx (* just go on *)
  879. | (State.Accepted,_,_,_,_,_) ->
  880. k
  881. |> fold2pages pagesize (flush_page ~base ~oc prefix) ctx
  882. |> Result.ok )
  883. | e ->
  884. Logr.err (fun m -> m "%s %s.%s foohoo" E.e1008 "Ap.Followers" "cdb2coll");
  885. e) (Ok (0,0,[],0)) cdb in
  886. flush_page ~base prefix ~oc ~finish:true res;
  887. Ok (prefix ^ "index.jsa")
  888. let dir = apub ^ "notify/"
  889. let target = dir ^ "index.jsa"
  890. let rule = {Atom.rule
  891. with
  892. target;
  893. prerequisites = Person.rule.target
  894. :: (cdb |> (fun (Mapcdb.Cdb v) -> v))
  895. :: [];
  896. command = fun _pre _ _ ->
  897. File.out_channel' (fun oc ->
  898. let* base = Cfg.Base.(from_file fn) in
  899. coll_of_cdb ~base ~oc dir cdb)
  900. }
  901. end
  902. (* notify the followers (uri) and do the local effect *)
  903. let snd_accept
  904. ?(tnow = Ptime_clock.now ())
  905. ~uuid
  906. ~base
  907. ~key
  908. ?(cdb = cdb)
  909. me
  910. (siac : As2_vocab.Types.person)
  911. (fo : As2_vocab.Types.follow) =
  912. Logr.warn(fun m -> m "%s.%s %a %a" "Ap.Followers" "snd_accept" Uri.pp fo.actor Uuidm.pp uuid);
  913. assert (not (me |> Uri.equal fo.actor));
  914. let ke = fo.actor |> Uri.to_string in
  915. let side_ok _ =
  916. let v = State.(of_actor tnow Accepted siac |> encode) |> Csexp.to_string in
  917. let _ = Mapcdb.update_string ke v cdb in
  918. let _ = Make.make [Json.rule] Json.target in
  919. let _ = Atom.(make target) in
  920. () in
  921. match Option.bind
  922. (Mapcdb.find_string_opt ke cdb)
  923. (fun s -> s |> Csexp.parse_string |> State.decode') with
  924. | None ->
  925. (* Immediately accept *)
  926. let msg = ({
  927. id = fo.id;
  928. actor = me;
  929. obj = fo;
  930. published = Some tnow;
  931. } : As2_vocab.Types.follow As2_vocab.Types.accept)
  932. |> As2_vocab.Encode.(accept (follow ~context:None ~base)) ~base in
  933. send ~key side_ok siac.inbox msg
  934. | Some (Accepted,tnow,_,_,_,_)
  935. | Some (Pending,tnow,_,_,_,_) ->
  936. let msg = ({
  937. id = fo.id;
  938. actor = me;
  939. obj = fo;
  940. published = Some tnow;
  941. } : As2_vocab.Types.follow As2_vocab.Types.accept)
  942. |> As2_vocab.Encode.(accept (follow ~context:None ~base)) ~base in
  943. send ~key side_ok siac.inbox msg
  944. | Some (Blocked,_,_tnow,_,_,_) -> Lwt.return Http.s403
  945. (* do the local effect *)
  946. let snd_accept_undo
  947. ?(tnow = Ptime_clock.now ())
  948. ~uuid
  949. ~base
  950. ~key
  951. me
  952. (siac : As2_vocab.Types.person)
  953. (ufo : As2_vocab.Types.follow As2_vocab.Types.undo) =
  954. Logr.warn(fun m -> m "%s.%s %a %a" "Ap.Follower" "snd_accept_undo" Uri.pp ufo.obj.actor Uuidm.pp uuid);
  955. Logr.warn(fun m -> m "%s.%s TODO persist local effects" "Ap.Followers" "undo follow");
  956. assert (not (me |> Uri.equal ufo.actor));
  957. let ke = ufo.actor |> Uri.to_string in
  958. let side_ok _ =
  959. let _ = Mapcdb.remove_string ke cdb in
  960. let _ = Make.make [Json.rule] Json.target in
  961. let _ = Atom.(make target) in
  962. () in
  963. assert (ufo.actor |> Uri.equal ufo.obj.actor );
  964. let msg = ({
  965. id = ufo.id;
  966. actor = me;
  967. obj = ufo;
  968. published = Some tnow;
  969. } : As2_vocab.Types.follow As2_vocab.Types.undo As2_vocab.Types.accept)
  970. |> As2_vocab.Encode.(accept (undo ~context:None ~base (follow ~context:None ~base))) ~base in
  971. send ~key side_ok siac.inbox msg
  972. end
  973. module Following = struct
  974. let n = "subscribed"
  975. let cdb = Mapcdb.Cdb ("app/var/db/" ^ n ^ ".cdb")
  976. let dir = apub ^ n ^ "/"
  977. let subscribed ?(cdb = cdb) id =
  978. match Followers.find_uri ~cdb id with
  979. | Some s -> s |> Followers.State.to_yn
  980. | None -> Some As2.No_p_yes.No
  981. let blocked ?(cdb = cdb) id =
  982. match Followers.find_uri ~cdb id with
  983. | Some s -> s |> Followers.State.to_yn ~invert:true
  984. | None -> Some As2.No_p_yes.No
  985. module Atom = struct
  986. let target = dir ^ "index.xml"
  987. let rule : Make.t = {
  988. target;
  989. prerequisites = PersonX.rule.target
  990. :: (cdb |> (fun (Mapcdb.Cdb v) -> v))
  991. :: [];
  992. fresh = Make.Outdated;
  993. command = fun _pre _ _ _ ->
  994. Followers.Atom.of_cdb
  995. ~cdb
  996. ~title:"👂 Subscribed (Following)"
  997. ~xsl:(Rfc4287.xsl "subscribed.xsl" target)
  998. ~rel:(Some Rfc4287.Link.subscribed)
  999. ~page_size:50 dir
  1000. }
  1001. end
  1002. module Json = struct
  1003. let target = dir ^ "index.jsa"
  1004. let rule : Make.t = {
  1005. target;
  1006. prerequisites = Person.rule.target
  1007. :: (cdb |> (fun (Mapcdb.Cdb v) -> v))
  1008. :: [];
  1009. fresh = Make.Outdated;
  1010. command = fun _pre _ _ ->
  1011. File.out_channel' (fun oc ->
  1012. let* base = Cfg.Base.(from_file fn) in
  1013. Followers.Json.coll_of_cdb ~base ~oc dir cdb)
  1014. }
  1015. end
  1016. let follow ~me ~inbox reac : As2_vocab.Activitypub.Types.follow =
  1017. assert (not (me |> Uri.equal reac));
  1018. {
  1019. id = Uri.with_fragment reac (Some "subscribe");
  1020. actor = me;
  1021. cc = [];
  1022. object_ = reac;
  1023. state = None;
  1024. to_ = [inbox];
  1025. }
  1026. let undo ~me (o : As2_vocab.Types.follow) : As2_vocab.Types.follow As2_vocab.Types.undo =
  1027. assert (not (me |> Uri.equal o.object_));
  1028. assert (me |> Uri.equal o.actor );
  1029. {
  1030. id = Uri.with_fragment o.id (Some "subscribe#undo");
  1031. actor = me;
  1032. obj = o;
  1033. published= None;
  1034. }
  1035. let rcv_accept
  1036. ?(tnow = Ptime_clock.now ())
  1037. ?(subscribed = cdb)
  1038. ~uuid
  1039. ~base
  1040. me
  1041. (siac : As2_vocab.Types.person)
  1042. (fo : As2_vocab.Types.follow) =
  1043. Logr.debug (fun m -> m "%s.%s %a %a" "Ap.Following" "accept" Uuidm.pp uuid Uri.pp fo.object_);
  1044. assert (not (me |> Uri.equal siac.id)) ;
  1045. assert (me |> Uri.equal fo.actor) ;
  1046. assert (not (fo.actor |> Uri.equal fo.object_));
  1047. assert (siac.id |> Uri.equal fo.object_) ;
  1048. Logr.warn (fun m -> m "%s.%s TODO only take those that I expect" "Ap.Following" "accept");
  1049. let _ = base in
  1050. let ke = siac.id |> Uri.to_string in
  1051. let v = Followers.State.(of_actor tnow Accepted siac |> encode) |> Csexp.to_string in
  1052. let _ = Mapcdb.update_string ke v subscribed in
  1053. let _ = Json.(Make.make [rule] target) in
  1054. let _ = Atom.(Make.make [rule] target) in
  1055. Ok (`Created, [Http.H.ct_plain], Cgi.Response.body "created")
  1056. |> Lwt.return
  1057. end
  1058. module Note = struct
  1059. let actor_from_author _author =
  1060. Uri.make ~path:proj ()
  1061. let followers actor =
  1062. Uri.make ~path:"notify/index.jsa" () |> Http.reso ~base:actor
  1063. let of_rfc4287
  1064. ?(to_ = [As2_vocab.Constants.ActivityStreams.public])
  1065. (e : Rfc4287.Entry.t)
  1066. : As2_vocab.Types.note =
  1067. Logr.debug (fun m -> m "%s.%s %a" "As2.Note" "of_rfc4287" Uri.pp e.id);
  1068. let tag init (lbl,term,base) =
  1069. let ty = `Hashtag in
  1070. let open Rfc4287.Category in
  1071. let (Label (Single name)) = lbl
  1072. and (Term (Single term)) = term in
  1073. let path = term ^ "/" in
  1074. let href = Uri.make ~path () |> Http.reso ~base in
  1075. ({ty; name; href} : As2_vocab.Types.tag) :: init
  1076. in
  1077. let id = e.id in
  1078. let actor = actor_from_author e.author in
  1079. let cc = [actor |> followers] in
  1080. let Rfc3339.T published = e.published in
  1081. let published = match published |> Ptime.of_rfc3339 with
  1082. | Ok (t,_,_) -> Some t
  1083. | _ -> None in
  1084. (* let Rfc4287.Rfc3066 lang = e.lang in *)
  1085. let tags = e.categories |> List.fold_left tag [] in
  1086. let summary,content = match e.title,e.content with
  1087. | "","" -> None,"." (* empty is forbidden *)
  1088. | t,"" -> None,t
  1089. | t,c -> Some t,c in
  1090. let url = e.links |> List.fold_left (
  1091. (* sift those without a rel *)
  1092. fun i (l : Rfc4287.Link.t) ->
  1093. match l.rel with
  1094. | None -> l.href :: i
  1095. | Some _ -> i) [] in
  1096. assert (not (content |> String.equal ""));
  1097. {
  1098. id;
  1099. actor;
  1100. attachment=[];
  1101. cc;
  1102. content;
  1103. content_map=[];
  1104. in_reply_to=[];
  1105. media_type=(Some Http.Mime.text_plain);
  1106. published;
  1107. sensitive=false;
  1108. source=None;
  1109. summary;
  1110. summary_map=[];
  1111. tags;
  1112. to_;
  1113. url;
  1114. }
  1115. let txt2html s =
  1116. (* care about :
  1117. * - newlines
  1118. * - urls
  1119. * - tags
  1120. * - mentions
  1121. *)
  1122. s
  1123. (* Mastodon uses the summary as content warning. That's not what the summary intends.
  1124. formerly know as pleistocenify *)
  1125. let diluviate (n : As2_vocab.Types.note) =
  1126. let c = match n.summary with
  1127. | None -> ""
  1128. | Some t -> (t |> txt2html) ^ "<br/>\n" in
  1129. let c = n.url |> List.fold_left (fun i u ->
  1130. let s = u |> Uri.to_string in
  1131. Printf.sprintf "%s<a href='%s'>%s</a><br/>\n" i s s) c in
  1132. let c = if c |> String.equal ""
  1133. then c
  1134. else (* add an emoty line *) c ^ "<br/>\n" in
  1135. let c = c ^ (n.content |> txt2html) in
  1136. {n with
  1137. summary = None;
  1138. content = c;
  1139. url = [n.id] }
  1140. module Create = struct
  1141. let make (obj : As2_vocab.Types.note) : As2_vocab.Types.note As2_vocab.Types.create =
  1142. let frag = match obj.id |> Uri.fragment with
  1143. | None -> Some "Create"
  1144. | Some f -> Some (f ^ "/Create") in
  1145. {
  1146. id = frag |> Uri.with_fragment obj.id;
  1147. actor = obj.actor;
  1148. published = obj.published;
  1149. to_ = obj.to_;
  1150. cc = obj.cc;
  1151. direct_message = false;
  1152. obj = obj;
  1153. }
  1154. let to_json ~base n =
  1155. n
  1156. |> of_rfc4287
  1157. |> diluviate
  1158. (* let c = {c with to_ = [id]} in *)
  1159. |> make
  1160. |> As2_vocab.Encode.(create ~base ~context:As2_vocab.Constants.ActivityStreams.und
  1161. (note ~base))
  1162. end
  1163. module Delete = struct
  1164. let make (obj : As2_vocab.Types.note) : As2_vocab.Types.note As2_vocab.Types.delete =
  1165. let frag = match obj.id |> Uri.fragment with
  1166. | None -> Some "Delete"
  1167. | Some f -> Some (f ^ "/Delete") in
  1168. {
  1169. id = frag |> Uri.with_fragment obj.id;
  1170. actor = obj.actor;
  1171. published = obj.published; (* rather use tnow *)
  1172. obj = obj;
  1173. }
  1174. let to_json ~base n =
  1175. n
  1176. |> of_rfc4287
  1177. |> make
  1178. |> As2_vocab.Encode.(delete ~base (note ~base))
  1179. end
  1180. let _5381_63 = 5381 |> Optint.Int63.of_int
  1181. (* http://cr.yp.to/cdb/cdb.txt *)
  1182. let hash63_gen len f_get : Optint.Int63.t =
  1183. let mask = Optint.Int63.max_int
  1184. and ( +. ) = Optint.Int63.add
  1185. and ( << ) = Optint.Int63.shift_left
  1186. and ( ^ ) = Optint.Int63.logxor
  1187. and ( land ) = Optint.Int63.logand in
  1188. let rec fkt (idx : int) (h : Optint.Int63.t) =
  1189. if idx = len
  1190. then h
  1191. else
  1192. let c = idx |> f_get |> Char.code |> Optint.Int63.of_int in
  1193. (((h << 5) +. h) ^ c) land mask
  1194. |> fkt (idx + 1)
  1195. in
  1196. fkt 0 _5381_63
  1197. let hash63_str dat : Optint.Int63.t =
  1198. hash63_gen (String.length dat) (String.get dat)
  1199. let uhash ?(off = 0) ?(buf = Bytes.make (Optint.Int63.encoded_size) (Char.chr 0)) u =
  1200. u
  1201. |> Uri.to_string
  1202. |> hash63_str
  1203. |> Optint.Int63.encode buf ~off;
  1204. buf
  1205. |> Bytes.to_string
  1206. |> Base64.encode_string ~pad:false ~alphabet:Base64.uri_safe_alphabet
  1207. let ibc_dir = "app/var/cache/inbox/"
  1208. let do_cache
  1209. ?(tnow = Ptime_clock.now ())
  1210. ?(dir = ibc_dir)
  1211. ~(base : Uri.t)
  1212. (a : As2_vocab.Types.note As2_vocab.Types.create) =
  1213. let _ = tnow in
  1214. Logr.debug (fun m -> m "%s.%s TODO %a" "Ap.Note" "do_cache" Uri.pp a.id);
  1215. let fn = a.obj.id
  1216. |> uhash
  1217. |> Printf.sprintf "note-%s.json" in
  1218. let tmp = Some (dir ^ "tmp/" ^ fn) in
  1219. File.out_channel ~tmp (dir ^ "new/" ^ fn)
  1220. (fun oc ->
  1221. a
  1222. |> As2_vocab.Encode.(create ~context:None ~base (note ~context:None ~base))
  1223. |> Ezjsonm.value_to_channel oc)
  1224. let do_cache'
  1225. ?(tnow = Ptime_clock.now ())
  1226. ?(dir = ibc_dir)
  1227. ~(base : Uri.t)
  1228. (a : As2_vocab.Types.note As2_vocab.Types.update) =
  1229. let _ = tnow in
  1230. Logr.debug (fun m -> m "%s.%s TODO %a" "Ap.Note" "do_cache" Uri.pp a.id);
  1231. let fn = a.obj.id
  1232. |> uhash
  1233. |> Printf.sprintf "note-%s.json" in
  1234. let tmp = Some (dir ^ "tmp/" ^ fn) in
  1235. File.out_channel ~tmp (dir ^ "new/" ^ fn)
  1236. (fun oc ->
  1237. a
  1238. |> As2_vocab.Encode.(update ~context:None ~base (note ~context:None ~base))
  1239. |> Ezjsonm.value_to_channel oc)
  1240. let rcv_create
  1241. ?(tnow = Ptime_clock.now ())
  1242. ~uuid
  1243. ~(base : Uri.t)
  1244. (siac : As2_vocab.Types.person)
  1245. (a : As2_vocab.Types.note As2_vocab.Types.create) : Cgi.Response.t' Lwt.t =
  1246. Logr.err (fun m -> m "%s.%s TODO %a %a" "Ap.Note" "rcv_create" Uri.pp a.obj.actor Uuidm.pp uuid);
  1247. assert (siac.id |> Uri.equal a.obj.actor);
  1248. let _ = do_cache ~tnow ~base a in
  1249. Ok (`Created, [Http.H.ct_plain], Cgi.Response.body "created")
  1250. |> Lwt.return
  1251. let rcv_update
  1252. ?(tnow = Ptime_clock.now ())
  1253. ~uuid
  1254. ~(base : Uri.t)
  1255. (siac : As2_vocab.Types.person)
  1256. (a : As2_vocab.Types.note As2_vocab.Types.update) : Cgi.Response.t' Lwt.t =
  1257. Logr.err (fun m -> m "%s.%s TODO %a %a" "Ap.Note" "rcv_create" Uri.pp a.obj.actor Uuidm.pp uuid);
  1258. assert (siac.id |> Uri.equal a.obj.actor);
  1259. let _ = do_cache' ~tnow ~base a in
  1260. Ok (`Created, [Http.H.ct_plain], Cgi.Response.body "created")
  1261. |> Lwt.return
  1262. end