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 me); 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"; ] (* 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 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 me)) 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 tgt txt = let* me = require_registered t in let* dst = try match Irc.name_type tgt with | `chan -> Ok (`to_chan (Router.find_chan t.router tgt)) | _ -> Ok (`to_user (Router.find_user t.router tgt)) with Not_found -> Error (nosuchnick tgt) 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 msg = Irc.Msg.make "PRIVMSG" [tgt; 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_self; Router.relay msg ~from:me (`to_chan chan); (* 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_self; Router.relay msg ~from:me (`to_chan chan); 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 user_get_mode user me = let modestr = Fmt.str "+%a" Irc.Mode.Set.pp (User.mode user) in let msg = Irc.Msg.make "MODE" [User.nick user; modestr] ~always_trailing:true in Router.relay msg ~from:me `to_self; Ok () let user_set_mode user me modestr _args = (* TODO: only +o can set modes for users besides themselves *) let* set = try Ok (Irc.Mode.Parse.user modestr) with Irc.Mode.Parse.Error -> Error modeunknownflag in let mode = User.mode user in (* TODO: only +o can set +o mode *) let mode, modestr = let open Irc.Mode.Set in let add = diff set.add mode in let rem = inter set.rem mode in let mode = union mode add in let mode = diff mode rem in mode, Fmt.str "%a" Irc.Mode.Parse.pp_user_mode_set { add; rem } in let msg = Irc.Msg.make "MODE" [User.nick user; modestr] ~always_trailing:true in (* TODO: if setting mode for user besides self, notify them too *) Router.relay msg ~from:me `to_self; User.set_mode user mode; Ok () let chan_get_mode chan me = let _ = me, chan in (* TODO *) Error (tryagain "MODE") let chan_set_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* set, get = try match Irc.name_type name with | `nick -> let u = Router.find_user t.router name in Ok (user_set_mode u, user_get_mode u) | `chan -> let c = Router.find_chan t.router name in Ok (chan_set_mode c, chan_get_mode c) | `invalid -> raise Not_found with Not_found -> Error (nosuchnick name) in match args with | [] -> get me | modestr :: args -> set me modestr args (* misc *) let about t me = 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; user_get_mode me me 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)