(*************************************************************************)
(* This program is free software: you can redistribute it and/or modify *)
(* it under the terms of the version 3 of the GNU General Public License *)
(* as published by the Free Software Foundation. *)
(* *)
(* This program is distributed in the hope that it will be useful, but *)
(* WITHOUT ANY WARRANTY; without even the implied warranty of *)
(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *)
(* General Public License for more details. *)
(* *)
(* You should have received a copy of the GNU General Public License *)
(* along with this program. If not, see . *)
(* *)
(* Written and (C) by Francois Fleuret *)
(* Contact for comments & bug reports *)
(*************************************************************************)
open Connection;;
(* I removed all the dcc stuff between v0.25 and v0.5 because it was a
biiiig pile of crap. Feel free to write it ! *)
let version = "IRCML v0.5";;
let term_bold = ref "[1m"
and term_underline = ref "[4m"
and term_inverse = ref "[7m"
and term_reset = ref "[m";;
(*****************************************************************************)
(* Various stuff *)
(*****************************************************************************)
let keys h = Hashtbl.fold (fun key data accu -> key :: accu) h []
let verbose = ref false;;
(* Return the element after the given one, or the first of the list if
the given one is the last *)
let circular_succ =
fun x l ->
let rec next_rec x = function
[] -> raise Not_found
| h::t -> if h = x then List.hd t else next_rec x t
in try next_rec x l with Failure(e) -> List.hd l;;
let string_of_size size =
if size > 1048576 then (string_of_int (size / 1048576))^"Mb"
else if size > 1024 then (string_of_int (size / 1024))^"Kb"
else (string_of_int size) ^ "b";;
let duration_of_seconds duration =
(if duration >= 3600 then (string_of_int (duration/3600))^"h" else "") ^
(if (duration mod 3600) >= 60 then (string_of_int ((duration mod 3600)/60))^"min" else "") ^
(if (duration mod 60) > 0 then (string_of_int (duration mod 60))^"s" else "");;
(*****************************************************************************)
(* The global variables *)
(*****************************************************************************)
type user_status = { mutable operator: bool }
let string_of_user = fun u -> (if u.operator then "@" else "")
type channel = { name: string; people: (string, user_status) Hashtbl.t ; modes: (char, string) Hashtbl.t };;
let ssfe = ref false
and my_host = ref ""
and desired_nick = ref "b0z0"
and desired_server = ref "localhost"
and desired_port = ref 6667
and current_nick = ref ""
and (current_modes: (char, string) Hashtbl.t) = Hashtbl.create 10
and default_channel = ref ""
and login = ref "c4mlp0w4"
and name = ref ("Using " ^ version ^ ", the light IRC client in CAML")
and alive = ref true;;
(* Print something with a 'header character' à la sirc *)
(* '<' Leaving channel (part, kick, signoff, etc.) *)
(* '>' Joining a channel *)
(* 'E' Error *)
(* 'I' Information *)
(* 'X' Internal error, should never meet one *)
let printh h s = output_string Pervasives.stdout
begin
match h with
"E" -> (!term_bold ^ "*" ^ h ^ "*" ^ " " ^ s ^ !term_reset ^ "\n")
| _ -> (!term_bold ^ "*" ^ h ^ "*" ^ !term_reset ^ " " ^ s ^ "\n")
end;;
(* Simply print something in the console *)
let print s = output_string Pervasives.stdout (s ^ "\n");;
type ssfe_cmd = Status of string | Tabulable of string;;
let current_status = ref "";;
let tell_ssfe x =
if !ssfe then begin
match x with
Status(s) -> if not (s = !current_status) then (current_status := s; print ("`#ssfe#s" ^ s))
| Tabulable(s) -> print ("`#ssfe#t" ^ s)
end;;
(*****************************************************************************)
(* Command line parameter parsing *)
(*****************************************************************************)
Arg.parse [ ("-nick", Arg.String(fun n -> desired_nick := n), "Set the desired nick");
("-server", Arg.String(fun s -> desired_server := s), "Set the desired server");
("-port", Arg.Int(fun p -> desired_port := p), "Set the desired port");
("-login", Arg.String(fun s -> login := s), "Set the login");
("-name", Arg.String(fun s -> name := s), "Set the name") ]
(fun s -> ()) "Unknown option";;
(*****************************************************************************)
(* The connection with the server *)
(*****************************************************************************)
type connection =
Established of Unix.file_descr
| Broken;;
let last_server_msg = ref 0;;
let server_connection = ref Broken;;
(* This is the list of file descriptors active for reading. Each one
corresponds to a reader which will be called when something is
readable and a exception-handler, called if the connection is lost,
with the reason *)
type fd_machine = { reader: unit -> unit; error_handler: string -> unit };;
let (fd_active: (Unix.file_descr, fd_machine) Hashtbl.t) = Hashtbl.create 10;;
(*****************************************************************************)
(* Handling of the channel list *)
(*****************************************************************************)
let (joined_channels: (string, channel) Hashtbl.t) = Hashtbl.create 10;;
let find_joined c = Hashtbl.find joined_channels (String.uppercase c)
and mem_joined c = Hashtbl.mem joined_channels (String.uppercase c)
and set_joined c x = Hashtbl.replace joined_channels (String.uppercase c) x
and remove_joined c = Hashtbl.remove joined_channels (String.uppercase c)
and same_channel c1 c2 = (String.uppercase c1) = (String.uppercase c2);;
let part_channel channel =
remove_joined channel;
if !default_channel = channel
then match (keys joined_channels) with
[] -> default_channel := ""; if not !ssfe then printh "I" "Not in a channel anymore"
| c::t -> default_channel := c; printh "I" ("Talking to " ^ !default_channel ^ " now");;
(*****************************************************************************)
(* Takes a string and returns an array of strings corresponding to the *)
(* tokens in the IRC protocole syntax. I.e. tokens are separated by white *)
(* spaces except the tail which is after a ':' *)
(*****************************************************************************)
let (irc_tokenize: string -> string array) = fun s ->
let l = (String.length s) in
let rec irc_tokenize s n =
if String.get s n == ':' then [String.sub s (n + 1) (l - n - 2)] else
try
let m = String.index_from s n ' ' in
let h = String.sub s n (m - n) in
h::irc_tokenize s (m+1)
with
Not_found -> if n < l then [String.sub s n (l - n - 1)] else []
in
(* Ignore the prefix if there is one *)
if (String.get s 0) == ':' then
let n = (String.index s ' ') in Array.of_list ((String.sub s 1 (n - 1))::(irc_tokenize s (n+1)))
else Array.of_list (""::(irc_tokenize s 0));;
(* Function to split the "nick!login@host" *)
let (split_prefix: string -> string * string * string) = fun prefix ->
let n1 = (String.index prefix '!') in
let n2 = (String.index_from prefix n1 '@') in
(String.sub prefix 0 n1,
String.sub prefix (n1 + 1) (n2 - (n1 + 1)),
String.sub prefix (n2 + 1) ((String.length prefix) - (n2 + 1)));;
(*****************************************************************************)
(* The aliases *)
(*****************************************************************************)
let (cmd_aliases: (string, string) Hashtbl.t) = Hashtbl.create 100;;
List.iter (fun (a, b) -> Hashtbl.replace cmd_aliases a b) [
("J", "JOIN"); ("M", "MSG"); ("K", "KICK"); ("L", "CHANINFO");
("N", "NICK"); ("Q", "QUIT"); ("T", "TOPIC"); ("W", "WHOIS");
("DC", "DISCONNECT"); ("S", "SERVER"); ("H", "HELP")
];;
(*****************************************************************************)
(* The managers will be filled later *)
(*****************************************************************************)
let (protocole_managers: (string, string array -> unit) Hashtbl.t) = Hashtbl.create 100;;
let (cmd_managers: (string, string * ((unit -> string) -> (unit -> string) -> unit)) Hashtbl.t) = Hashtbl.create 100;;
let (ctcp_managers: (string, (unit -> string) -> (unit -> string) -> string -> string -> unit) Hashtbl.t) = Hashtbl.create 10;;
(*****************************************************************************)
(* The routines handling lines from the server or the user *)
(*****************************************************************************)
let print_unknown_protocole = fun tokens ->
let buffer = Buffer.create 100 in
Buffer.add_string buffer "Unknown protocole";
for i = 0 to ((Array.length tokens) - 1) do
Buffer.add_string buffer (" " ^ (string_of_int i) ^ ":(" ^ tokens.(i) ^ ")");
done;
printh "X" (Buffer.contents buffer);;
let server_line_handler = fun line ->
if !verbose then (print_string ("SERVER [" ^ line ^ "]\n"); flush stdout);
last_server_msg := int_of_float (Unix.time ());
let tokens = (irc_tokenize line) in
try
let f = Hashtbl.find protocole_managers tokens.(1) in
try f tokens with Not_found -> printh "E" ("Protocole " ^ tokens.(1) ^ " raised Not_found!")
with
Not_found -> print_unknown_protocole tokens;;
let kill_server_connection error_message =
match !server_connection with
Established(fd) ->
begin
Hashtbl.remove fd_active fd;
server_connection := Broken;
printh "E" ("Connection lost (" ^ error_message ^ ")");
default_channel := "";
Hashtbl.clear joined_channels
end
| Broken -> exit 1;;
let tell_server line =
match !server_connection with
Established(fd) ->
begin
try
if (Unix.write fd line 0 (String.length line)) <= 0 then kill_server_connection ("write error")
with
Unix.Unix_error(e, fname, fparam) -> kill_server_connection (fname ^ ": " ^ (Unix.error_message e))
end
| Broken -> printh "E" "No connection";;
let establish_connection () =
match !server_connection with
Established(_) -> printh "E" "Connection already established (try /DISCONNECT first)"
| Broken ->
printh "I" ("Attempting connection to " ^ !desired_server ^
":" ^ (string_of_int !desired_port) ^
" with nick " ^ !desired_nick);
try
let fd = Connection.connect !desired_server !desired_port in
begin
match Unix.getsockname fd with
Unix.ADDR_INET(ai, _) -> my_host := Connection.dot_to_number (Unix.string_of_inet_addr ai)
| _ -> printh "E" "Internal error #0";
end;
server_connection := Established(fd);
Hashtbl.replace fd_active fd { reader = linizer fd server_line_handler; error_handler = kill_server_connection };
tell_server ("USER " ^ !login ^ " caml rulez :" ^ !name ^ "\n");
tell_server ("NICK " ^ !desired_nick ^ "\n");
last_server_msg := int_of_float (Unix.time ());
printh "I" ("Connection established, waiting for server checkings.");
with
Network_error(e) -> printh "E" ("Can not connect to host: " ^ e);;
let stdin_line_handler = fun line ->
(*print_string ("STDIN [" ^ line ^ "]\n"); flush stdout;*)
if String.length line > 0 then
match String.get line 0 with
'/' ->
begin
let (cl_token, cl_tail) = tokenizer line ' ' in
let cmd = String.uppercase (cl_token ()) in let cmd = String.sub cmd 1 ((String.length cmd) - 1) in
let cmd = try Hashtbl.find cmd_aliases cmd with Not_found -> cmd in
try
let (_, f) = (Hashtbl.find cmd_managers cmd) in
begin
try
f cl_token cl_tail
with
Not_found -> printh "E" ("Command " ^ cmd ^ " raised Not_found!")
end
with
Not_found -> printh "E" ("Unknown command '" ^ cmd ^ "'");
end
| '@' ->
begin
let (cl_token, cl_tail) = tokenizer line ' ' in
match cl_token () with
"@ssfe@i" ->
begin
ssfe := true;
printh "I" "You are using ssfe";
term_bold := "";
term_underline := "";
term_inverse := "";
term_reset := "";
end
| _ -> ()
end
| _ ->
if !default_channel = ""
then printh "E" "Not in a channel"
else
begin
print ("<" ^ !current_nick ^ "> " ^ line);
tell_server ("PRIVMSG " ^ !default_channel ^ " :" ^ line ^ "\n")
end;;
let console_reader = linizer Unix.stdin stdin_line_handler
and console_error = fun error_message -> printh "E" ("Stdin error (" ^ error_message ^ ")"); exit 1 in
Hashtbl.replace fd_active Unix.stdin { reader = console_reader; error_handler = console_error };;
(*****************************************************************************)
(* Functions to handle scripts *)
(*****************************************************************************)
Hashtbl.add cmd_managers
"LOAD"
begin
": Loads the given bytecode object",
fun token tail ->
try
let module_name = token () in
printh "I" ("loading module " ^ module_name);
try
Dynlink.loadfile(module_name)
with
Dynlink.Error error -> printh "E" ("Module loading error (" ^ (Dynlink.error_message error) ^ ")");
| Sys_error error_message -> printh "E" ("Module loading error (" ^ error_message ^ ")");
with
Not_found -> printh "E" "Missing argument"
end;
(*****************************************************************************)
(* Functions to handle the various user commands *)
(*****************************************************************************)
(* Each manager gets two (unit -> string), the first one to get tokens
one after another and the second one to get the tail when required *)
Hashtbl.add cmd_managers
"TEST"
begin
"",
fun token tail ->
Hashtbl.iter
begin
fun name chan ->
Hashtbl.iter
(fun nick user -> print ("on " ^ name ^ " : [" ^ (string_of_user user) ^ "] " ^ nick))
chan.people
end
joined_channels
end;
Hashtbl.add cmd_managers
"HELP"
begin
"[]: Shows the help for the given command, or for all of them",
fun token tail ->
try
let cmd = String.uppercase (token ()) in
try
let (h, f) = (Hashtbl.find cmd_managers cmd) in
printh "H" ("/" ^ cmd ^ " " ^ h)
with
Not_found -> printh "H" ("Unknown command " ^ cmd)
with
Not_found -> Hashtbl.iter (fun cmd (help, f) -> (printh "H" ("/" ^ cmd ^ " " ^ help))) cmd_managers
end;
Hashtbl.add cmd_managers
"ME"
begin
"[]: Describes an action in the default channel",
fun token tail ->
if !default_channel = ""
then printh "E" "Not in a channel"
else let msg = try (tail ()) with Not_found -> "" in
tell_server ("PRIVMSG " ^ !default_channel ^ " :ACTION " ^ msg ^ "\n");
print ("* " ^ !current_nick ^ " " ^ msg)
end;
Hashtbl.add cmd_managers
"AWAY"
begin
"[]: Sets yourself away",
fun token tail ->
try tell_server ("AWAY :" ^ (tail ()) ^ "\n")
with Not_found -> tell_server "AWAY\n";
end;
Hashtbl.add cmd_managers
"MSG"
begin
" : Sends a message to a user, or a channel",
fun token tail ->
try
let dest = (token ()) and msg = (tail ()) in
tell_ssfe (Tabulable ("/msg " ^ dest ^ " "));
match String.get dest 0 with
'#' | '&' ->
begin
print ("<" ^ !current_nick ^ (if same_channel !default_channel dest then "" else "/" ^ dest) ^ "> " ^ msg);
tell_server ("PRIVMSG " ^ dest ^ " :" ^ msg ^ "\n")
end
| _ ->
begin
print (">" ^ dest ^ "< " ^ msg);
tell_server ("PRIVMSG " ^ dest ^ " :" ^ msg ^ "\n")
end
with Not_found -> printh "E" "Missing parameter"
end;
Hashtbl.add cmd_managers
"CTCP"
begin
" : Sends a CTCP (string quoted between ^A) to a user or a channel",
fun token tail ->
try
let dest = (token ()) and msg = (tail ()) in
tell_ssfe (Tabulable ("/msg " ^ dest ^ " "));
printh "I" ("Sending a CTCP " ^ msg ^ " to " ^ dest);
tell_server ("PRIVMSG " ^ dest ^ " :" ^ msg ^ "\n")
with Not_found -> printh "E" "Missing parameter"
end;
Hashtbl.add cmd_managers
"NOTICE"
begin
" : Sends a notice to a user or a channel",
fun token tail ->
try
let dest = (token ()) and msg = (tail ()) in
print ("-> -" ^ dest ^ "- " ^ msg); tell_server ("PRIVMSG " ^ dest ^ " :" ^ msg ^ "\n")
with Not_found -> printh "E" "Missing parameter"
end;
Hashtbl.add cmd_managers
"WHOIS"
begin
"[]: Requests information about a user",
fun token tail ->
let nick = try token () with Not_found -> !current_nick in
if nick = ""
then printh "E" "Missing argument"
else tell_server ("WHOIS " ^ nick ^ "\n")
end;
Hashtbl.add cmd_managers
"WI"
begin
"[]: Requests information about a user on his server (get the idle)",
fun token tail ->
let nick = try token () with Not_found -> !current_nick in
if nick = ""
then printh "E" "Missing argument"
else tell_server ("WHOIS " ^ nick ^ " " ^ nick ^ "\n")
end;
Hashtbl.add cmd_managers
"SERVER"
begin
"[[:]]: Establishes a connection to a new server",
fun token tail ->
begin
try
let (stoken, stail) = tokenizer (token ()) ':' in
desired_server := stoken ();
desired_port := int_of_string (stoken ())
with
Not_found -> ()
| Failure(_) -> printh "E" "Syntax error"
end;
establish_connection ()
end;
Hashtbl.add cmd_managers
"NEXT"
begin
": Selects the next joined channel as default",
fun token tail ->
try
default_channel := (find_joined (circular_succ (String.uppercase !default_channel) (keys joined_channels))).name;
if not !ssfe then printh "I" ("Talking to " ^ !default_channel ^ " now")
with
Not_found -> printh "E" "You have to join a channel first"
end;
Hashtbl.add cmd_managers
"JOIN"
begin
"[<#channel> []]: Joins a channel or tells what is the default channel",
fun token tail ->
try
let channel = token () in let chan = match String.get channel 0 with '#' | '&' -> channel | _ -> "#" ^ channel in
let key = try " " ^ (token ()) with Not_found -> "" in
if mem_joined channel
then (default_channel := chan; if not !ssfe then printh "I" ("Talking to " ^ chan ^ " now"))
else tell_server ("JOIN " ^ chan ^ key ^ "\n")
with Not_found ->
if !default_channel = ""
then printh "I" "You are not in a channel"
else printh "I" ("You current channel is " ^ !default_channel)
end;
Hashtbl.add cmd_managers
"TOPIC"
begin
"[]: Sets or requests the topic in the current channel",
fun token tail ->
if !default_channel = ""
then printh "E" "Not in a channel"
else
try let topic = tail () in tell_server ("TOPIC " ^ !default_channel ^ " :" ^ topic ^ "\n")
with Not_found -> tell_server ("TOPIC " ^ !default_channel ^ "\n")
end;
Hashtbl.add cmd_managers
"MODE"
begin
"[]: Changes mode or shows the mode of the current channel",
fun token tail ->
let chan = try token () with Not_found -> !default_channel in
if chan = ""
then printh "E" "Need to specify a channel"
else
let chan = match String.get chan 0 with '#' | '&' -> chan | _ -> "#" ^ chan in
try tell_server ("MODE " ^ chan ^ " " ^ (tail ()) ^ "\n")
with Not_found -> tell_server ("MODE " ^ chan ^ "\n")
end;
Hashtbl.add cmd_managers
"KICK"
begin
"[<#channel>] []: Kicks the given user from the given (or current) channel",
fun token tail ->
if !default_channel = ""
then printh "E" "Not in a channel"
else try
let t = token () in
let (channel, nick) = match String.get t 0 with '#' | '&' -> (t, token ()) | _ -> (!default_channel, t)
and comment = try tail () with Not_found -> "B1G d0Rk5 SuCk M00z b4LLz" in
tell_server ("KICK " ^ !default_channel ^ " " ^ nick ^ " :" ^ comment ^ "\n")
with Not_found -> printh "E" "Missing parameter"
end;
Hashtbl.add cmd_managers
"LEAVE"
begin
"[<#channel>]: Leaves the current or the specified channel",
fun token tail ->
let chan = try token () with Not_found -> !default_channel in
if chan = ""
then printh "E" "Need to specify a channel"
else let chan = match String.get chan 0 with '#' | '&' -> chan | _ -> "#" ^ chan in
tell_server ("PART " ^ chan ^ "\n")
end;
Hashtbl.add cmd_managers
"CHANINFO"
begin
"[<#channel>]: Gets the user list for the given channel",
fun token tail ->
let chan = try token () with Not_found -> !default_channel in
if chan = "" then printh "E" "Need to specify a channel"
else
let chan = match String.get chan 0 with '#' | '&' -> chan | _ -> "#" ^ chan in
tell_server ("LIST " ^ chan ^ "\n");
tell_server ("NAMES " ^ chan ^ "\n")
end;
Hashtbl.add cmd_managers
"NICK"
begin
"[]: Changes or requests the current nick",
fun token tail ->
try let nick = token () in tell_server ("NICK " ^ nick ^ "\n")
with Not_found -> printh "I" ("You current nick is " ^ !current_nick)
end;
Hashtbl.add cmd_managers
"DISCONNECT"
begin
"[]: Signoffs on the current server with the given quit message",
fun token tail ->
let msg = try tail () with Not_found -> (version ^ ", the light IRC client in CAML") in
tell_server ("QUIT :" ^ msg ^ "\n");
end;
Hashtbl.add cmd_managers
"QUIT"
begin
"[]: Signoffs on the current server if connected, and terminates operations",
fun token tail ->
if !server_connection != Broken then begin
let msg = try tail () with Not_found -> (version ^ ", the light IRC client in CAML") in
tell_server ("QUIT :" ^ msg ^ "\n");
end;
alive := false
end;
Hashtbl.add cmd_managers
"QUOTE"
begin
": Sends the line to the server, as it is",
fun token tail ->
try tell_server (tail () ^ "\n")
with Not_found -> printh "E" "Missing parameter"
end;
Hashtbl.add cmd_managers
"VERBOSE"
begin
": Set the verbose mode (everything form the server will be printed)",
fun token tail ->
verbose := not !verbose;
if !verbose then printh "I" "Verbose mode on" else printh "I" "Verbose mode off"
end;
(*****************************************************************************)
(* Function to handle the various CTCPs *)
(*****************************************************************************)
Hashtbl.add ctcp_managers
"VERSION"
begin
fun token tail from dest -> tell_server ("NOTICE " ^ from ^ " :" ^ version ^ ", the light IRC client in CAML\n")
end;;
Hashtbl.add ctcp_managers
"ACTION"
begin
fun token tail from dest -> try print ("* " ^ from ^ " " ^ (tail())) with Not_found -> print ("* " ^ from)
end;;
(*****************************************************************************)
(* Functions to handle the mode changes (that part of the protocole sucks) *)
(*****************************************************************************)
let rec flatten_modes = fun polarity mode_string mode_indice param_strings param_indice ->
if mode_indice < (String.length mode_string)
then begin
match (String.get mode_string mode_indice) with
'+' -> flatten_modes 1 mode_string (mode_indice+1) param_strings param_indice
| '-' -> flatten_modes (-1) mode_string (mode_indice+1) param_strings param_indice
| 'k' | 'o' | 'v' | 'b' as c ->
(c, polarity, param_strings.(param_indice))::(flatten_modes polarity mode_string (mode_indice+1) param_strings (param_indice+1))
| 'm' | 'i' | 's' | 't' | 'n' as c ->
(c, polarity, "")::(flatten_modes polarity mode_string (mode_indice+1) param_strings param_indice)
| 'l' ->
if polarity = 1
then ('l', polarity, param_strings.(param_indice))::(flatten_modes polarity mode_string (mode_indice+1) param_strings (param_indice+1))
else ('l', polarity, "")::(flatten_modes polarity mode_string (mode_indice+1) param_strings param_indice)
| other -> (other, polarity, "")::(flatten_modes polarity mode_string (mode_indice+1) param_strings param_indice)
end
else []
;;
let rec string_of_modelist = function
[] -> ""
| (c, l, p)::t ->
(if l > 0 then " +" else " -") ^
(String.make 1 c) ^
(if p = "" then "" else " " ^ p) ^
(string_of_modelist t)
let set_modes channel l =
let chan = find_joined channel in
List.iter
begin
function
| ('b', pol, pattern) -> ()
| ('v', pol, nick) -> ()
| ('o', pol, nick) -> (Hashtbl.find chan.people nick).operator <- (pol > 0)
| (c, 1, param) -> Hashtbl.replace chan.modes c param
| (c, -1, param) -> Hashtbl.remove chan.modes c
| _ -> ()
end
l;;
let get_status = fun () ->
" [" ^ version ^ "] " ^
(try string_of_user (Hashtbl.find (find_joined !default_channel).people !current_nick)
with Not_found -> "")
^ !current_nick ^
" (+" ^
(if Hashtbl.mem current_modes 'i' then "i" else "") ^
(if Hashtbl.mem current_modes 'O' then "i" else "") ^
(if Hashtbl.mem current_modes 'o' then "i" else "") ^
(if Hashtbl.mem current_modes 'w' then "w" else "") ^
(if Hashtbl.mem current_modes 'a' then "a" else "") ^
")" ^
if !default_channel = "" then ""
else
" on " ^ !default_channel ^
let modes = (find_joined !default_channel).modes in
" (+" ^
(if Hashtbl.mem modes 'm' then "m" else "") ^
(if Hashtbl.mem modes 'i' then "i" else "") ^
(if Hashtbl.mem modes 's' then "s" else "") ^
(if Hashtbl.mem modes 'n' then "n" else "") ^
(if Hashtbl.mem modes 't' then "t" else "") ^
(try " key=" ^ (Hashtbl.find modes 'k') with Not_found -> "") ^
(try " limit=" ^ (Hashtbl.find modes 'l') with Not_found -> "") ^
")"
;;
(*****************************************************************************)
(* Function to handle the various commands received from the server *)
(*****************************************************************************)
(* This one just write a certain token as it is *)
let print_information i = fun tokens -> printh "I" tokens.(i);;
(* Here is the serious stuff ... each entry is a fun able to
interprete the tokens given as parameters *)
Hashtbl.add protocole_managers "001"
begin
fun tokens -> current_nick := tokens.(2);
printh "I" ("You are now connected as " ^ tokens.(2))
end;
Hashtbl.add protocole_managers "PING"
begin
fun tokens -> tell_server ("PONG " ^ tokens.(2) ^ "\n")
end;
Hashtbl.add protocole_managers "JOIN"
begin
fun tokens ->
let (nick, login, host) = split_prefix tokens.(0) in
if nick = !current_nick
then begin
printh ">" ("You have joined " ^ tokens.(2));
set_joined tokens.(2) { name=tokens.(2); people= Hashtbl.create 10; modes = Hashtbl.create 10 };
default_channel := tokens.(2);
tell_server ("MODE " ^ tokens.(2) ^ "\n")
end
else printh ">" (nick ^ " (" ^ login ^ "@" ^ host ^ ") has joined " ^ tokens.(2));
Hashtbl.replace (find_joined tokens.(2)).people nick { operator = false }
end;
Hashtbl.add protocole_managers "PART"
begin
fun tokens ->
let (nick, login, host) = split_prefix tokens.(0) in
if nick = !current_nick
then begin
printh "<" ("You have left " ^ tokens.(2));
part_channel tokens.(2)
end
else begin
printh "<" (nick ^ " (" ^ login ^ "@" ^ host ^ ") has left " ^ tokens.(2));
Hashtbl.remove (find_joined tokens.(2)).people nick;
end
end;
Hashtbl.add protocole_managers "QUIT"
begin
fun tokens -> let (nick, login, host) = split_prefix tokens.(0) in
let channels =
Hashtbl.fold
(fun name data s -> if Hashtbl.mem data.people nick then s ^ " " ^ data.name else s)
joined_channels "" in
printh "<" ("Signoff: " ^ nick ^ " (" ^ tokens.(2) ^ ") from" ^ channels);
Hashtbl.iter (fun name data -> Hashtbl.remove data.people nick) joined_channels;
end;
Hashtbl.add protocole_managers "KICK"
begin
fun tokens ->
let (nick, login, host) = split_prefix tokens.(0) in
if tokens.(3) = !current_nick
then begin
printh "<" ("You have been kicked from " ^ tokens.(2) ^ " by " ^ nick ^ " (" ^ tokens.(4)^ ")");
part_channel tokens.(2)
end
else printh "<" (tokens.(3) ^ " has been kicked from " ^ tokens.(2) ^ " by " ^ nick ^ " (" ^ tokens.(4) ^ ")");
Hashtbl.remove (find_joined tokens.(2)).people tokens.(3);
end;
Hashtbl.add protocole_managers "NICK"
begin
fun tokens -> let (nick, login, host) = split_prefix tokens.(0) in
if nick = !current_nick
then begin
printh "I" ("You are now known as " ^ tokens.(2));
current_nick := tokens.(2)
end
else printh "I" (nick ^ " is now known as " ^ tokens.(2));
let rename = begin
fun name data ->
let u = Hashtbl.find data.people nick in
try
Hashtbl.remove data.people nick;
Hashtbl.add data.people tokens.(2) u
with
Not_found -> ()
end in
Hashtbl.iter rename joined_channels;
end;
Hashtbl.add protocole_managers "MODE"
begin
fun tokens ->
let (nick, login, host) = try split_prefix tokens.(0) with Not_found -> (tokens.(0), "", "") in
let modes = flatten_modes 1 tokens.(3) 0 tokens 4 in
if match String.get tokens.(2) 0 with '#' | '&' -> true | _ -> false
then
begin
set_modes (String.uppercase tokens.(2)) modes;
printh "I" ("Mode change by " ^ nick ^ " on channel " ^ tokens.(2) ^ (string_of_modelist modes))
end
else
begin
List.iter
(function (c, 1, x) -> Hashtbl.replace current_modes c x | (c, -1, x) -> Hashtbl.remove current_modes c | _ -> ())
modes;
printh "I" ("Mode change" ^ (string_of_modelist modes) ^ " by " ^ nick ^ " for user " ^ tokens.(2))
end
end;
Hashtbl.add protocole_managers "TOPIC"
begin
fun tokens ->
let (nick, login, host) = split_prefix tokens.(0) in
printh "I" (nick ^ " has changed the topic on " ^ tokens.(2) ^ " to \"" ^ tokens.(3) ^ "\"")
end;
Hashtbl.add protocole_managers "ERROR"
begin
fun tokens -> printh "E" tokens.(2)
end;
Hashtbl.add protocole_managers "PRIVMSG"
begin
fun tokens ->
let (nick, login, host) = split_prefix tokens.(0) in
try
let n1 = String.index tokens.(3) '' in
let n2 = String.index_from tokens.(3) (n1 + 1) '' in
let ctcp_line = (String.sub tokens.(3) (n1 + 1) (n2 - n1 - 1)) in
let (ctcp_token, ctcp_tail) = tokenizer ctcp_line ' ' in
let cmd = ctcp_token () in
try
(Hashtbl.find ctcp_managers cmd) ctcp_token ctcp_tail nick tokens.(2)
with
Not_found -> printh "E" ("Unknown CTCP [" ^ ctcp_line ^ "] from " ^ nick);
tell_server ("NOTICE " ^ nick ^ " :Unknown CTCP " ^ cmd ^ "'\n")
with
Not_found ->
begin
if (String.get tokens.(2) 0) = '#'
then print ("<" ^ nick ^ (if same_channel tokens.(2) !default_channel then "" else ("/" ^ tokens.(2))) ^ "> " ^
tokens.(3))
else begin
tell_ssfe (Tabulable ("/msg " ^ nick ^ " "));
print ("[" ^ nick ^ "] " ^ tokens.(3))
end
end
end;
Hashtbl.add protocole_managers "NOTICE"
begin
fun tokens ->
let (nick, login, host) = split_prefix tokens.(0) in
print ("-" ^ nick ^ (if tokens.(2) = !current_nick then "" else "/" ^ tokens.(2)) ^ "- " ^ tokens.(3))
end;
Hashtbl.add protocole_managers "301"
begin
fun tokens -> printh "I" (tokens.(3) ^ " is away (" ^ tokens.(4) ^ ")")
end;
Hashtbl.add protocole_managers "311"
begin
fun tokens -> printh "I" (tokens.(3) ^ " is " ^ tokens.(4) ^ "@" ^ tokens.(5) ^ " (" ^ tokens.(7) ^ ")")
end;
Hashtbl.add protocole_managers "312"
begin
fun tokens -> printh "I" ("on IRC via server " ^ tokens.(4) ^ " (" ^ tokens.(5) ^ ")")
end;
Hashtbl.add protocole_managers "317"
begin
fun tokens ->
let duration = int_of_string tokens.(4) in
printh "I" (tokens.(3) ^ " has been idle for " ^ (duration_of_seconds duration))
end;
Hashtbl.add protocole_managers "319"
begin
fun tokens -> printh "I" ("on channels: " ^ tokens.(4))
end;
Hashtbl.add protocole_managers "324"
begin
fun tokens -> set_modes tokens.(3) (flatten_modes 1 tokens.(4) 0 tokens 5)
end;
Hashtbl.add protocole_managers "322"
begin
fun tokens -> printh "I" ("There are " ^ tokens.(4) ^ " users on " ^ tokens.(3))
end;
Hashtbl.add protocole_managers "331"
begin
fun tokens -> printh "I" ("No topic is set on " ^ tokens.(3))
end;
Hashtbl.add protocole_managers "333"
begin
fun tokens -> printh "I" ("Topic for " ^ tokens.(3) ^ " set by " ^ tokens.(2))
end;
List.iter
begin
fun s -> Hashtbl.add protocole_managers s (print_information 3)
end
[ "002"; "003"; "005"; "020"; "042";
"251"; "252"; "253"; "255"; "265"; "266";
"372"; "375"; "376";
"422"; "451" ];
Hashtbl.add protocole_managers "305"
begin
fun tokens -> printh "I" tokens.(3); Hashtbl.remove current_modes 'a'
end;
Hashtbl.add protocole_managers "306"
begin
fun tokens -> printh "I" tokens.(3); Hashtbl.replace current_modes 'a' ""
end;
Hashtbl.add protocole_managers "403" (print_information 4);
List.iter
begin
fun s -> Hashtbl.add protocole_managers s (fun tokens -> ())
end
[ "318"; "323" ];
Hashtbl.add protocole_managers "004"
begin
fun tokens -> printh "I" (tokens.(3) ^ " " ^ tokens.(4) ^ " " ^ tokens.(5) ^ " " ^ tokens.(6))
end;
Hashtbl.add protocole_managers "254"
begin
fun tokens -> printh "I" ("There are " ^ tokens.(3) ^ " " ^ tokens.(4))
end;
Hashtbl.add protocole_managers "332"
begin
fun tokens -> printh "I" ("Topic for " ^ tokens.(3) ^ " is '" ^ tokens.(4) ^ "'")
end;
Hashtbl.add protocole_managers "353"
begin
fun tokens ->
printh "I" ("Users on " ^ tokens.(4) ^ ": " ^ tokens.(5));
let (n_token, n_tail) = tokenizer tokens.(5) ' '
and people_table = (find_joined tokens.(4)).people in
let rec add_nick = fun () ->
try
(let nick = n_token () in match String.get nick 0 with
'@' -> Hashtbl.replace people_table (String.sub nick 1 ((String.length nick) - 1)) { operator = true }
| _ -> Hashtbl.replace people_table nick { operator = false });
add_nick ()
with Not_found -> ()
in add_nick ();
end;
Hashtbl.add protocole_managers "366"
begin
fun tokens -> ()
(*let users = Hashtbl.fold (fun nick status s -> " " ^ nick ^ s) (find_joined tokens.(3)).people "" in
printh "I" ("Users on " ^ tokens.(3) ^ ":" ^ users)*)
end;
Hashtbl.add protocole_managers "401"
begin
fun tokens -> printh "E" ("Can not find " ^ tokens.(3) ^ " on IRC")
end;
Hashtbl.add protocole_managers "433"
begin
fun tokens ->
if tokens.(2) = "*"
then printh "E" ("Nickname " ^ tokens.(3) ^ " already in used, type /nick to choose another one")
else printh "E" ("Nickname " ^ tokens.(3) ^ " already in used, you keep " ^ tokens.(2))
end;
Hashtbl.add protocole_managers "441"
begin
fun tokens -> printh "E" (tokens.(3) ^ " is not on channel " ^ tokens.(4))
end;
Hashtbl.add protocole_managers "461"
begin
fun tokens -> printh "E" ("Not enough parameters for " ^ tokens.(3))
end;
Hashtbl.add protocole_managers "472"
begin
fun tokens -> printh "E" ("Mode '" ^ tokens.(3) ^ "' " ^ tokens.(4))
end;
Hashtbl.add protocole_managers "474"
begin
fun tokens -> printh "E" ("Can not join " ^ tokens.(3) ^ " (banned)")
end;
Hashtbl.add protocole_managers "475"
begin
fun tokens -> printh "E" (tokens.(3) ^ ": " ^ tokens.(4))
end;
Hashtbl.add protocole_managers "482"
begin
fun tokens -> printh "E" ("You are not an operator on " ^ tokens.(3))
end;
(*****************************************************************************)
(* The module initialization *)
(*****************************************************************************)
try
Dynlink.init ();
Dynlink.add_interfaces
[ "Ircml"; "Buffer"; "List"; "Arg"; "String"; "Digest"; "Dynlink"; "Hashtbl"; "Sys";
"Connection"; "Unix"; "Pervasives"; "Array" ]
[ Sys.getcwd(); "/home/fleuret/local/lib/ocaml" ];
with
Dynlink.Error error -> printh "E" ("Module loading error (" ^ (Dynlink.error_message error) ^ ")");;
try ignore (Unix.tcgetattr Unix.stdout) with Unix.Unix_error _ -> print "`#ssfe#i";;
(*****************************************************************************)
(* The main loop *)
(*****************************************************************************)
print "-----------------------------------------------------------------------------";
print (" " ^ version ^ ", the IRC client in OCAML");
print " Written and (c) by ";
print "-----------------------------------------------------------------------------";
establish_connection ();
while !alive do
flush stdout;
let (reading, _, _) = Unix.select (keys fd_active) [] [] 1.0 in
List.iter
begin
fun fd ->
let h = Hashtbl.find fd_active fd in
try h.reader () with Network_error(e) -> h.error_handler e
end
reading;
tell_ssfe (Status (get_status ()))
done;
printh "I" "You'll be back, ho yes ... you'll be back"