rfc4287.ml 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. (*
  2. * _ _ ____ _
  3. * _| || |_/ ___| ___ _ __ _ __ ___ | |
  4. * |_ .. _\___ \ / _ \ '_ \| '_ \ / _ \| |
  5. * |_ _|___) | __/ |_) | |_) | (_) |_|
  6. * |_||_| |____/ \___| .__/| .__/ \___/(_)
  7. * |_| |_|
  8. *
  9. * Personal Social Web.
  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 ( >>= ) = Result.bind
  27. let ( let* ) = Result.bind
  28. let defa = Uri.make ~path:"o/p/" ()
  29. let tagu = Uri.make ~path:"o/t/" ()
  30. (** map until the first Error *)
  31. let list f xs =
  32. let it xs x =
  33. let* xs = xs in
  34. let* x = f x in
  35. Ok (List.cons x xs)
  36. in
  37. xs |> List.fold_left it (Ok [])
  38. type single = Single of string
  39. type multi = Multi of string
  40. type rfc4646 = Rfc4646 of string (* bcp47 *)
  41. (* See also Cohttp.Link.Rel *)
  42. module Link = struct
  43. type rel = Rel of single
  44. type t = {
  45. href : Uri.t;
  46. rel : rel option;
  47. title : string option;
  48. rfc7565 : string option; (* Webfinger.Client.t is a cycle *)
  49. }
  50. let self = Rel (Single "self")
  51. let last = Rel (Single "last")
  52. let first = Rel (Single "first")
  53. let next = Rel (Single "next")
  54. let prev = Rel (Single "previous")
  55. let edit = Rel (Single "edit")
  56. let subscribers= Rel (Single "subscribers") (* idiosyncratic *)
  57. let subscribed_to= Rel (Single "subscribed_to") (* idiosyncratic *)
  58. let blocked = Rel (Single "blocked") (* idiosyncratic *)
  59. let inbox = Rel (Single "ap_inbox") (* idiosyncratic *)
  60. let make ?(title = None) ?(rfc7565 = None) ?(rel = None) href = { href; rel; title; rfc7565; }
  61. let encode (r : t) =
  62. let open Csexp in
  63. let str n v l = Atom n :: Atom v :: l in
  64. let uri n v l = l |> str n (v |> Uri.to_string) in
  65. let opt f n v l = match v with | None -> l
  66. | Some u -> l |> f n u in
  67. let sing f n v l = match v with | None -> l
  68. | Some (Rel (Single u)) -> l |> f n u in
  69. List ([]
  70. |> sing str "rel" r.rel
  71. |> opt str "title" r.title
  72. |> opt str "rfc7565" r.rfc7565
  73. |> uri "href" r.href)
  74. let decode s =
  75. let open Csexp in
  76. let rec pairs xs r =
  77. match xs with
  78. | Atom "rel" :: Atom x :: tl -> pairs tl {r with rel=Some (Rel (Single x))}
  79. | Atom "title" :: Atom x :: tl -> pairs tl {r with title=Some x}
  80. | Atom "rfc7033":: Atom x :: tl (* legacy *)
  81. | Atom "rfc7565":: Atom x :: tl -> pairs tl {r with rfc7565=Some x}
  82. | [] -> Ok r
  83. | _ -> Error "unexpected field"
  84. in
  85. match s with
  86. | List ( Atom "href" :: Atom href :: tl ) ->
  87. href
  88. |> Uri.of_string
  89. |> make
  90. |> pairs tl
  91. | _ -> Error "unexpected field"
  92. let to_atom ?(base = Uri.empty) li =
  93. let href = match li.href |> Http.reso ~base |> Uri.to_string with
  94. | "" -> "."
  95. | s -> s in
  96. let attr = [] in
  97. let attr = match li.rfc7565 with
  98. | None -> attr
  99. | Some x -> (("","rfc7565"), x) :: attr in
  100. let attr = match li.title with
  101. | None -> attr
  102. | Some x -> (("","title"), x) :: attr in
  103. let attr = (("","href"), href) :: attr in
  104. let attr = match li.rel with
  105. | None -> attr
  106. | Some Rel Single x -> (("","rel"), x) :: attr in
  107. `El (((Xml.ns_a,"link"),attr),[])
  108. let link ~rfc7565 ~title ~rel ~href : t =
  109. let rel = Some rel in
  110. make ~rfc7565 ~title ~rel href
  111. end
  112. let sep n = `Data ("\n" ^ String.make (2*n) ' ')
  113. module Inreplyto = struct
  114. type t = {
  115. ref_ : Uri.t;
  116. href : Uri.t option;
  117. source : Uri.t option;
  118. type_ : string option;
  119. }
  120. let make ?(href = None) ?(source = None) ?(type_ = None) ref_ = { ref_; href; source; type_; }
  121. let encode (r : t) =
  122. let open Csexp in
  123. let str n v l = Atom n :: Atom v :: l in
  124. let uri n v l = l |> str n (v |> Uri.to_string) in
  125. let opt f n v l = match v with | None -> l
  126. | Some u -> l |> f n u in
  127. List ([]
  128. |> opt str "type" r.type_
  129. |> opt uri "source" r.source
  130. |> opt uri "href" r.href
  131. |> uri "ref" r.ref_)
  132. let decode s =
  133. let open Csexp in
  134. let rec pairs xs r =
  135. match xs with
  136. | Atom "href" :: Atom x :: tl -> pairs tl {r with href=Some (Uri.of_string x)}
  137. | Atom "source":: Atom x :: tl -> pairs tl {r with source=Some (Uri.of_string x)}
  138. | Atom "type" :: Atom x :: tl -> pairs tl {r with type_=Some x}
  139. | [] -> Ok r
  140. | _ -> Error "unexpected field"
  141. in
  142. match s with
  143. | List ( Atom "ref" :: Atom ref_ :: tl ) ->
  144. ref_
  145. |> Uri.of_string
  146. |> make
  147. |> pairs tl
  148. | _ -> Error "unexpected field"
  149. (* https://www.rfc-editor.org/rfc/rfc4685#section-3 *)
  150. let to_xml init (o : t) =
  151. let atts = [ (("","ref"), o.ref_ |> Uri.to_string) ] in
  152. sep 2
  153. :: `El (((Xml.ns_thr,"in-reply-to"), atts),[])
  154. :: init
  155. end
  156. module Category = struct
  157. type label = Label of single
  158. type term = Term of single
  159. type t = label * term * Uri.t
  160. let encode (Label (Single l), Term (Single t), u) =
  161. Csexp.(List [
  162. Atom "label"; Atom l;
  163. Atom "term"; Atom t;
  164. Atom "scheme"; Atom (u |> Uri.to_string)
  165. ])
  166. let decode s =
  167. match s with
  168. | Csexp.(List [
  169. Atom "label"; Atom l;
  170. Atom "term"; Atom t;
  171. Atom "scheme"; Atom u
  172. ]) -> Ok (Label (Single l), Term (Single t), u |> Uri.of_string)
  173. | _ -> Error "expected category but found none"
  174. let to_atom ?(base = Uri.empty) (Label (Single lbl),Term (Single trm),sch) =
  175. let sch = sch |> Http.reso ~base |> Uri.to_string in
  176. `El (((Xml.ns_a,"category"),[
  177. (("","label"),lbl);
  178. (("","term"),trm);
  179. (("","scheme"),sch);
  180. ]),[])
  181. end
  182. type id = Id of string
  183. (* Being "super-careful" https://code.mro.name/mro/ProgrammableWebSwartz2013/src/master/content/pages/2-building-for-users.md
  184. *
  185. * geohash uses a base 32 https://codeberg.org/mro/geohash/src/commit/ed8e71a03e377b472054a3468979a1cd77fc090d/lib/geohash.ml#L73
  186. *
  187. * See also https://opam.ocaml.org/packages/base32/ for int (we need more bits)
  188. *)
  189. module Base24 = struct
  190. open Optint.Int63
  191. let alphabet = Bytes.of_string "23456789abcdefghkrstuxyz"
  192. let base = 24 |> of_int
  193. (* encode the n right chars of x *)
  194. let encode chars x =
  195. let int_to_char i = i |> Bytes.get alphabet in
  196. let rec f i x' b =
  197. match i with
  198. | -1 -> b
  199. | _ ->
  200. rem x' base |> to_int |> int_to_char |> Bytes.set b i;
  201. f (Int.pred i) (div x' base) b
  202. in
  203. chars |> Bytes.create |> f (Int.pred chars) x |> Bytes.to_string
  204. let decode hash =
  205. let int_of_char c =
  206. (* if we want it fast, either do binary search or construct a sparse LUT from chars 0-z -> int *)
  207. match c |> Bytes.index_opt alphabet with
  208. | None -> Error c
  209. | Some i -> Ok i
  210. and len = hash |> String.length in
  211. match len <= 7 with
  212. | false -> Error '_'
  213. | true ->
  214. let rec f idx x =
  215. match len - idx with
  216. | 0 -> Ok x
  217. | _ ->
  218. let* v = hash.[idx] |> int_of_char in
  219. v |> of_int
  220. |> add (mul x base)
  221. |> f (Int.succ idx)
  222. in
  223. f 0 zero
  224. end
  225. let mk_auth ~base a =
  226. let host = Uri.host base |> Option.value ~default:"-" in
  227. let userinfo = Uri.user a |> Option.value ~default:"-" in
  228. let s = Uri.make ~host ~userinfo () |> Uri.to_string in
  229. let le = s |> String.length in
  230. "@" ^ String.sub s 2 (le-2)
  231. (* RFC7565 *)
  232. let mk_auth_acct ~base a =
  233. let host = Uri.host base |> Option.value ~default:"-" in
  234. let userinfo = Uri.user a |> Option.value ~default:"-" in
  235. let s = Uri.make ~host ~userinfo () |> Uri.to_string in
  236. let le = s |> String.length in
  237. "acct:" ^ String.sub s 2 (le-2)
  238. open Xml
  239. let xsl fn_xsl fn_xml =
  240. let x = fn_xml
  241. |> String.split_on_char '/'
  242. |> List.tl
  243. |> List.fold_left (fun a _ -> ".." :: a) ["themes";"current";fn_xsl]
  244. |> String.concat "/" in
  245. Some x
  246. module Person = struct
  247. (* https://www.rfc-editor.org/rfc/rfc4287#section-3.2 *)
  248. type t = {
  249. name : string;
  250. uri : Uri.t option;
  251. email : string option;
  252. }
  253. let empty = ({
  254. name = "";
  255. uri = None;
  256. email = None;
  257. } : t)
  258. let encode e =
  259. let open Csexp in
  260. let v = [] in
  261. let v = match e.uri with
  262. | Some x -> Atom "uri" :: Atom (x |> Uri.to_string) :: v
  263. | None -> v in
  264. let v = match e.email with
  265. | Some x -> Atom "email" :: Atom x :: v
  266. | None -> v in
  267. let v = Atom "name" :: Atom e.name :: v in
  268. List v
  269. let decode s =
  270. let open Csexp in
  271. let rec pairs xs r =
  272. match xs with
  273. | Atom "uri" :: Atom x :: tl -> pairs tl {r with uri=Some (Uri.of_string x)}
  274. | Atom "email":: Atom x :: tl -> pairs tl {r with email=Some x}
  275. | Atom "name" :: Atom x :: tl -> pairs tl {r with name = x}
  276. | [] -> Ok r
  277. | _ -> Error "unexpected field"
  278. in
  279. match s with
  280. | List l -> empty |> pairs l
  281. | Atom uri ->
  282. let ur = uri |> Uri.of_string in
  283. let r = {empty with
  284. name = uri;
  285. uri = Some ur} in
  286. Ok (match Uri.user ur with
  287. | Some us -> {r with name = us}
  288. | None -> r
  289. )
  290. end
  291. (* https://www.rfc-editor.org/rfc/rfc4287#section-4.1.2 *)
  292. module Entry = struct
  293. type t = {
  294. id : Uri.t; (* https://www.rfc-editor.org/rfc/rfc4287#section-4.2.6 *)
  295. in_reply_to: Inreplyto.t list; (* https://www.rfc-editor.org/rfc/rfc4685#section-3 *)
  296. (* assumes an antry has one language for title, tags, content. *)
  297. lang : rfc4646; (* https://www.w3.org/TR/2004/REC-xml-20040204/#sec-lang-tag *)
  298. author : Person.t; (* https://www.rfc-editor.org/rfc/rfc4287#section-4.2.1 *)
  299. title : string; (* https://www.rfc-editor.org/rfc/rfc4287#section-4.2.14 *)
  300. published : Rfc3339.t; (* https://www.rfc-editor.org/rfc/rfc4287#section-4.2.9 *)
  301. updated : Rfc3339.t; (* https://www.rfc-editor.org/rfc/rfc4287#section-4.2.15 *)
  302. sensitive : bool;
  303. links : Link.t list; (* https://www.rfc-editor.org/rfc/rfc4287#section-4.2.7 *)
  304. categories : Category.t list; (* https://www.rfc-editor.org/rfc/rfc4287#section-4.2.2 *)
  305. content : string; (* https://www.rfc-editor.org/rfc/rfc4287#section-4.1.3 *)
  306. }
  307. let compare a b =
  308. a.published |> Rfc3339.compare b.published
  309. let empty =
  310. ({
  311. id = Uri.empty;
  312. in_reply_to = [];
  313. lang = Rfc4646 "nl";
  314. author = Person.empty;
  315. title = "";
  316. published = Rfc3339.epoch;
  317. updated = Rfc3339.epoch;
  318. sensitive = false;
  319. links = [];
  320. categories = [];
  321. content = "";
  322. } : t)
  323. (** inspired by https://code.mro.name/mro/ShaarliGo/src/cb798ebfae17431732e37a94ee80b29bd3b78911/atom.go#L302 *)
  324. let id_make t =
  325. let secs_since_epoch t : Optint.Int63.t =
  326. let (d',ps') = Ptime.epoch |> Ptime.diff t |> Ptime.Span.to_d_ps in
  327. let open Optint.Int63 in
  328. let ( +. ) = add
  329. and ( *. ) = mul
  330. and s = Int64.div ps' 1_000_000_000_000L |> of_int64
  331. and day_s = 24 * 60 * 60 |> of_int
  332. and d' = d' |> of_int in
  333. d' *. day_s +. s
  334. in
  335. let path = t |> secs_since_epoch |> Base24.encode 7 in
  336. Logr.debug (fun m -> m "id_make %s" path);
  337. Uri.make ~path ()
  338. let compare_published_desc a b =
  339. let Rfc3339.T a' = a.published
  340. and Rfc3339.T b' = b.published in
  341. let r = String.compare b' a' in
  342. Logr.debug (fun m -> m "%s.%s %s %s = %d" "Rfc4287" "compare_published_desc" b' a' r);
  343. r
  344. let encode e =
  345. let Rfc4646 lang = e.lang
  346. and Rfc3339.T published = e.published
  347. and Rfc3339.T updated = e.updated
  348. and author = e.author |> Person.encode
  349. in
  350. Csexp.(
  351. List [
  352. Atom "id"; Atom (e.id |> Uri.to_string);
  353. Atom "in-reply-to";List (e.in_reply_to |> List.map Inreplyto.encode);
  354. Atom "lang"; Atom lang;
  355. Atom "title"; Atom e.title;
  356. Atom "author"; author ;
  357. Atom "published"; Atom published;
  358. Atom "updated"; Atom updated;
  359. Atom "links"; List (e.links |> List.map Link.encode);
  360. Atom "categories"; List (e.categories |> List.map Category.encode);
  361. Atom "content"; Atom e.content;
  362. ] )
  363. (* I am unsure if similar to https://opam.ocaml.org/packages/decoders-sexplib/
  364. * could help.
  365. *)
  366. let decode s =
  367. match s with
  368. | Csexp.(List [Atom _; Atom _]) -> Error "deleted"
  369. | Csexp.(List [
  370. Atom "id"; Atom id;
  371. Atom "in-reply-to";List in_reply_to;
  372. Atom "lang"; Atom lang;
  373. Atom "title"; Atom title;
  374. Atom "author"; person;
  375. Atom "published"; Atom published;
  376. Atom "updated"; Atom updated;
  377. Atom "links"; List links;
  378. Atom "categories"; List categories;
  379. Atom "content"; Atom content;
  380. ]) ->
  381. let id = id |> Uri.of_string
  382. and lang = Rfc4646 lang
  383. and author = person |> Person.decode |> Result.fold ~ok:(fun x -> x) ~error:(fun _ -> Person.empty)
  384. and published = Rfc3339.T published
  385. and updated = Rfc3339.T updated
  386. and sensitive = false in
  387. let* in_reply_to = in_reply_to|> list Inreplyto.decode in
  388. let* links = links |> list Link.decode in
  389. let* categories = categories |> list Category.decode in
  390. Ok { id; in_reply_to; lang; author; title; published; updated; sensitive; links; categories; content }
  391. | _ -> Error ("can't decode '" ^ (Csexp.to_string s) ^ "'")
  392. let decode_channel ic =
  393. let* lst = ic |> Csexp.input_many in
  394. let* lst = lst |> list decode in
  395. Ok lst
  396. let one_from_channel ic =
  397. let* r = ic |> Csexp.input in
  398. r |> decode
  399. let from_text_plain ~published ~author ~lang ~uri title content =
  400. Logr.debug (fun m -> m "new note %s\n%s" title content);
  401. let in_reply_to = [] in
  402. let links = [] in
  403. let categories = []
  404. and links = (if uri |> Uri.host |> Option.is_none
  405. then links
  406. else (uri |> Link.make) :: links)
  407. and updated = published
  408. and sensitive = false in
  409. let* t = published |> Rfc3339.to_ptime in
  410. let id = t |> id_make in
  411. (*
  412. * - add attributedTo, id
  413. * - extract microformats (tags, mentions)
  414. * - via and thanks -> link via
  415. * - emojis -> tags
  416. *)
  417. Ok { id; in_reply_to; lang; author; published; updated; sensitive; links; title; categories; content }
  418. let from_channel ?(published = Ptime_clock.now ()) ?(author = Person.empty) ~lang ~tz ic =
  419. Logr.debug (fun m -> m "Rfc4287.from_channel");
  420. let l1 = input_line ic
  421. and buf = Buffer.create 512
  422. and published = published |> Rfc3339.of_ptime ~tz in
  423. let uri = l1 |> Uri.of_string in
  424. let l1,uri = (if uri |> Uri.host |> Option.is_none
  425. then (l1, Uri.empty)
  426. else
  427. let l1 = try
  428. input_line ic
  429. with End_of_file -> "" in
  430. (l1,uri) ) in
  431. (try
  432. while true do
  433. ic
  434. |> input_line
  435. |> Buffer.add_string buf;
  436. Buffer.add_char buf '\n'
  437. done
  438. with End_of_file -> ());
  439. buf
  440. |> Buffer.contents
  441. |> from_text_plain ~published ~author ~lang ~uri l1
  442. let save _ =
  443. (*
  444. * - apend to storage csexp (tag feed
  445. * - update indices (id & url cdbs)
  446. * - recreate recent pages
  447. * - queue subscriber notification (aka followers)
  448. *)
  449. Error ("not implemented yet " ^ __LOC__)
  450. let to_atom ?(attr = []) ~base e : _ Xmlm.frag =
  451. let Rfc4646 lang = e.lang in
  452. let self = e.id |> Http.reso ~base in
  453. let id = self |> Uri.to_string in
  454. let lifo init item = sep 2 :: Link.to_atom ~base item :: init in
  455. let cafo init item = sep 2 :: Category.to_atom ~base item :: init in
  456. let autho =
  457. let uri_to_wf u =
  458. match Uri.user u, Uri.host u with
  459. | Some us, Some ho -> "acct:" ^ us ^ "@" ^ ho
  460. | _ -> ""
  461. in
  462. let ur = e.author.uri |> Option.value ~default:Uri.empty in
  463. sep 3 :: `El (((ns_a,"name"),[]),[`Data e.author.name])
  464. :: sep 3 :: `El (((ns_rfc7033,"uri"),[]),[`Data (uri_to_wf ur)])
  465. :: sep 3 :: `El (((ns_a,"uri"),[]),[`Data (ur |> Uri.to_string)])
  466. :: []
  467. in
  468. let tl = [sep 1] in
  469. let tl = sep 2 :: `El (((ns_a,"content"),[(("","type"),"text")]),[`Data (match e.content with "" -> " " | c -> c)])
  470. :: tl in
  471. let tl = e.categories |> List.fold_left cafo tl in
  472. let tl = e.links |> List.fold_left lifo tl in
  473. let tl = e.in_reply_to |> List.fold_left Inreplyto.to_xml tl in
  474. `El (((ns_a,"entry"),
  475. ((Xmlm.ns_xml,"lang"),lang)
  476. :: attr),
  477. sep 2 :: `El (((ns_a,"id"),[]),[`Data id])
  478. :: sep 2 :: `El (((ns_a,"title"),[(("","type"),"text")]),[`Data e.title])
  479. :: sep 2 :: Rfc3339.to_xml "updated" e.updated
  480. :: sep 2 :: Rfc3339.to_xml "published" e.published
  481. :: sep 2 :: `El (((ns_as,"sensitive"),[]), [`Data (match e.sensitive with | false -> "false" | true -> "true")])
  482. :: sep 2 :: `El (((ns_a,"author"),[]), autho )
  483. :: sep 2 :: (Link.link ~rfc7565:None ~title:None ~href:self ~rel:Link.self |> Link.to_atom)
  484. :: tl
  485. )
  486. let to_atom' ~base e = Ok (to_atom ~base e)
  487. end
  488. module Feed = struct
  489. let compute_links ?(min = 0) ~max ~base (a,b : string * int) =
  490. let j = ["";"-";"/index.xml"] in
  491. let compute_self ~base j v =
  492. let p0 = v |> Make.Jig.paste j |> Option.value ~default:"-" in
  493. let p0 = p0 |> St.before ~suffix:"/index.xml" |> Option.value ~default:"" in
  494. let path = base |> Uri.path in
  495. Uri.with_path base (path ^ p0 ^ "/")
  496. in
  497. let compute_first ~base j v =
  498. let v = match v with
  499. | [x;_] -> [x;"dirt"]
  500. | x -> x in
  501. let p0 = v |> Make.Jig.paste j |> Option.value ~default:"-" in
  502. let p0 = p0 |> St.before ~suffix:"-dirt/index.xml" |> Option.value ~default:"" in
  503. let path = base |> Uri.path in
  504. Uri.with_path base (path ^ p0 ^ "/")
  505. in
  506. let compute_last ~base j v =
  507. let v = match v with
  508. | [x;_] -> [x;"0"]
  509. | x -> x in
  510. let p0 = v |> Make.Jig.paste j |> Option.value ~default:"-" in
  511. let p0 = p0 |> St.before ~suffix:"/index.xml" |> Option.value ~default:"" in
  512. let path = base |> Uri.path in
  513. Uri.with_path base (path ^ p0 ^ "/")
  514. in
  515. let compute_prev ~max ~base j v =
  516. match v with
  517. | [a;b] -> let b = b |> int_of_string in
  518. if b <= max
  519. then
  520. let v = [a;succ b |> string_of_int] in
  521. let p0 = v |> Make.Jig.paste j |> Option.value ~default:"-" in
  522. let p0 = p0 |> St.before ~suffix:"/index.xml" |> Option.value ~default:"" in
  523. let path = base |> Uri.path in
  524. Some (Uri.with_path base (path ^ p0 ^ "/"))
  525. else None
  526. | _ -> None
  527. in
  528. let compute_next ?(min = 0) ~base j v =
  529. match v with
  530. | [a;b] -> let b = b |> int_of_string in
  531. if b > min
  532. then
  533. let v = [a;b |> pred |> string_of_int] in
  534. let p0 = v |> Make.Jig.paste j |> Option.value ~default:"-" in
  535. let p0 = p0 |> St.before ~suffix:"/index.xml" |> Option.value ~default:"" in
  536. let path = base |> Uri.path in
  537. Some (Uri.with_path base (path ^ p0 ^ "/"))
  538. else None
  539. | _ -> None
  540. in
  541. assert (Uri.empty |> Uri.equal base || base |> Uri.to_string |> St.is_suffix ~affix:"/");
  542. let v = [a;b |> string_of_int] in
  543. compute_self ~base j v,
  544. compute_first ~base j v,
  545. compute_last ~base j v,
  546. compute_prev ~max ~base j v,
  547. compute_next ~min ~base j v
  548. let head_to_atom
  549. ~base
  550. ~(self : Uri.t)
  551. ~prev
  552. ~next
  553. ~first
  554. ~last
  555. ~title
  556. ~updated
  557. ~lang
  558. ~(author : Person.t)
  559. (init : _ Xmlm.frag list) : _ Xmlm.frag =
  560. let _ = author in
  561. let id = self |> Http.reso ~base |> Uri.to_string in
  562. let Rfc4646 lang = lang in
  563. let uri_to_page_num u =
  564. let u = u |> Uri.to_string in
  565. try Scanf.sscanf u "%[^-]-%d/" (fun _ num -> Some (string_of_int (succ num)))
  566. with | _ -> None
  567. in
  568. let init = match next with
  569. | None -> init
  570. | Some href -> (Link.link ~rfc7565:None ~title:(uri_to_page_num href) ~href ~rel:Link.next |> Link.to_atom) :: sep 1 :: init in
  571. let init = match prev with
  572. | None -> init
  573. | Some href -> (Link.link ~rfc7565:None ~title:(uri_to_page_num href) ~href ~rel:Link.prev |> Link.to_atom) :: sep 1 :: init in
  574. `El (((ns_a,"feed"),[
  575. ((Xmlm.ns_xmlns,"xmlns"),ns_a);
  576. ((Xmlm.ns_xmlns,"thr"),ns_thr);
  577. ((Xmlm.ns_xmlns,"wf"),ns_rfc7033);
  578. ((Xmlm.ns_xmlns,"as"),ns_as);
  579. ((Xmlm.ns_xml,"lang"),lang);
  580. ((Xmlm.ns_xml,"base"),base |> Uri.to_string);
  581. ]),
  582. sep 1 :: `El (((ns_a,"id"),[]),[`Data id])
  583. :: sep 1 :: `El (((ns_a,"title"),[(("","type"),"text")]),[`Data title])
  584. :: sep 1 :: Rfc3339.to_xml "updated" updated
  585. :: sep 1 :: `El (((ns_a,"generator"),[ (("","uri"),St.seppo_s) ]),
  586. `Data St.seppo_c :: [] )
  587. :: sep 1 :: (Link.link ~rfc7565:None ~title:(uri_to_page_num self) ~href:self ~rel:Link.self |> Link.to_atom)
  588. :: sep 1 :: (Link.link ~rfc7565:None ~title:(Some "last") ~href:first ~rel:Link.first |> Link.to_atom)
  589. :: sep 1 :: (Link.link ~rfc7565:None ~title:(Some "1") ~href:last ~rel:Link.last |> Link.to_atom)
  590. :: sep 1 :: init)
  591. let to_atom
  592. ~base
  593. ~self
  594. ~prev
  595. ~next
  596. ~first
  597. ~last
  598. ~title
  599. ~updated
  600. ~lang
  601. ~(author : Person.t)
  602. entries : _ Xmlm.frag =
  603. let entry init item = Entry.to_atom ~base item :: sep 1 :: init in
  604. entries |> List.fold_left entry []
  605. |> head_to_atom
  606. ~base
  607. ~self
  608. ~prev
  609. ~next
  610. ~first
  611. ~last
  612. ~title
  613. ~updated
  614. ~lang
  615. ~author
  616. let to_atom_
  617. ~base
  618. ~self
  619. ~prev
  620. ~next
  621. ~first
  622. ~last
  623. ~title
  624. ~updated
  625. ~lang
  626. ~(author : Person.t)
  627. _dst (es : (Entry.t,string) result list) : _ Xmlm.frag =
  628. es |> List.fold_left (fun a e ->
  629. match e with
  630. | Error e ->
  631. Logr.warn (fun m -> m "%s.%s ignore broken entry: %s" "Rfc4287.Feed" "to_atom_" e);
  632. a
  633. | Ok e -> e :: a) []
  634. |> to_atom
  635. ~base
  636. ~self
  637. ~prev
  638. ~next
  639. ~first
  640. ~last
  641. ~title
  642. ~updated
  643. ~lang
  644. ~author
  645. let to_file fn (x : _ Xmlm.frag) =
  646. let xsl = fn |> xsl "posts.xsl" in
  647. fn |> File.out_channel_replace (Xml.to_chan ~xsl x);
  648. Ok fn
  649. end