OCaml CLI and library to the Karakeep bookmarking app

port to eio/requests

+1892 -2155
+1
.gitignore
··· 2 2 karakeep-src 3 3 .karakeep-api 4 4 .*.swp 5 + ocaml-requests
+7 -4
dune-project
··· 13 13 (description "OCaml client library for the Karakeep bookmark service API") 14 14 (depends 15 15 (ocaml (>= "5.2.0")) 16 - ezjsonm 17 - lwt 18 - cohttp-lwt-unix 16 + karakeep-proto 17 + requests 18 + eio 19 + jsont 20 + bytesrw 19 21 ptime 20 - fmt)) 22 + fmt 23 + uri))
+6 -3
karakeep.opam
··· 8 8 depends: [ 9 9 "dune" {>= "3.17"} 10 10 "ocaml" {>= "5.2.0"} 11 - "ezjsonm" 12 - "lwt" 13 - "cohttp-lwt-unix" 11 + "karakeep-proto" 12 + "requests" 13 + "eio" 14 + "jsont" 15 + "bytesrw" 14 16 "ptime" 15 17 "fmt" 18 + "uri" 16 19 "odoc" {with-doc} 17 20 ] 18 21 build: [
+1 -1
lib/dune
··· 1 1 (library 2 2 (name karakeep) 3 3 (public_name karakeep) 4 - (libraries lwt cohttp cohttp-lwt-unix ezjsonm fmt ptime)) 4 + (libraries karakeep.proto requests eio jsont jsont.bytesrw ptime fmt))
+296 -1247
lib/karakeep.ml
··· 1 1 (** Karakeep API client implementation *) 2 2 3 - open Lwt.Infix 4 - module J = Ezjsonm 3 + include Karakeep_proto 5 4 6 - (** {1 Core Types} *) 5 + (** {1 Error Handling} *) 7 6 8 - type asset_id = string 9 - (** Asset identifier type *) 7 + type error = 8 + | Api_error of { status : int; code : string; message : string } 9 + | Json_error of { reason : string } 10 10 11 - type bookmark_id = string 12 - (** Bookmark identifier type *) 11 + type Eio.Exn.err += E of error 13 12 14 - type list_id = string 15 - (** List identifier type *) 13 + let err e = Eio.Exn.create (E e) 16 14 17 - type tag_id = string 18 - (** Tag identifier type *) 15 + let is_api_error = function Api_error _ -> true | _ -> false 19 16 20 - type highlight_id = string 21 - (** Highlight identifier type *) 17 + let is_not_found = function 18 + | Api_error { status = 404; _ } -> true 19 + | _ -> false 22 20 23 - (** Type of content a bookmark can have *) 24 - type bookmark_content_type = 25 - | Link (** A URL to a webpage *) 26 - | Text (** Plain text content *) 27 - | Asset (** An attached asset (image, PDF, etc.) *) 28 - | Unknown (** Unknown content type *) 29 - 30 - (** Type of asset *) 31 - type asset_type = 32 - | Screenshot (** Screenshot of a webpage *) 33 - | AssetScreenshot (** Screenshot of an asset *) 34 - | BannerImage (** Banner image *) 35 - | FullPageArchive (** Archive of a full webpage *) 36 - | Video (** Video asset *) 37 - | BookmarkAsset (** Generic bookmark asset *) 38 - | PrecrawledArchive (** Pre-crawled archive *) 39 - | Unknown (** Unknown asset type *) 21 + let error_to_string = function 22 + | Api_error { status; code; message } -> 23 + Printf.sprintf "API error %d (%s): %s" status code message 24 + | Json_error { reason } -> Printf.sprintf "JSON error: %s" reason 40 25 41 - (** Type of tagging status *) 42 - type tagging_status = 43 - | Success (** Tagging was successful *) 44 - | Failure (** Tagging failed *) 45 - | Pending (** Tagging is pending *) 46 - 47 - (** Type of bookmark list *) 48 - type list_type = 49 - | Manual (** List is manually managed *) 50 - | Smart (** List is dynamically generated based on a query *) 51 - 52 - (** Highlight color *) 53 - type highlight_color = 54 - | Yellow (** Yellow highlight *) 55 - | Red (** Red highlight *) 56 - | Green (** Green highlight *) 57 - | Blue (** Blue highlight *) 58 - 59 - (** Type of how a tag was attached *) 60 - type tag_attachment_type = 61 - | AI (** Tag was attached by AI *) 62 - | Human (** Tag was attached by a human *) 63 - 64 - type link_content = { 65 - url : string; (** The URL of the bookmarked page *) 66 - title : string option; (** Title from the link *) 67 - description : string option; (** Description from the link *) 68 - image_url : string option; (** URL of an image from the link *) 69 - image_asset_id : asset_id option; (** ID of an image asset *) 70 - screenshot_asset_id : asset_id option; (** ID of a screenshot asset *) 71 - full_page_archive_asset_id : asset_id option; 72 - (** ID of a full page archive asset *) 73 - precrawled_archive_asset_id : asset_id option; 74 - (** ID of a pre-crawled archive asset *) 75 - video_asset_id : asset_id option; (** ID of a video asset *) 76 - favicon : string option; (** URL of the favicon *) 77 - html_content : string option; (** HTML content of the page *) 78 - crawled_at : Ptime.t option; (** When the page was crawled *) 79 - author : string option; (** Author of the content *) 80 - publisher : string option; (** Publisher of the content *) 81 - date_published : Ptime.t option; (** When the content was published *) 82 - date_modified : Ptime.t option; (** When the content was last modified *) 83 - } 84 - (** Link content for a bookmark *) 85 - 86 - type text_content = { 87 - text : string; (** The text content *) 88 - source_url : string option; (** Optional source URL for the text *) 89 - } 90 - (** Text content for a bookmark *) 91 - 92 - type asset_content = { 93 - asset_type : [ `Image | `PDF ]; (** Type of the asset *) 94 - asset_id : asset_id; (** ID of the asset *) 95 - file_name : string option; (** Name of the file *) 96 - source_url : string option; (** Source URL for the asset *) 97 - size : int option; (** Size of the asset in bytes *) 98 - content : string option; (** Extracted content from the asset *) 99 - } 100 - (** Asset content for a bookmark *) 101 - 102 - (** Content of a bookmark *) 103 - type content = 104 - | Link of link_content (** Link-type content *) 105 - | Text of text_content (** Text-type content *) 106 - | Asset of asset_content (** Asset-type content *) 107 - | Unknown (** Unknown content type *) 108 - 109 - type asset = { 110 - id : asset_id; (** ID of the asset *) 111 - asset_type : asset_type; (** Type of the asset *) 112 - } 113 - (** Asset attached to a bookmark *) 114 - 115 - type bookmark_tag = { 116 - id : tag_id; (** ID of the tag *) 117 - name : string; (** Name of the tag *) 118 - attached_by : tag_attachment_type; (** How the tag was attached *) 119 - } 120 - (** Tag with attachment information *) 121 - 122 - type bookmark = { 123 - id : bookmark_id; (** Unique identifier for the bookmark *) 124 - created_at : Ptime.t; (** Timestamp when the bookmark was created *) 125 - modified_at : Ptime.t option; (** Optional timestamp of the last update *) 126 - title : string option; (** Optional title of the bookmarked page *) 127 - archived : bool; (** Whether the bookmark is archived *) 128 - favourited : bool; (** Whether the bookmark is marked as a favorite *) 129 - tagging_status : tagging_status option; (** Status of automatic tagging *) 130 - note : string option; (** Optional user note associated with the bookmark *) 131 - summary : string option; (** Optional AI-generated summary *) 132 - tags : bookmark_tag list; (** Tags associated with the bookmark *) 133 - content : content; (** Content of the bookmark *) 134 - assets : asset list; (** Assets attached to the bookmark *) 135 - } 136 - (** A bookmark from the Karakeep service *) 137 - 138 - type paginated_bookmarks = { 139 - bookmarks : bookmark list; (** List of bookmarks in the current page *) 140 - next_cursor : string option; (** Optional cursor for fetching the next page *) 141 - } 142 - (** Paginated response of bookmarks *) 143 - 144 - type _list = { 145 - id : list_id; (** ID of the list *) 146 - name : string; (** Name of the list *) 147 - description : string option; (** Optional description of the list *) 148 - icon : string; (** Icon for the list *) 149 - parent_id : list_id option; (** Optional parent list ID *) 150 - list_type : list_type; (** Type of the list *) 151 - query : string option; (** Optional query for smart lists *) 152 - } 153 - (** List in Karakeep *) 154 - 155 - type tag = { 156 - id : tag_id; (** ID of the tag *) 157 - name : string; (** Name of the tag *) 158 - num_bookmarks : int; (** Number of bookmarks with this tag *) 159 - num_bookmarks_by_attached_type : (tag_attachment_type * int) list; 160 - (** Number of bookmarks by attachment type *) 161 - } 162 - (** Tag in Karakeep *) 163 - 164 - type highlight = { 165 - bookmark_id : bookmark_id; (** ID of the bookmark *) 166 - start_offset : int; (** Start position of the highlight *) 167 - end_offset : int; (** End position of the highlight *) 168 - color : highlight_color; (** Color of the highlight *) 169 - text : string option; (** Text of the highlight *) 170 - note : string option; (** Note for the highlight *) 171 - id : highlight_id; (** ID of the highlight *) 172 - user_id : string; (** ID of the user who created the highlight *) 173 - created_at : Ptime.t; (** When the highlight was created *) 174 - } 175 - (** Highlight in Karakeep *) 176 - 177 - type paginated_highlights = { 178 - highlights : highlight list; (** List of highlights in the current page *) 179 - next_cursor : string option; (** Optional cursor for fetching the next page *) 180 - } 181 - (** Paginated response of highlights *) 182 - 183 - type user_info = { 184 - id : string; (** ID of the user *) 185 - name : string option; (** Name of the user *) 186 - email : string option; (** Email of the user *) 187 - } 188 - (** User information *) 26 + let pp_error fmt e = Format.pp_print_string fmt (error_to_string e) 189 27 190 - type user_stats = { 191 - num_bookmarks : int; (** Number of bookmarks *) 192 - num_favorites : int; (** Number of favorite bookmarks *) 193 - num_archived : int; (** Number of archived bookmarks *) 194 - num_tags : int; (** Number of tags *) 195 - num_lists : int; (** Number of lists *) 196 - num_highlights : int; (** Number of highlights *) 197 - } 198 - (** User statistics *) 28 + (** {1 Client} *) 199 29 200 - type error_response = { 201 - code : string; (** Error code *) 202 - message : string; (** Error message *) 30 + type t = { 31 + session : Requests.t; 32 + base_url : string; 203 33 } 204 - (** Error response from the API *) 205 34 206 - (** {1 Helper Functions} *) 207 - 208 - (** Parse a date string to Ptime.t, defaulting to epoch if invalid *) 209 - let parse_date str = 210 - match Ptime.of_rfc3339 str with 211 - | Ok (date, _, _) -> date 212 - | Error _ -> ( 213 - Fmt.epr "Warning: could not parse date '%s'\n" str; 214 - (* Default to epoch time *) 215 - let span_opt = Ptime.Span.of_d_ps (0, 0L) in 216 - match span_opt with 217 - | None -> failwith "Internal error: couldn't create epoch time span" 218 - | Some span -> ( 219 - match Ptime.of_span span with 220 - | Some t -> t 221 - | None -> failwith "Internal error: couldn't create epoch time")) 222 - 223 - (** Extract a string field from JSON, returns None if not present or not a 224 - string *) 225 - let get_string_opt json path = 226 - try Some (J.find json path |> J.get_string) with _ -> None 227 - 228 - (** Extract an int field from JSON, returns None if not present or not an int *) 229 - let get_int_opt json path = 230 - try Some (J.find json path |> J.get_int) with _ -> None 231 - 232 - (** Extract a date field from JSON, returns None if not present or invalid *) 233 - let get_date_opt json path = 234 - match get_string_opt json path with 235 - | Some date_str -> ( try Some (parse_date date_str) with _ -> None) 236 - | None -> None 237 - 238 - (** Extract a boolean field from JSON, with default value *) 239 - let get_bool_def json path default = 240 - try J.find json path |> J.get_bool with _ -> default 241 - 242 - (** Convert string to asset_type *) 243 - let asset_type_of_string = function 244 - | "screenshot" -> Screenshot 245 - | "assetScreenshot" -> AssetScreenshot 246 - | "bannerImage" -> BannerImage 247 - | "fullPageArchive" -> FullPageArchive 248 - | "video" -> Video 249 - | "bookmarkAsset" -> BookmarkAsset 250 - | "precrawledArchive" -> PrecrawledArchive 251 - | _ -> Unknown 252 - 253 - (** Convert asset_type to string *) 254 - let string_of_asset_type = function 255 - | Screenshot -> "screenshot" 256 - | AssetScreenshot -> "assetScreenshot" 257 - | BannerImage -> "bannerImage" 258 - | FullPageArchive -> "fullPageArchive" 259 - | Video -> "video" 260 - | BookmarkAsset -> "bookmarkAsset" 261 - | PrecrawledArchive -> "precrawledArchive" 262 - | Unknown -> "unknown" 263 - 264 - (** Convert string to tagging_status *) 265 - let tagging_status_of_string = function 266 - | "success" -> Success 267 - | "failure" -> Failure 268 - | "pending" -> Pending 269 - | _ -> Success (* Default to success if unknown *) 270 - 271 - (** Convert tagging_status to string *) 272 - let string_of_tagging_status = function 273 - | Success -> "success" 274 - | Failure -> "failure" 275 - | Pending -> "pending" 276 - 277 - (** Convert string to list_type *) 278 - let list_type_of_string = function 279 - | "manual" -> Manual 280 - | "smart" -> Smart 281 - | _ -> Manual (* Default to manual if unknown *) 282 - 283 - (** Convert list_type to string *) 284 - let string_of_list_type = function Manual -> "manual" | Smart -> "smart" 285 - 286 - (** Convert string to highlight_color *) 287 - let highlight_color_of_string = function 288 - | "yellow" -> Yellow 289 - | "red" -> Red 290 - | "green" -> Green 291 - | "blue" -> Blue 292 - | _ -> Yellow (* Default to yellow if unknown *) 293 - 294 - (** Convert highlight_color to string *) 295 - let string_of_highlight_color = function 296 - | Yellow -> "yellow" 297 - | Red -> "red" 298 - | Green -> "green" 299 - | Blue -> "blue" 300 - 301 - (** Convert string to tag_attachment_type *) 302 - let tag_attachment_type_of_string = function 303 - | "ai" -> AI 304 - | "human" -> Human 305 - | _ -> Human (* Default to human if unknown *) 306 - 307 - (** Convert tag_attachment_type to string *) 308 - let string_of_tag_attachment_type = function AI -> "ai" | Human -> "human" 309 - 310 - (** Parse link content from JSON *) 311 - let parse_link_content json = 312 - { 313 - url = J.find json [ "url" ] |> J.get_string; 314 - title = get_string_opt json [ "title" ]; 315 - description = get_string_opt json [ "description" ]; 316 - image_url = get_string_opt json [ "imageUrl" ]; 317 - image_asset_id = get_string_opt json [ "imageAssetId" ]; 318 - screenshot_asset_id = get_string_opt json [ "screenshotAssetId" ]; 319 - full_page_archive_asset_id = 320 - get_string_opt json [ "fullPageArchiveAssetId" ]; 321 - precrawled_archive_asset_id = 322 - get_string_opt json [ "precrawledArchiveAssetId" ]; 323 - video_asset_id = get_string_opt json [ "videoAssetId" ]; 324 - favicon = get_string_opt json [ "favicon" ]; 325 - html_content = get_string_opt json [ "htmlContent" ]; 326 - crawled_at = get_date_opt json [ "crawledAt" ]; 327 - author = get_string_opt json [ "author" ]; 328 - publisher = get_string_opt json [ "publisher" ]; 329 - date_published = get_date_opt json [ "datePublished" ]; 330 - date_modified = get_date_opt json [ "dateModified" ]; 331 - } 332 - 333 - (** Parse text content from JSON *) 334 - let parse_text_content json = 335 - { 336 - text = J.find json [ "text" ] |> J.get_string; 337 - source_url = get_string_opt json [ "sourceUrl" ]; 338 - } 339 - 340 - (** Parse asset content from JSON *) 341 - let parse_asset_content json = 342 - let asset_type_str = get_string_opt json [ "assetType" ] in 343 - let asset_type = 344 - match asset_type_str with 345 - | Some "image" -> `Image 346 - | Some "pdf" -> `PDF 347 - | _ -> `Image (* Default to image if unknown *) 348 - in 349 - { 350 - asset_type; 351 - asset_id = J.find json [ "assetId" ] |> J.get_string; 352 - file_name = get_string_opt json [ "fileName" ]; 353 - source_url = get_string_opt json [ "sourceUrl" ]; 354 - size = get_int_opt json [ "size" ]; 355 - content = get_string_opt json [ "content" ]; 356 - } 357 - 358 - (** Parse content from JSON *) 359 - let parse_content json = 360 - let content_type = get_string_opt json [ "type" ] in 361 - match content_type with 362 - | Some "link" -> Link (parse_link_content json) 363 - | Some "text" -> Text (parse_text_content json) 364 - | Some "asset" -> Asset (parse_asset_content json) 365 - | _ -> Unknown 366 - 367 - (** Extract a meaningful title from bookmark content *) 368 - let title = function 369 - | Link lc -> 370 - begin match lc.title with 371 - | Some t when String.trim t <> "" -> t 372 - | _ -> lc.url 373 - end 374 - | Text tc -> 375 - let text_excerpt = 376 - let s = tc.text in 377 - let max_len = 40 in 378 - if String.length s <= max_len then s 379 - else String.sub s 0 max_len ^ "..." 380 - in 381 - begin match tc.source_url with 382 - | Some url -> "Text from " ^ url 383 - | None -> "Text: " ^ text_excerpt 384 - end 385 - | Asset ac -> 386 - begin match ac.file_name with 387 - | Some fn -> fn 388 - | None -> 389 - begin match ac.source_url with 390 - | Some url -> "Asset from " ^ url 391 - | None -> 392 - match ac.asset_type with 393 - | `Image -> "Image asset" 394 - | `PDF -> "PDF asset" 395 - end 396 - end 397 - | Unknown -> "Unnamed bookmark" 398 - 399 - (** Parse asset from JSON *) 400 - let parse_asset json = 401 - { 402 - id = J.find json [ "id" ] |> J.get_string; 403 - asset_type = 404 - J.find json [ "assetType" ] |> J.get_string |> asset_type_of_string; 405 - } 406 - 407 - (** Parse bookmark tag from JSON *) 408 - let parse_bookmark_tag json = 409 - { 410 - id = J.find json [ "id" ] |> J.get_string; 411 - name = J.find json [ "name" ] |> J.get_string; 412 - attached_by = 413 - get_string_opt json [ "attachedBy" ] 414 - |> Option.value ~default:"human" 415 - |> tag_attachment_type_of_string; 416 - } 417 - 418 - (** Parse a bookmark from JSON *) 419 - let parse_bookmark json = 420 - let id = J.find json [ "id" ] |> J.get_string in 421 - let created_at = J.find json [ "createdAt" ] |> J.get_string |> parse_date in 422 - let tags = 423 - try 424 - let tags_json = J.find json [ "tags" ] in 425 - J.get_list parse_bookmark_tag tags_json 426 - with _ -> [] 35 + let create ~sw ~env ~base_url ~api_key = 36 + let session = Requests.create ~sw env in 37 + let session = 38 + Requests.set_auth session (Requests.Auth.bearer ~token:api_key) 427 39 in 428 - let assets = 429 - try 430 - let assets_json = J.find json [ "assets" ] in 431 - J.get_list parse_asset assets_json 432 - with _ -> [] 433 - in 434 - let tagging_status = 435 - get_string_opt json [ "taggingStatus" ] 436 - |> Option.map tagging_status_of_string 437 - in 438 - { 439 - id; 440 - created_at; 441 - modified_at = get_date_opt json [ "modifiedAt" ]; 442 - title = get_string_opt json [ "title" ]; 443 - archived = get_bool_def json [ "archived" ] false; 444 - favourited = get_bool_def json [ "favourited" ] false; 445 - tagging_status; 446 - note = get_string_opt json [ "note" ]; 447 - summary = get_string_opt json [ "summary" ]; 448 - tags; 449 - content = 450 - (try parse_content (J.find json [ "content" ]) with _ -> Unknown); 451 - assets; 452 - } 40 + { session; base_url } 453 41 454 - (** Get the best available title for a bookmark *) 455 - let bookmark_title bookmark = 456 - match bookmark.title with 457 - | Some t when String.trim t <> "" -> t 458 - | _ -> title bookmark.content 42 + (** {1 Internal Helpers} *) 459 43 460 - (** Parse paginated bookmarks from JSON *) 461 - let parse_paginated_bookmarks json = 462 - let bookmarks = 463 - try 464 - let bookmarks_json = J.find json [ "bookmarks" ] in 465 - J.get_list parse_bookmark bookmarks_json 466 - with _ -> ( 467 - try 468 - let bookmarks_json = J.find json [ "data" ] in 469 - J.get_list parse_bookmark bookmarks_json 470 - with _ -> []) 44 + let ( / ) base path = 45 + let base = 46 + if String.ends_with ~suffix:"/" base then 47 + String.sub base 0 (String.length base - 1) 48 + else base 471 49 in 472 - let next_cursor = get_string_opt json [ "nextCursor" ] in 473 - { bookmarks; next_cursor } 474 - 475 - (** Parse tag from JSON *) 476 - let parse_tag json = 477 - let attachment_types = 478 - try 479 - let stats_json = J.find json [ "numBookmarksByAttachedType" ] in 480 - match stats_json with 481 - | `O fields -> 482 - List.map 483 - (fun (k, v) -> (tag_attachment_type_of_string k, J.get_int v)) 484 - fields 485 - | _ -> [] 486 - with _ -> [] 50 + let path = 51 + if String.starts_with ~prefix:"/" path then 52 + String.sub path 1 (String.length path - 1) 53 + else path 487 54 in 488 - { 489 - id = J.find json [ "id" ] |> J.get_string; 490 - name = J.find json [ "name" ] |> J.get_string; 491 - num_bookmarks = 492 - get_int_opt json [ "numBookmarks" ] |> Option.value ~default:0; 493 - num_bookmarks_by_attached_type = attachment_types; 494 - } 495 - 496 - (** Parse list from JSON *) 497 - let parse_list json = 498 - { 499 - id = J.find json [ "id" ] |> J.get_string; 500 - name = J.find json [ "name" ] |> J.get_string; 501 - description = get_string_opt json [ "description" ]; 502 - icon = get_string_opt json [ "icon" ] |> Option.value ~default:"default"; 503 - parent_id = get_string_opt json [ "parentId" ]; 504 - list_type = 505 - get_string_opt json [ "listType" ] 506 - |> Option.value ~default:"manual" 507 - |> list_type_of_string; 508 - query = get_string_opt json [ "query" ]; 509 - } 510 - 511 - (** Parse highlight from JSON *) 512 - let parse_highlight json = 513 - { 514 - bookmark_id = J.find json [ "bookmarkId" ] |> J.get_string; 515 - start_offset = J.find json [ "startOffset" ] |> J.get_int; 516 - end_offset = J.find json [ "endOffset" ] |> J.get_int; 517 - color = 518 - get_string_opt json [ "color" ] 519 - |> Option.value ~default:"yellow" 520 - |> highlight_color_of_string; 521 - text = get_string_opt json [ "text" ]; 522 - note = get_string_opt json [ "note" ]; 523 - id = J.find json [ "id" ] |> J.get_string; 524 - user_id = J.find json [ "userId" ] |> J.get_string; 525 - created_at = J.find json [ "createdAt" ] |> J.get_string |> parse_date; 526 - } 55 + base ^ "/" ^ path 527 56 528 - (** Parse paginated highlights from JSON *) 529 - let parse_paginated_highlights json = 530 - let highlights = 531 - try 532 - let highlights_json = J.find json [ "highlights" ] in 533 - J.get_list parse_highlight highlights_json 534 - with _ -> [] 535 - in 536 - let next_cursor = get_string_opt json [ "nextCursor" ] in 537 - { highlights; next_cursor } 57 + let query_string params = 58 + match params with 59 + | [] -> "" 60 + | _ -> 61 + "?" 62 + ^ String.concat "&" 63 + (List.map (fun (k, v) -> Uri.pct_encode k ^ "=" ^ Uri.pct_encode v) params) 538 64 539 - (** Parse user info from JSON *) 540 - let parse_user_info json = 541 - { 542 - id = J.find json [ "id" ] |> J.get_string; 543 - name = get_string_opt json [ "name" ]; 544 - email = get_string_opt json [ "email" ]; 545 - } 65 + let decode_json codec body_str = 66 + match Jsont_bytesrw.decode_string' codec body_str with 67 + | Ok v -> v 68 + | Error e -> 69 + raise (err (Json_error { reason = Jsont.Error.to_string e })) 546 70 547 - (** Parse user stats from JSON *) 548 - let parse_user_stats json = 549 - { 550 - num_bookmarks = 551 - get_int_opt json [ "numBookmarks" ] |> Option.value ~default:0; 552 - num_favorites = 553 - get_int_opt json [ "numFavorites" ] |> Option.value ~default:0; 554 - num_archived = get_int_opt json [ "numArchived" ] |> Option.value ~default:0; 555 - num_tags = get_int_opt json [ "numTags" ] |> Option.value ~default:0; 556 - num_lists = get_int_opt json [ "numLists" ] |> Option.value ~default:0; 557 - num_highlights = 558 - get_int_opt json [ "numHighlights" ] |> Option.value ~default:0; 559 - } 71 + let encode_json codec value = 72 + match Jsont_bytesrw.encode_string' codec value with 73 + | Ok s -> s 74 + | Error e -> 75 + raise (err (Json_error { reason = Jsont.Error.to_string e })) 560 76 561 - (** Parse error response from JSON *) 562 - let parse_error_response json = 563 - { 564 - code = 565 - get_string_opt json [ "code" ] |> Option.value ~default:"unknown_error"; 566 - message = 567 - get_string_opt json [ "message" ] |> Option.value ~default:"Unknown error"; 568 - } 77 + let handle_error_response status body = 78 + match Jsont_bytesrw.decode_string' error_response_jsont body with 79 + | Ok err_resp -> 80 + raise (err (Api_error { status; code = err_resp.code; message = err_resp.message })) 81 + | Error _ -> 82 + raise (err (Api_error { status; code = "unknown"; message = body })) 569 83 570 - (** Helper function to consume and return response body data *) 571 - let consume_body body = 572 - Cohttp_lwt.Body.to_string body >>= fun _ -> Lwt.return_unit 84 + let get_json t url codec = 85 + let response = Requests.get t.session url in 86 + let body = Requests.Response.text response in 87 + if not (Requests.Response.ok response) then 88 + handle_error_response (Requests.Response.status_code response) body; 89 + decode_json codec body 573 90 574 - (** Helper function to make API requests *) 575 - let make_request ~api_key ~method_ ?(headers = []) ?(body = None) url = 576 - let open Cohttp_lwt_unix in 577 - let uri = Uri.of_string url in 91 + let post_json t url req_codec req_value resp_codec = 92 + let body_str = encode_json req_codec req_value in 93 + let body = Requests.Body.of_string Requests.Mime.json body_str in 94 + let response = Requests.post t.session url ~body in 95 + let resp_body = Requests.Response.text response in 96 + if not (Requests.Response.ok response) then 97 + handle_error_response (Requests.Response.status_code response) resp_body; 98 + decode_json resp_codec resp_body 578 99 579 - (* Set up headers with API key *) 580 - let base_headers = [ ("Authorization", "Bearer " ^ api_key) ] in 581 - let all_headers = base_headers @ headers in 582 - let headers = Cohttp.Header.of_list all_headers in 100 + let post_json_no_body t url resp_codec = 101 + let response = Requests.post t.session url in 102 + let resp_body = Requests.Response.text response in 103 + if not (Requests.Response.ok response) then 104 + handle_error_response (Requests.Response.status_code response) resp_body; 105 + decode_json resp_codec resp_body 583 106 584 - let body_opt = 585 - match body with 586 - | Some content -> Some (Cohttp_lwt.Body.of_string content) 587 - | None -> None 588 - in 107 + let patch_json t url req_codec req_value resp_codec = 108 + let body_str = encode_json req_codec req_value in 109 + let body = Requests.Body.of_string Requests.Mime.json body_str in 110 + let response = Requests.patch t.session url ~body in 111 + let resp_body = Requests.Response.text response in 112 + if not (Requests.Response.ok response) then 113 + handle_error_response (Requests.Response.status_code response) resp_body; 114 + decode_json resp_codec resp_body 589 115 590 - match method_ with 591 - | `GET -> Client.get ~headers uri 592 - | `POST -> Client.post ~headers ?body:body_opt uri 593 - | `PUT -> Client.put ~headers ?body:body_opt uri 594 - | `DELETE -> Client.delete ~headers ?body:body_opt uri 116 + let delete_json t url = 117 + let response = Requests.delete t.session url in 118 + let resp_body = Requests.Response.text response in 119 + if not (Requests.Response.ok response) then 120 + handle_error_response (Requests.Response.status_code response) resp_body 595 121 596 - (** Process API response *) 597 - let process_response resp body parse_fn = 598 - let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in 599 - if status_code >= 200 && status_code < 300 then 600 - Cohttp_lwt.Body.to_string body >>= fun body_str -> 601 - try 602 - let json = J.from_string body_str in 603 - Lwt.return (parse_fn json) 604 - with e -> 605 - Lwt.fail_with 606 - ("Failed to parse response: " ^ Printexc.to_string e 607 - ^ "\nResponse body: " ^ body_str) 608 - else 609 - Cohttp_lwt.Body.to_string body >>= fun body_str -> 610 - let error_msg = 611 - try 612 - let json = J.from_string body_str in 613 - let error_resp = parse_error_response json in 614 - Printf.sprintf "API Error (%s): %s" error_resp.code error_resp.message 615 - with _ -> Printf.sprintf "HTTP error %d: %s" status_code body_str 616 - in 617 - Lwt.fail_with error_msg 122 + let put_json t url req_codec req_value = 123 + let body_str = encode_json req_codec req_value in 124 + let body = Requests.Body.of_string Requests.Mime.json body_str in 125 + let response = Requests.put t.session url ~body in 126 + let resp_body = Requests.Response.text response in 127 + if not (Requests.Response.ok response) then 128 + handle_error_response (Requests.Response.status_code response) resp_body; 129 + resp_body 618 130 619 131 (** {1 Bookmark Operations} *) 620 132 621 - let fetch_bookmarks ~api_key ?limit ?cursor ?include_content ?archived 622 - ?favourited base_url = 623 - (* Build query parameters *) 133 + let fetch_bookmarks t ?limit ?cursor ?include_content ?archived ?favourited () = 624 134 let params = [] in 625 135 let params = 626 - match limit with 627 - | Some l -> ("limit", string_of_int l) :: params 628 - | None -> params 136 + match limit with Some l -> ("limit", string_of_int l) :: params | None -> params 629 137 in 630 138 let params = 631 139 match cursor with Some c -> ("cursor", c) :: params | None -> params 632 140 in 633 141 let params = 634 142 match include_content with 635 - | Some ic -> ("includeContent", string_of_bool ic) :: params 143 + | Some true -> ("includeContent", "true") :: params 144 + | Some false -> ("includeContent", "false") :: params 636 145 | None -> params 637 146 in 638 147 let params = 639 148 match archived with 640 - | Some a -> ("archived", string_of_bool a) :: params 149 + | Some true -> ("archived", "true") :: params 150 + | Some false -> ("archived", "false") :: params 641 151 | None -> params 642 152 in 643 153 let params = 644 154 match favourited with 645 - | Some f -> ("favourited", string_of_bool f) :: params 155 + | Some true -> ("favourited", "true") :: params 156 + | Some false -> ("favourited", "false") :: params 646 157 | None -> params 647 158 in 648 - 649 - (* Construct URL with query parameters *) 650 - let query_str = 651 - if params = [] then "" 652 - else 653 - "?" 654 - ^ String.concat "&" 655 - (List.map (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v) params) 656 - in 657 - let url = base_url ^ "/api/v1/bookmarks" ^ query_str in 658 - 659 - (* Make the request *) 660 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 661 - process_response resp body parse_paginated_bookmarks 662 - 663 - let fetch_all_bookmarks ~api_key ?page_size ?max_pages ?archived ?favourited 664 - base_url = 665 - let rec fetch_pages cursor acc page_num = 666 - let limit = Option.value page_size ~default:50 in 667 - fetch_bookmarks ~api_key ~limit ?cursor ?archived ?favourited base_url 668 - >>= fun response -> 669 - let all_bookmarks = acc @ response.bookmarks in 670 - 671 - let more_pages = response.next_cursor <> None in 672 - let under_max = 673 - match max_pages with Some max -> page_num < max | None -> true 674 - in 159 + let url = t.base_url / "api/v1/bookmarks" ^ query_string params in 160 + get_json t url paginated_bookmarks_jsont 675 161 676 - if more_pages && under_max then 677 - fetch_pages response.next_cursor all_bookmarks (page_num + 1) 678 - else Lwt.return all_bookmarks 162 + let fetch_all_bookmarks t ?page_size ?max_pages ?archived ?favourited () = 163 + let limit = Option.value page_size ~default:50 in 164 + let rec fetch_all acc cursor pages_fetched = 165 + match max_pages with 166 + | Some max when pages_fetched >= max -> List.rev acc 167 + | _ -> 168 + let result = fetch_bookmarks t ~limit ?cursor ?archived ?favourited () in 169 + let acc = List.rev_append result.bookmarks acc in 170 + (match result.next_cursor with 171 + | None -> List.rev acc 172 + | Some c -> fetch_all acc (Some c) (pages_fetched + 1)) 679 173 in 680 - 681 - fetch_pages None [] 0 174 + fetch_all [] None 0 682 175 683 - let search_bookmarks ~api_key ~query ?limit ?cursor ?include_content base_url = 684 - (* Build query parameters *) 176 + let search_bookmarks t ~query ?limit ?cursor ?include_content () = 685 177 let params = [ ("q", query) ] in 686 178 let params = 687 - match limit with 688 - | Some l -> ("limit", string_of_int l) :: params 689 - | None -> params 179 + match limit with Some l -> ("limit", string_of_int l) :: params | None -> params 690 180 in 691 181 let params = 692 182 match cursor with Some c -> ("cursor", c) :: params | None -> params 693 183 in 694 184 let params = 695 185 match include_content with 696 - | Some ic -> ("includeContent", string_of_bool ic) :: params 186 + | Some true -> ("includeContent", "true") :: params 187 + | Some false -> ("includeContent", "false") :: params 697 188 | None -> params 698 189 in 190 + let url = t.base_url / "api/v1/bookmarks/search" ^ query_string params in 191 + get_json t url paginated_bookmarks_jsont 699 192 700 - (* Construct URL with query parameters *) 701 - let query_str = 702 - "?" 703 - ^ String.concat "&" 704 - (List.map (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v) params) 705 - in 706 - let url = base_url ^ "/api/v1/bookmarks/search" ^ query_str in 193 + let fetch_bookmark_details t bookmark_id = 194 + let url = t.base_url / "api/v1/bookmarks" / bookmark_id in 195 + get_json t url bookmark_jsont 707 196 708 - (* Make the request *) 709 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 710 - process_response resp body parse_paginated_bookmarks 711 - 712 - let fetch_bookmark_details ~api_key base_url bookmark_id = 713 - let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id in 714 - 715 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 716 - process_response resp body parse_bookmark 717 - 718 - let create_bookmark ~api_key ~url ?title ?note ?summary ?favourited ?archived 719 - ?created_at ?tags base_url = 720 - (* Prepare the bookmark request body *) 721 - let body_obj = [ ("url", `String url) ] in 722 - 723 - (* Add optional fields *) 724 - let body_obj = 725 - match title with 726 - | Some t -> ("title", `String t) :: body_obj 727 - | None -> body_obj 197 + let rec create_bookmark t ~url ?title ?note ?summary ?favourited ?archived ?created_at 198 + ?tags () = 199 + let api_url = t.base_url / "api/v1/bookmarks" in 200 + let req : create_bookmark_request = 201 + { 202 + type_ = "link"; 203 + url = Some url; 204 + text = None; 205 + title; 206 + note; 207 + summary; 208 + archived; 209 + favourited; 210 + created_at; 211 + } 728 212 in 729 - let body_obj = 730 - match note with 731 - | Some n -> ("note", `String n) :: body_obj 732 - | None -> body_obj 733 - in 734 - let body_obj = 735 - match summary with 736 - | Some s -> ("summary", `String s) :: body_obj 737 - | None -> body_obj 738 - in 739 - let body_obj = 740 - match favourited with 741 - | Some f -> ("favourited", `Bool f) :: body_obj 742 - | None -> body_obj 743 - in 744 - let body_obj = 745 - match archived with 746 - | Some a -> ("archived", `Bool a) :: body_obj 747 - | None -> body_obj 748 - in 749 - let body_obj = 750 - match created_at with 751 - | Some date -> 752 - let iso_date = Ptime.to_rfc3339 date in 753 - ("createdAt", `String iso_date) :: body_obj 754 - | None -> body_obj 755 - in 756 - let body_obj = 757 - match tags with 758 - | Some tag_list when tag_list <> [] -> 759 - let tag_objs = 760 - List.map (fun t -> `O [ ("name", `String t) ]) tag_list 761 - in 762 - ("tags", `A tag_objs) :: body_obj 763 - | _ -> body_obj 764 - in 765 - 766 - (* Convert to JSON *) 767 - let body_json = `O body_obj in 768 - let body_str = J.to_string body_json in 769 - 770 - (* Make the request *) 771 - let headers = [ ("Content-Type", "application/json") ] in 772 - let url = base_url ^ "/api/v1/bookmarks" in 773 - 774 - make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url 775 - >>= fun (resp, body) -> process_response resp body parse_bookmark 776 - 777 - let update_bookmark ~api_key ?title ?note ?summary ?favourited ?archived ?url 778 - ?description ?author ?publisher ?date_published ?date_modified ?text 779 - ?asset_content base_url bookmark_id = 780 - (* Prepare the update request body *) 781 - let body_obj = [] in 782 - 783 - (* Add optional fields *) 784 - let body_obj = 785 - match title with 786 - | Some t -> ("title", `String t) :: body_obj 787 - | None -> body_obj 788 - in 789 - let body_obj = 790 - match note with 791 - | Some n -> ("note", `String n) :: body_obj 792 - | None -> body_obj 793 - in 794 - let body_obj = 795 - match summary with 796 - | Some s -> ("summary", `String s) :: body_obj 797 - | None -> body_obj 798 - in 799 - let body_obj = 800 - match favourited with 801 - | Some f -> ("favourited", `Bool f) :: body_obj 802 - | None -> body_obj 803 - in 804 - let body_obj = 805 - match archived with 806 - | Some a -> ("archived", `Bool a) :: body_obj 807 - | None -> body_obj 808 - in 809 - let body_obj = 810 - match url with Some u -> ("url", `String u) :: body_obj | None -> body_obj 811 - in 812 - let body_obj = 813 - match description with 814 - | Some d -> ("description", `String d) :: body_obj 815 - | None -> body_obj 816 - in 817 - let body_obj = 818 - match author with 819 - | Some a -> ("author", `String a) :: body_obj 820 - | None -> body_obj 821 - in 822 - let body_obj = 823 - match publisher with 824 - | Some p -> ("publisher", `String p) :: body_obj 825 - | None -> body_obj 826 - in 827 - let body_obj = 828 - match date_published with 829 - | Some date -> 830 - let iso_date = Ptime.to_rfc3339 date in 831 - ("datePublished", `String iso_date) :: body_obj 832 - | None -> body_obj 833 - in 834 - let body_obj = 835 - match date_modified with 836 - | Some date -> 837 - let iso_date = Ptime.to_rfc3339 date in 838 - ("dateModified", `String iso_date) :: body_obj 839 - | None -> body_obj 840 - in 841 - let body_obj = 842 - match text with 843 - | Some t -> ("text", `String t) :: body_obj 844 - | None -> body_obj 845 - in 846 - let body_obj = 847 - match asset_content with 848 - | Some c -> ("assetContent", `String c) :: body_obj 849 - | None -> body_obj 850 - in 851 - 852 - (* Only proceed if there are updates to make *) 853 - if body_obj = [] then 854 - (* No updates, just fetch the current bookmark *) 855 - fetch_bookmark_details ~api_key base_url bookmark_id 856 - else 857 - (* Convert to JSON *) 858 - let body_json = `O body_obj in 859 - let body_str = J.to_string body_json in 860 - 861 - (* Make the request *) 862 - let headers = [ ("Content-Type", "application/json") ] in 863 - let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id in 864 - 865 - make_request ~api_key ~method_:`PUT ~headers ~body:(Some body_str) url 866 - >>= fun (resp, body) -> process_response resp body parse_bookmark 867 - 868 - let delete_bookmark ~api_key base_url bookmark_id = 869 - let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id in 870 - 871 - make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) -> 872 - let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in 873 - if status_code >= 200 && status_code < 300 then 874 - consume_body body >>= fun () -> Lwt.return_unit 875 - else 876 - Cohttp_lwt.Body.to_string body >>= fun body_str -> 877 - Lwt.fail_with ("Failed to delete bookmark: " ^ body_str) 878 - 879 - let summarize_bookmark ~api_key base_url bookmark_id = 880 - let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/summarize" in 881 - 882 - make_request ~api_key ~method_:`POST url >>= fun (resp, body) -> 883 - process_response resp body parse_bookmark 884 - 885 - (** {1 Tag Operations} *) 213 + let bookmark = post_json t api_url create_bookmark_request_jsont req bookmark_jsont in 214 + (* Attach tags if provided *) 215 + match tags with 216 + | None | Some [] -> bookmark 217 + | Some tag_names -> 218 + let tag_refs = List.map (fun n -> TagName n) tag_names in 219 + let _ = 220 + attach_tags t ~tag_refs:(List.map (fun r -> match r with TagName n -> `TagName n | TagId i -> `TagId i) tag_refs) bookmark.id 221 + in 222 + (* Refetch the bookmark to get updated tags *) 223 + fetch_bookmark_details t bookmark.id 886 224 887 - let attach_tags ~api_key ~tag_refs base_url bookmark_id = 888 - (* Prepare the tag request body *) 889 - let tag_objs = 225 + and attach_tags t ~tag_refs bookmark_id = 226 + let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "tags" in 227 + let tags = 890 228 List.map 891 - (function 892 - | `TagId id -> `O [ ("tagId", `String id) ] 893 - | `TagName name -> `O [ ("tagName", `String name) ]) 229 + (function `TagId id -> TagId id | `TagName name -> TagName name) 894 230 tag_refs 895 231 in 232 + let req = { tags } in 233 + let resp = post_json t url attach_tags_request_jsont req attach_tags_response_jsont in 234 + resp.attached 896 235 897 - let body_json = `O [ ("tags", `A tag_objs) ] in 898 - let body_str = J.to_string body_json in 236 + let update_bookmark t bookmark_id ?title ?note ?summary ?favourited ?archived () = 237 + let url = t.base_url / "api/v1/bookmarks" / bookmark_id in 238 + let req : update_bookmark_request = { title; note; summary; archived; favourited } in 239 + patch_json t url update_bookmark_request_jsont req bookmark_jsont 899 240 900 - (* Make the request *) 901 - let headers = [ ("Content-Type", "application/json") ] in 902 - let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/tags" in 241 + let delete_bookmark t bookmark_id = 242 + let url = t.base_url / "api/v1/bookmarks" / bookmark_id in 243 + delete_json t url 903 244 904 - make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url 905 - >>= fun (resp, body) -> 906 - process_response resp body (fun json -> 907 - try 908 - let tags_json = J.find json [ "tags" ] in 909 - J.get_list 910 - (fun tag_json -> J.find tag_json [ "id" ] |> J.get_string) 911 - tags_json 912 - with _ -> []) 245 + let summarize_bookmark t bookmark_id = 246 + let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "summarize" in 247 + post_json_no_body t url bookmark_jsont 248 + 249 + (** {1 Tag Operations} *) 913 250 914 - let detach_tags ~api_key ~tag_refs base_url bookmark_id = 915 - (* Prepare the tag request body *) 916 - let tag_objs = 251 + let detach_tags t ~tag_refs bookmark_id = 252 + let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "tags" in 253 + let tags = 917 254 List.map 918 - (function 919 - | `TagId id -> `O [ ("tagId", `String id) ] 920 - | `TagName name -> `O [ ("tagName", `String name) ]) 255 + (function `TagId id -> TagId id | `TagName name -> TagName name) 921 256 tag_refs 922 257 in 923 - 924 - let body_json = `O [ ("tags", `A tag_objs) ] in 925 - let body_str = J.to_string body_json in 926 - 927 - (* Make the request *) 928 - let headers = [ ("Content-Type", "application/json") ] in 929 - let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/tags/detach" in 258 + let req = { tags } in 259 + (* DELETE with body - use request function directly *) 260 + let body_str = encode_json attach_tags_request_jsont req in 261 + let body = Requests.Body.of_string Requests.Mime.json body_str in 262 + let response = Requests.request t.session ~method_:`DELETE ~body url in 263 + let resp_body = Requests.Response.text response in 264 + if not (Requests.Response.ok response) then 265 + handle_error_response (Requests.Response.status_code response) resp_body; 266 + let resp = decode_json detach_tags_response_jsont resp_body in 267 + resp.detached 930 268 931 - make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url 932 - >>= fun (resp, body) -> 933 - process_response resp body (fun json -> 934 - try 935 - let tags_json = J.find json [ "tags" ] in 936 - J.get_list 937 - (fun tag_json -> J.find tag_json [ "id" ] |> J.get_string) 938 - tags_json 939 - with _ -> []) 269 + let fetch_all_tags t = 270 + let url = t.base_url / "api/v1/tags" in 271 + let resp = get_json t url tags_response_jsont in 272 + resp.tags 940 273 941 - let fetch_all_tags ~api_key base_url = 942 - let url = base_url ^ "/api/v1/tags" in 274 + let fetch_tag_details t tag_id = 275 + let url = t.base_url / "api/v1/tags" / tag_id in 276 + get_json t url tag_jsont 943 277 944 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 945 - process_response resp body (fun json -> 946 - try 947 - let tags_json = J.find json [ "tags" ] in 948 - J.get_list parse_tag tags_json 949 - with _ -> []) 950 - 951 - let fetch_tag_details ~api_key base_url tag_id = 952 - let url = base_url ^ "/api/v1/tags/" ^ tag_id in 953 - 954 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 955 - process_response resp body parse_tag 956 - 957 - let fetch_bookmarks_with_tag ~api_key ?limit ?cursor ?include_content base_url 958 - tag_id = 959 - (* Build query parameters *) 278 + let fetch_bookmarks_with_tag t ?limit ?cursor ?include_content tag_id = 960 279 let params = [] in 961 280 let params = 962 - match limit with 963 - | Some l -> ("limit", string_of_int l) :: params 964 - | None -> params 281 + match limit with Some l -> ("limit", string_of_int l) :: params | None -> params 965 282 in 966 283 let params = 967 284 match cursor with Some c -> ("cursor", c) :: params | None -> params 968 285 in 969 286 let params = 970 287 match include_content with 971 - | Some ic -> ("includeContent", string_of_bool ic) :: params 288 + | Some true -> ("includeContent", "true") :: params 289 + | Some false -> ("includeContent", "false") :: params 972 290 | None -> params 973 291 in 974 - 975 - (* Construct URL with query parameters *) 976 - let query_str = 977 - if params = [] then "" 978 - else 979 - "?" 980 - ^ String.concat "&" 981 - (List.map (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v) params) 982 - in 983 - let url = base_url ^ "/api/v1/tags/" ^ tag_id ^ "/bookmarks" ^ query_str in 984 - 985 - (* Make the request *) 986 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 987 - process_response resp body parse_paginated_bookmarks 988 - 989 - let update_tag ~api_key ~name base_url tag_id = 990 - let body_json = `O [ ("name", `String name) ] in 991 - let body_str = J.to_string body_json in 992 - 993 - (* Make the request *) 994 - let headers = [ ("Content-Type", "application/json") ] in 995 - let url = base_url ^ "/api/v1/tags/" ^ tag_id in 996 - 997 - make_request ~api_key ~method_:`PUT ~headers ~body:(Some body_str) url 998 - >>= fun (resp, body) -> process_response resp body parse_tag 292 + let url = t.base_url / "api/v1/tags" / tag_id / "bookmarks" ^ query_string params in 293 + get_json t url paginated_bookmarks_jsont 999 294 1000 - let delete_tag ~api_key base_url tag_id = 1001 - let url = base_url ^ "/api/v1/tags/" ^ tag_id in 295 + let update_tag t ~name tag_id = 296 + let url = t.base_url / "api/v1/tags" / tag_id in 297 + let req : update_tag_request = { name } in 298 + patch_json t url update_tag_request_jsont req tag_jsont 1002 299 1003 - make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) -> 1004 - let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in 1005 - if status_code >= 200 && status_code < 300 then 1006 - consume_body body >>= fun () -> Lwt.return_unit 1007 - else 1008 - Cohttp_lwt.Body.to_string body >>= fun body_str -> 1009 - Lwt.fail_with ("Failed to delete tag: " ^ body_str) 300 + let delete_tag t tag_id = 301 + let url = t.base_url / "api/v1/tags" / tag_id in 302 + delete_json t url 1010 303 1011 304 (** {1 List Operations} *) 1012 305 1013 - let fetch_all_lists ~api_key base_url = 1014 - let url = base_url ^ "/api/v1/lists" in 1015 - 1016 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 1017 - process_response resp body (fun json -> 1018 - try 1019 - let lists_json = J.find json [ "lists" ] in 1020 - J.get_list parse_list lists_json 1021 - with _ -> []) 1022 - 1023 - let fetch_list_details ~api_key base_url list_id = 1024 - let url = base_url ^ "/api/v1/lists/" ^ list_id in 1025 - 1026 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 1027 - process_response resp body parse_list 306 + let fetch_all_lists t = 307 + let url = t.base_url / "api/v1/lists" in 308 + let resp = get_json t url lists_response_jsont in 309 + resp.lists 1028 310 1029 - let create_list ~api_key ~name ~icon ?description ?parent_id ?list_type ?query 1030 - base_url = 1031 - (* Prepare the list request body *) 1032 - let body_obj = [ ("name", `String name); ("icon", `String icon) ] in 311 + let fetch_list_details t list_id = 312 + let url = t.base_url / "api/v1/lists" / list_id in 313 + get_json t url list_jsont 1033 314 1034 - (* Add optional fields *) 1035 - let body_obj = 1036 - match description with 1037 - | Some d -> ("description", `String d) :: body_obj 1038 - | None -> body_obj 1039 - in 1040 - let body_obj = 1041 - match parent_id with 1042 - | Some id -> ("parentId", `String id) :: body_obj 1043 - | None -> body_obj 1044 - in 1045 - let body_obj = 315 + let create_list t ~name ~icon ?description ?parent_id ?list_type ?query () = 316 + let url = t.base_url / "api/v1/lists" in 317 + let type_ = 1046 318 match list_type with 1047 - | Some lt -> ("listType", `String (string_of_list_type lt)) :: body_obj 1048 - | None -> body_obj 319 + | Some Manual -> Some "manual" 320 + | Some Smart -> Some "smart" 321 + | None -> None 1049 322 in 1050 - let body_obj = 1051 - match query with 1052 - | Some q -> ("query", `String q) :: body_obj 1053 - | None -> body_obj 1054 - in 323 + let req : create_list_request = { name; icon; description; parent_id; type_; query } in 324 + post_json t url create_list_request_jsont req list_jsont 1055 325 1056 - (* Convert to JSON *) 1057 - let body_json = `O body_obj in 1058 - let body_str = J.to_string body_json in 1059 - 1060 - (* Make the request *) 1061 - let headers = [ ("Content-Type", "application/json") ] in 1062 - let url = base_url ^ "/api/v1/lists" in 1063 - 1064 - make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url 1065 - >>= fun (resp, body) -> process_response resp body parse_list 326 + let update_list t ?name ?description ?icon ?parent_id ?query list_id = 327 + let url = t.base_url / "api/v1/lists" / list_id in 328 + let req : update_list_request = { name; icon; description; parent_id; query } in 329 + patch_json t url update_list_request_jsont req list_jsont 1066 330 1067 - let update_list ~api_key ?name ?description ?icon ?parent_id ?query base_url 1068 - list_id = 1069 - (* Prepare the update request body *) 1070 - let body_obj = [] in 331 + let delete_list t list_id = 332 + let url = t.base_url / "api/v1/lists" / list_id in 333 + delete_json t url 1071 334 1072 - (* Add optional fields *) 1073 - let body_obj = 1074 - match name with 1075 - | Some n -> ("name", `String n) :: body_obj 1076 - | None -> body_obj 1077 - in 1078 - let body_obj = 1079 - match description with 1080 - | Some d -> ("description", `String d) :: body_obj 1081 - | None -> body_obj 1082 - in 1083 - let body_obj = 1084 - match icon with 1085 - | Some i -> ("icon", `String i) :: body_obj 1086 - | None -> body_obj 1087 - in 1088 - let body_obj = 1089 - match parent_id with 1090 - | Some (Some id) -> ("parentId", `String id) :: body_obj 1091 - | Some None -> ("parentId", `Null) :: body_obj 1092 - | None -> body_obj 1093 - in 1094 - let body_obj = 1095 - match query with 1096 - | Some q -> ("query", `String q) :: body_obj 1097 - | None -> body_obj 1098 - in 1099 - 1100 - (* Only proceed if there are updates to make *) 1101 - if body_obj = [] then 1102 - (* No updates, just fetch the current list *) 1103 - fetch_list_details ~api_key base_url list_id 1104 - else 1105 - (* Convert to JSON *) 1106 - let body_json = `O body_obj in 1107 - let body_str = J.to_string body_json in 1108 - 1109 - (* Make the request *) 1110 - let headers = [ ("Content-Type", "application/json") ] in 1111 - let url = base_url ^ "/api/v1/lists/" ^ list_id in 1112 - 1113 - make_request ~api_key ~method_:`PUT ~headers ~body:(Some body_str) url 1114 - >>= fun (resp, body) -> process_response resp body parse_list 1115 - 1116 - let delete_list ~api_key base_url list_id = 1117 - let url = base_url ^ "/api/v1/lists/" ^ list_id in 1118 - 1119 - make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) -> 1120 - let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in 1121 - if status_code >= 200 && status_code < 300 then 1122 - consume_body body >>= fun () -> Lwt.return_unit 1123 - else 1124 - Cohttp_lwt.Body.to_string body >>= fun body_str -> 1125 - Lwt.fail_with ("Failed to delete list: " ^ body_str) 1126 - 1127 - let fetch_bookmarks_in_list ~api_key ?limit ?cursor ?include_content base_url 1128 - list_id = 1129 - (* Build query parameters *) 335 + let fetch_bookmarks_in_list t ?limit ?cursor ?include_content list_id = 1130 336 let params = [] in 1131 337 let params = 1132 - match limit with 1133 - | Some l -> ("limit", string_of_int l) :: params 1134 - | None -> params 338 + match limit with Some l -> ("limit", string_of_int l) :: params | None -> params 1135 339 in 1136 340 let params = 1137 341 match cursor with Some c -> ("cursor", c) :: params | None -> params 1138 342 in 1139 343 let params = 1140 344 match include_content with 1141 - | Some ic -> ("includeContent", string_of_bool ic) :: params 345 + | Some true -> ("includeContent", "true") :: params 346 + | Some false -> ("includeContent", "false") :: params 1142 347 | None -> params 1143 348 in 1144 - 1145 - (* Construct URL with query parameters *) 1146 - let query_str = 1147 - if params = [] then "" 1148 - else 1149 - "?" 1150 - ^ String.concat "&" 1151 - (List.map (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v) params) 1152 - in 1153 - let url = base_url ^ "/api/v1/lists/" ^ list_id ^ "/bookmarks" ^ query_str in 1154 - 1155 - (* Make the request *) 1156 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 1157 - process_response resp body parse_paginated_bookmarks 1158 - 1159 - let add_bookmark_to_list ~api_key base_url list_id bookmark_id = 1160 - let url = 1161 - base_url ^ "/api/v1/lists/" ^ list_id ^ "/bookmarks/" ^ bookmark_id 1162 - in 1163 - 1164 - make_request ~api_key ~method_:`POST url >>= fun (resp, body) -> 1165 - let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in 1166 - if status_code >= 200 && status_code < 300 then 1167 - consume_body body >>= fun () -> Lwt.return_unit 1168 - else 1169 - Cohttp_lwt.Body.to_string body >>= fun body_str -> 1170 - Lwt.fail_with ("Failed to add bookmark to list: " ^ body_str) 349 + let url = t.base_url / "api/v1/lists" / list_id / "bookmarks" ^ query_string params in 350 + get_json t url paginated_bookmarks_jsont 1171 351 1172 - let remove_bookmark_from_list ~api_key base_url list_id bookmark_id = 1173 - let url = 1174 - base_url ^ "/api/v1/lists/" ^ list_id ^ "/bookmarks/" ^ bookmark_id 1175 - in 352 + let add_bookmark_to_list t list_id bookmark_id = 353 + let url = t.base_url / "api/v1/lists" / list_id / "bookmarks" / bookmark_id in 354 + let response = Requests.put t.session url in 355 + let resp_body = Requests.Response.text response in 356 + if not (Requests.Response.ok response) then 357 + handle_error_response (Requests.Response.status_code response) resp_body 1176 358 1177 - make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) -> 1178 - let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in 1179 - if status_code >= 200 && status_code < 300 then 1180 - consume_body body >>= fun () -> Lwt.return_unit 1181 - else 1182 - Cohttp_lwt.Body.to_string body >>= fun body_str -> 1183 - Lwt.fail_with ("Failed to remove bookmark from list: " ^ body_str) 359 + let remove_bookmark_from_list t list_id bookmark_id = 360 + let url = t.base_url / "api/v1/lists" / list_id / "bookmarks" / bookmark_id in 361 + delete_json t url 1184 362 1185 363 (** {1 Highlight Operations} *) 1186 364 1187 - let fetch_all_highlights ~api_key ?limit ?cursor base_url = 1188 - (* Build query parameters *) 365 + let fetch_all_highlights t ?limit ?cursor () = 1189 366 let params = [] in 1190 367 let params = 1191 - match limit with 1192 - | Some l -> ("limit", string_of_int l) :: params 1193 - | None -> params 368 + match limit with Some l -> ("limit", string_of_int l) :: params | None -> params 1194 369 in 1195 370 let params = 1196 371 match cursor with Some c -> ("cursor", c) :: params | None -> params 1197 372 in 1198 - 1199 - (* Construct URL with query parameters *) 1200 - let query_str = 1201 - if params = [] then "" 1202 - else 1203 - "?" 1204 - ^ String.concat "&" 1205 - (List.map (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v) params) 1206 - in 1207 - let url = base_url ^ "/api/v1/highlights" ^ query_str in 373 + let url = t.base_url / "api/v1/highlights" ^ query_string params in 374 + get_json t url paginated_highlights_jsont 1208 375 1209 - (* Make the request *) 1210 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 1211 - process_response resp body parse_paginated_highlights 376 + let fetch_bookmark_highlights t bookmark_id = 377 + let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "highlights" in 378 + let resp = get_json t url highlights_response_jsont in 379 + resp.highlights 1212 380 1213 - let fetch_bookmark_highlights ~api_key base_url bookmark_id = 1214 - let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/highlights" in 381 + let fetch_highlight_details t highlight_id = 382 + let url = t.base_url / "api/v1/highlights" / highlight_id in 383 + get_json t url highlight_jsont 1215 384 1216 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 1217 - process_response resp body (fun json -> 1218 - try 1219 - let highlights_json = J.find json [ "highlights" ] in 1220 - J.get_list parse_highlight highlights_json 1221 - with _ -> []) 1222 - 1223 - let fetch_highlight_details ~api_key base_url highlight_id = 1224 - let url = base_url ^ "/api/v1/highlights/" ^ highlight_id in 1225 - 1226 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 1227 - process_response resp body parse_highlight 1228 - 1229 - let create_highlight ~api_key ~bookmark_id ~start_offset ~end_offset ~text ?note 1230 - ?color base_url = 1231 - (* Prepare the highlight request body *) 1232 - let body_obj = 1233 - [ 1234 - ("bookmarkId", `String bookmark_id); 1235 - ("startOffset", `Float (float_of_int start_offset)); 1236 - ("endOffset", `Float (float_of_int end_offset)); 1237 - ("text", `String text); 1238 - ] 385 + let create_highlight t ~bookmark_id ~start_offset ~end_offset ~text ?note ?color () = 386 + let url = t.base_url / "api/v1/highlights" in 387 + let req : create_highlight_request = 388 + { bookmark_id; start_offset; end_offset; text; note; color } 1239 389 in 1240 - 1241 - (* Add optional fields *) 1242 - let body_obj = 1243 - match note with 1244 - | Some n -> ("note", `String n) :: body_obj 1245 - | None -> body_obj 1246 - in 1247 - let body_obj = 1248 - match color with 1249 - | Some c -> ("color", `String (string_of_highlight_color c)) :: body_obj 1250 - | None -> body_obj 1251 - in 1252 - 1253 - (* Convert to JSON *) 1254 - let body_json = `O body_obj in 1255 - let body_str = J.to_string body_json in 1256 - 1257 - (* Make the request *) 1258 - let headers = [ ("Content-Type", "application/json") ] in 1259 - let url = base_url ^ "/api/v1/highlights" in 1260 - 1261 - make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url 1262 - >>= fun (resp, body) -> process_response resp body parse_highlight 1263 - 1264 - let update_highlight ~api_key ?color base_url highlight_id = 1265 - (* Prepare the update request body *) 1266 - let body_obj = [] in 1267 - 1268 - (* Add optional fields *) 1269 - let body_obj = 1270 - match color with 1271 - | Some c -> ("color", `String (string_of_highlight_color c)) :: body_obj 1272 - | None -> body_obj 1273 - in 1274 - 1275 - (* Only proceed if there are updates to make *) 1276 - if body_obj = [] then 1277 - (* No updates, just fetch the current highlight *) 1278 - fetch_highlight_details ~api_key base_url highlight_id 1279 - else 1280 - (* Convert to JSON *) 1281 - let body_json = `O body_obj in 1282 - let body_str = J.to_string body_json in 1283 - 1284 - (* Make the request *) 1285 - let headers = [ ("Content-Type", "application/json") ] in 1286 - let url = base_url ^ "/api/v1/highlights/" ^ highlight_id in 1287 - 1288 - make_request ~api_key ~method_:`PUT ~headers ~body:(Some body_str) url 1289 - >>= fun (resp, body) -> process_response resp body parse_highlight 390 + post_json t url create_highlight_request_jsont req highlight_jsont 1290 391 1291 - let delete_highlight ~api_key base_url highlight_id = 1292 - let url = base_url ^ "/api/v1/highlights/" ^ highlight_id in 392 + let update_highlight t ?color highlight_id = 393 + let url = t.base_url / "api/v1/highlights" / highlight_id in 394 + let req : update_highlight_request = { color } in 395 + patch_json t url update_highlight_request_jsont req highlight_jsont 1293 396 1294 - make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) -> 1295 - let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in 1296 - if status_code >= 200 && status_code < 300 then 1297 - consume_body body >>= fun () -> Lwt.return_unit 1298 - else 1299 - Cohttp_lwt.Body.to_string body >>= fun body_str -> 1300 - Lwt.fail_with ("Failed to delete highlight: " ^ body_str) 397 + let delete_highlight t highlight_id = 398 + let url = t.base_url / "api/v1/highlights" / highlight_id in 399 + delete_json t url 1301 400 1302 401 (** {1 Asset Operations} *) 1303 402 1304 - let get_asset_url base_url asset_id = 1305 - Printf.sprintf "%s/api/assets/%s" base_url asset_id 1306 - 1307 - let fetch_asset ~api_key base_url asset_id = 1308 - let url = get_asset_url base_url asset_id in 1309 - 1310 - let open Cohttp_lwt_unix in 1311 - let headers = 1312 - Cohttp.Header.init () |> fun h -> 1313 - Cohttp.Header.add h "Authorization" ("Bearer " ^ api_key) 1314 - in 1315 - 1316 - Client.get ~headers (Uri.of_string url) >>= fun (resp, body) -> 1317 - let status_code = Cohttp.Code.code_of_status resp.status in 1318 - if status_code >= 200 && status_code < 300 then Cohttp_lwt.Body.to_string body 1319 - else 1320 - Cohttp_lwt.Body.to_string body >>= fun body_str -> 1321 - Lwt.fail_with ("Failed to fetch asset: " ^ body_str) 1322 - 1323 - let attach_asset ~api_key ~asset_id ~asset_type base_url bookmark_id = 1324 - (* Prepare the asset request body *) 1325 - let body_json = 1326 - `O 1327 - [ 1328 - ("assetId", `String asset_id); 1329 - ("assetType", `String (string_of_asset_type asset_type)); 1330 - ] 1331 - in 1332 - let body_str = J.to_string body_json in 1333 - 1334 - (* Make the request *) 1335 - let headers = [ ("Content-Type", "application/json") ] in 1336 - let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/assets" in 1337 - 1338 - make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url 1339 - >>= fun (resp, body) -> process_response resp body parse_asset 1340 - 1341 - let replace_asset ~api_key ~new_asset_id base_url bookmark_id asset_id = 1342 - (* Prepare the asset request body *) 1343 - let body_json = `O [ ("newAssetId", `String new_asset_id) ] in 1344 - let body_str = J.to_string body_json in 403 + let fetch_asset t asset_id = 404 + let url = t.base_url / "api/assets" / asset_id in 405 + let response = Requests.get t.session url in 406 + let body = Requests.Response.text response in 407 + if not (Requests.Response.ok response) then 408 + handle_error_response (Requests.Response.status_code response) body; 409 + body 1345 410 1346 - (* Make the request *) 1347 - let headers = [ ("Content-Type", "application/json") ] in 1348 - let url = 1349 - base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/assets/" ^ asset_id 1350 - ^ "/replace" 1351 - in 411 + let get_asset_url t asset_id = t.base_url / "api/assets" / asset_id 1352 412 1353 - make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url 1354 - >>= fun (resp, body) -> 1355 - let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in 1356 - if status_code >= 200 && status_code < 300 then 1357 - consume_body body >>= fun () -> Lwt.return_unit 1358 - else 1359 - Cohttp_lwt.Body.to_string body >>= fun body_str -> 1360 - Lwt.fail_with ("Failed to replace asset: " ^ body_str) 413 + let attach_asset t ~asset_id ~asset_type bookmark_id = 414 + let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "assets" in 415 + let req : attach_asset_request = { id = asset_id; asset_type } in 416 + post_json t url attach_asset_request_jsont req asset_jsont 1361 417 1362 - let detach_asset ~api_key base_url bookmark_id asset_id = 1363 - let url = 1364 - base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/assets/" ^ asset_id 1365 - in 418 + let replace_asset t ~new_asset_id bookmark_id asset_id = 419 + let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "assets" / asset_id in 420 + let req : replace_asset_request = { asset_id = new_asset_id } in 421 + let _ = put_json t url replace_asset_request_jsont req in 422 + () 1366 423 1367 - make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) -> 1368 - let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in 1369 - if status_code >= 200 && status_code < 300 then 1370 - consume_body body >>= fun () -> Lwt.return_unit 1371 - else 1372 - Cohttp_lwt.Body.to_string body >>= fun body_str -> 1373 - Lwt.fail_with ("Failed to detach asset: " ^ body_str) 424 + let detach_asset t bookmark_id asset_id = 425 + let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "assets" / asset_id in 426 + delete_json t url 1374 427 1375 428 (** {1 User Operations} *) 1376 429 1377 - let get_current_user ~api_key base_url = 1378 - let url = base_url ^ "/api/v1/user" in 1379 - 1380 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 1381 - process_response resp body parse_user_info 1382 - 1383 - let get_user_stats ~api_key base_url = 1384 - let url = base_url ^ "/api/v1/user/stats" in 430 + let get_current_user t = 431 + let url = t.base_url / "api/v1/user" in 432 + get_json t url user_info_jsont 1385 433 1386 - make_request ~api_key ~method_:`GET url >>= fun (resp, body) -> 1387 - process_response resp body parse_user_stats 434 + let get_user_stats t = 435 + let url = t.base_url / "api/v1/user/stats" in 436 + get_json t url user_stats_jsont
+205 -741
lib/karakeep.mli
··· 1 - (** Karakeep API client interface 1 + (** Karakeep API client 2 2 3 3 This module provides a client for interacting with the Karakeep bookmark 4 - service API. It allows fetching, creating, and managing bookmarks stored in 5 - a Karakeep instance. 4 + service API using Eio for structured concurrency. 6 5 7 6 {2 Basic Usage} 8 7 9 8 {[ 10 - (* Setup the client *) 11 - let api_key = "your_api_key" 12 - let base_url = "https://hoard.recoil.org" 9 + Eio_main.run @@ fun env -> 10 + Eio.Switch.run @@ fun sw -> 11 + 12 + (* Create the client *) 13 + let client = 14 + Karakeep.create ~sw ~env ~base_url:"https://hoard.recoil.org" 15 + ~api_key:"your_api_key" 16 + in 13 17 14 18 (* Fetch recent bookmarks *) 15 - let recent_bookmarks = 16 - Karakeep.fetch_bookmarks ~api_key ~limit:10 base_url 19 + let { bookmarks; next_cursor } = Karakeep.fetch_bookmarks client () in 17 20 18 21 (* Fetch all bookmarks (handles pagination automatically) *) 19 - let all_bookmarks = Karakeep.fetch_all_bookmarks ~api_key base_url 22 + let all_bookmarks = Karakeep.fetch_all_bookmarks client () in 20 23 21 24 (* Get a specific bookmark by ID *) 22 - let specific_bookmark = 23 - Karakeep.fetch_bookmark_details ~api_key base_url "bookmark_id" 25 + let bookmark = Karakeep.fetch_bookmark_details client "bookmark_id" in 24 26 25 27 (* Create a new bookmark *) 26 28 let new_bookmark = 27 - Karakeep.create_bookmark ~api_key ~url:"https://ocaml.org" 28 - ~title:"OCaml Programming Language" base_url 29 + Karakeep.create_bookmark client ~url:"https://ocaml.org" 30 + ~title:"OCaml Programming Language" () 31 + in 29 32 ]} 30 33 31 - {2 Pagination} 34 + {2 Error Handling} 32 35 33 - The Karakeep API uses pagination to return large result sets. There are two 34 - ways to handle pagination: 36 + All operations may raise [Eio.Io] exceptions with {!E} error payload: 35 37 36 - 1. Manually using {!fetch_bookmarks} with offset/cursor parameters for 37 - fine-grained control 2. Automatically using {!fetch_all_bookmarks} which 38 - handles pagination for you 38 + {[ 39 + try 40 + let bookmarks = Karakeep.fetch_bookmarks client () in 41 + (* ... *) 42 + with 43 + | Eio.Io (Karakeep.E err, _) -> 44 + Printf.eprintf "Karakeep error: %s\n" (Karakeep.error_to_string err) 45 + ]} 39 46 40 47 {2 API Key} 41 48 42 49 All operations require an API key that can be obtained from your Karakeep 43 - instance. *) 50 + instance settings. *) 44 51 45 - (** {1 Core Types} *) 52 + (** {1 Protocol Types} 46 53 47 - type asset_id = string 48 - (** Asset identifier type *) 54 + Re-export all protocol types and codecs from {!Karakeep_proto}. *) 49 55 50 - type bookmark_id = string 51 - (** Bookmark identifier type *) 56 + include module type of Karakeep_proto 52 57 53 - type list_id = string 54 - (** List identifier type *) 58 + (** {1 Error Handling} *) 55 59 56 - type tag_id = string 57 - (** Tag identifier type *) 60 + type error = 61 + | Api_error of { status : int; code : string; message : string } 62 + (** HTTP error from the API *) 63 + | Json_error of { reason : string } (** JSON parsing or encoding error *) 58 64 59 - type highlight_id = string 60 - (** Highlight identifier type *) 65 + (** Eio error type extension *) 66 + type Eio.Exn.err += E of error 61 67 62 - (** Type of content a bookmark can have *) 63 - type bookmark_content_type = 64 - | Link (** A URL to a webpage *) 65 - | Text (** Plain text content *) 66 - | Asset (** An attached asset (image, PDF, etc.) *) 67 - | Unknown (** Unknown content type *) 68 + val err : error -> exn 69 + (** [err e] creates an Eio exception from an error. 70 + Usage: [raise (err (Api_error { status = 404; code = "not_found"; message = "..." }))] *) 68 71 69 - (** Type of asset *) 70 - type asset_type = 71 - | Screenshot (** Screenshot of a webpage *) 72 - | AssetScreenshot (** Screenshot of an asset *) 73 - | BannerImage (** Banner image *) 74 - | FullPageArchive (** Archive of a full webpage *) 75 - | Video (** Video asset *) 76 - | BookmarkAsset (** Generic bookmark asset *) 77 - | PrecrawledArchive (** Pre-crawled archive *) 78 - | Unknown (** Unknown asset type *) 72 + val is_api_error : error -> bool 73 + (** [is_api_error e] returns [true] if the error is an API error. *) 79 74 80 - (** Type of tagging status *) 81 - type tagging_status = 82 - | Success (** Tagging was successful *) 83 - | Failure (** Tagging failed *) 84 - | Pending (** Tagging is pending *) 75 + val is_not_found : error -> bool 76 + (** [is_not_found e] returns [true] if the error is a 404 Not Found error. *) 85 77 86 - val string_of_tagging_status : tagging_status -> string 78 + val error_to_string : error -> string 79 + (** [error_to_string e] returns a human-readable description of the error. *) 87 80 88 - (** Type of bookmark list *) 89 - type list_type = 90 - | Manual (** List is manually managed *) 91 - | Smart (** List is dynamically generated based on a query *) 81 + val pp_error : Format.formatter -> error -> unit 82 + (** Pretty printer for errors. *) 92 83 93 - (** Highlight color *) 94 - type highlight_color = 95 - | Yellow (** Yellow highlight *) 96 - | Red (** Red highlight *) 97 - | Green (** Green highlight *) 98 - | Blue (** Blue highlight *) 84 + (** {1 Client} *) 99 85 100 - (** Type of how a tag was attached *) 101 - type tag_attachment_type = 102 - | AI (** Tag was attached by AI *) 103 - | Human (** Tag was attached by a human *) 86 + type t 87 + (** The Karakeep client type. Wraps a Requests session with the base URL 88 + and authentication. *) 104 89 105 - val string_of_tag_attachment_type : tag_attachment_type -> string 106 - 107 - type link_content = { 108 - url : string; (** The URL of the bookmarked page *) 109 - title : string option; (** Title from the link *) 110 - description : string option; (** Description from the link *) 111 - image_url : string option; (** URL of an image from the link *) 112 - image_asset_id : asset_id option; (** ID of an image asset *) 113 - screenshot_asset_id : asset_id option; (** ID of a screenshot asset *) 114 - full_page_archive_asset_id : asset_id option; 115 - (** ID of a full page archive asset *) 116 - precrawled_archive_asset_id : asset_id option; 117 - (** ID of a pre-crawled archive asset *) 118 - video_asset_id : asset_id option; (** ID of a video asset *) 119 - favicon : string option; (** URL of the favicon *) 120 - html_content : string option; (** HTML content of the page *) 121 - crawled_at : Ptime.t option; (** When the page was crawled *) 122 - author : string option; (** Author of the content *) 123 - publisher : string option; (** Publisher of the content *) 124 - date_published : Ptime.t option; (** When the content was published *) 125 - date_modified : Ptime.t option; (** When the content was last modified *) 126 - } 127 - (** Link content for a bookmark *) 128 - 129 - type text_content = { 130 - text : string; (** The text content *) 131 - source_url : string option; (** Optional source URL for the text *) 132 - } 133 - (** Text content for a bookmark *) 134 - 135 - type asset_content = { 136 - asset_type : [ `Image | `PDF ]; (** Type of the asset *) 137 - asset_id : asset_id; (** ID of the asset *) 138 - file_name : string option; (** Name of the file *) 139 - source_url : string option; (** Source URL for the asset *) 140 - size : int option; (** Size of the asset in bytes *) 141 - content : string option; (** Extracted content from the asset *) 142 - } 143 - (** Asset content for a bookmark *) 144 - 145 - (** Content of a bookmark *) 146 - type content = 147 - | Link of link_content (** Link-type content *) 148 - | Text of text_content (** Text-type content *) 149 - | Asset of asset_content (** Asset-type content *) 150 - | Unknown (** Unknown content type *) 151 - 152 - val title : content -> string 153 - (** [title content] extracts a meaningful title from the bookmark content. 154 - For Link content, it returns the title if available, otherwise the URL. 155 - For Text content, it returns a short excerpt from the text. 156 - For Asset content, it returns the filename if available, otherwise a generic title. 157 - For Unknown content, it returns a generic title. *) 158 - 159 - type asset = { 160 - id : asset_id; (** ID of the asset *) 161 - asset_type : asset_type; (** Type of the asset *) 162 - } 163 - (** Asset attached to a bookmark *) 90 + val create : 91 + sw:Eio.Switch.t -> 92 + env:< clock : _ Eio.Time.clock ; net : _ Eio.Net.t ; fs : Eio.Fs.dir_ty Eio.Path.t ; .. > -> 93 + base_url:string -> 94 + api_key:string -> 95 + t 96 + (** [create ~sw ~env ~base_url ~api_key] creates a new Karakeep client. 164 97 165 - type bookmark_tag = { 166 - id : tag_id; (** ID of the tag *) 167 - name : string; (** Name of the tag *) 168 - attached_by : tag_attachment_type; (** How the tag was attached *) 169 - } 170 - (** Tag with attachment information *) 171 - 172 - type bookmark = { 173 - id : bookmark_id; (** Unique identifier for the bookmark *) 174 - created_at : Ptime.t; (** Timestamp when the bookmark was created *) 175 - modified_at : Ptime.t option; (** Optional timestamp of the last update *) 176 - title : string option; (** Optional title of the bookmarked page *) 177 - archived : bool; (** Whether the bookmark is archived *) 178 - favourited : bool; (** Whether the bookmark is marked as a favorite *) 179 - tagging_status : tagging_status option; (** Status of automatic tagging *) 180 - note : string option; (** Optional user note associated with the bookmark *) 181 - summary : string option; (** Optional AI-generated summary *) 182 - tags : bookmark_tag list; (** Tags associated with the bookmark *) 183 - content : content; (** Content of the bookmark *) 184 - assets : asset list; (** Assets attached to the bookmark *) 185 - } 186 - (** A bookmark from the Karakeep service *) 187 - 188 - val bookmark_title : bookmark -> string 189 - (** [bookmark_title bookmark] returns the best available title for a bookmark. 190 - It prioritizes the bookmark's title field if available, and falls back to 191 - extracting a title from the bookmark's content. *) 192 - 193 - type paginated_bookmarks = { 194 - bookmarks : bookmark list; (** List of bookmarks in the current page *) 195 - next_cursor : string option; (** Optional cursor for fetching the next page *) 196 - } 197 - (** Paginated response of bookmarks *) 198 - 199 - type _list = { 200 - id : list_id; (** ID of the list *) 201 - name : string; (** Name of the list *) 202 - description : string option; (** Optional description of the list *) 203 - icon : string; (** Icon for the list *) 204 - parent_id : list_id option; (** Optional parent list ID *) 205 - list_type : list_type; (** Type of the list *) 206 - query : string option; (** Optional query for smart lists *) 207 - } 208 - (** List in Karakeep *) 209 - 210 - type tag = { 211 - id : tag_id; (** ID of the tag *) 212 - name : string; (** Name of the tag *) 213 - num_bookmarks : int; (** Number of bookmarks with this tag *) 214 - num_bookmarks_by_attached_type : (tag_attachment_type * int) list; 215 - (** Number of bookmarks by attachment type *) 216 - } 217 - (** Tag in Karakeep *) 218 - 219 - type highlight = { 220 - bookmark_id : bookmark_id; (** ID of the bookmark *) 221 - start_offset : int; (** Start position of the highlight *) 222 - end_offset : int; (** End position of the highlight *) 223 - color : highlight_color; (** Color of the highlight *) 224 - text : string option; (** Text of the highlight *) 225 - note : string option; (** Note for the highlight *) 226 - id : highlight_id; (** ID of the highlight *) 227 - user_id : string; (** ID of the user who created the highlight *) 228 - created_at : Ptime.t; (** When the highlight was created *) 229 - } 230 - (** Highlight in Karakeep *) 231 - 232 - type paginated_highlights = { 233 - highlights : highlight list; (** List of highlights in the current page *) 234 - next_cursor : string option; (** Optional cursor for fetching the next page *) 235 - } 236 - (** Paginated response of highlights *) 237 - 238 - type user_info = { 239 - id : string; (** ID of the user *) 240 - name : string option; (** Name of the user *) 241 - email : string option; (** Email of the user *) 242 - } 243 - (** User information *) 244 - 245 - type user_stats = { 246 - num_bookmarks : int; (** Number of bookmarks *) 247 - num_favorites : int; (** Number of favorite bookmarks *) 248 - num_archived : int; (** Number of archived bookmarks *) 249 - num_tags : int; (** Number of tags *) 250 - num_lists : int; (** Number of lists *) 251 - num_highlights : int; (** Number of highlights *) 252 - } 253 - (** User statistics *) 254 - 255 - type error_response = { 256 - code : string; (** Error code *) 257 - message : string; (** Error message *) 258 - } 259 - (** Error response from the API *) 98 + @param sw Switch for resource management 99 + @param env Eio environment providing clock and network 100 + @param base_url Base URL of the Karakeep instance (e.g., "https://hoard.recoil.org") 101 + @param api_key API key for authentication *) 260 102 261 103 (** {1 Bookmark Operations} *) 262 104 263 105 val fetch_bookmarks : 264 - api_key:string -> 106 + t -> 265 107 ?limit:int -> 266 108 ?cursor:string -> 267 109 ?include_content:bool -> 268 110 ?archived:bool -> 269 111 ?favourited:bool -> 270 - string -> 271 - paginated_bookmarks Lwt.t 272 - (** [fetch_bookmarks ~api_key ?limit ?cursor ?include_content ?archived 273 - ?favourited base_url] fetches a page of bookmarks from a Karakeep instance. 274 - 275 - This function provides fine-grained control over pagination with 276 - cursor-based pagination. It returns a {!paginated_bookmarks} that includes 277 - pagination information like the next cursor. 112 + unit -> 113 + paginated_bookmarks 114 + (** [fetch_bookmarks client ()] fetches a page of bookmarks. 278 115 279 - @param api_key API key for authentication 280 116 @param limit Number of bookmarks to fetch per page (default: 50) 281 - @param cursor Optional pagination cursor for cursor-based pagination 117 + @param cursor Optional pagination cursor 282 118 @param include_content Whether to include full content (default: true) 283 - @param archived Whether to filter for archived bookmarks 284 - @param favourited Whether to filter for favourited bookmarks 285 - @param base_url Base URL of the Karakeep instance 286 - @return 287 - A Lwt promise with the bookmark response containing a single page of 288 - results *) 119 + @param archived Filter for archived bookmarks 120 + @param favourited Filter for favourited bookmarks 121 + @raise Eio.Io with {!E} on API or network errors *) 289 122 290 123 val fetch_all_bookmarks : 291 - api_key:string -> 124 + t -> 292 125 ?page_size:int -> 293 126 ?max_pages:int -> 294 127 ?archived:bool -> 295 128 ?favourited:bool -> 296 - string -> 297 - bookmark list Lwt.t 298 - (** [fetch_all_bookmarks ~api_key ?page_size ?max_pages ?archived ?favourited 299 - base_url] fetches all bookmarks from a Karakeep instance, automatically 300 - handling pagination. 129 + unit -> 130 + bookmark list 131 + (** [fetch_all_bookmarks client ()] fetches all bookmarks, handling pagination. 301 132 302 - This function handles pagination internally and returns a flattened list of 303 - all bookmarks. It will continue making API requests until all pages have 304 - been fetched or the max_pages limit is reached. 305 - 306 - Use this function when you: 307 - - Want to retrieve all bookmarks with minimal code 308 - - Don't need pagination metadata 309 - - Are only interested in the bookmarks themselves 310 - 311 - @param api_key API key for authentication 312 - @param page_size Number of bookmarks to fetch per page (default: 50) 313 - @param max_pages Maximum number of pages to fetch (None for all pages) 314 - @param archived Whether to filter for archived bookmarks 315 - @param favourited Whether to filter for favourited bookmarks 316 - @param base_url Base URL of the Karakeep instance 317 - @return A Lwt promise with all bookmarks combined into a single list *) 133 + @param page_size Number of bookmarks per page (default: 50) 134 + @param max_pages Maximum number of pages to fetch 135 + @param archived Filter for archived bookmarks 136 + @param favourited Filter for favourited bookmarks 137 + @raise Eio.Io with {!E} on API or network errors *) 318 138 319 139 val search_bookmarks : 320 - api_key:string -> 140 + t -> 321 141 query:string -> 322 142 ?limit:int -> 323 143 ?cursor:string -> 324 144 ?include_content:bool -> 325 - string -> 326 - paginated_bookmarks Lwt.t 327 - (** [search_bookmarks ~api_key ~query ?limit ?cursor ?include_content base_url] 328 - searches for bookmarks matching a query. 145 + unit -> 146 + paginated_bookmarks 147 + (** [search_bookmarks client ~query ()] searches for bookmarks. 329 148 330 - @param api_key API key for authentication 331 - @param query Search query 332 - @param limit Number of bookmarks to fetch per page (default: 50) 149 + @param query Search query string 150 + @param limit Number of results per page (default: 50) 333 151 @param cursor Optional pagination cursor 334 152 @param include_content Whether to include full content (default: true) 335 - @param base_url Base URL of the Karakeep instance 336 - @return A Lwt promise with the bookmark response containing search results 337 - *) 338 - 339 - val fetch_bookmark_details : 340 - api_key:string -> string -> bookmark_id -> bookmark Lwt.t 341 - (** [fetch_bookmark_details ~api_key base_url bookmark_id] fetches detailed 342 - information for a single bookmark by ID. 153 + @raise Eio.Io with {!E} on API or network errors *) 343 154 344 - This function retrieves complete details for a specific bookmark identified 345 - by its ID. It provides a convenient way to access a single bookmark's 346 - detailed information. 347 - 348 - @param api_key API key for authentication 349 - @param base_url Base URL of the Karakeep instance 350 - @param bookmark_id ID of the bookmark to fetch 351 - @return A Lwt promise with the complete bookmark details *) 155 + val fetch_bookmark_details : t -> bookmark_id -> bookmark 156 + (** [fetch_bookmark_details client id] fetches a single bookmark by ID. 157 + @raise Eio.Io with {!E} on API or network errors *) 352 158 353 159 val create_bookmark : 354 - api_key:string -> 160 + t -> 355 161 url:string -> 356 162 ?title:string -> 357 163 ?note:string -> ··· 360 166 ?archived:bool -> 361 167 ?created_at:Ptime.t -> 362 168 ?tags:string list -> 363 - string -> 364 - bookmark Lwt.t 365 - (** [create_bookmark ~api_key ~url ?title ?note ?summary ?favourited ?archived 366 - ?created_at ?tags base_url] creates a new bookmark in Karakeep. 169 + unit -> 170 + bookmark 171 + (** [create_bookmark client ~url ()] creates a new URL bookmark. 367 172 368 - This function adds a new bookmark to the Karakeep instance for the given 369 - URL. It supports setting various bookmark attributes and adding tags. 370 - 371 - Example: 372 - {[ 373 - let new_bookmark = 374 - create_bookmark ~api_key ~url:"https://ocaml.org" 375 - ~title:"OCaml Programming Language" 376 - ~tags:[ "programming"; "language"; "functional" ] 377 - "https://hoard.recoil.org" 378 - ]} 379 - 380 - @param api_key API key for authentication 381 173 @param url The URL to bookmark 382 - @param title Optional title for the bookmark 383 - @param note Optional note to add to the bookmark 384 - @param summary Optional summary for the bookmark 385 - @param favourited 386 - Whether the bookmark should be marked as favourite (default: false) 387 - @param archived Whether the bookmark should be archived (default: false) 388 - @param created_at Optional timestamp for when the bookmark was created 389 - @param tags Optional list of tag names to add to the bookmark 390 - @param base_url Base URL of the Karakeep instance 391 - @return A Lwt promise with the created bookmark *) 174 + @param title Optional title 175 + @param note Optional note 176 + @param summary Optional summary 177 + @param favourited Whether to mark as favourite 178 + @param archived Whether to archive 179 + @param created_at Optional creation timestamp 180 + @param tags Optional list of tag names to add 181 + @raise Eio.Io with {!E} on API or network errors *) 392 182 393 183 val update_bookmark : 394 - api_key:string -> 184 + t -> 185 + bookmark_id -> 395 186 ?title:string -> 396 187 ?note:string -> 397 188 ?summary:string -> 398 189 ?favourited:bool -> 399 190 ?archived:bool -> 400 - ?url:string -> 401 - ?description:string -> 402 - ?author:string -> 403 - ?publisher:string -> 404 - ?date_published:Ptime.t -> 405 - ?date_modified:Ptime.t -> 406 - ?text:string -> 407 - ?asset_content:string -> 408 - string -> 409 - bookmark_id -> 410 - bookmark Lwt.t 411 - (** [update_bookmark ~api_key ?title ?note ?summary ?favourited ?archived ?url 412 - ?description ?author ?publisher ?date_published ?date_modified ?text 413 - ?asset_content base_url bookmark_id] updates a bookmark by its ID. 191 + unit -> 192 + bookmark 193 + (** [update_bookmark client id ()] updates a bookmark. 194 + @raise Eio.Io with {!E} on API or network errors *) 414 195 415 - This function updates various attributes of an existing bookmark. Only the 416 - fields provided will be updated. 417 - 418 - @param api_key API key for authentication 419 - @param title Optional new title for the bookmark 420 - @param note Optional new note for the bookmark 421 - @param summary Optional new summary for the bookmark 422 - @param favourited Whether the bookmark should be marked as favourite 423 - @param archived Whether the bookmark should be archived 424 - @param url Optional new URL for the bookmark 425 - @param description Optional new description for the bookmark 426 - @param author Optional new author for the bookmark 427 - @param publisher Optional new publisher for the bookmark 428 - @param date_published Optional new publication date for the bookmark 429 - @param date_modified Optional new modification date for the bookmark 430 - @param text Optional new text content for the bookmark 431 - @param asset_content Optional new asset content for the bookmark 432 - @param base_url Base URL of the Karakeep instance 433 - @param bookmark_id ID of the bookmark to update 434 - @return A Lwt promise with the updated bookmark details *) 435 - 436 - val delete_bookmark : api_key:string -> string -> bookmark_id -> unit Lwt.t 437 - (** [delete_bookmark ~api_key base_url bookmark_id] deletes a bookmark by its 438 - ID. 439 - 440 - This function permanently removes a bookmark from the Karakeep instance. 441 - 442 - @param api_key API key for authentication 443 - @param base_url Base URL of the Karakeep instance 444 - @param bookmark_id ID of the bookmark to delete 445 - @return A Lwt promise that completes when the bookmark is deleted *) 446 - 447 - val summarize_bookmark : 448 - api_key:string -> string -> bookmark_id -> bookmark Lwt.t 449 - (** [summarize_bookmark ~api_key base_url bookmark_id] generates a summary for a 450 - bookmark. 451 - 452 - This function uses AI to generate a summary of the bookmark's content. The 453 - summary is added to the bookmark. 196 + val delete_bookmark : t -> bookmark_id -> unit 197 + (** [delete_bookmark client id] deletes a bookmark. 198 + @raise Eio.Io with {!E} on API or network errors *) 454 199 455 - @param api_key API key for authentication 456 - @param base_url Base URL of the Karakeep instance 457 - @param bookmark_id ID of the bookmark to summarize 458 - @return A Lwt promise with the updated bookmark including the summary *) 200 + val summarize_bookmark : t -> bookmark_id -> bookmark 201 + (** [summarize_bookmark client id] generates an AI summary for a bookmark. 202 + @raise Eio.Io with {!E} on API or network errors *) 459 203 460 204 (** {1 Tag Operations} *) 461 205 462 206 val attach_tags : 463 - api_key:string -> 464 - tag_refs:[ `TagId of tag_id | `TagName of string ] list -> 465 - string -> 466 - bookmark_id -> 467 - tag_id list Lwt.t 468 - (** [attach_tags ~api_key ~tag_refs base_url bookmark_id] attaches tags to a 469 - bookmark. 470 - 471 - This function adds one or more tags to a bookmark. Tags can be referred to 472 - either by their ID or by their name. If a tag name is provided and the tag 473 - doesn't exist, it will be created. 474 - 475 - @param api_key API key for authentication 476 - @param tag_refs List of tag references (either by ID or name) 477 - @param base_url Base URL of the Karakeep instance 478 - @param bookmark_id ID of the bookmark to tag 479 - @return A Lwt promise with the list of attached tag IDs *) 207 + t -> tag_refs:[ `TagId of tag_id | `TagName of string ] list -> bookmark_id -> tag_id list 208 + (** [attach_tags client ~tag_refs bookmark_id] attaches tags to a bookmark. 209 + @raise Eio.Io with {!E} on API or network errors *) 480 210 481 211 val detach_tags : 482 - api_key:string -> 483 - tag_refs:[ `TagId of tag_id | `TagName of string ] list -> 484 - string -> 485 - bookmark_id -> 486 - tag_id list Lwt.t 487 - (** [detach_tags ~api_key ~tag_refs base_url bookmark_id] detaches tags from a 488 - bookmark. 489 - 490 - This function removes one or more tags from a bookmark. Tags can be referred 491 - to either by their ID or by their name. 492 - 493 - @param api_key API key for authentication 494 - @param tag_refs List of tag references (either by ID or name) 495 - @param base_url Base URL of the Karakeep instance 496 - @param bookmark_id ID of the bookmark to remove tags from 497 - @return A Lwt promise with the list of detached tag IDs *) 212 + t -> tag_refs:[ `TagId of tag_id | `TagName of string ] list -> bookmark_id -> tag_id list 213 + (** [detach_tags client ~tag_refs bookmark_id] detaches tags from a bookmark. 214 + @raise Eio.Io with {!E} on API or network errors *) 498 215 499 - val fetch_all_tags : api_key:string -> string -> tag list Lwt.t 500 - (** [fetch_all_tags ~api_key base_url] fetches all tags. 216 + val fetch_all_tags : t -> tag list 217 + (** [fetch_all_tags client] fetches all tags. 218 + @raise Eio.Io with {!E} on API or network errors *) 501 219 502 - This function retrieves all tags from the Karakeep instance. 503 - 504 - @param api_key API key for authentication 505 - @param base_url Base URL of the Karakeep instance 506 - @return A Lwt promise with the list of all tags *) 507 - 508 - val fetch_tag_details : api_key:string -> string -> tag_id -> tag Lwt.t 509 - (** [fetch_tag_details ~api_key base_url tag_id] fetches detailed information 510 - for a single tag by ID. 511 - 512 - This function retrieves complete details for a specific tag identified by 513 - its ID. 514 - 515 - @param api_key API key for authentication 516 - @param base_url Base URL of the Karakeep instance 517 - @param tag_id ID of the tag to fetch 518 - @return A Lwt promise with the complete tag details *) 220 + val fetch_tag_details : t -> tag_id -> tag 221 + (** [fetch_tag_details client id] fetches a single tag by ID. 222 + @raise Eio.Io with {!E} on API or network errors *) 519 223 520 224 val fetch_bookmarks_with_tag : 521 - api_key:string -> 225 + t -> 522 226 ?limit:int -> 523 227 ?cursor:string -> 524 228 ?include_content:bool -> 525 - string -> 526 229 tag_id -> 527 - paginated_bookmarks Lwt.t 528 - (** [fetch_bookmarks_with_tag ~api_key ?limit ?cursor ?include_content base_url 529 - tag_id] fetches bookmarks with a specific tag. 230 + paginated_bookmarks 231 + (** [fetch_bookmarks_with_tag client tag_id] fetches bookmarks with a tag. 232 + @raise Eio.Io with {!E} on API or network errors *) 530 233 531 - This function retrieves bookmarks that have been tagged with a specific tag. 234 + val update_tag : t -> name:string -> tag_id -> tag 235 + (** [update_tag client ~name tag_id] updates a tag's name. 236 + @raise Eio.Io with {!E} on API or network errors *) 532 237 533 - @param api_key API key for authentication 534 - @param limit Number of bookmarks to fetch per page (default: 50) 535 - @param cursor Optional pagination cursor 536 - @param include_content Whether to include full content (default: true) 537 - @param base_url Base URL of the Karakeep instance 538 - @param tag_id ID of the tag to filter by 539 - @return 540 - A Lwt promise with the bookmark response containing bookmarks with the tag 541 - *) 542 - 543 - val update_tag : api_key:string -> name:string -> string -> tag_id -> tag Lwt.t 544 - (** [update_tag ~api_key ~name base_url tag_id] updates a tag's name. 545 - 546 - This function changes the name of an existing tag. 547 - 548 - @param api_key API key for authentication 549 - @param name New name for the tag 550 - @param base_url Base URL of the Karakeep instance 551 - @param tag_id ID of the tag to update 552 - @return A Lwt promise with the updated tag details *) 553 - 554 - val delete_tag : api_key:string -> string -> tag_id -> unit Lwt.t 555 - (** [delete_tag ~api_key base_url tag_id] deletes a tag by its ID. 556 - 557 - This function permanently removes a tag from the Karakeep instance and 558 - detaches it from all bookmarks. 559 - 560 - @param api_key API key for authentication 561 - @param base_url Base URL of the Karakeep instance 562 - @param tag_id ID of the tag to delete 563 - @return A Lwt promise that completes when the tag is deleted *) 238 + val delete_tag : t -> tag_id -> unit 239 + (** [delete_tag client id] deletes a tag. 240 + @raise Eio.Io with {!E} on API or network errors *) 564 241 565 242 (** {1 List Operations} *) 566 243 567 - val fetch_all_lists : api_key:string -> string -> _list list Lwt.t 568 - (** [fetch_all_lists ~api_key base_url] fetches all lists. 569 - 570 - This function retrieves all lists from the Karakeep instance. 571 - 572 - @param api_key API key for authentication 573 - @param base_url Base URL of the Karakeep instance 574 - @return A Lwt promise with the list of all lists *) 575 - 576 - val fetch_list_details : api_key:string -> string -> list_id -> _list Lwt.t 577 - (** [fetch_list_details ~api_key base_url list_id] fetches detailed information 578 - for a single list by ID. 244 + val fetch_all_lists : t -> _list list 245 + (** [fetch_all_lists client] fetches all lists. 246 + @raise Eio.Io with {!E} on API or network errors *) 579 247 580 - This function retrieves complete details for a specific list identified by 581 - its ID. 582 - 583 - @param api_key API key for authentication 584 - @param base_url Base URL of the Karakeep instance 585 - @param list_id ID of the list to fetch 586 - @return A Lwt promise with the complete list details *) 248 + val fetch_list_details : t -> list_id -> _list 249 + (** [fetch_list_details client id] fetches a single list by ID. 250 + @raise Eio.Io with {!E} on API or network errors *) 587 251 588 252 val create_list : 589 - api_key:string -> 253 + t -> 590 254 name:string -> 591 255 icon:string -> 592 256 ?description:string -> 593 257 ?parent_id:list_id -> 594 258 ?list_type:list_type -> 595 259 ?query:string -> 596 - string -> 597 - _list Lwt.t 598 - (** [create_list ~api_key ~name ~icon ?description ?parent_id ?list_type ?query 599 - base_url] creates a new list in Karakeep. 600 - 601 - This function adds a new list to the Karakeep instance. Lists can be 602 - hierarchical with parent-child relationships, and can be either manual or 603 - smart (query-based). 604 - 605 - @param api_key API key for authentication 606 - @param name Name of the list (max 40 characters) 607 - @param icon Icon for the list 608 - @param description Optional description for the list (max 100 characters) 609 - @param parent_id Optional parent list ID for hierarchical organization 610 - @param list_type Type of the list (default: Manual) 611 - @param query Optional query string for smart lists 612 - @param base_url Base URL of the Karakeep instance 613 - @return A Lwt promise with the created list *) 260 + unit -> 261 + _list 262 + (** [create_list client ~name ~icon ()] creates a new list. 263 + @raise Eio.Io with {!E} on API or network errors *) 614 264 615 265 val update_list : 616 - api_key:string -> 266 + t -> 617 267 ?name:string -> 618 268 ?description:string -> 619 269 ?icon:string -> 620 270 ?parent_id:list_id option -> 621 271 ?query:string -> 622 - string -> 623 272 list_id -> 624 - _list Lwt.t 625 - (** [update_list ~api_key ?name ?description ?icon ?parent_id ?query base_url 626 - list_id] updates a list by its ID. 627 - 628 - This function updates various attributes of an existing list. Only the 629 - fields provided will be updated. 630 - 631 - @param api_key API key for authentication 632 - @param name Optional new name for the list 633 - @param description Optional new description for the list 634 - @param icon Optional new icon for the list 635 - @param parent_id Optional new parent list ID (use None to remove the parent) 636 - @param query Optional new query for smart lists 637 - @param base_url Base URL of the Karakeep instance 638 - @param list_id ID of the list to update 639 - @return A Lwt promise with the updated list details *) 640 - 641 - val delete_list : api_key:string -> string -> list_id -> unit Lwt.t 642 - (** [delete_list ~api_key base_url list_id] deletes a list by its ID. 273 + _list 274 + (** [update_list client list_id] updates a list. 275 + @raise Eio.Io with {!E} on API or network errors *) 643 276 644 - This function permanently removes a list from the Karakeep instance. Note 645 - that this does not delete the bookmarks in the list. 646 - 647 - @param api_key API key for authentication 648 - @param base_url Base URL of the Karakeep instance 649 - @param list_id ID of the list to delete 650 - @return A Lwt promise that completes when the list is deleted *) 277 + val delete_list : t -> list_id -> unit 278 + (** [delete_list client id] deletes a list. 279 + @raise Eio.Io with {!E} on API or network errors *) 651 280 652 281 val fetch_bookmarks_in_list : 653 - api_key:string -> 282 + t -> 654 283 ?limit:int -> 655 284 ?cursor:string -> 656 285 ?include_content:bool -> 657 - string -> 658 286 list_id -> 659 - paginated_bookmarks Lwt.t 660 - (** [fetch_bookmarks_in_list ~api_key ?limit ?cursor ?include_content base_url 661 - list_id] fetches bookmarks in a specific list. 287 + paginated_bookmarks 288 + (** [fetch_bookmarks_in_list client list_id] fetches bookmarks in a list. 289 + @raise Eio.Io with {!E} on API or network errors *) 662 290 663 - This function retrieves bookmarks that have been added to a specific list. 291 + val add_bookmark_to_list : t -> list_id -> bookmark_id -> unit 292 + (** [add_bookmark_to_list client list_id bookmark_id] adds a bookmark to a list. 293 + @raise Eio.Io with {!E} on API or network errors *) 664 294 665 - @param api_key API key for authentication 666 - @param limit Number of bookmarks to fetch per page (default: 50) 667 - @param cursor Optional pagination cursor 668 - @param include_content Whether to include full content (default: true) 669 - @param base_url Base URL of the Karakeep instance 670 - @param list_id ID of the list to get bookmarks from 671 - @return 672 - A Lwt promise with the bookmark response containing bookmarks in the list 673 - *) 674 - 675 - val add_bookmark_to_list : 676 - api_key:string -> string -> list_id -> bookmark_id -> unit Lwt.t 677 - (** [add_bookmark_to_list ~api_key base_url list_id bookmark_id] adds a bookmark 678 - to a list. 679 - 680 - This function adds a bookmark to a manual list. Smart lists cannot be 681 - directly modified. 682 - 683 - @param api_key API key for authentication 684 - @param base_url Base URL of the Karakeep instance 685 - @param list_id ID of the list to add the bookmark to 686 - @param bookmark_id ID of the bookmark to add 687 - @return A Lwt promise that completes when the bookmark is added to the list 688 - *) 689 - 690 - val remove_bookmark_from_list : 691 - api_key:string -> string -> list_id -> bookmark_id -> unit Lwt.t 692 - (** [remove_bookmark_from_list ~api_key base_url list_id bookmark_id] removes a 693 - bookmark from a list. 694 - 695 - This function removes a bookmark from a manual list. Smart lists cannot be 696 - directly modified. 697 - 698 - @param api_key API key for authentication 699 - @param base_url Base URL of the Karakeep instance 700 - @param list_id ID of the list to remove the bookmark from 701 - @param bookmark_id ID of the bookmark to remove 702 - @return 703 - A Lwt promise that completes when the bookmark is removed from the list *) 295 + val remove_bookmark_from_list : t -> list_id -> bookmark_id -> unit 296 + (** [remove_bookmark_from_list client list_id bookmark_id] removes a bookmark from a list. 297 + @raise Eio.Io with {!E} on API or network errors *) 704 298 705 299 (** {1 Highlight Operations} *) 706 300 707 301 val fetch_all_highlights : 708 - api_key:string -> 709 - ?limit:int -> 710 - ?cursor:string -> 711 - string -> 712 - paginated_highlights Lwt.t 713 - (** [fetch_all_highlights ~api_key ?limit ?cursor base_url] fetches all 714 - highlights. 715 - 716 - This function retrieves highlights from the Karakeep instance with 717 - pagination. 718 - 719 - @param api_key API key for authentication 720 - @param limit Number of highlights to fetch per page (default: 50) 721 - @param cursor Optional pagination cursor 722 - @param base_url Base URL of the Karakeep instance 723 - @return A Lwt promise with the paginated highlights response *) 724 - 725 - val fetch_bookmark_highlights : 726 - api_key:string -> string -> bookmark_id -> highlight list Lwt.t 727 - (** [fetch_bookmark_highlights ~api_key base_url bookmark_id] fetches highlights 728 - for a specific bookmark. 729 - 730 - This function retrieves all highlights that have been created for a specific 731 - bookmark. 732 - 733 - @param api_key API key for authentication 734 - @param base_url Base URL of the Karakeep instance 735 - @param bookmark_id ID of the bookmark to get highlights for 736 - @return A Lwt promise with the list of highlights for the bookmark *) 737 - 738 - val fetch_highlight_details : 739 - api_key:string -> string -> highlight_id -> highlight Lwt.t 740 - (** [fetch_highlight_details ~api_key base_url highlight_id] fetches detailed 741 - information for a single highlight by ID. 302 + t -> ?limit:int -> ?cursor:string -> unit -> paginated_highlights 303 + (** [fetch_all_highlights client ()] fetches all highlights with pagination. 304 + @raise Eio.Io with {!E} on API or network errors *) 742 305 743 - This function retrieves complete details for a specific highlight identified 744 - by its ID. 306 + val fetch_bookmark_highlights : t -> bookmark_id -> highlight list 307 + (** [fetch_bookmark_highlights client bookmark_id] fetches highlights for a bookmark. 308 + @raise Eio.Io with {!E} on API or network errors *) 745 309 746 - @param api_key API key for authentication 747 - @param base_url Base URL of the Karakeep instance 748 - @param highlight_id ID of the highlight to fetch 749 - @return A Lwt promise with the complete highlight details *) 310 + val fetch_highlight_details : t -> highlight_id -> highlight 311 + (** [fetch_highlight_details client id] fetches a single highlight by ID. 312 + @raise Eio.Io with {!E} on API or network errors *) 750 313 751 314 val create_highlight : 752 - api_key:string -> 315 + t -> 753 316 bookmark_id:bookmark_id -> 754 317 start_offset:int -> 755 318 end_offset:int -> 756 319 text:string -> 757 320 ?note:string -> 758 321 ?color:highlight_color -> 759 - string -> 760 - highlight Lwt.t 761 - (** [create_highlight ~api_key ~bookmark_id ~start_offset ~end_offset ~text 762 - ?note ?color base_url] creates a new highlight in Karakeep. 763 - 764 - This function adds a new highlight to a bookmark in the Karakeep instance. 765 - Highlights mark specific portions of the bookmark's content. 766 - 767 - @param api_key API key for authentication 768 - @param bookmark_id ID of the bookmark to highlight 769 - @param start_offset Starting position of the highlight 770 - @param end_offset Ending position of the highlight 771 - @param text Text content of the highlight 772 - @param note Optional note for the highlight 773 - @param color Color of the highlight (default: Yellow) 774 - @param base_url Base URL of the Karakeep instance 775 - @return A Lwt promise with the created highlight *) 776 - 777 - val update_highlight : 778 - api_key:string -> 779 - ?color:highlight_color -> 780 - string -> 781 - highlight_id -> 782 - highlight Lwt.t 783 - (** [update_highlight ~api_key ?color base_url highlight_id] updates a highlight 784 - by its ID. 785 - 786 - This function updates the color of an existing highlight. 787 - 788 - @param api_key API key for authentication 789 - @param color New color for the highlight 790 - @param base_url Base URL of the Karakeep instance 791 - @param highlight_id ID of the highlight to update 792 - @return A Lwt promise with the updated highlight details *) 793 - 794 - val delete_highlight : api_key:string -> string -> highlight_id -> unit Lwt.t 795 - (** [delete_highlight ~api_key base_url highlight_id] deletes a highlight by its 796 - ID. 322 + unit -> 323 + highlight 324 + (** [create_highlight client ~bookmark_id ~start_offset ~end_offset ~text ()] 325 + creates a new highlight. 326 + @raise Eio.Io with {!E} on API or network errors *) 797 327 798 - This function permanently removes a highlight from the Karakeep instance. 328 + val update_highlight : t -> ?color:highlight_color -> highlight_id -> highlight 329 + (** [update_highlight client highlight_id] updates a highlight. 330 + @raise Eio.Io with {!E} on API or network errors *) 799 331 800 - @param api_key API key for authentication 801 - @param base_url Base URL of the Karakeep instance 802 - @param highlight_id ID of the highlight to delete 803 - @return A Lwt promise that completes when the highlight is deleted *) 332 + val delete_highlight : t -> highlight_id -> unit 333 + (** [delete_highlight client id] deletes a highlight. 334 + @raise Eio.Io with {!E} on API or network errors *) 804 335 805 336 (** {1 Asset Operations} *) 806 337 807 - val fetch_asset : api_key:string -> string -> asset_id -> string Lwt.t 808 - (** [fetch_asset ~api_key base_url asset_id] fetches an asset from the Karakeep 809 - server as a binary string. 810 - 811 - Assets can include images, PDFs, or other files attached to bookmarks. This 812 - function retrieves the binary content of an asset by its ID. 338 + val fetch_asset : t -> asset_id -> string 339 + (** [fetch_asset client id] fetches an asset's binary data. 340 + @raise Eio.Io with {!E} on API or network errors *) 813 341 814 - @param api_key API key for authentication 815 - @param base_url Base URL of the Karakeep instance 816 - @param asset_id ID of the asset to fetch 817 - @return A Lwt promise with the binary asset data *) 818 - 819 - val get_asset_url : string -> asset_id -> string 820 - (** [get_asset_url base_url asset_id] returns the full URL for a given asset ID. 821 - 822 - This is a pure function that constructs the URL for an asset without making 823 - any API calls. The returned URL can be used to access the asset directly, 824 - assuming proper authentication. 825 - 826 - @param base_url Base URL of the Karakeep instance 827 - @param asset_id ID of the asset 828 - @return The full URL to the asset *) 342 + val get_asset_url : t -> asset_id -> string 343 + (** [get_asset_url client id] returns the URL for an asset. Pure function. *) 829 344 830 345 val attach_asset : 831 - api_key:string -> 832 - asset_id:asset_id -> 833 - asset_type:asset_type -> 834 - string -> 835 - bookmark_id -> 836 - asset Lwt.t 837 - (** [attach_asset ~api_key ~asset_id ~asset_type base_url bookmark_id] attaches 838 - an asset to a bookmark. 839 - 840 - This function adds an existing asset to a bookmark. 841 - 842 - @param api_key API key for authentication 843 - @param asset_id ID of the asset to attach 844 - @param asset_type Type of the asset 845 - @param base_url Base URL of the Karakeep instance 846 - @param bookmark_id ID of the bookmark to attach the asset to 847 - @return A Lwt promise with the attached asset details *) 848 - 849 - val replace_asset : 850 - api_key:string -> 851 - new_asset_id:asset_id -> 852 - string -> 853 - bookmark_id -> 854 - asset_id -> 855 - unit Lwt.t 856 - (** [replace_asset ~api_key ~new_asset_id base_url bookmark_id asset_id] 857 - replaces an asset on a bookmark with a new one. 858 - 859 - This function replaces an existing asset on a bookmark with a different 860 - asset. 861 - 862 - @param api_key API key for authentication 863 - @param new_asset_id ID of the new asset 864 - @param base_url Base URL of the Karakeep instance 865 - @param bookmark_id ID of the bookmark 866 - @param asset_id ID of the asset to replace 867 - @return A Lwt promise that completes when the asset is replaced *) 346 + t -> asset_id:asset_id -> asset_type:asset_type -> bookmark_id -> asset 347 + (** [attach_asset client ~asset_id ~asset_type bookmark_id] attaches an asset. 348 + @raise Eio.Io with {!E} on API or network errors *) 868 349 869 - val detach_asset : 870 - api_key:string -> string -> bookmark_id -> asset_id -> unit Lwt.t 871 - (** [detach_asset ~api_key base_url bookmark_id asset_id] detaches an asset from 872 - a bookmark. 350 + val replace_asset : t -> new_asset_id:asset_id -> bookmark_id -> asset_id -> unit 351 + (** [replace_asset client ~new_asset_id bookmark_id asset_id] replaces an asset. 352 + @raise Eio.Io with {!E} on API or network errors *) 873 353 874 - This function removes an asset from a bookmark. 875 - 876 - @param api_key API key for authentication 877 - @param base_url Base URL of the Karakeep instance 878 - @param bookmark_id ID of the bookmark 879 - @param asset_id ID of the asset to detach 880 - @return A Lwt promise that completes when the asset is detached *) 354 + val detach_asset : t -> bookmark_id -> asset_id -> unit 355 + (** [detach_asset client bookmark_id asset_id] detaches an asset. 356 + @raise Eio.Io with {!E} on API or network errors *) 881 357 882 358 (** {1 User Operations} *) 883 359 884 - val get_current_user : api_key:string -> string -> user_info Lwt.t 885 - (** [get_current_user ~api_key base_url] gets information about the current 886 - user. 360 + val get_current_user : t -> user_info 361 + (** [get_current_user client] gets the current user's info. 362 + @raise Eio.Io with {!E} on API or network errors *) 887 363 888 - This function retrieves details about the authenticated user. 889 - 890 - @param api_key API key for authentication 891 - @param base_url Base URL of the Karakeep instance 892 - @return A Lwt promise with the user information *) 893 - 894 - val get_user_stats : api_key:string -> string -> user_stats Lwt.t 895 - (** [get_user_stats ~api_key base_url] gets statistics about the current user. 896 - 897 - This function retrieves statistics about the authenticated user, such as the 898 - number of bookmarks, tags, lists, etc. 899 - 900 - @param api_key API key for authentication 901 - @param base_url Base URL of the Karakeep instance 902 - @return A Lwt promise with the user statistics *) 364 + val get_user_stats : t -> user_stats 365 + (** [get_user_stats client] gets the current user's statistics. 366 + @raise Eio.Io with {!E} on API or network errors *)
+4
lib/proto/dune
··· 1 + (library 2 + (name karakeep_proto) 3 + (public_name karakeep.proto) 4 + (libraries jsont ptime))
+741
lib/proto/karakeep_proto.ml
··· 1 + (** Karakeep API protocol types and JSON codecs *) 2 + 3 + (** {1 Helper Codecs} *) 4 + 5 + let ptime_jsont = 6 + let dec s = 7 + match Ptime.of_rfc3339 s with 8 + | Ok (t, _, _) -> Ok t 9 + | Error _ -> Error (Printf.sprintf "Invalid timestamp: %s" s) 10 + in 11 + let enc t = 12 + let (y, m, d), ((hh, mm, ss), _) = Ptime.to_date_time t in 13 + Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ" y m d hh mm ss 14 + in 15 + Jsont.of_of_string ~kind:"Ptime.t" dec ~enc 16 + 17 + let ptime_option_jsont = 18 + let null = Jsont.null None in 19 + let some = Jsont.map ~dec:(fun t -> Some t) ~enc:(function Some t -> t | None -> assert false) ptime_jsont in 20 + Jsont.any ~dec_null:null ~dec_string:some ~enc:(function None -> null | Some _ -> some) () 21 + 22 + (** {1 ID Types} *) 23 + 24 + type asset_id = string 25 + type bookmark_id = string 26 + type list_id = string 27 + type tag_id = string 28 + type highlight_id = string 29 + 30 + (** {1 Enum Types} *) 31 + 32 + type bookmark_content_type = Link | Text | Asset | Unknown 33 + 34 + let bookmark_content_type_jsont = 35 + let dec s = 36 + match String.lowercase_ascii s with 37 + | "link" -> Ok Link 38 + | "text" -> Ok Text 39 + | "asset" -> Ok Asset 40 + | _ -> Ok Unknown 41 + in 42 + let enc = function 43 + | Link -> "link" 44 + | Text -> "text" 45 + | Asset -> "asset" 46 + | Unknown -> "unknown" 47 + in 48 + Jsont.of_of_string ~kind:"bookmark_content_type" dec ~enc 49 + 50 + type asset_type = 51 + | Screenshot 52 + | AssetScreenshot 53 + | BannerImage 54 + | FullPageArchive 55 + | Video 56 + | BookmarkAsset 57 + | PrecrawledArchive 58 + | Unknown 59 + 60 + let asset_type_jsont = 61 + let dec s = 62 + match s with 63 + | "screenshot" -> Ok Screenshot 64 + | "assetScreenshot" -> Ok AssetScreenshot 65 + | "bannerImage" -> Ok BannerImage 66 + | "fullPageArchive" -> Ok FullPageArchive 67 + | "video" -> Ok Video 68 + | "bookmarkAsset" -> Ok BookmarkAsset 69 + | "precrawledArchive" -> Ok PrecrawledArchive 70 + | _ -> Ok Unknown 71 + in 72 + let enc = function 73 + | Screenshot -> "screenshot" 74 + | AssetScreenshot -> "assetScreenshot" 75 + | BannerImage -> "bannerImage" 76 + | FullPageArchive -> "fullPageArchive" 77 + | Video -> "video" 78 + | BookmarkAsset -> "bookmarkAsset" 79 + | PrecrawledArchive -> "precrawledArchive" 80 + | Unknown -> "unknown" 81 + in 82 + Jsont.of_of_string ~kind:"asset_type" dec ~enc 83 + 84 + type tagging_status = Success | Failure | Pending 85 + 86 + let tagging_status_jsont = 87 + let dec s = 88 + match String.lowercase_ascii s with 89 + | "success" -> Ok Success 90 + | "failure" -> Ok Failure 91 + | "pending" -> Ok Pending 92 + | _ -> Ok Pending 93 + in 94 + let enc = function 95 + | Success -> "success" 96 + | Failure -> "failure" 97 + | Pending -> "pending" 98 + in 99 + Jsont.of_of_string ~kind:"tagging_status" dec ~enc 100 + 101 + let string_of_tagging_status = function 102 + | Success -> "success" 103 + | Failure -> "failure" 104 + | Pending -> "pending" 105 + 106 + type list_type = Manual | Smart 107 + 108 + let list_type_jsont = 109 + let dec s = 110 + match String.lowercase_ascii s with 111 + | "manual" -> Ok Manual 112 + | "smart" -> Ok Smart 113 + | _ -> Ok Manual 114 + in 115 + let enc = function Manual -> "manual" | Smart -> "smart" 116 + in 117 + Jsont.of_of_string ~kind:"list_type" dec ~enc 118 + 119 + type highlight_color = Yellow | Red | Green | Blue 120 + 121 + let highlight_color_jsont = 122 + let dec s = 123 + match String.lowercase_ascii s with 124 + | "yellow" -> Ok Yellow 125 + | "red" -> Ok Red 126 + | "green" -> Ok Green 127 + | "blue" -> Ok Blue 128 + | _ -> Ok Yellow 129 + in 130 + let enc = function 131 + | Yellow -> "yellow" 132 + | Red -> "red" 133 + | Green -> "green" 134 + | Blue -> "blue" 135 + in 136 + Jsont.of_of_string ~kind:"highlight_color" dec ~enc 137 + 138 + let string_of_highlight_color = function 139 + | Yellow -> "yellow" 140 + | Red -> "red" 141 + | Green -> "green" 142 + | Blue -> "blue" 143 + 144 + type tag_attachment_type = AI | Human 145 + 146 + let tag_attachment_type_jsont = 147 + let dec s = 148 + match String.lowercase_ascii s with 149 + | "ai" -> Ok AI 150 + | "human" -> Ok Human 151 + | _ -> Ok Human 152 + in 153 + let enc = function AI -> "ai" | Human -> "human" 154 + in 155 + Jsont.of_of_string ~kind:"tag_attachment_type" dec ~enc 156 + 157 + let string_of_tag_attachment_type = function AI -> "ai" | Human -> "human" 158 + 159 + (** {1 Content Types} *) 160 + 161 + type link_content = { 162 + url : string; 163 + title : string option; 164 + description : string option; 165 + image_url : string option; 166 + image_asset_id : asset_id option; 167 + screenshot_asset_id : asset_id option; 168 + full_page_archive_asset_id : asset_id option; 169 + precrawled_archive_asset_id : asset_id option; 170 + video_asset_id : asset_id option; 171 + favicon : string option; 172 + html_content : string option; 173 + crawled_at : Ptime.t option; 174 + author : string option; 175 + publisher : string option; 176 + date_published : Ptime.t option; 177 + date_modified : Ptime.t option; 178 + } 179 + 180 + (** Helper codec for optional string that handles both absent members and null values *) 181 + let string_option = Jsont.option Jsont.string 182 + 183 + let link_content_jsont = 184 + let make url title description image_url image_asset_id screenshot_asset_id 185 + full_page_archive_asset_id precrawled_archive_asset_id video_asset_id 186 + favicon html_content crawled_at author publisher date_published date_modified = 187 + { url; title; description; image_url; image_asset_id; screenshot_asset_id; 188 + full_page_archive_asset_id; precrawled_archive_asset_id; video_asset_id; 189 + favicon; html_content; crawled_at; author; publisher; date_published; date_modified } 190 + in 191 + Jsont.Object.map ~kind:"link_content" make 192 + |> Jsont.Object.mem "url" Jsont.string ~enc:(fun l -> l.url) 193 + |> Jsont.Object.mem "title" string_option ~dec_absent:None ~enc:(fun l -> l.title) 194 + |> Jsont.Object.mem "description" string_option ~dec_absent:None ~enc:(fun l -> l.description) 195 + |> Jsont.Object.mem "imageUrl" string_option ~dec_absent:None ~enc:(fun l -> l.image_url) 196 + |> Jsont.Object.mem "imageAssetId" string_option ~dec_absent:None ~enc:(fun l -> l.image_asset_id) 197 + |> Jsont.Object.mem "screenshotAssetId" string_option ~dec_absent:None ~enc:(fun l -> l.screenshot_asset_id) 198 + |> Jsont.Object.mem "fullPageArchiveAssetId" string_option ~dec_absent:None ~enc:(fun l -> l.full_page_archive_asset_id) 199 + |> Jsont.Object.mem "precrawledArchiveAssetId" string_option ~dec_absent:None ~enc:(fun l -> l.precrawled_archive_asset_id) 200 + |> Jsont.Object.mem "videoAssetId" string_option ~dec_absent:None ~enc:(fun l -> l.video_asset_id) 201 + |> Jsont.Object.mem "favicon" string_option ~dec_absent:None ~enc:(fun l -> l.favicon) 202 + |> Jsont.Object.mem "htmlContent" string_option ~dec_absent:None ~enc:(fun l -> l.html_content) 203 + |> Jsont.Object.mem "crawledAt" ptime_option_jsont ~dec_absent:None ~enc:(fun l -> l.crawled_at) 204 + |> Jsont.Object.mem "author" string_option ~dec_absent:None ~enc:(fun l -> l.author) 205 + |> Jsont.Object.mem "publisher" string_option ~dec_absent:None ~enc:(fun l -> l.publisher) 206 + |> Jsont.Object.mem "datePublished" ptime_option_jsont ~dec_absent:None ~enc:(fun l -> l.date_published) 207 + |> Jsont.Object.mem "dateModified" ptime_option_jsont ~dec_absent:None ~enc:(fun l -> l.date_modified) 208 + |> Jsont.Object.finish 209 + 210 + type text_content = { 211 + text : string; 212 + source_url : string option; 213 + } 214 + 215 + let text_content_jsont = 216 + let make text source_url = { text; source_url } in 217 + Jsont.Object.map ~kind:"text_content" make 218 + |> Jsont.Object.mem "text" Jsont.string ~enc:(fun t -> t.text) 219 + |> Jsont.Object.mem "sourceUrl" string_option ~dec_absent:None ~enc:(fun t -> t.source_url) 220 + |> Jsont.Object.finish 221 + 222 + type asset_content = { 223 + asset_type : [ `Image | `PDF ]; 224 + asset_id : asset_id; 225 + file_name : string option; 226 + source_url : string option; 227 + size : int option; 228 + content : string option; 229 + } 230 + 231 + let asset_content_type_jsont = 232 + let dec s = 233 + match String.lowercase_ascii s with 234 + | "image" -> Ok `Image 235 + | "pdf" -> Ok `PDF 236 + | _ -> Ok `Image 237 + in 238 + let enc = function `Image -> "image" | `PDF -> "pdf" 239 + in 240 + Jsont.of_of_string ~kind:"asset_content_type" dec ~enc 241 + 242 + let int_option = Jsont.option Jsont.int 243 + 244 + let asset_content_jsont = 245 + let make asset_type asset_id file_name source_url size content = 246 + { asset_type; asset_id; file_name; source_url; size; content } 247 + in 248 + Jsont.Object.map ~kind:"asset_content" make 249 + |> Jsont.Object.mem "assetType" asset_content_type_jsont ~enc:(fun a -> a.asset_type) 250 + |> Jsont.Object.mem "assetId" Jsont.string ~enc:(fun a -> a.asset_id) 251 + |> Jsont.Object.mem "fileName" string_option ~dec_absent:None ~enc:(fun a -> a.file_name) 252 + |> Jsont.Object.mem "sourceUrl" string_option ~dec_absent:None ~enc:(fun a -> a.source_url) 253 + |> Jsont.Object.mem "size" int_option ~dec_absent:None ~enc:(fun a -> a.size) 254 + |> Jsont.Object.mem "content" string_option ~dec_absent:None ~enc:(fun a -> a.content) 255 + |> Jsont.Object.finish 256 + 257 + type content = 258 + | Link of link_content 259 + | Text of text_content 260 + | Asset of asset_content 261 + | Unknown 262 + 263 + (* Content is represented as an object with a "type" field discriminator *) 264 + let content_jsont = 265 + let link_case = Jsont.Object.Case.map "link" link_content_jsont ~dec:(fun l -> Link l) in 266 + let text_case = Jsont.Object.Case.map "text" text_content_jsont ~dec:(fun t -> Text t) in 267 + let asset_case = Jsont.Object.Case.map "asset" asset_content_jsont ~dec:(fun a -> Asset a) in 268 + let enc_case = function 269 + | Link l -> Jsont.Object.Case.value link_case l 270 + | Text t -> Jsont.Object.Case.value text_case t 271 + | Asset a -> Jsont.Object.Case.value asset_case a 272 + | Unknown -> Jsont.Object.Case.value link_case { url = ""; title = None; description = None; 273 + image_url = None; image_asset_id = None; screenshot_asset_id = None; 274 + full_page_archive_asset_id = None; precrawled_archive_asset_id = None; 275 + video_asset_id = None; favicon = None; html_content = None; crawled_at = None; 276 + author = None; publisher = None; date_published = None; date_modified = None } 277 + in 278 + let cases = Jsont.Object.Case.[make link_case; make text_case; make asset_case] in 279 + Jsont.Object.map ~kind:"content" Fun.id 280 + |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases 281 + |> Jsont.Object.finish 282 + 283 + let title content = 284 + match content with 285 + | Link l -> Option.value l.title ~default:l.url 286 + | Text t -> 287 + let excerpt = if String.length t.text > 50 then String.sub t.text 0 50 ^ "..." else t.text in 288 + excerpt 289 + | Asset a -> Option.value a.file_name ~default:"Asset" 290 + | Unknown -> "Unknown content" 291 + 292 + (** {1 Resource Types} *) 293 + 294 + type asset = { 295 + id : asset_id; 296 + asset_type : asset_type; 297 + } 298 + 299 + let asset_jsont = 300 + let make id asset_type = { id; asset_type } in 301 + Jsont.Object.map ~kind:"asset" make 302 + |> Jsont.Object.mem "id" Jsont.string ~enc:(fun a -> a.id) 303 + |> Jsont.Object.mem "assetType" asset_type_jsont ~enc:(fun a -> a.asset_type) 304 + |> Jsont.Object.finish 305 + 306 + type bookmark_tag = { 307 + id : tag_id; 308 + name : string; 309 + attached_by : tag_attachment_type; 310 + } 311 + 312 + let bookmark_tag_jsont = 313 + let make id name attached_by = { id; name; attached_by } in 314 + Jsont.Object.map ~kind:"bookmark_tag" make 315 + |> Jsont.Object.mem "id" Jsont.string ~enc:(fun t -> t.id) 316 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun t -> t.name) 317 + |> Jsont.Object.mem "attachedBy" tag_attachment_type_jsont ~enc:(fun t -> t.attached_by) 318 + |> Jsont.Object.finish 319 + 320 + type bookmark = { 321 + id : bookmark_id; 322 + created_at : Ptime.t; 323 + modified_at : Ptime.t option; 324 + title : string option; 325 + archived : bool; 326 + favourited : bool; 327 + tagging_status : tagging_status option; 328 + note : string option; 329 + summary : string option; 330 + tags : bookmark_tag list; 331 + content : content; 332 + assets : asset list; 333 + } 334 + 335 + let bookmark_jsont = 336 + let make id created_at modified_at title archived favourited tagging_status note summary tags content assets = 337 + { id; created_at; modified_at; title; archived; favourited; tagging_status; note; summary; tags; content; assets } 338 + in 339 + Jsont.Object.map ~kind:"bookmark" make 340 + |> Jsont.Object.mem "id" Jsont.string ~enc:(fun b -> b.id) 341 + |> Jsont.Object.mem "createdAt" ptime_jsont ~enc:(fun b -> b.created_at) 342 + |> Jsont.Object.mem "modifiedAt" ptime_option_jsont ~dec_absent:None ~enc:(fun b -> b.modified_at) 343 + |> Jsont.Object.mem "title" (Jsont.option Jsont.string) ~dec_absent:None ~enc:(fun b -> b.title) 344 + |> Jsont.Object.mem "archived" Jsont.bool ~dec_absent:false ~enc:(fun b -> b.archived) 345 + |> Jsont.Object.mem "favourited" Jsont.bool ~dec_absent:false ~enc:(fun b -> b.favourited) 346 + |> Jsont.Object.mem "taggingStatus" (Jsont.option tagging_status_jsont) ~dec_absent:None ~enc:(fun b -> b.tagging_status) 347 + |> Jsont.Object.mem "note" (Jsont.option Jsont.string) ~dec_absent:None ~enc:(fun b -> b.note) 348 + |> Jsont.Object.mem "summary" (Jsont.option Jsont.string) ~dec_absent:None ~enc:(fun b -> b.summary) 349 + |> Jsont.Object.mem "tags" (Jsont.list bookmark_tag_jsont) ~dec_absent:[] ~enc:(fun b -> b.tags) 350 + |> Jsont.Object.mem "content" content_jsont ~enc:(fun b -> b.content) 351 + |> Jsont.Object.mem "assets" (Jsont.list asset_jsont) ~dec_absent:[] ~enc:(fun b -> b.assets) 352 + |> Jsont.Object.finish 353 + 354 + let bookmark_title bookmark = 355 + match bookmark.title with 356 + | Some t -> t 357 + | None -> title bookmark.content 358 + 359 + (** {1 Paginated Responses} *) 360 + 361 + type paginated_bookmarks = { 362 + bookmarks : bookmark list; 363 + next_cursor : string option; 364 + } 365 + 366 + let paginated_bookmarks_jsont = 367 + let make bookmarks next_cursor = { bookmarks; next_cursor } in 368 + Jsont.Object.map ~kind:"paginated_bookmarks" make 369 + |> Jsont.Object.mem "bookmarks" (Jsont.list bookmark_jsont) ~dec_absent:[] ~enc:(fun p -> p.bookmarks) 370 + |> Jsont.Object.mem "nextCursor" string_option ~dec_absent:None ~enc:(fun p -> p.next_cursor) 371 + |> Jsont.Object.finish 372 + 373 + (** {1 List Type} *) 374 + 375 + type _list = { 376 + id : list_id; 377 + name : string; 378 + description : string option; 379 + icon : string; 380 + parent_id : list_id option; 381 + list_type : list_type; 382 + query : string option; 383 + } 384 + 385 + let list_jsont = 386 + let make id name description icon parent_id list_type query = 387 + { id; name; description; icon; parent_id; list_type; query } 388 + in 389 + Jsont.Object.map ~kind:"list" make 390 + |> Jsont.Object.mem "id" Jsont.string ~enc:(fun l -> l.id) 391 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun l -> l.name) 392 + |> Jsont.Object.mem "description" string_option ~dec_absent:None ~enc:(fun l -> l.description) 393 + |> Jsont.Object.mem "icon" Jsont.string ~dec_absent:"" ~enc:(fun l -> l.icon) 394 + |> Jsont.Object.mem "parentId" string_option ~dec_absent:None ~enc:(fun l -> l.parent_id) 395 + |> Jsont.Object.mem "type" list_type_jsont ~dec_absent:Manual ~enc:(fun l -> l.list_type) 396 + |> Jsont.Object.mem "query" string_option ~dec_absent:None ~enc:(fun l -> l.query) 397 + |> Jsont.Object.finish 398 + 399 + type lists_response = { lists : _list list } 400 + 401 + let lists_response_jsont = 402 + let make lists = { lists } in 403 + Jsont.Object.map ~kind:"lists_response" make 404 + |> Jsont.Object.mem "lists" (Jsont.list list_jsont) ~dec_absent:[] ~enc:(fun r -> r.lists) 405 + |> Jsont.Object.finish 406 + 407 + (** {1 Tag Types} *) 408 + 409 + type tag = { 410 + id : tag_id; 411 + name : string; 412 + num_bookmarks : int; 413 + num_bookmarks_by_attached_type : (tag_attachment_type * int) list; 414 + } 415 + 416 + (* The API returns numBookmarksByAttachedType as an object like {"ai": 5, "human": 10} *) 417 + let num_bookmarks_by_type_jsont = 418 + let make ai human = 419 + let result = [] in 420 + let result = if ai > 0 then (AI, ai) :: result else result in 421 + let result = if human > 0 then (Human, human) :: result else result in 422 + result 423 + in 424 + let enc_ai lst = List.assoc_opt AI lst |> Option.value ~default:0 in 425 + let enc_human lst = List.assoc_opt Human lst |> Option.value ~default:0 in 426 + Jsont.Object.map ~kind:"num_bookmarks_by_type" make 427 + |> Jsont.Object.mem "ai" Jsont.int ~dec_absent:0 ~enc:enc_ai 428 + |> Jsont.Object.mem "human" Jsont.int ~dec_absent:0 ~enc:enc_human 429 + |> Jsont.Object.finish 430 + 431 + let tag_jsont = 432 + let make id name num_bookmarks num_bookmarks_by_attached_type = 433 + { id; name; num_bookmarks; num_bookmarks_by_attached_type } 434 + in 435 + Jsont.Object.map ~kind:"tag" make 436 + |> Jsont.Object.mem "id" Jsont.string ~enc:(fun t -> t.id) 437 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun t -> t.name) 438 + |> Jsont.Object.mem "numBookmarks" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.num_bookmarks) 439 + |> Jsont.Object.mem "numBookmarksByAttachedType" num_bookmarks_by_type_jsont 440 + ~dec_absent:[] ~enc:(fun t -> t.num_bookmarks_by_attached_type) 441 + |> Jsont.Object.finish 442 + 443 + type tags_response = { tags : tag list } 444 + 445 + let tags_response_jsont = 446 + let make tags = { tags } in 447 + Jsont.Object.map ~kind:"tags_response" make 448 + |> Jsont.Object.mem "tags" (Jsont.list tag_jsont) ~dec_absent:[] ~enc:(fun r -> r.tags) 449 + |> Jsont.Object.finish 450 + 451 + (** {1 Highlight Types} *) 452 + 453 + type highlight = { 454 + bookmark_id : bookmark_id; 455 + start_offset : int; 456 + end_offset : int; 457 + color : highlight_color; 458 + text : string option; 459 + note : string option; 460 + id : highlight_id; 461 + user_id : string; 462 + created_at : Ptime.t; 463 + } 464 + 465 + let highlight_jsont = 466 + let make bookmark_id start_offset end_offset color text note id user_id created_at = 467 + { bookmark_id; start_offset; end_offset; color; text; note; id; user_id; created_at } 468 + in 469 + Jsont.Object.map ~kind:"highlight" make 470 + |> Jsont.Object.mem "bookmarkId" Jsont.string ~enc:(fun h -> h.bookmark_id) 471 + |> Jsont.Object.mem "startOffset" Jsont.int ~enc:(fun h -> h.start_offset) 472 + |> Jsont.Object.mem "endOffset" Jsont.int ~enc:(fun h -> h.end_offset) 473 + |> Jsont.Object.mem "color" highlight_color_jsont ~dec_absent:Yellow ~enc:(fun h -> h.color) 474 + |> Jsont.Object.mem "text" string_option ~dec_absent:None ~enc:(fun h -> h.text) 475 + |> Jsont.Object.mem "note" string_option ~dec_absent:None ~enc:(fun h -> h.note) 476 + |> Jsont.Object.mem "id" Jsont.string ~enc:(fun h -> h.id) 477 + |> Jsont.Object.mem "userId" Jsont.string ~enc:(fun h -> h.user_id) 478 + |> Jsont.Object.mem "createdAt" ptime_jsont ~enc:(fun h -> h.created_at) 479 + |> Jsont.Object.finish 480 + 481 + type paginated_highlights = { 482 + highlights : highlight list; 483 + next_cursor : string option; 484 + } 485 + 486 + let paginated_highlights_jsont = 487 + let make highlights next_cursor = { highlights; next_cursor } in 488 + Jsont.Object.map ~kind:"paginated_highlights" make 489 + |> Jsont.Object.mem "highlights" (Jsont.list highlight_jsont) ~dec_absent:[] ~enc:(fun p -> p.highlights) 490 + |> Jsont.Object.mem "nextCursor" string_option ~dec_absent:None ~enc:(fun p -> p.next_cursor) 491 + |> Jsont.Object.finish 492 + 493 + type highlights_response = { highlights : highlight list } 494 + 495 + let highlights_response_jsont = 496 + let make highlights = { highlights } in 497 + Jsont.Object.map ~kind:"highlights_response" make 498 + |> Jsont.Object.mem "highlights" (Jsont.list highlight_jsont) ~dec_absent:[] ~enc:(fun r -> r.highlights) 499 + |> Jsont.Object.finish 500 + 501 + (** {1 User Types} *) 502 + 503 + type user_info = { 504 + id : string; 505 + name : string option; 506 + email : string option; 507 + } 508 + 509 + let user_info_jsont = 510 + let make id name email = { id; name; email } in 511 + Jsont.Object.map ~kind:"user_info" make 512 + |> Jsont.Object.mem "id" Jsont.string ~enc:(fun u -> u.id) 513 + |> Jsont.Object.mem "name" string_option ~dec_absent:None ~enc:(fun u -> u.name) 514 + |> Jsont.Object.mem "email" string_option ~dec_absent:None ~enc:(fun u -> u.email) 515 + |> Jsont.Object.finish 516 + 517 + type user_stats = { 518 + num_bookmarks : int; 519 + num_favorites : int; 520 + num_archived : int; 521 + num_tags : int; 522 + num_lists : int; 523 + num_highlights : int; 524 + } 525 + 526 + let user_stats_jsont = 527 + let make num_bookmarks num_favorites num_archived num_tags num_lists num_highlights = 528 + { num_bookmarks; num_favorites; num_archived; num_tags; num_lists; num_highlights } 529 + in 530 + Jsont.Object.map ~kind:"user_stats" make 531 + |> Jsont.Object.mem "numBookmarks" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_bookmarks) 532 + |> Jsont.Object.mem "numFavourites" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_favorites) 533 + |> Jsont.Object.mem "numArchived" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_archived) 534 + |> Jsont.Object.mem "numTags" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_tags) 535 + |> Jsont.Object.mem "numLists" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_lists) 536 + |> Jsont.Object.mem "numHighlights" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_highlights) 537 + |> Jsont.Object.finish 538 + 539 + (** {1 Error Response} *) 540 + 541 + type error_response = { 542 + code : string; 543 + message : string; 544 + } 545 + 546 + let error_response_jsont = 547 + let make code message = { code; message } in 548 + Jsont.Object.map ~kind:"error_response" make 549 + |> Jsont.Object.mem "code" Jsont.string ~dec_absent:"unknown" ~enc:(fun e -> e.code) 550 + |> Jsont.Object.mem "message" Jsont.string ~dec_absent:"Unknown error" ~enc:(fun e -> e.message) 551 + |> Jsont.Object.finish 552 + 553 + (** {1 Request Types} *) 554 + 555 + type create_bookmark_request = { 556 + type_ : string; 557 + url : string option; 558 + text : string option; 559 + title : string option; 560 + note : string option; 561 + summary : string option; 562 + archived : bool option; 563 + favourited : bool option; 564 + created_at : Ptime.t option; 565 + } 566 + 567 + let create_bookmark_request_jsont = 568 + let make type_ url text title note summary archived favourited created_at = 569 + { type_; url; text; title; note; summary; archived; favourited; created_at } 570 + in 571 + Jsont.Object.map ~kind:"create_bookmark_request" make 572 + |> Jsont.Object.mem "type" Jsont.string ~enc:(fun r -> r.type_) 573 + |> Jsont.Object.opt_mem "url" Jsont.string ~enc:(fun r -> r.url) 574 + |> Jsont.Object.opt_mem "text" Jsont.string ~enc:(fun r -> r.text) 575 + |> Jsont.Object.opt_mem "title" Jsont.string ~enc:(fun r -> r.title) 576 + |> Jsont.Object.opt_mem "note" Jsont.string ~enc:(fun r -> r.note) 577 + |> Jsont.Object.opt_mem "summary" Jsont.string ~enc:(fun r -> r.summary) 578 + |> Jsont.Object.opt_mem "archived" Jsont.bool ~enc:(fun r -> r.archived) 579 + |> Jsont.Object.opt_mem "favourited" Jsont.bool ~enc:(fun r -> r.favourited) 580 + |> Jsont.Object.opt_mem "createdAt" ptime_jsont ~enc:(fun r -> r.created_at) 581 + |> Jsont.Object.finish 582 + 583 + type update_bookmark_request = { 584 + title : string option; 585 + note : string option; 586 + summary : string option; 587 + archived : bool option; 588 + favourited : bool option; 589 + } 590 + 591 + let update_bookmark_request_jsont = 592 + let make title note summary archived favourited = 593 + { title; note; summary; archived; favourited } 594 + in 595 + Jsont.Object.map ~kind:"update_bookmark_request" make 596 + |> Jsont.Object.opt_mem "title" Jsont.string ~enc:(fun r -> r.title) 597 + |> Jsont.Object.opt_mem "note" Jsont.string ~enc:(fun r -> r.note) 598 + |> Jsont.Object.opt_mem "summary" Jsont.string ~enc:(fun r -> r.summary) 599 + |> Jsont.Object.opt_mem "archived" Jsont.bool ~enc:(fun r -> r.archived) 600 + |> Jsont.Object.opt_mem "favourited" Jsont.bool ~enc:(fun r -> r.favourited) 601 + |> Jsont.Object.finish 602 + 603 + type tag_ref = TagId of tag_id | TagName of string 604 + 605 + let tag_ref_jsont = 606 + (* Each tag ref is an object with either tagId or tagName *) 607 + let make tagid tagname = 608 + match tagid, tagname with 609 + | Some id, _ -> TagId id 610 + | _, Some name -> TagName name 611 + | None, None -> TagName "" 612 + in 613 + Jsont.Object.map ~kind:"tag_ref" make 614 + |> Jsont.Object.opt_mem "tagId" Jsont.string ~enc:(function TagId id -> Some id | _ -> None) 615 + |> Jsont.Object.opt_mem "tagName" Jsont.string ~enc:(function TagName n -> Some n | _ -> None) 616 + |> Jsont.Object.finish 617 + 618 + type attach_tags_request = { tags : tag_ref list } 619 + 620 + let attach_tags_request_jsont = 621 + let make tags = { tags } in 622 + Jsont.Object.map ~kind:"attach_tags_request" make 623 + |> Jsont.Object.mem "tags" (Jsont.list tag_ref_jsont) ~enc:(fun r -> r.tags) 624 + |> Jsont.Object.finish 625 + 626 + type attach_tags_response = { attached : tag_id list } 627 + 628 + let attach_tags_response_jsont = 629 + let make attached = { attached } in 630 + Jsont.Object.map ~kind:"attach_tags_response" make 631 + |> Jsont.Object.mem "attached" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun r -> r.attached) 632 + |> Jsont.Object.finish 633 + 634 + type detach_tags_response = { detached : tag_id list } 635 + 636 + let detach_tags_response_jsont = 637 + let make detached = { detached } in 638 + Jsont.Object.map ~kind:"detach_tags_response" make 639 + |> Jsont.Object.mem "detached" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun r -> r.detached) 640 + |> Jsont.Object.finish 641 + 642 + type create_list_request = { 643 + name : string; 644 + icon : string; 645 + description : string option; 646 + parent_id : list_id option; 647 + type_ : string option; 648 + query : string option; 649 + } 650 + 651 + let create_list_request_jsont = 652 + let make name icon description parent_id type_ query = 653 + { name; icon; description; parent_id; type_; query } 654 + in 655 + Jsont.Object.map ~kind:"create_list_request" make 656 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name) 657 + |> Jsont.Object.mem "icon" Jsont.string ~enc:(fun r -> r.icon) 658 + |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun r -> r.description) 659 + |> Jsont.Object.opt_mem "parentId" Jsont.string ~enc:(fun r -> r.parent_id) 660 + |> Jsont.Object.opt_mem "type" Jsont.string ~enc:(fun r -> r.type_) 661 + |> Jsont.Object.opt_mem "query" Jsont.string ~enc:(fun r -> r.query) 662 + |> Jsont.Object.finish 663 + 664 + type update_list_request = { 665 + name : string option; 666 + icon : string option; 667 + description : string option; 668 + parent_id : list_id option option; 669 + query : string option; 670 + } 671 + 672 + let update_list_request_jsont = 673 + let make name icon description parent_id query = 674 + (* parent_id here comes from opt_mem so it's already option *) 675 + { name; icon; description; parent_id = Some parent_id; query } 676 + in 677 + Jsont.Object.map ~kind:"update_list_request" make 678 + |> Jsont.Object.opt_mem "name" Jsont.string ~enc:(fun r -> r.name) 679 + |> Jsont.Object.opt_mem "icon" Jsont.string ~enc:(fun r -> r.icon) 680 + |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun r -> r.description) 681 + |> Jsont.Object.opt_mem "parentId" Jsont.string ~enc:(fun r -> Option.join r.parent_id) 682 + |> Jsont.Object.opt_mem "query" Jsont.string ~enc:(fun r -> r.query) 683 + |> Jsont.Object.finish 684 + 685 + type create_highlight_request = { 686 + bookmark_id : bookmark_id; 687 + start_offset : int; 688 + end_offset : int; 689 + text : string; 690 + note : string option; 691 + color : highlight_color option; 692 + } 693 + 694 + let create_highlight_request_jsont = 695 + let make bookmark_id start_offset end_offset text note color = 696 + { bookmark_id; start_offset; end_offset; text; note; color } 697 + in 698 + Jsont.Object.map ~kind:"create_highlight_request" make 699 + |> Jsont.Object.mem "bookmarkId" Jsont.string ~enc:(fun r -> r.bookmark_id) 700 + |> Jsont.Object.mem "startOffset" Jsont.int ~enc:(fun r -> r.start_offset) 701 + |> Jsont.Object.mem "endOffset" Jsont.int ~enc:(fun r -> r.end_offset) 702 + |> Jsont.Object.mem "text" Jsont.string ~enc:(fun r -> r.text) 703 + |> Jsont.Object.opt_mem "note" Jsont.string ~enc:(fun r -> r.note) 704 + |> Jsont.Object.opt_mem "color" highlight_color_jsont ~enc:(fun r -> r.color) 705 + |> Jsont.Object.finish 706 + 707 + type update_highlight_request = { color : highlight_color option } 708 + 709 + let update_highlight_request_jsont = 710 + let make color = { color } in 711 + Jsont.Object.map ~kind:"update_highlight_request" make 712 + |> Jsont.Object.opt_mem "color" highlight_color_jsont ~enc:(fun r -> r.color) 713 + |> Jsont.Object.finish 714 + 715 + type update_tag_request = { name : string } 716 + 717 + let update_tag_request_jsont = 718 + let make name = { name } in 719 + Jsont.Object.map ~kind:"update_tag_request" make 720 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name) 721 + |> Jsont.Object.finish 722 + 723 + type attach_asset_request = { 724 + id : asset_id; 725 + asset_type : asset_type; 726 + } 727 + 728 + let attach_asset_request_jsont = 729 + let make id asset_type = { id; asset_type } in 730 + Jsont.Object.map ~kind:"attach_asset_request" make 731 + |> Jsont.Object.mem "id" Jsont.string ~enc:(fun r -> r.id) 732 + |> Jsont.Object.mem "assetType" asset_type_jsont ~enc:(fun r -> r.asset_type) 733 + |> Jsont.Object.finish 734 + 735 + type replace_asset_request = { asset_id : asset_id } 736 + 737 + let replace_asset_request_jsont = 738 + let make asset_id = { asset_id } in 739 + Jsont.Object.map ~kind:"replace_asset_request" make 740 + |> Jsont.Object.mem "assetId" Jsont.string ~enc:(fun r -> r.asset_id) 741 + |> Jsont.Object.finish
+408
lib/proto/karakeep_proto.mli
··· 1 + (** Karakeep API protocol types and JSON codecs 2 + 3 + This module provides type definitions and jsont codecs for the Karakeep 4 + bookmark service API protocol messages. *) 5 + 6 + (** {1 ID Types} *) 7 + 8 + type asset_id = string 9 + (** Asset identifier type *) 10 + 11 + type bookmark_id = string 12 + (** Bookmark identifier type *) 13 + 14 + type list_id = string 15 + (** List identifier type *) 16 + 17 + type tag_id = string 18 + (** Tag identifier type *) 19 + 20 + type highlight_id = string 21 + (** Highlight identifier type *) 22 + 23 + (** {1 Enum Types} *) 24 + 25 + (** Type of content a bookmark can have *) 26 + type bookmark_content_type = 27 + | Link (** A URL to a webpage *) 28 + | Text (** Plain text content *) 29 + | Asset (** An attached asset (image, PDF, etc.) *) 30 + | Unknown (** Unknown content type *) 31 + 32 + val bookmark_content_type_jsont : bookmark_content_type Jsont.t 33 + 34 + (** Type of asset *) 35 + type asset_type = 36 + | Screenshot (** Screenshot of a webpage *) 37 + | AssetScreenshot (** Screenshot of an asset *) 38 + | BannerImage (** Banner image *) 39 + | FullPageArchive (** Archive of a full webpage *) 40 + | Video (** Video asset *) 41 + | BookmarkAsset (** Generic bookmark asset *) 42 + | PrecrawledArchive (** Pre-crawled archive *) 43 + | Unknown (** Unknown asset type *) 44 + 45 + val asset_type_jsont : asset_type Jsont.t 46 + 47 + (** Type of tagging status *) 48 + type tagging_status = 49 + | Success (** Tagging was successful *) 50 + | Failure (** Tagging failed *) 51 + | Pending (** Tagging is pending *) 52 + 53 + val tagging_status_jsont : tagging_status Jsont.t 54 + val string_of_tagging_status : tagging_status -> string 55 + 56 + (** Type of bookmark list *) 57 + type list_type = 58 + | Manual (** List is manually managed *) 59 + | Smart (** List is dynamically generated based on a query *) 60 + 61 + val list_type_jsont : list_type Jsont.t 62 + 63 + (** Highlight color *) 64 + type highlight_color = 65 + | Yellow (** Yellow highlight *) 66 + | Red (** Red highlight *) 67 + | Green (** Green highlight *) 68 + | Blue (** Blue highlight *) 69 + 70 + val highlight_color_jsont : highlight_color Jsont.t 71 + val string_of_highlight_color : highlight_color -> string 72 + 73 + (** Type of how a tag was attached *) 74 + type tag_attachment_type = 75 + | AI (** Tag was attached by AI *) 76 + | Human (** Tag was attached by a human *) 77 + 78 + val tag_attachment_type_jsont : tag_attachment_type Jsont.t 79 + val string_of_tag_attachment_type : tag_attachment_type -> string 80 + 81 + (** {1 Content Types} *) 82 + 83 + type link_content = { 84 + url : string; (** The URL of the bookmarked page *) 85 + title : string option; (** Title from the link *) 86 + description : string option; (** Description from the link *) 87 + image_url : string option; (** URL of an image from the link *) 88 + image_asset_id : asset_id option; (** ID of an image asset *) 89 + screenshot_asset_id : asset_id option; (** ID of a screenshot asset *) 90 + full_page_archive_asset_id : asset_id option; 91 + (** ID of a full page archive asset *) 92 + precrawled_archive_asset_id : asset_id option; 93 + (** ID of a pre-crawled archive asset *) 94 + video_asset_id : asset_id option; (** ID of a video asset *) 95 + favicon : string option; (** URL of the favicon *) 96 + html_content : string option; (** HTML content of the page *) 97 + crawled_at : Ptime.t option; (** When the page was crawled *) 98 + author : string option; (** Author of the content *) 99 + publisher : string option; (** Publisher of the content *) 100 + date_published : Ptime.t option; (** When the content was published *) 101 + date_modified : Ptime.t option; (** When the content was last modified *) 102 + } 103 + (** Link content for a bookmark *) 104 + 105 + val link_content_jsont : link_content Jsont.t 106 + 107 + type text_content = { 108 + text : string; (** The text content *) 109 + source_url : string option; (** Optional source URL for the text *) 110 + } 111 + (** Text content for a bookmark *) 112 + 113 + val text_content_jsont : text_content Jsont.t 114 + 115 + type asset_content = { 116 + asset_type : [ `Image | `PDF ]; (** Type of the asset *) 117 + asset_id : asset_id; (** ID of the asset *) 118 + file_name : string option; (** Name of the file *) 119 + source_url : string option; (** Source URL for the asset *) 120 + size : int option; (** Size of the asset in bytes *) 121 + content : string option; (** Extracted content from the asset *) 122 + } 123 + (** Asset content for a bookmark *) 124 + 125 + val asset_content_jsont : asset_content Jsont.t 126 + 127 + (** Content of a bookmark *) 128 + type content = 129 + | Link of link_content (** Link-type content *) 130 + | Text of text_content (** Text-type content *) 131 + | Asset of asset_content (** Asset-type content *) 132 + | Unknown (** Unknown content type *) 133 + 134 + val content_jsont : content Jsont.t 135 + 136 + val title : content -> string 137 + (** [title content] extracts a meaningful title from the bookmark content. 138 + For Link content, it returns the title if available, otherwise the URL. 139 + For Text content, it returns a short excerpt from the text. 140 + For Asset content, it returns the filename if available, otherwise a 141 + generic title. 142 + For Unknown content, it returns a generic title. *) 143 + 144 + (** {1 Resource Types} *) 145 + 146 + type asset = { 147 + id : asset_id; (** ID of the asset *) 148 + asset_type : asset_type; (** Type of the asset *) 149 + } 150 + (** Asset attached to a bookmark *) 151 + 152 + val asset_jsont : asset Jsont.t 153 + 154 + type bookmark_tag = { 155 + id : tag_id; (** ID of the tag *) 156 + name : string; (** Name of the tag *) 157 + attached_by : tag_attachment_type; (** How the tag was attached *) 158 + } 159 + (** Tag with attachment information *) 160 + 161 + val bookmark_tag_jsont : bookmark_tag Jsont.t 162 + 163 + type bookmark = { 164 + id : bookmark_id; (** Unique identifier for the bookmark *) 165 + created_at : Ptime.t; (** Timestamp when the bookmark was created *) 166 + modified_at : Ptime.t option; (** Optional timestamp of the last update *) 167 + title : string option; (** Optional title of the bookmarked page *) 168 + archived : bool; (** Whether the bookmark is archived *) 169 + favourited : bool; (** Whether the bookmark is marked as a favorite *) 170 + tagging_status : tagging_status option; (** Status of automatic tagging *) 171 + note : string option; (** Optional user note associated with the bookmark *) 172 + summary : string option; (** Optional AI-generated summary *) 173 + tags : bookmark_tag list; (** Tags associated with the bookmark *) 174 + content : content; (** Content of the bookmark *) 175 + assets : asset list; (** Assets attached to the bookmark *) 176 + } 177 + (** A bookmark from the Karakeep service *) 178 + 179 + val bookmark_jsont : bookmark Jsont.t 180 + 181 + val bookmark_title : bookmark -> string 182 + (** [bookmark_title bookmark] returns the best available title for a bookmark. 183 + It prioritizes the bookmark's title field if available, and falls back to 184 + extracting a title from the bookmark's content. *) 185 + 186 + (** {1 Paginated Responses} *) 187 + 188 + type paginated_bookmarks = { 189 + bookmarks : bookmark list; (** List of bookmarks in the current page *) 190 + next_cursor : string option; (** Optional cursor for fetching the next page *) 191 + } 192 + (** Paginated response of bookmarks *) 193 + 194 + val paginated_bookmarks_jsont : paginated_bookmarks Jsont.t 195 + 196 + (** {1 List Type} *) 197 + 198 + type _list = { 199 + id : list_id; (** ID of the list *) 200 + name : string; (** Name of the list *) 201 + description : string option; (** Optional description of the list *) 202 + icon : string; (** Icon for the list *) 203 + parent_id : list_id option; (** Optional parent list ID *) 204 + list_type : list_type; (** Type of the list *) 205 + query : string option; (** Optional query for smart lists *) 206 + } 207 + (** List in Karakeep *) 208 + 209 + val list_jsont : _list Jsont.t 210 + 211 + type lists_response = { lists : _list list } 212 + (** Response containing a list of lists *) 213 + 214 + val lists_response_jsont : lists_response Jsont.t 215 + 216 + (** {1 Tag Types} *) 217 + 218 + type tag = { 219 + id : tag_id; (** ID of the tag *) 220 + name : string; (** Name of the tag *) 221 + num_bookmarks : int; (** Number of bookmarks with this tag *) 222 + num_bookmarks_by_attached_type : (tag_attachment_type * int) list; 223 + (** Number of bookmarks by attachment type *) 224 + } 225 + (** Tag in Karakeep *) 226 + 227 + val tag_jsont : tag Jsont.t 228 + 229 + type tags_response = { tags : tag list } 230 + (** Response containing a list of tags *) 231 + 232 + val tags_response_jsont : tags_response Jsont.t 233 + 234 + (** {1 Highlight Types} *) 235 + 236 + type highlight = { 237 + bookmark_id : bookmark_id; (** ID of the bookmark *) 238 + start_offset : int; (** Start position of the highlight *) 239 + end_offset : int; (** End position of the highlight *) 240 + color : highlight_color; (** Color of the highlight *) 241 + text : string option; (** Text of the highlight *) 242 + note : string option; (** Note for the highlight *) 243 + id : highlight_id; (** ID of the highlight *) 244 + user_id : string; (** ID of the user who created the highlight *) 245 + created_at : Ptime.t; (** When the highlight was created *) 246 + } 247 + (** Highlight in Karakeep *) 248 + 249 + val highlight_jsont : highlight Jsont.t 250 + 251 + type paginated_highlights = { 252 + highlights : highlight list; (** List of highlights in the current page *) 253 + next_cursor : string option; (** Optional cursor for fetching the next page *) 254 + } 255 + (** Paginated response of highlights *) 256 + 257 + val paginated_highlights_jsont : paginated_highlights Jsont.t 258 + 259 + type highlights_response = { highlights : highlight list } 260 + (** Response containing a list of highlights *) 261 + 262 + val highlights_response_jsont : highlights_response Jsont.t 263 + 264 + (** {1 User Types} *) 265 + 266 + type user_info = { 267 + id : string; (** ID of the user *) 268 + name : string option; (** Name of the user *) 269 + email : string option; (** Email of the user *) 270 + } 271 + (** User information *) 272 + 273 + val user_info_jsont : user_info Jsont.t 274 + 275 + type user_stats = { 276 + num_bookmarks : int; (** Number of bookmarks *) 277 + num_favorites : int; (** Number of favorite bookmarks *) 278 + num_archived : int; (** Number of archived bookmarks *) 279 + num_tags : int; (** Number of tags *) 280 + num_lists : int; (** Number of lists *) 281 + num_highlights : int; (** Number of highlights *) 282 + } 283 + (** User statistics *) 284 + 285 + val user_stats_jsont : user_stats Jsont.t 286 + 287 + (** {1 Error Response} *) 288 + 289 + type error_response = { 290 + code : string; (** Error code *) 291 + message : string; (** Error message *) 292 + } 293 + (** Error response from the API *) 294 + 295 + val error_response_jsont : error_response Jsont.t 296 + 297 + (** {1 Request Types} *) 298 + 299 + type create_bookmark_request = { 300 + type_ : string; (** Bookmark type: "link", "text", or "asset" *) 301 + url : string option; (** URL for link bookmarks *) 302 + text : string option; (** Text for text bookmarks *) 303 + title : string option; (** Optional title *) 304 + note : string option; (** Optional note *) 305 + summary : string option; (** Optional summary *) 306 + archived : bool option; (** Whether to archive *) 307 + favourited : bool option; (** Whether to favourite *) 308 + created_at : Ptime.t option; (** Optional creation timestamp *) 309 + } 310 + (** Request to create a bookmark *) 311 + 312 + val create_bookmark_request_jsont : create_bookmark_request Jsont.t 313 + 314 + type update_bookmark_request = { 315 + title : string option; 316 + note : string option; 317 + summary : string option; 318 + archived : bool option; 319 + favourited : bool option; 320 + } 321 + (** Request to update a bookmark *) 322 + 323 + val update_bookmark_request_jsont : update_bookmark_request Jsont.t 324 + 325 + type tag_ref = 326 + | TagId of tag_id 327 + | TagName of string 328 + 329 + type attach_tags_request = { tags : tag_ref list } 330 + (** Request to attach tags to a bookmark *) 331 + 332 + val attach_tags_request_jsont : attach_tags_request Jsont.t 333 + 334 + type attach_tags_response = { attached : tag_id list } 335 + (** Response from attaching tags *) 336 + 337 + val attach_tags_response_jsont : attach_tags_response Jsont.t 338 + 339 + type detach_tags_response = { detached : tag_id list } 340 + (** Response from detaching tags *) 341 + 342 + val detach_tags_response_jsont : detach_tags_response Jsont.t 343 + 344 + type create_list_request = { 345 + name : string; 346 + icon : string; 347 + description : string option; 348 + parent_id : list_id option; 349 + type_ : string option; (** "manual" or "smart" *) 350 + query : string option; 351 + } 352 + (** Request to create a list *) 353 + 354 + val create_list_request_jsont : create_list_request Jsont.t 355 + 356 + type update_list_request = { 357 + name : string option; 358 + icon : string option; 359 + description : string option; 360 + parent_id : list_id option option; (** None to not update, Some None to clear *) 361 + query : string option; 362 + } 363 + (** Request to update a list *) 364 + 365 + val update_list_request_jsont : update_list_request Jsont.t 366 + 367 + type create_highlight_request = { 368 + bookmark_id : bookmark_id; 369 + start_offset : int; 370 + end_offset : int; 371 + text : string; 372 + note : string option; 373 + color : highlight_color option; 374 + } 375 + (** Request to create a highlight *) 376 + 377 + val create_highlight_request_jsont : create_highlight_request Jsont.t 378 + 379 + type update_highlight_request = { color : highlight_color option } 380 + (** Request to update a highlight *) 381 + 382 + val update_highlight_request_jsont : update_highlight_request Jsont.t 383 + 384 + type update_tag_request = { name : string } 385 + (** Request to update a tag *) 386 + 387 + val update_tag_request_jsont : update_tag_request Jsont.t 388 + 389 + type attach_asset_request = { 390 + id : asset_id; 391 + asset_type : asset_type; 392 + } 393 + (** Request to attach an asset *) 394 + 395 + val attach_asset_request_jsont : attach_asset_request Jsont.t 396 + 397 + type replace_asset_request = { asset_id : asset_id } 398 + (** Request to replace an asset *) 399 + 400 + val replace_asset_request_jsont : replace_asset_request Jsont.t 401 + 402 + (** {1 Helper Codecs} *) 403 + 404 + val ptime_jsont : Ptime.t Jsont.t 405 + (** Codec for Ptime.t values (ISO 8601 format) *) 406 + 407 + val ptime_option_jsont : Ptime.t option Jsont.t 408 + (** Codec for optional Ptime.t values *)
+62 -70
test/asset_test.ml
··· 1 - open Lwt.Infix 2 1 open Karakeep 3 2 4 3 let () = ··· 8 7 let ic = open_in ".karakeep-api" in 9 8 let key = input_line ic in 10 9 close_in ic; 11 - key 10 + String.trim key 12 11 with _ -> 13 12 Printf.eprintf "Error: Could not load API key from .karakeep-api file\n"; 14 13 exit 1 ··· 17 16 (* Test configuration *) 18 17 let base_url = "https://hoard.recoil.org" in 19 18 20 - (* Test asset URL and optionally fetch asset *) 21 - let run_test () = 22 - (* First get a bookmark with assets *) 23 - Printf.printf "Fetching bookmarks with assets...\n"; 19 + Eio_main.run @@ fun env -> 20 + Eio.Switch.run @@ fun sw -> 24 21 25 - Lwt.catch 26 - (fun () -> 27 - fetch_bookmarks ~api_key ~limit:5 base_url >>= fun response -> 28 - (* Find a bookmark with assets *) 29 - let bookmark_with_assets = 30 - List.find_opt (fun b -> List.length b.assets > 0) response.bookmarks 31 - in 22 + let client = Karakeep.create ~sw ~env ~base_url ~api_key in 32 23 33 - match bookmark_with_assets with 34 - | None -> 35 - Printf.printf "No bookmarks with assets found in the first 5 results.\n"; 36 - Lwt.return_unit 37 - | Some bookmark -> ( 38 - (* Print assets info *) 39 - let bookmark_title_str = bookmark_title bookmark in 40 - let url = 41 - match bookmark.content with 42 - | Link lc -> lc.url 43 - | Text tc -> Option.value tc.source_url ~default:"(text content)" 44 - | Asset ac -> Option.value ac.source_url ~default:"(asset content)" 45 - | Unknown -> "(unknown content)" 46 - in 47 - Printf.printf "Found bookmark \"%s\" with %d assets: %s\n" 48 - bookmark_title_str 49 - (List.length bookmark.assets) 50 - url; 24 + (* Test asset URL and optionally fetch asset *) 25 + Printf.printf "Fetching bookmarks with assets...\n"; 51 26 52 - List.iter 53 - (fun asset -> 54 - let asset_type_str = 55 - match asset.asset_type with 56 - | Screenshot -> "screenshot" 57 - | AssetScreenshot -> "assetScreenshot" 58 - | BannerImage -> "bannerImage" 59 - | FullPageArchive -> "fullPageArchive" 60 - | Video -> "video" 61 - | BookmarkAsset -> "bookmarkAsset" 62 - | PrecrawledArchive -> "precrawledArchive" 63 - | Unknown -> "unknown" 64 - in 65 - Printf.printf "- Asset ID: %s, Type: %s\n" asset.id asset_type_str; 27 + (try 28 + let response = fetch_bookmarks client ~limit:5 () in 29 + (* Find a bookmark with assets *) 30 + let bookmark_with_assets = 31 + List.find_opt (fun b -> List.length b.assets > 0) response.bookmarks 32 + in 66 33 67 - (* Get asset URL *) 68 - let asset_url = get_asset_url base_url asset.id in 69 - Printf.printf " URL: %s\n" asset_url) 70 - bookmark.assets; 34 + match bookmark_with_assets with 35 + | None -> 36 + Printf.printf "No bookmarks with assets found in the first 5 results.\n" 37 + | Some bookmark -> ( 38 + (* Print assets info *) 39 + let bookmark_title_str = bookmark_title bookmark in 40 + let url = 41 + match bookmark.content with 42 + | Link lc -> lc.url 43 + | Text tc -> Option.value tc.source_url ~default:"(text content)" 44 + | Asset ac -> Option.value ac.source_url ~default:"(asset content)" 45 + | Unknown -> "(unknown content)" 46 + in 47 + Printf.printf "Found bookmark \"%s\" with %d assets: %s\n" 48 + bookmark_title_str 49 + (List.length bookmark.assets) 50 + url; 71 51 72 - (* Optionally fetch one asset to verify it works *) 73 - match bookmark.assets with 74 - | asset :: _ -> 75 - Printf.printf "\nFetching asset %s...\n" asset.id; 76 - Lwt.catch 77 - (fun () -> 78 - fetch_asset ~api_key base_url asset.id >>= fun data -> 79 - Printf.printf "Successfully fetched asset. Size: %d bytes\n" 80 - (String.length data); 81 - Lwt.return_unit) 82 - (fun e -> 83 - Printf.printf "Error fetching asset: %s\n" (Printexc.to_string e); 84 - Lwt.return_unit) 85 - | [] -> Lwt.return_unit)) 86 - (fun e -> 87 - Printf.printf "Error in asset test: %s\n" (Printexc.to_string e); 88 - Printf.printf "Skipping the asset test due to API error.\n"; 89 - Lwt.return_unit) 90 - in 52 + List.iter 53 + (fun (asset : asset) -> 54 + let asset_type_str = 55 + match asset.asset_type with 56 + | Screenshot -> "screenshot" 57 + | AssetScreenshot -> "assetScreenshot" 58 + | BannerImage -> "bannerImage" 59 + | FullPageArchive -> "fullPageArchive" 60 + | Video -> "video" 61 + | BookmarkAsset -> "bookmarkAsset" 62 + | PrecrawledArchive -> "precrawledArchive" 63 + | Unknown -> "unknown" 64 + in 65 + Printf.printf "- Asset ID: %s, Type: %s\n" asset.id asset_type_str; 66 + 67 + (* Get asset URL *) 68 + let asset_url = get_asset_url client asset.id in 69 + Printf.printf " URL: %s\n" asset_url) 70 + bookmark.assets; 91 71 92 - (* Run test *) 93 - Lwt_main.run (run_test ()) 72 + (* Optionally fetch one asset to verify it works *) 73 + match bookmark.assets with 74 + | asset :: _ -> ( 75 + Printf.printf "\nFetching asset %s...\n" asset.id; 76 + try 77 + let data = fetch_asset client asset.id in 78 + Printf.printf "Successfully fetched asset. Size: %d bytes\n" 79 + (String.length data) 80 + with e -> 81 + Printf.printf "Error fetching asset: %s\n" (Printexc.to_string e)) 82 + | [] -> ()) 83 + with e -> 84 + Printf.printf "Error in asset test: %s\n" (Printexc.to_string e); 85 + Printf.printf "Skipping the asset test due to API error.\n")
+29 -34
test/create_test.ml
··· 1 - open Lwt.Infix 2 1 open Karakeep 3 2 4 3 let () = ··· 8 7 let ic = open_in ".karakeep-api" in 9 8 let key = input_line ic in 10 9 close_in ic; 11 - key 10 + String.trim key 12 11 with _ -> 13 12 Printf.eprintf "Error: Could not load API key from .karakeep-api file\n"; 14 13 exit 1 ··· 17 16 (* Test configuration *) 18 17 let base_url = "https://hoard.recoil.org" in 19 18 20 - (* Test creating a new bookmark *) 21 - let run_test () = 22 - Printf.printf "Creating a new bookmark...\n"; 19 + Eio_main.run @@ fun env -> 20 + Eio.Switch.run @@ fun sw -> 23 21 24 - let url = "https://ocaml.org" in 25 - let title = "OCaml Programming Language" in 26 - let tags = [ "programming"; "ocaml"; "functional" ] in 22 + let client = Karakeep.create ~sw ~env ~base_url ~api_key in 27 23 28 - Lwt.catch 29 - (fun () -> 30 - create_bookmark ~api_key ~url ~title ~tags base_url >>= fun bookmark -> 31 - Printf.printf "Successfully created bookmark:\n"; 32 - Printf.printf "- ID: %s\n" bookmark.id; 33 - Printf.printf "- Title: %s\n" (bookmark_title bookmark); 24 + (* Test creating a new bookmark *) 25 + Printf.printf "Creating a new bookmark...\n"; 34 26 35 - let url = 36 - match bookmark.content with 37 - | Link lc -> lc.url 38 - | Text tc -> Option.value tc.source_url ~default:"(text content)" 39 - | Asset ac -> Option.value ac.source_url ~default:"(asset content)" 40 - | Unknown -> "(unknown content)" 41 - in 42 - Printf.printf "- URL: %s\n" url; 43 - Printf.printf "- Created: %s\n" (Ptime.to_rfc3339 bookmark.created_at); 44 - Printf.printf "- Tags: %s\n" 45 - (String.concat ", " 46 - (List.map (fun (tag : bookmark_tag) -> tag.name) bookmark.tags)); 27 + let url = "https://ocaml.org" in 28 + let title = "OCaml Programming Language" in 29 + let tags = [ "programming"; "ocaml"; "functional" ] in 47 30 48 - Lwt.return_unit) 49 - (fun e -> 50 - Printf.printf "Error creating bookmark: %s\n" (Printexc.to_string e); 51 - Printf.printf "Skipping the creation test due to API error.\n"; 52 - Lwt.return_unit) 53 - in 31 + (try 32 + let bookmark = create_bookmark client ~url ~title ~tags () in 33 + Printf.printf "Successfully created bookmark:\n"; 34 + Printf.printf "- ID: %s\n" bookmark.id; 35 + Printf.printf "- Title: %s\n" (bookmark_title bookmark); 54 36 55 - (* Run test *) 56 - Lwt_main.run (run_test ()) 37 + let url = 38 + match bookmark.content with 39 + | Link lc -> lc.url 40 + | Text tc -> Option.value tc.source_url ~default:"(text content)" 41 + | Asset ac -> Option.value ac.source_url ~default:"(asset content)" 42 + | Unknown -> "(unknown content)" 43 + in 44 + Printf.printf "- URL: %s\n" url; 45 + Printf.printf "- Created: %s\n" (Ptime.to_rfc3339 bookmark.created_at); 46 + Printf.printf "- Tags: %s\n" 47 + (String.concat ", " 48 + (List.map (fun (tag : bookmark_tag) -> tag.name) bookmark.tags)) 49 + with e -> 50 + Printf.printf "Error creating bookmark: %s\n" (Printexc.to_string e); 51 + Printf.printf "Skipping the creation test due to API error.\n")
+4 -4
test/dune
··· 1 1 (executable 2 2 (name test) 3 - (libraries karakeep lwt.unix)) 3 + (libraries karakeep eio_main)) 4 4 5 5 (executable 6 6 (name create_test) 7 - (libraries karakeep lwt.unix)) 7 + (libraries karakeep eio_main)) 8 8 9 9 (executable 10 10 (name asset_test) 11 - (libraries karakeep lwt.unix)) 11 + (libraries karakeep eio_main)) 12 12 13 13 (executable 14 14 (name search_test) 15 - (libraries karakeep lwt.unix)) 15 + (libraries karakeep eio_main))
+88
test/search_test.ml
··· 1 + open Karakeep 2 + 3 + let print_bookmark bookmark = 4 + let title = bookmark_title bookmark in 5 + let url = 6 + match bookmark.content with 7 + | Link lc -> lc.url 8 + | Text tc -> Option.value tc.source_url ~default:"(text content)" 9 + | Asset ac -> Option.value ac.source_url ~default:"(asset content)" 10 + | Unknown -> "(unknown content)" 11 + in 12 + let tags_str = 13 + String.concat ", " 14 + (List.map (fun (tag : bookmark_tag) -> tag.name) bookmark.tags) 15 + in 16 + Printf.printf "- %s\n URL: %s\n Created: %s\n Tags: %s\n---\n\n" title url 17 + (Ptime.to_rfc3339 bookmark.created_at) 18 + tags_str 19 + 20 + let () = 21 + (* Load API key from file *) 22 + let api_key = 23 + try 24 + let ic = open_in ".karakeep-api" in 25 + let key = input_line ic in 26 + close_in ic; 27 + String.trim key 28 + with _ -> 29 + Printf.eprintf "Error: Could not load API key from .karakeep-api file\n"; 30 + exit 1 31 + in 32 + 33 + (* Test configuration *) 34 + let base_url = "https://hoard.recoil.org" in 35 + 36 + Eio_main.run @@ fun env -> 37 + Eio.Switch.run @@ fun sw -> 38 + 39 + let client = Karakeep.create ~sw ~env ~base_url ~api_key in 40 + 41 + Printf.printf "=== Test: search_bookmarks ===\n"; 42 + 43 + (* Use a reliable search term that should return results *) 44 + let search_term = "ocaml" in 45 + 46 + Printf.printf "Searching for bookmarks with query: \"%s\"\n\n" search_term; 47 + 48 + (try 49 + (* Search for bookmarks with the search term *) 50 + let search_results = search_bookmarks client ~query:search_term ~limit:3 () in 51 + 52 + Printf.printf "Found %d matching bookmarks\n" (List.length search_results.bookmarks); 53 + Printf.printf "Next cursor: %s\n\n" 54 + (match search_results.next_cursor with Some c -> c | None -> "none"); 55 + 56 + (* Display the search results *) 57 + List.iter print_bookmark search_results.bookmarks; 58 + 59 + (* Test pagination if we have a next page *) 60 + (match search_results.next_cursor with 61 + | Some cursor -> 62 + Printf.printf "=== Testing search pagination ===\n"; 63 + Printf.printf "Fetching next page with cursor: %s\n\n" cursor; 64 + 65 + let next_page = search_bookmarks client ~query:search_term ~limit:3 ~cursor () in 66 + 67 + Printf.printf "Found %d more bookmarks on next page\n\n" 68 + (List.length next_page.bookmarks); 69 + 70 + List.iter print_bookmark next_page.bookmarks 71 + | None -> 72 + Printf.printf "No more pages available for this search query.\n") 73 + with e -> 74 + Printf.printf "An error occurred while searching: %s\n" (Printexc.to_string e); 75 + Printf.printf "\nFalling back to testing with a simple search term: \"ocaml\"\n\n"; 76 + 77 + try 78 + (* Try again with a simple, reliable search term *) 79 + let search_results = search_bookmarks client ~query:"ocaml" ~limit:3 () in 80 + 81 + Printf.printf "Found %d matching bookmarks\n" (List.length search_results.bookmarks); 82 + Printf.printf "Next cursor: %s\n\n" 83 + (match search_results.next_cursor with Some c -> c | None -> "none"); 84 + 85 + (* Display the search results *) 86 + List.iter print_bookmark search_results.bookmarks 87 + with e -> 88 + Printf.printf "Fallback search also failed: %s\n" (Printexc.to_string e))
+40 -51
test/test.ml
··· 1 - open Lwt.Infix 2 1 open Karakeep 3 2 4 3 let print_bookmark bookmark = ··· 25 24 let ic = open_in ".karakeep-api" in 26 25 let key = input_line ic in 27 26 close_in ic; 28 - key 27 + String.trim key 29 28 with _ -> 30 29 Printf.eprintf "Error: Could not load API key from .karakeep-api file\n"; 31 30 exit 1 ··· 34 33 (* Test configuration *) 35 34 let base_url = "https://hoard.recoil.org" in 36 35 37 - (* Test both fetch methods *) 38 - let run_tests () = 39 - Lwt.catch 40 - (fun () -> 41 - (* Test 1: fetch_bookmarks - get a single page with pagination info *) 42 - Printf.printf "=== Test 1: fetch_bookmarks (paginated) ===\n"; 43 - fetch_bookmarks ~api_key ~limit:3 base_url >>= fun response -> 44 - Printf.printf "Found bookmarks, showing %d (page 1)\n" 45 - (List.length response.bookmarks); 46 - Printf.printf "Next cursor: %s\n\n" 47 - (match response.next_cursor with Some c -> c | None -> "none"); 36 + Eio_main.run @@ fun env -> 37 + Eio.Switch.run @@ fun sw -> 48 38 49 - List.iter print_bookmark response.bookmarks; 39 + let client = Karakeep.create ~sw ~env ~base_url ~api_key in 50 40 51 - (* Test 2: fetch_all_bookmarks - get multiple pages automatically *) 52 - Printf.printf "=== Test 2: fetch_all_bookmarks (with limit) ===\n"; 53 - fetch_all_bookmarks ~api_key ~page_size:2 ~max_pages:2 base_url 54 - >>= fun all_bookmarks -> 55 - Printf.printf "Fetched %d bookmarks from up to 2 pages\n\n" 56 - (List.length all_bookmarks); 41 + (* Test 1: fetch_bookmarks - get a single page with pagination info *) 42 + Printf.printf "=== Test 1: fetch_bookmarks (paginated) ===\n"; 43 + (try 44 + let response = fetch_bookmarks client ~limit:3 () in 45 + Printf.printf "Found bookmarks, showing %d (page 1)\n" 46 + (List.length response.bookmarks); 47 + Printf.printf "Next cursor: %s\n\n" 48 + (match response.next_cursor with Some c -> c | None -> "none"); 49 + List.iter print_bookmark response.bookmarks; 57 50 58 - List.iter print_bookmark 59 - (List.fold_left 60 - (fun acc x -> if List.length acc < 4 then acc @ [ x ] else acc) 61 - [] all_bookmarks); 62 - Printf.printf "... and %d more bookmarks\n\n" 63 - (max 0 (List.length all_bookmarks - 4)); 51 + (* Test 2: fetch_all_bookmarks - get multiple pages automatically *) 52 + Printf.printf "=== Test 2: fetch_all_bookmarks (with limit) ===\n"; 53 + let all_bookmarks = fetch_all_bookmarks client ~page_size:2 ~max_pages:2 () in 54 + Printf.printf "Fetched %d bookmarks from up to 2 pages\n\n" 55 + (List.length all_bookmarks); 64 56 65 - (* Test 3: fetch_bookmark_details - get a specific bookmark *) 66 - match response.bookmarks with 67 - | first_bookmark :: _ -> 68 - Printf.printf "=== Test 3: fetch_bookmark_details ===\n"; 69 - Printf.printf "Fetching details for bookmark ID: %s\n\n" 70 - first_bookmark.id; 57 + List.iter print_bookmark 58 + (List.fold_left 59 + (fun acc x -> if List.length acc < 4 then acc @ [ x ] else acc) 60 + [] all_bookmarks); 61 + Printf.printf "... and %d more bookmarks\n\n" 62 + (max 0 (List.length all_bookmarks - 4)); 71 63 72 - Lwt.catch 73 - (fun () -> 74 - fetch_bookmark_details ~api_key base_url first_bookmark.id 75 - >>= fun bookmark -> 76 - print_bookmark bookmark; 77 - Lwt.return_unit) 78 - (fun e -> 79 - Printf.printf "Error fetching bookmark details: %s\n" (Printexc.to_string e); 80 - Lwt.return_unit) 81 - | [] -> 82 - Printf.printf "No bookmarks found to test fetch_bookmark_details\n"; 83 - Lwt.return_unit) 84 - (fun e -> 85 - Printf.printf "Error in basic tests: %s\n" (Printexc.to_string e); 86 - Printf.printf "Skipping remaining tests due to API error.\n"; 87 - Lwt.return_unit) 88 - in 64 + (* Test 3: fetch_bookmark_details - get a specific bookmark *) 65 + (match response.bookmarks with 66 + | first_bookmark :: _ -> 67 + Printf.printf "=== Test 3: fetch_bookmark_details ===\n"; 68 + Printf.printf "Fetching details for bookmark ID: %s\n\n" 69 + first_bookmark.id; 89 70 90 - (* Run all tests *) 91 - Lwt_main.run (run_tests ()) 71 + (try 72 + let bookmark = fetch_bookmark_details client first_bookmark.id in 73 + print_bookmark bookmark 74 + with e -> 75 + Printf.printf "Error fetching bookmark details: %s\n" (Printexc.to_string e)) 76 + | [] -> 77 + Printf.printf "No bookmarks found to test fetch_bookmark_details\n") 78 + with e -> 79 + Printf.printf "Error in basic tests: %s\n" (Printexc.to_string e); 80 + Printf.printf "Skipping remaining tests due to API error.\n")