this repo has no description
0
fork

Configure Feed

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

more

+187 -19
+88 -12
stack/sortal/lib/sortal.ml
··· 1 + module Feed = struct 2 + type feed_type = 3 + | Atom 4 + | Rss 5 + | Json 6 + 7 + type t = { 8 + feed_type : feed_type; 9 + url : string; 10 + name : string option; 11 + } 12 + 13 + let make ~feed_type ~url ?name () = 14 + { feed_type; url; name } 15 + 16 + let feed_type t = t.feed_type 17 + let url t = t.url 18 + let name t = t.name 19 + 20 + let set_name t name = { t with name = Some name } 21 + 22 + let feed_type_to_string = function 23 + | Atom -> "atom" 24 + | Rss -> "rss" 25 + | Json -> "json" 26 + 27 + let feed_type_of_string = function 28 + | "atom" -> Some Atom 29 + | "rss" -> Some Rss 30 + | "json" -> Some Json 31 + | _ -> None 32 + 33 + let json_t = 34 + let open Jsont in 35 + let open Jsont.Object in 36 + let make feed_type url name = 37 + match feed_type_of_string feed_type with 38 + | Some ft -> { feed_type = ft; url; name } 39 + | None -> failwith ("Invalid feed type: " ^ feed_type) 40 + in 41 + map ~kind:"Feed" make 42 + |> mem "type" string ~enc:(fun f -> feed_type_to_string f.feed_type) 43 + |> mem "url" string ~enc:(fun f -> f.url) 44 + |> opt_mem "name" string ~enc:(fun f -> f.name) 45 + |> finish 46 + 47 + let pp ppf t = 48 + let open Fmt in 49 + pf ppf "%s: %s%a" 50 + (feed_type_to_string t.feed_type) 51 + t.url 52 + (option (fun ppf name -> pf ppf " (%s)" name)) t.name 53 + end 54 + 1 55 module Contact = struct 2 56 type t = { 3 57 handle : string; ··· 10 64 mastodon : string option; 11 65 orcid : string option; 12 66 url : string option; 13 - atom_feeds : string list option; 67 + feeds : Feed.t list option; 14 68 } 15 69 16 70 let make ~handle ~names ?email ?icon ?github ?twitter ?bluesky ?mastodon 17 - ?orcid ?url ?atom_feeds () = 71 + ?orcid ?url ?feeds () = 18 72 { handle; names; email; icon; github; twitter; bluesky; mastodon; 19 - orcid; url; atom_feeds } 73 + orcid; url; feeds } 20 74 21 75 let handle t = t.handle 22 76 let names t = t.names 23 77 let name t = List.hd t.names 78 + let primary_name = name 24 79 let email t = t.email 25 80 let icon t = t.icon 26 81 let github t = t.github ··· 29 84 let mastodon t = t.mastodon 30 85 let orcid t = t.orcid 31 86 let url t = t.url 32 - let atom_feeds t = t.atom_feeds 87 + let feeds t = t.feeds 88 + 89 + let add_feed t feed = 90 + let feeds = match t.feeds with 91 + | Some fs -> Some (feed :: fs) 92 + | None -> Some [feed] 93 + in 94 + { t with feeds } 95 + 96 + let remove_feed t url = 97 + let feeds = match t.feeds with 98 + | Some fs -> Some (List.filter (fun f -> Feed.url f <> url) fs) 99 + | None -> None 100 + in 101 + { t with feeds } 33 102 34 103 let best_url t = 35 104 match t.url with ··· 46 115 let open Jsont in 47 116 let open Jsont.Object in 48 117 let mem_opt f v ~enc = mem f v ~dec_absent:None ~enc_omit:Option.is_none ~enc in 49 - let make handle names email icon github twitter bluesky mastodon orcid url atom_feeds = 118 + let make handle names email icon github twitter bluesky mastodon orcid url feeds = 50 119 { handle; names; email; icon; github; twitter; bluesky; mastodon; 51 - orcid; url; atom_feeds } 120 + orcid; url; feeds } 52 121 in 53 122 map ~kind:"Contact" make 54 123 |> mem "handle" string ~enc:handle ··· 61 130 |> mem_opt "mastodon" (some string) ~enc:mastodon 62 131 |> mem_opt "orcid" (some string) ~enc:orcid 63 132 |> mem_opt "url" (some string) ~enc:url 64 - |> mem_opt "atom_feeds" (some (list string)) ~enc:atom_feeds 133 + |> mem_opt "feeds" (some (list Feed.json_t)) ~enc:feeds 65 134 |> finish 66 135 67 136 let compare a b = String.compare a.handle b.handle ··· 102 171 (match t.icon with 103 172 | Some i -> pf ppf "%a: %a@," (styled `Bold string) "Icon" string i 104 173 | None -> ()); 105 - (match t.atom_feeds with 174 + (match t.feeds with 106 175 | Some feeds when feeds <> [] -> 107 - pf ppf "%a: @[<h>%a@]@," (styled `Bold string) "Atom Feeds" 108 - (list ~sep:comma string) feeds 176 + pf ppf "%a:@," (styled `Bold string) "Feeds"; 177 + List.iter (fun feed -> 178 + pf ppf " - %a@," Feed.pp feed 179 + ) feeds 109 180 | _ -> ()); 110 181 pf ppf "@]" 111 182 end ··· 280 351 let with_github = List.filter (fun c -> Contact.github c <> None) contacts |> List.length in 281 352 let with_orcid = List.filter (fun c -> Contact.orcid c <> None) contacts |> List.length in 282 353 let with_url = List.filter (fun c -> Contact.url c <> None) contacts |> List.length in 283 - let with_feeds = List.filter (fun c -> Contact.atom_feeds c <> None) contacts |> List.length in 354 + let with_feeds = List.filter (fun c -> Contact.feeds c <> None) contacts |> List.length in 355 + let total_feeds = List.fold_left (fun acc c -> 356 + match Contact.feeds c with 357 + | Some feeds -> acc + List.length feeds 358 + | None -> acc 359 + ) 0 contacts in 284 360 285 361 Logs.app (fun m -> m "Contact Database Statistics:"); 286 362 Logs.app (fun m -> m " Total contacts: %d" total); ··· 288 364 Logs.app (fun m -> m " With GitHub: %d (%.1f%%)" with_github (float_of_int with_github /. float_of_int total *. 100.)); 289 365 Logs.app (fun m -> m " With ORCID: %d (%.1f%%)" with_orcid (float_of_int with_orcid /. float_of_int total *. 100.)); 290 366 Logs.app (fun m -> m " With URL: %d (%.1f%%)" with_url (float_of_int with_url /. float_of_int total *. 100.)); 291 - Logs.app (fun m -> m " With Atom feeds: %d (%.1f%%)" with_feeds (float_of_int with_feeds /. float_of_int total *. 100.)); 367 + Logs.app (fun m -> m " With feeds: %d (%.1f%%), total %d feeds" with_feeds (float_of_int with_feeds /. float_of_int total *. 100.) total_feeds); 292 368 0 293 369 294 370 (* Command info objects *)
+63 -5
stack/sortal/lib/sortal.mli
··· 29 29 ]} 30 30 *) 31 31 32 + (** {1 Feed Metadata} *) 33 + 34 + module Feed : sig 35 + (** Feed subscription with type and URL. 36 + 37 + A feed represents a subscription to a content source (Atom, RSS, or JSONFeed). *) 38 + type t 39 + 40 + (** Feed type identifier. *) 41 + type feed_type = 42 + | Atom (** Atom feed format *) 43 + | Rss (** RSS feed format *) 44 + | Json (** JSON Feed format *) 45 + 46 + (** [make ~feed_type ~url ?name ()] creates a new feed. 47 + 48 + @param feed_type The type of feed (Atom, RSS, or JSON) 49 + @param url The feed URL 50 + @param name Optional human-readable name/label for the feed 51 + *) 52 + val make : feed_type:feed_type -> url:string -> ?name:string -> unit -> t 53 + 54 + (** [feed_type t] returns the feed type. *) 55 + val feed_type : t -> feed_type 56 + 57 + (** [url t] returns the feed URL. *) 58 + val url : t -> string 59 + 60 + (** [name t] returns the feed name if set. *) 61 + val name : t -> string option 62 + 63 + (** [set_name t name] returns a new feed with the name updated. *) 64 + val set_name : t -> string -> t 65 + 66 + (** [feed_type_to_string ft] converts a feed type to a string. *) 67 + val feed_type_to_string : feed_type -> string 68 + 69 + (** [feed_type_of_string s] parses a feed type from a string. 70 + Returns [None] if the string is not recognized. *) 71 + val feed_type_of_string : string -> feed_type option 72 + 73 + (** [json_t] is the jsont encoder/decoder for feeds. *) 74 + val json_t : t Jsont.t 75 + 76 + (** [pp ppf t] pretty prints a feed. *) 77 + val pp : Format.formatter -> t -> unit 78 + end 79 + 32 80 (** {1 Contact Metadata} *) 33 81 34 82 module Contact : sig ··· 39 87 type t 40 88 41 89 (** [make ~handle ~names ?email ?icon ?github ?twitter ?bluesky ?mastodon 42 - ?orcid ?url ?atom_feeds ()] creates a new contact. 90 + ?orcid ?url ?feeds ()] creates a new contact. 43 91 44 92 @param handle A unique identifier/username for this contact (required) 45 93 @param names A list of names for this contact, with the first being primary (required) ··· 51 99 @param mastodon Mastodon handle (including instance) 52 100 @param orcid ORCID identifier 53 101 @param url Personal or professional website URL 54 - @param atom_feeds List of Atom/RSS feed URLs associated with this contact 102 + @param feeds List of feed subscriptions (Atom/RSS/JSON) associated with this contact 55 103 *) 56 104 val make : 57 105 handle:string -> ··· 64 112 ?mastodon:string -> 65 113 ?orcid:string -> 66 114 ?url:string -> 67 - ?atom_feeds:string list -> 115 + ?feeds:Feed.t list -> 68 116 unit -> 69 117 t 70 118 ··· 79 127 (** [name t] returns the primary (first) name. *) 80 128 val name : t -> string 81 129 130 + (** [primary_name t] returns the primary (first) name. 131 + This is an alias for {!name} for clarity. *) 132 + val primary_name : t -> string 133 + 82 134 (** [email t] returns the email address if available. *) 83 135 val email : t -> string option 84 136 ··· 103 155 (** [url t] returns the personal/professional website URL if available. *) 104 156 val url : t -> string option 105 157 106 - (** [atom_feeds t] returns the list of Atom/RSS feed URLs if available. *) 107 - val atom_feeds : t -> string list option 158 + (** [feeds t] returns the list of feed subscriptions if available. *) 159 + val feeds : t -> Feed.t list option 160 + 161 + (** [add_feed t feed] returns a new contact with the feed added. *) 162 + val add_feed : t -> Feed.t -> t 163 + 164 + (** [remove_feed t url] returns a new contact with the feed matching the URL removed. *) 165 + val remove_feed : t -> string -> t 108 166 109 167 (** {2 Derived Information} *) 110 168
+36 -2
stack/sortal/scripts/import_yaml_contacts.py
··· 31 31 return yaml.safe_load(yaml_content) 32 32 return None 33 33 34 + def detect_feed_type(url): 35 + """Detect feed type based on URL patterns.""" 36 + url_lower = url.lower() 37 + if 'json' in url_lower or url_lower.endswith('.json'): 38 + return 'json' 39 + elif 'rss' in url_lower or url_lower.endswith('.rss') or url_lower.endswith('.xml'): 40 + return 'rss' 41 + else: 42 + # Default to atom for most feeds 43 + return 'atom' 44 + 34 45 def convert_to_sortal_format(yaml_data, handle): 35 46 """Convert YAML contact data to Sortal JSON format.""" 36 47 sortal_contact = { ··· 55 66 sortal_contact["orcid"] = yaml_data["orcid"] 56 67 if "url" in yaml_data: 57 68 sortal_contact["url"] = yaml_data["url"] 69 + 70 + # Convert atom feeds to new feed structure 58 71 if "atom" in yaml_data: 59 - sortal_contact["atom_feeds"] = yaml_data["atom"] 72 + atom_feeds = yaml_data["atom"] 73 + if atom_feeds: 74 + feeds = [] 75 + for feed_url in atom_feeds: 76 + feed_type = detect_feed_type(feed_url) 77 + feed_obj = { 78 + "type": feed_type, 79 + "url": feed_url 80 + } 81 + feeds.append(feed_obj) 82 + sortal_contact["feeds"] = feeds 60 83 61 84 return sortal_contact 62 85 ··· 77 100 print(f"Error: Source directory does not exist: {source_dir}") 78 101 return 1 79 102 103 + # Delete existing contacts to avoid old schema 104 + print("Clearing existing contacts...") 105 + for existing_file in dest_dir.glob('*.json'): 106 + existing_file.unlink() 107 + 80 108 imported_count = 0 81 109 error_count = 0 110 + total_feeds = 0 82 111 83 112 # Process each .md file 84 113 for md_file in sorted(source_dir.glob('*.md')): ··· 100 129 json.dump(sortal_contact, f, indent=2, ensure_ascii=False) 101 130 102 131 name = sortal_contact.get('names', [handle])[0] if sortal_contact.get('names') else handle 103 - print(f"✓ Imported: {handle} ({name})") 132 + feed_count = len(sortal_contact.get('feeds', [])) 133 + total_feeds += feed_count 134 + 135 + feed_info = f" ({feed_count} feed{'s' if feed_count != 1 else ''})" if feed_count > 0 else "" 136 + print(f"✓ Imported: {handle} ({name}){feed_info}") 104 137 imported_count += 1 105 138 106 139 except Exception as e: ··· 110 143 print() 111 144 print(f"Import complete!") 112 145 print(f" Successfully imported: {imported_count}") 146 + print(f" Total feeds: {total_feeds}") 113 147 print(f" Errors: {error_count}") 114 148 print(f" Output directory: {dest_dir}") 115 149