(* when all you can do is type, making things more complicated than a list is hard? we need to design this somehow before implementing it really the graphical drawing / window management funcitons i think at this point. features: - message drafts? more like, if you send too many messages to someone all at once it will hold them so you can respond later and not flood people....... - i mean really what you want is an editable stream, so you can stage messages for later - because i mean, if this is a bicycle, and you can make it however you want, you can just fuck with the conversation thread with computer assistance instaed of just relying on your memory. *) open Lwt open Lwt_react module F = Fmt module Communicator = struct let base_path = "communicator" let topch = "top" module Istore = struct include Human.Store let from_storeview (sv : storeview) = sv.store include Human.Store.Istore end module Message = struct type t = {time: string list; content: string} let make ?(time = Unix.gettimeofday ()) content = let tm = Unix.localtime time in { time= List.map string_of_int [tm.tm_year + 1900; tm.tm_mon + 1; tm.tm_mday; tm.tm_hour] @ [ string_of_float ( float_of_int (tm.tm_min * tm.tm_sec) +. fst (modf time) ) ] ; content } end module Channel = struct (* a channels step key may not be blank (i.e. "") *) type t = {store: Istore.t; path: Istore.key} let make (store : Istore.t) ~path ~(name : string) = Lwt.return {store; path= path @ ["#" ^ name]} let add_msg {store; path} (msg : Message.t) : unit Lwt.t = F.epr "add_msg path=[" ; F.list ~sep:F.semi F.string F.stderr (path @ msg.time) ; F.epr "] content=%s @." msg.content ; Istore.set_exn store ~info:Irmin.Info.none (path @ msg.time) msg.content end module Tree = struct open Message type selection = Istore.Key.t type t = {store: Istore.t; view: Istore.key} let contents {store; view} (s : selection) : Istore.Contents.t option Lwt.t = Istore.find store (view @ s) let make_top ?(view = [base_path]) gitpath branchname : t Lwt.t = Istore.Repo.v (Irmin_git.config gitpath) >>= fun repo -> Istore.of_branch repo branchname >>= fun store -> let t = {store; view} in Channel.make store ~path:view ~name:topch >>= fun ch_top -> Channel.add_msg ch_top (Message.make "Communicator restarting...") >>= fun () -> Channel.add_msg ch_top (Message.make "Currently only IRC is implemented") >>= fun () -> Lwt.return t let add {store; view} ~(name : string list) ~(config : Istore.tree) : t Lwt.t = Istore.get_tree store name >>= fun tree -> Istore.Tree.remove tree ["_config"] >>= fun tree -> Istore.Tree.add_tree tree [] config >>= fun tree -> Istore.set_tree_exn ~info:Irmin.Info.none store name tree >>= fun () -> Lwt.return {store; view} end module Protocol = struct type t = Irc | Email | Rss | Mublog | ActivityPub let to_string = function | Irc -> ("IRC", "Internet Relay Chat") | Email -> ("E-mail", "Electronic Mail") | Rss -> ("RSS", "Really Simple Subscriptions???") | Mublog -> ("uBlog", "Microblogging (Twitter)") | ActivityPub -> ("ActivityPub", "Mastodon, etc.") let id t = fst (to_string t) let desc t = snd (to_string t) end module Irc = struct module C = Irc_client_tls module M = Irc_message module Config = struct type t = Istore.tree open Lwt.Infix let path = "_config" let make_connection Tree.{store; view} server port nick = let name = F.str "%s@%s:%d" nick server port in Istore.Tree.add Istore.Tree.empty ["server"] server >>= fun t' -> Istore.Tree.add t' ["port"] (string_of_int port) >>= fun t' -> Istore.Tree.add t' ["nick"] nick >>= fun t' -> Istore.Tree.add t' ["protocol"] (Protocol.id Irc) >>= fun t' -> F.epr "Creating connection config /%s/%s/@." name path ; Istore.set_tree_exn ~info:Irmin.Info.none store (view @ [name; path]) t' >>= fun _ -> Lwt.return_unit let server t : string Lwt.t = Istore.Tree.get t [path; "server"] let port t : int Lwt.t = Istore.Tree.get t [path; "port"] >|= fun p -> int_of_string p let nick t : string Lwt.t = Istore.Tree.get t [path; "nick"] let protocol t : string option Lwt.t = Istore.Tree.find t [path; "protocol"] end let get_channels ~store ~path = Istore.list store path >>= fun c -> let rec iter l = Lwt_list.filter_map_p (fun (s, _) -> if String.length s > 1 && String.get s 0 = '#' then Lwt.return (Some s) else Lwt.return None ) l in iter c let connect ?(path = [base_path]) ({store; _} : Tree.t) : unit Lwt.t = (* search for all connections and start them *) (* also need ot figure out how to preserve custom ordering of items like servers and channels maybe just a _order file that has the ordering of files listed and hten gets updated etc. *) Channel.make store ~path ~name:topch >>= fun top_channel -> let _top_msg str = Channel.add_msg top_channel (Message.make str) in let channel_assoc = ref [] in let make_channel store path (name : string) = Channel.make store ~path ~name >>= fun ch -> channel_assoc := (name, ch) :: !channel_assoc ; Channel.add_msg ch (Message.make (F.str "channel %s created" name)) >>= fun () -> Lwt.return ch in Istore.list store path >>= fun servers -> Lwt_list.filter_p (fun (_, tree) -> Config.protocol tree >|= function Some p -> p = Protocol.id Irc | None -> false ) servers (* filter out non-irc protocols, TODO currently relying on this to filter out non-server folders too *) >>= fun servers -> F.epr "protocols filtered for irc@." ; Lwt_list.iter_p (fun (name, tree) -> F.epr "Irc.connect server=%s @." name ; Config.nick tree >>= fun nick -> Config.server tree >>= fun server -> Config.port tree >>= fun port -> Channel.make store ~path:(path @ [name]) ~name:topch >>= fun server_channel -> let add_msg s = Channel.add_msg server_channel (Message.make s) in C.reconnect_loop ~after:30 ~connect:(fun () -> add_msg "Connecting..." >>= fun () -> C.connect_by_name ~server ~port ~nick () >>= fun c -> Lwt.return c ) ~f:(fun connection -> F.epr "Irc.connect C.reconnect_loop ~f:(Connected...)@." ; add_msg "Connected" >>= fun () -> get_channels ~store ~path:[name] >>= fun chs -> Lwt_list.iter_p (fun chname -> C.send_join ~connection ~channel:chname >>= fun () -> ignore (make_channel store [name] chname) ; Lwt.return_unit ) chs ) ~callback:(fun _connection result -> match result with | Result.Ok ({M.command= M.Other _; _} as msg) -> add_msg (M.to_string msg) | Result.Ok {M.command= M.PRIVMSG (target, data); prefix= user} -> ( let user = match user with | Some u -> List.hd (String.split_on_char '!' u) | None -> "unknown" in match List.assoc_opt target !channel_assoc with | Some ch -> Channel.add_msg ch (Message.make (F.str "<%s> %s" user data)) | None -> make_channel store [server] target >>= fun ch -> Channel.add_msg ch (Message.make (F.str "<%s> %s" user data)) ) | Result.Ok msg -> add_msg (M.to_string msg) >>= fun () -> Lwt.return_unit | Result.Error e -> Lwt_io.printl e ) () ) servers end module Panel = struct open Panel open Panel.Ui type viewer = { step: string ; var: view Lwd.var ; mutable parent: view ; mutable node: viewer list } and view = [`Empty | `View of viewer] let add v node = ( match v with | `View v -> v.node <- node :: v.node ; Lwd.set v.var (`View v) | `Empty -> () ) ; node.parent <- v ; Lwd.set node.var (`View node) ; `View node let make step parent node = let v = {step; var= Lwd.var `Empty; parent; node} in ( match parent with | `View parent -> parent.node <- v :: parent.node ; Lwd.set parent.var (`View parent) | `Empty -> () ) ; let rec iter = function | [] -> () | x :: xs -> x.parent <- `View v ; Lwd.set x.var (`View x) ; iter xs in iter node ; Lwd.set v.var (`View v) ; `View v let rec last = function | [] -> None | [x] -> Some x | _ :: xs -> last xs let rec last_def = function | [] -> "[]" | [x] -> x | _ :: xs -> last_def xs let find_node ~step ~view = match view with | `Empty -> None | `View v -> List.find_opt (fun v' -> v'.step = step) v.node let string_of_path path = "[" ^ F.str "%a" (F.list ~sep:F.semi F.string) path ^ "]" let remove (v : viewer) = Lwd.set v.var `Empty ; `Empty let storeview store path = Istore.get_tree store path >>= fun tree -> let update d key (view : view) : view option Lwt.t = F.epr "fold ~pre:update key=%s @." (string_of_path key) ; Lwt.return ( match ( List.rev key , find_node ~step:(Option.value (last key) ~default:"[]") ~view , d ) with | [], None, `Added | [], None, `Updated -> Some (make "[]" view []) | [], Some v, _ -> Some (`View v) | [], None, `Removed -> None | _ :: k :: _, _, _ when k.[0] == '#' -> None | k :: _, None, `Added | k :: _, None, `Updated -> Some (make k view []) | _ :: _, None, `Removed -> None | _ :: _, Some v, _ -> Some (`View v) ) in (* if pre returns None, the children of that node are skipped. *) let rec map ?(key = []) ~node tree (acc : view) : view Lwt.t = let acc = match acc with | `Empty -> make (last_def key) acc [] | v -> v in Istore.Tree.list tree [] >>= fun tree -> Lwt_list.iter_s (fun (s, t) -> let k = key @ [s] in node k acc >|= function | Some a -> F.epr "storeview Fold step=%s @." s ; ignore (map ~key:k ~node t a) | None -> F.epr "storeview None step=%s @." s ) tree >|= fun () -> acc in map ~node:(update `Added) tree `Empty >>= fun t -> let root = Lwd.var t in Istore.watch_key store path (fun diff -> let d, tree = match diff with | `Added (_, tree) -> (`Added, tree) | `Removed (_, tree) -> (`Removed, tree) | `Updated (_, (_, tree)) -> (`Updated, tree) in map ~node:(update d) tree t >>= fun t' -> Lwd.set root t' ; Lwt.return_unit ) >>= fun watch -> Lwt.return (watch, root) let channelview (store, path) = storeview store path >>= fun (_watch, root) -> let ui = Lwd.join (Lwd.map (Lwd.get root) ~f:(function | `Empty -> failwith "channelview says root Lwd.var is `Empty" | `View v -> let rec iter ?(indent = 0) (v : viewer) = Lwd.bind (Lwd.get v.var) ~f:(function | `Empty -> Lwd.return Ui.empty | `View v' -> let sub = Lwd_utils.pack Ui.pack_y (List.map (iter ~indent:(indent + 1)) v'.node ) in Lwd.map sub ~f:(fun sub -> Ui.join_y (Ui.string ( String.make indent '>' ^ " " ^ v'.step ) ) sub ) ) in iter v ) ) in let chs, chs_push = Lwt_stream.create () in Channel.make store ~path:[base_path] ~name:topch >>= fun ch -> chs_push (Some ch) ; Lwt.return (chs, ui) let messagelist ({store; path} : Channel.t) mlist : Istore.watch Lwt.t = let mlist' () = Istore.get_tree store path >>= fun tree -> Istore.Tree.fold ~depth:(`Eq 5) ~contents:(fun key contents view -> match key with | [y; m; d; h; s] -> Lwt.return (((y, m, d, h, s), contents) :: view) | _ -> F.epr "ERROR: messagelist (fold ~depth:(`Eq 5)) got \ wrong number of steps@." ; Lwt.return view ) ~node:(fun _key _node view -> F.epr "ERROR: messagelist (fold ~depth:(`Eq 5)) found a \ node@." ; Lwt.return view ) tree [] in mlist' () >>= fun ml -> Lwd.set mlist ml ; Istore.watch_key store path (fun _ -> mlist' () >>= fun mlist' -> Lwt.return (Lwd.set mlist mlist') ) let messageview ch = let mlist = Lwd.var [(("", "", "", "", ""), "")] in let rec update_messagelist watch () = Lwt_stream.last_new ch >>= fun ch -> ( match watch with | None -> Lwt.return_unit | Some w -> Istore.unwatch w ) >>= fun () -> messagelist ch mlist >>= fun watch -> update_messagelist (Some watch) () in Lwt.async (update_messagelist None) ; Lwt.return (Lwd.map (Lwd.get mlist) ~f:(fun mlist -> scroll (List.fold_left (fun doc ((year, month, day, hour, sec), content) -> F.epr "Communicator.Panel.messagelist ch.content=%s@." content ; doc ^/^ Ui.string (F.str "%s.%s.%s.%s.%s" year month day hour sec ) ^^ Ui.string " | " ^^ string content ) Ui.empty mlist ) ) ) let commview (store, path) = channelview (store, List.rev (List.tl (List.rev path))) >>= fun (ch, cv) -> messageview ch >>= fun mv -> Lwt.return (Lwd.map2 cv mv ~f:(fun c m -> join_x c m)) let panel ({store; view} : Tree.t) : (Event.t -> atom Lwt.t) Lwt.t = commview (store, view) >>= fun cv -> Panel.Ui.panel cv end end (** program starts... - spawn connections to servers - these connections will populate the Channel.t in a Channel.tree **) let _ = Lwt.async (fun () -> Communicator.Tree.make_top "commstore" "current" >>= fun comm -> Communicator.Irc.Config.make_connection comm "irc.hackint.org" 6697 "cqcaml" >>= fun () -> Lwt.async (fun () -> Communicator.Irc.connect comm) ; F.epr "root_actor := std_actor (Communicator.Panel.panel comm)@." ; Communicator.Panel.panel comm >|= fun f -> root_actor := std_actor (Lwt.return Panel. { act= (fun _ events -> Lwt_list.fold_left_s (fun _ ev -> f ev >>= fun i -> Lwt.return (fun s -> ( s , ( Gg.Box2.of_pts Gg.V2.zero (snd i) , fst i ) ) ) ) Display.pane_empty events ) ; subpanels= [] ; tag= "irc" } ) )