package calculon
Source file Core.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252(** {1 Core IRC state} *) module Format_ = Format open Prelude open Lwt_infix module Msg = Irc_message let logs_src = Logs.Src.create ~doc:"logs for calculon" "calculon" module Log = (val Logs.src_log logs_src) type irc_msg = Irc_message.t type privmsg = { nick: string; (* author *) to_: string; (* target *) message: string; } let is_chan s = (not (String.equal s "")) && Char.equal s.[0] '#' && not (String.contains s ' ') let nick msg = msg.nick let reply_to msg = if is_chan msg.to_ then msg.to_ (* reply on same channel *) else msg.nick (* in pv *) let privmsg_of_msg msg = match msg.Msg.command with | Msg.PRIVMSG (to_, message) -> Some { nick = Option.get_or "msg prefix" msg.Msg.prefix |> get_nick; to_; message; } | _ -> None let string_of_privmsg msg = Printf.sprintf "{nick:%S; to:%S; msg: %S}" msg.nick msg.to_ msg.message module type S = sig module I : Irc_client.CLIENT with type 'a Io.t = 'a Lwt.t type connection = I.connection_t val connection : connection val init : unit Lwt.t val exit : unit Lwt.t val send_exit : unit -> unit val messages : Msg.t Signal.t val privmsg : privmsg Signal.t val line_cut_threshold : int ref (** Above [!line_cut_threshold], multi-line messages are cut with "..." *) val send_privmsg_l : target:string -> messages:string list -> unit Lwt.t val send_privmsg_l_nolimit : ?delay:float -> target:string -> messages:string list -> unit -> unit Lwt.t (** Version of {!send_privmsg_l} that does not enforce cut threshold. @param delay optional delay between each sent message *) val send_privmsg : target:string -> message:string -> unit Lwt.t (** Helper for sending messages, splitting lines, etc. *) val send_notice_l : target:string -> messages:string list -> unit Lwt.t val send_notice : target:string -> message:string -> unit Lwt.t (** Helper for sending notices, splitting lines, etc. *) val send_join : channel:string -> unit Lwt.t val send_part : channel:string -> unit Lwt.t val talk : target:string -> Talk.t -> unit Lwt.t end type t = (module S) module Make (I : Irc_client.CLIENT with type 'a Io.t = 'a Lwt.t) (Conn : sig val c : I.connection_t end) = struct module I = I type connection = I.connection_t let connection = Conn.c let init = Lwt.return_unit (* already done! *) let exit, send_exit = Lwt.wait () let send_exit () = Lwt.wakeup send_exit () let messages = Signal.create () let privmsg = Signal.filter_map messages privmsg_of_msg let line_cut_threshold = ref 10 let process_list_ ?(bypass_limit = false) ?sep ~f ~target ~messages:lines () = (* keep at most 4, unless [bypass_limit=true] *) let lines = let len = List.length lines in if (not bypass_limit) && len > !line_cut_threshold then CCList.take 4 lines @ [ Printf.sprintf "(…%d more lines…)" (len - 4) ] else lines in let delay_between = ref 0.3 in Lwt_list.iter_s (fun message -> let* () = f ~connection ~target ~message in let* () = Lwt_unix.sleep !delay_between in delay_between := CCFloat.min (!delay_between +. 0.2) 1.0; match sep with | None -> Lwt.return () | Some f -> f ()) lines let split_lines_ = CCString.Split.list_cpy ~by:"\n" let flat_map = CCList.flat_map let send_privmsg_l ~target ~messages = process_list_ ~f:I.send_privmsg ~target ~messages:(flat_map split_lines_ messages) () let send_privmsg_l_nolimit ?(delay = 0.5) ~target ~messages () = process_list_ ~f:I.send_privmsg ~sep:(fun () -> Lwt_unix.sleep delay) ~target ~bypass_limit:true ~messages:(flat_map split_lines_ messages) () let send_notice_l ~target ~messages = process_list_ ~f:I.send_notice ~target ~bypass_limit:false ~messages:(flat_map split_lines_ messages) () let send_privmsg ~target ~message = process_list_ ~target ~messages:(split_lines_ message) ~f:I.send_privmsg () let send_notice ~target ~message = process_list_ ~target ~messages:(split_lines_ message) ~f:I.send_notice () let send_join ~channel = I.send_join ~connection ~channel let send_part ~channel = I.send ~connection Irc_message.{ prefix = None; command = PART ([ channel ], "bye y'all") } let talk ~target ty = let message = Talk.select ty in send_privmsg ~target ~message end module Run (I : Irc_client.CLIENT with type 'a Io.t = 'a Lwt.t) (F : sig val connect : unit -> I.connection_t option Lwt.t val conn_info : string val init : t -> unit Lwt.t end) = struct let run () : unit Lwt.t = let self : t option ref = ref None in I.reconnect_loop ~keepalive:{ I.mode = `Passive; timeout = 300 } ~after:60 ~connect:(fun () -> Log.info (fun k -> k "trying to (re)connect%s…" (if String.equal F.conn_info "" then "" else " to " ^ F.conn_info)); F.connect ()) ~callback:(fun _ msg_or_err -> match !self with | None -> Lwt.return () | Some (module C) -> (match msg_or_err with | Result.Ok msg -> Signal.send C.messages msg | Result.Error err -> Log.err (fun k -> k "error: %s" err); Lwt.return ())) ~f:(fun conn -> Log.info (fun k -> k "connected, instantiate core"); let module C = Make (I) (struct let c = conn end) in let new_c = (module C : S) in self := Some new_c; F.init new_c) () end let loop_ssl ?(conn_info = "") ~connect ~init () : unit Lwt.t = let module R = Run (Irc_client_lwt_ssl) (struct let connect = connect let conn_info = conn_info let init = init end) in R.run () let loop_unsafe ?(conn_info = "") ~connect ~init () : unit Lwt.t = let module R = Run (Irc_client_lwt) (struct let connect = connect let conn_info = conn_info let init = init end) in R.run () let run conf ~init () = let module C = Config in let init (core : t) = let (module C) = core in init core in let conn_info = Printf.sprintf "%s/%d" conf.C.server conf.C.port in if conf.C.tls then ( let tls_config = Irc_client_lwt_ssl.Config.default in let connect () = Irc_client_lwt_ssl.connect_by_name ~username:conf.C.username ~realname:conf.C.realname ~nick:conf.C.nick ?password:conf.C.password ~server:conf.C.server ~port:conf.C.port ~config:tls_config ~sasl:conf.C.sasl () in loop_ssl ~conn_info ~connect ~init () ) else ( let connect () = Irc_client_lwt.connect_by_name ~username:conf.C.username ~realname:conf.C.realname ~nick:conf.C.nick ~server:conf.C.server ~port:conf.C.port () in loop_unsafe ~conn_info ~connect ~init () )