open! Import open Result_syntax module User = Router.User module Chan = Router.Chan include (val Logging.sublogs logger "Connection") type t = { router : Router.t; addr : sockaddr; outbox : Outbox.t; mutable user : User.t option; mutable pending_nick : string option; mutable pending_userinfo : Irc.userinfo option; } let make ~(router : Router.t) ~(addr : sockaddr) : t = let outbox = Outbox.make () in { router; addr; outbox; user = None; pending_nick = None; pending_userinfo = None } let outbox t = t.outbox let shutdown ?reason t = Option.iter (fun me -> let reason = Option.value reason ~default:"Goot bye" in Router.relay (Irc.Msg.make "QUIT" [reason]) ~from:me [`to_interested]; User.part_all me; (* TODO: BUG: unregister empty channels *) User.unregister me ~router:t.router) t.user; Outbox.close t.outbox (* TODO: configure these in some centralized location *) let srv_host = "irc.tali.software" let srv_ver = "0.0.0" let srv_created = "Sun Jan 7 09:58:24 PM EST 2024" let srv_motd_lines = [ "MEOW MEOW MEOW MEOW MEOW"; "meow meow meow meow meow"; "meowmeowmeowmeowmeowmeow"; ] let initial_user_modestr = "iw" (* let initial_chan_modestr = "nst" *) (* numeric replies *) type reply = string * string list type 'a result = ('a, reply) Result.t let reply t (num, params) = let prefix = Irc.Msg.Server_prefix srv_host in let target = match t.user with | Some me -> User.nick me | None -> "*" in Outbox.send t.outbox (Irc.Msg.make ~prefix num (target :: params)) let tryagain cmd = "263", [cmd; "Please wait a while and try again."] let nosuchnick tgt = "401", [tgt; "No such nick/channel"] let nosuchchannel tgt = "403", [tgt; "No such channel"] let norecipient = "411", ["No recipient given (PRIVMSG)"] let notexttosend = "412", ["No text to send"] let unknowncommand cmd = "421", [cmd; "Unknown command"] let nonicknamegiven = "431", ["No nickname given"] let erroneusnickname nick = "432", [nick; "Erroneus nickname"] let nicknameinuse nick = "433", [nick; "Nickname is already in use"] let notonchannel chan = "442", [chan; "You're not on that channel"] let notregistered = "451", ["You have not registered"] let needmoreparams cmd = "461", [cmd; "Not enough parameters"] let alreadyregistered = "462", ["Unauthorized command (already registered)"] let modeunknownflag = "501", ["Unknown MODE flag"] (* user registration *) let require_registered t : User.t result = match t.user with | Some me -> Ok me | None -> Error notregistered let attempt_to_register ~welcome t = (* [welcome : t -> user -> unit result] is defined near the bottom of this file *) match t.pending_nick, t.pending_userinfo with | Some nick, Some userinfo -> t.pending_nick <- None; let me = User.make ~userinfo ~outbox:t.outbox in begin match User.set_nick me nick ~router:t.router with | `nick_in_use -> Error (nicknameinuse nick) | `nick_set -> t.user <- Some me; welcome t me; Ok () end | _, _ -> Ok () let on_msg_nick ~welcome t nick = let* () = match Irc.name_type nick with | `nick -> Ok () | `chan | `invalid -> Error (erroneusnickname nick) in match t.user with | Some me -> begin let msg = Irc.Msg.make "NICK" [nick] in match User.set_nick me nick ~router:t.router ~success_callback:(fun () -> Router.relay msg ~from:me [`to_interested]) with | `nick_in_use -> Error (nicknameinuse nick) | `nick_set -> Ok () end | None -> t.pending_nick <- Some nick; attempt_to_register t ~welcome let on_msg_user ~welcome t username realname = match t.user with | Some _me -> Error alreadyregistered | None -> (* TODO: configure hiding hostnames *) let hostname = match t.addr with | ADDR_INET (ia, _) -> Unix.string_of_inet_addr ia | ADDR_UNIX path -> path in t.pending_userinfo <- Some { username; realname; hostname }; attempt_to_register t ~welcome (* messages and channels *) let on_msg_privmsg t name txt = let* me = require_registered t in let* tgt = try match Irc.name_type name with | `chan -> Ok (`chan (Router.find_chan t.router name)) | _ -> Ok (`user (Router.find_user t.router name)) with Not_found -> Error (nosuchnick name) in (* TODO: check if user is away *) (* TODO: check if channel is +n and user is not a member *) (* TODO: check if channel is +m and user is not priviledged *) (* TODO: check if channel is +b *) let name, dst = match tgt with | `chan c -> Chan.name c, [`to_chan c] | `user u -> User.nick u, [`to_user u] in let msg = Irc.Msg.make "PRIVMSG" [name; txt] ~always_trailing:true in Router.relay msg ~from:me dst; Ok () let list_names t chan = begin List.iter (fun user -> (* TODO: check if user is +i and not in channel with them *) reply t ("353", ["@"; Chan.name chan; User.nick user])) (Chan.members chan); reply t ("366", [Chan.name chan; "End of NAMES list"]) end let on_msg_names t name = let* _me = require_registered t in let* chan = try match Irc.name_type name with | `chan -> Ok (Router.find_chan t.router name) | _ -> Error (nosuchchannel name) with Not_found -> Error (nosuchchannel name) in (* TODO: check if channel is +s and user not member of channel *) list_names t chan; Ok () let on_msg_join t name = let* me = require_registered t in (* TODO: keys parameter *) (* TODO: "0" parameter means part from all channels *) let* chan = try match Irc.name_type name with | `chan -> Ok (Router.find_chan t.router name) | _ -> Error (nosuchchannel name) with Not_found -> debug (fun m -> m "making new channel %S" name); let chan = Chan.make ~name in Chan.register chan ~router:t.router; (* TODO: make user +o *) Ok chan in (* TODO: check if channel is +k *) Chan.join chan me; let msg = Irc.Msg.make "JOIN" [name] in Router.relay msg ~from:me [`to_chan chan; `to_self]; (* TODO: send channel topic *) list_names t chan; Ok () let on_msg_part t name = let* me = require_registered t in (* TODO: part reason *) let* chan = try match Irc.name_type name with | `chan -> Ok (Router.find_chan t.router name) | `nick | `invalid -> raise Not_found with Not_found -> Error (nosuchchannel name) in let* () = if Chan.is_member chan me then Ok () else Error (notonchannel name) in let msg = Irc.Msg.make "PART" [name] in Router.relay msg ~from:me [`to_chan chan; `to_self]; Chan.part chan me; if Chan.no_members chan then begin debug (fun m -> m "recycling channel %S" name); Chan.unregister chan ~router:t.router; end; Ok () (* modes *) let set_user_mode from user chg = let mode, chg = Irc.Mode.Set.normalize (User.mode user) chg in let modestr = Fmt.str "%a" Irc.Mode.Set.pp_change chg in let msg = Irc.Msg.make "MODE" [User.nick user; modestr] ~always_trailing:true in Router.relay msg ~from [`to_user user; `to_self]; User.set_mode user mode let on_get_user_mode user _me = (* TODO: only +o can get/set modes for users besides themselves *) Ok [ "221", [Fmt.str "+%a" Irc.Mode.Set.pp (User.mode user)] ] let on_set_user_mode user me modestr _args = (* TODO: only +o can get/set modes for users besides themselves *) let* chg = try Ok (Irc.Mode.Parse.user_modes modestr) with Irc.Mode.Parse.Error -> (* TODO: "If one or more modes sent are not implemented on the server, the server MUST apply the modes that are implemented, and then send the ERR_UMODEUNKNOWNFLAG (501) in reply along with the MODE message." *) Error modeunknownflag in (* TODO: only +o can set +o mode *) set_user_mode me user chg; Ok () let on_get_chan_mode chan me = let _ = me, chan in (* If is not given, the RPL_CHANNELMODEIS (324) numeric is returned. Servers MAY choose to hide sensitive information such as channel keys when sending the current modes. Servers MAY also return the RPL_CREATIONTIME (329) numeric following RPL_CHANNELMODEIS. *) Error (tryagain "MODE") let on_set_chan_mode chan me modestr args = let _ = me, chan, modestr, args in (* TODO *) Error (tryagain "MODE") let on_msg_mode t name args = let* me = require_registered t in let* on_set, on_get = try match Irc.name_type name with | `nick -> let u = Router.find_user t.router name in Ok (on_set_user_mode u, on_get_user_mode u) | `chan -> let c = Router.find_chan t.router name in Ok (on_set_chan_mode c, on_get_chan_mode c) | `invalid -> raise Not_found with Not_found -> Error (nosuchnick name) in match args with | [] -> let+ rpls = on_get me in List.iter (reply t) rpls | modestr :: args -> on_set me modestr args (* misc *) let about t me : unit = let who = Irc.Msg.prefix_string (User.prefix me) in begin reply t ("001", [Fmt.str "Welcome to the tali IRC network %s" who]); reply t ("002", [Fmt.str "Your host is %s, running version %s" srv_host srv_ver]); reply t ("003", [Fmt.str "This server was created %s" srv_created]); reply t ("004", [srv_host; srv_ver; "iow"; "imnst"; "bklov"]); reply t ("005", ["CASEMAPPING=ascii"; "CHANTYPES=#"; "CHANMODES=b,k,l,imstn"; "PREFIX=(ov)@+"; "are supported by this server"]); end let motd t = begin reply t ("375", [Fmt.str "- %s Message of the day - " srv_host]); List.iter (fun ln -> reply t ("372", ["- " ^ ln])) srv_motd_lines; reply t ("376", ["End of /MOTD command"]); end let welcome t me = about t me; motd t; set_user_mode me me { add = Irc.Mode.Set.of_string initial_user_modestr; rem = Irc.Mode.Set.empty; } let on_msg_nick = on_msg_nick ~welcome let on_msg_user = on_msg_user ~welcome let on_msg_motd t = let* _me = require_registered t in motd t; Ok () let on_msg_quit t reason = (* TODO: '''When connections are terminated by a client-sent QUIT command, servers SHOULD prepend with the ASCII string "Quit: " when sending QUIT messages to other clients''' *) let reason = String.concat " " reason in shutdown t ~reason; Ok () (* message parsing *) let dispatch t = function | "NICK", nick :: _ when nick <> "" -> on_msg_nick t nick | "NICK", _ -> Error nonicknamegiven | "USER", unm :: _ :: _ :: rnm :: _ -> on_msg_user t unm rnm | "QUIT", why -> on_msg_quit t why | "MOTD", _ -> on_msg_motd t | "PRIVMSG", ([] | "" :: _) -> Error norecipient | "PRIVMSG", ([_] | _ :: "" :: _) -> Error notexttosend | "PRIVMSG", tgt :: msg :: _ -> on_msg_privmsg t tgt msg | "JOIN", tgt :: _ when tgt <> "" -> on_msg_join t tgt | "NAMES", tgt :: _ when tgt <> "" -> on_msg_names t tgt | "PART", tgt :: _ when tgt <> "" -> on_msg_part t tgt | "MODE", tgt :: args when tgt <> "" -> on_msg_mode t tgt args | ("USER" | "JOIN" | "NAMES" | "PART" | "MODE") as cmd, _ -> Error (needmoreparams cmd) | cmd, _ -> Error (unknowncommand cmd) let split_command_params cmd params = match cmd, params with | ("PRIVMSG" | "JOIN" | "NAMES" | "PART"), tgts :: rest when String.contains tgts ',' -> (* TODO: "JOIN" should be handled specially *) String.split_on_char ',' tgts |> List.map (fun tgt -> cmd, tgt :: rest) | _ -> [cmd, params] let pp_args ppf (cmd, params) = Fmt.pf ppf "@[%s@ %a@]" cmd (Fmt.list (Fmt.fmt "%S") ~sep:Fmt.sp) params let on_msg t (msg : Irc.Msg.t) : unit = split_command_params msg.command msg.params |> List.iter (fun args -> trace (fun m -> m "@[%a:@ %a@]" pp_sockaddr t.addr pp_args args); match dispatch t args with | Ok () -> () | Error err -> reply t err)