OCaml client for the LinkedIn Voyager API
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

json: rename mem -> member / finish -> seal across the codec + value API

Object combinators: [Object.mem] -> [Object.member], [Object.opt_mem]
-> [Object.opt_member], [Object.case_mem] -> [Object.case_member]. The
sibling submodules [Object.Mem] / [Object.Mems] become
[Object.Member] / [Object.Members]. RFC 8259 §4 calls these
"name/value pairs, referred to as the members", so mirror the spec
name rather than the shortened [mem].

[Object.finish] -> [Object.seal]. "Seal" reads as "close the map, no
more members added", which is what the operation does.

Value constructors/queries: [Value.mem] (function) -> [Value.member];
[Value.mem_find] -> [Value.member_key]; [Value.mem_names] ->
[Value.member_names]; [Value.mem_keys] -> [Value.member_keys].
[type mem = ...] -> [type member = ...]; [type object'] still points
at [member list].

Downstream (~80 files across slack, sbom, stripe, sigstore, requests,
claude, irmin, freebox) updated via perl-pie. dune build clean,
dune test ocaml-json clean.

+44 -43
+1 -1
README.md
··· 9 9 macOS. 10 10 11 11 The library uses `requests` for HTTP, `jsont` for JSON encoding/decoding, 12 - `cookeio` for cookie management, and Eio for concurrency. 12 + `cookie` for cookie management, and Eio for concurrency. 13 13 14 14 ## Installation 15 15
+3 -3
dune-project
··· 18 18 "Access LinkedIn profile data and feed posts from OCaml using the Voyager API. Provides typed bindings for profiles and posts.") 19 19 (depends 20 20 (ocaml (>= 5.1)) 21 - (bytesrw (>= 0.1)) 22 - (cookeio (>= 0.1)) 21 + (cookie (>= 0.1)) 23 22 (eio (>= 1.0)) 24 23 (ptime (>= 1.0)) 25 24 (requests (>= 0.1)) ··· 30 29 (kdf (>= 0.1)) 31 30 (uri (>= 4.0)) 32 31 (cmdliner (>= 1.2)) 33 - (alcotest :with-test))) 32 + (alcotest :with-test) 33 + loc))
+8 -8
lib/api.ml
··· 35 35 if n >= 2 && s.[0] = '"' && s.[n - 1] = '"' then String.sub s 1 (n - 2) else s 36 36 37 37 let cookie ~now ~name ~value = 38 - Cookeio.v ~domain:"linkedin.com" ~path:"/" ~name ~value ~secure:true 38 + Cookie.v ~domain:"linkedin.com" ~path:"/" ~name ~value ~secure:true 39 39 ~http_only:true ~creation_time:now ~last_access:now () 40 40 41 41 let v ~sw env ~li_at ~jsessionid = ··· 60 60 in 61 61 (* Add our auth cookies to the session's cookie jar *) 62 62 let jar = Requests.cookies session in 63 - Cookeio_jar.add_cookie jar (cookie ~now ~name:"li_at" ~value:li_at); 64 - Cookeio_jar.add_cookie jar 63 + Cookie_jar.add_cookie jar (cookie ~now ~name:"li_at" ~value:li_at); 64 + Cookie_jar.add_cookie jar 65 65 (cookie ~now ~name:"JSESSIONID" ~value:(Fmt.str "\"%s\"" csrf_token)); 66 66 { session; now } 67 67 ··· 69 69 let jar = Requests.cookies t.session in 70 70 List.iter 71 71 (fun (name, value) -> 72 - Cookeio_jar.add_cookie jar (cookie ~now:t.now ~name ~value)) 72 + Cookie_jar.add_cookie jar (cookie ~now:t.now ~name ~value)) 73 73 cookies 74 74 75 75 let pp ppf _t = Fmt.pf ppf "LinkedIn(session)" ··· 79 79 let get t path = 80 80 try 81 81 let jar = Requests.cookies t.session in 82 - let all = Cookeio_jar.all_cookies jar in 82 + let all = Cookie_jar.all_cookies jar in 83 83 let summary = 84 84 List.map 85 85 (fun c -> 86 - Fmt.str "%s=<%d chars> (domain=%s, path=%s)" (Cookeio.name c) 87 - (String.length (Cookeio.value c)) 88 - (Cookeio.domain c) (Cookeio.path c)) 86 + Fmt.str "%s=<%d chars> (domain=%s, path=%s)" (Cookie.name c) 87 + (String.length (Cookie.value c)) 88 + (Cookie.domain c) (Cookie.path c)) 89 89 all 90 90 in 91 91 Log.debug (fun m ->
+2 -2
lib/dune
··· 9 9 crypto 10 10 kdf.pbkdf 11 11 requests 12 - cookeio 13 - cookeio.jar 12 + cookie 13 + cookie.jar 14 14 ptime 15 15 uri))
+11 -11
lib/post.ml
··· 41 41 num_likes = Option.value ~default:0 num_likes; 42 42 num_comments = Option.value ~default:0 num_comments; 43 43 }) 44 - |> Object.opt_mem "urn" string ~enc:(fun p -> Some p.urn) 45 - |> Object.opt_mem "commentary" string ~enc:(fun p -> Some p.text) 46 - |> Object.opt_mem "authorName" string ~enc:(fun p -> Some p.author_name) 47 - |> Object.opt_mem "createdTime" int ~enc:(fun p -> Some p.created_time) 48 - |> Object.opt_mem "numLikes" int ~enc:(fun p -> Some p.num_likes) 49 - |> Object.opt_mem "numComments" int ~enc:(fun p -> Some p.num_comments) 50 - |> Object.skip_unknown |> Object.finish 44 + |> Object.opt_member "urn" string ~enc:(fun p -> Some p.urn) 45 + |> Object.opt_member "commentary" string ~enc:(fun p -> Some p.text) 46 + |> Object.opt_member "authorName" string ~enc:(fun p -> Some p.author_name) 47 + |> Object.opt_member "createdTime" int ~enc:(fun p -> Some p.created_time) 48 + |> Object.opt_member "numLikes" int ~enc:(fun p -> Some p.num_likes) 49 + |> Object.opt_member "numComments" int ~enc:(fun p -> Some p.num_comments) 50 + |> Object.skip_unknown |> Object.seal 51 51 52 52 let feed_json = 53 53 let open Json.Codec in 54 54 Object.map ~kind:"feed_response" (fun elements -> 55 55 Option.value ~default:[] elements) 56 - |> Object.opt_mem "elements" (list json) ~enc:(fun ps -> Some ps) 57 - |> Object.skip_unknown |> Object.finish 56 + |> Object.opt_member "elements" (list json) ~enc:(fun ps -> Some ps) 57 + |> Object.skip_unknown |> Object.seal 58 58 59 59 (** Codec for a single post from a normalized Voyager API response. Extracts the 60 60 first update from the [included] array that has a [commentary] field. *) ··· 62 62 let open Json.Codec in 63 63 Object.map ~kind:"normalized_post_response" (fun included -> 64 64 match included with Some (p :: _) -> p | _ -> v ~urn:"" ()) 65 - |> Object.opt_mem "included" (list json) ~enc:(fun p -> Some [ p ]) 66 - |> Object.skip_unknown |> Object.finish 65 + |> Object.opt_member "included" (list json) ~enc:(fun p -> Some [ p ]) 66 + |> Object.skip_unknown |> Object.seal
+17 -16
lib/profile.ml
··· 55 55 summary = Option.value ~default:"" summary; 56 56 location = Option.value ~default:"" location; 57 57 }) 58 - |> Object.opt_mem "miniProfile.publicIdentifier" string ~enc:(fun p -> 58 + |> Object.opt_member "miniProfile.publicIdentifier" string ~enc:(fun p -> 59 59 Some p.public_id) 60 - |> Object.opt_mem "miniProfile.entityUrn" string ~enc:(fun p -> 60 + |> Object.opt_member "miniProfile.entityUrn" string ~enc:(fun p -> 61 61 Some p.entity_urn) 62 - |> Object.opt_mem "firstName" string ~enc:(fun p -> Some p.first_name) 63 - |> Object.opt_mem "lastName" string ~enc:(fun p -> Some p.last_name) 64 - |> Object.opt_mem "headline" string ~enc:(fun p -> Some p.headline) 65 - |> Object.opt_mem "summary" string ~enc:(fun p -> Some p.summary) 66 - |> Object.opt_mem "locationName" string ~enc:(fun p -> Some p.location) 67 - |> Object.skip_unknown |> Object.finish 62 + |> Object.opt_member "firstName" string ~enc:(fun p -> Some p.first_name) 63 + |> Object.opt_member "lastName" string ~enc:(fun p -> Some p.last_name) 64 + |> Object.opt_member "headline" string ~enc:(fun p -> Some p.headline) 65 + |> Object.opt_member "summary" string ~enc:(fun p -> Some p.summary) 66 + |> Object.opt_member "locationName" string ~enc:(fun p -> Some p.location) 67 + |> Object.skip_unknown |> Object.seal 68 68 69 69 (** Codec for miniProfile objects found in the [included] array of normalized 70 70 LinkedIn Voyager API responses. Maps [occupation] to [headline]. *) ··· 81 81 summary = ""; 82 82 location = ""; 83 83 }) 84 - |> Object.opt_mem "publicIdentifier" string ~enc:(fun p -> Some p.public_id) 85 - |> Object.opt_mem "entityUrn" string ~enc:(fun p -> Some p.entity_urn) 86 - |> Object.opt_mem "firstName" string ~enc:(fun p -> Some p.first_name) 87 - |> Object.opt_mem "lastName" string ~enc:(fun p -> Some p.last_name) 88 - |> Object.opt_mem "occupation" string ~enc:(fun p -> Some p.headline) 89 - |> Object.skip_unknown |> Object.finish 84 + |> Object.opt_member "publicIdentifier" string ~enc:(fun p -> 85 + Some p.public_id) 86 + |> Object.opt_member "entityUrn" string ~enc:(fun p -> Some p.entity_urn) 87 + |> Object.opt_member "firstName" string ~enc:(fun p -> Some p.first_name) 88 + |> Object.opt_member "lastName" string ~enc:(fun p -> Some p.last_name) 89 + |> Object.opt_member "occupation" string ~enc:(fun p -> Some p.headline) 90 + |> Object.skip_unknown |> Object.seal 90 91 91 92 let empty = 92 93 { ··· 105 106 let open Json.Codec in 106 107 Object.map ~kind:"me_response" (fun included -> 107 108 match included with Some (p :: _) -> p | _ -> empty) 108 - |> Object.opt_mem "included" (list mini_profile_json) ~enc:(fun p -> 109 + |> Object.opt_member "included" (list mini_profile_json) ~enc:(fun p -> 109 110 Some [ p ]) 110 - |> Object.skip_unknown |> Object.finish 111 + |> Object.skip_unknown |> Object.seal
+2 -2
linkedin.opam
··· 10 10 depends: [ 11 11 "dune" {>= "3.21"} 12 12 "ocaml" {>= "5.1"} 13 - "bytesrw" {>= "0.1"} 14 - "cookeio" {>= "0.1"} 13 + "cookie" {>= "0.1"} 15 14 "eio" {>= "1.0"} 16 15 "ptime" {>= "1.0"} 17 16 "requests" {>= "0.1"} ··· 23 22 "uri" {>= "4.0"} 24 23 "cmdliner" {>= "1.2"} 25 24 "alcotest" {with-test} 25 + "loc" 26 26 "odoc" {with-doc} 27 27 ] 28 28 build: [