123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428moduleLog=Irc_helpers.LogmoduletypeCLIENT=sigmoduleIo:sigtype'attypeinet_addrtypeconfigendtypeconnection_tvalsend:connection:connection_t->Irc_message.t->unitIo.t(** Send the given message *)valsend_join:connection:connection_t->channel:string->unitIo.t(** Send the JOIN command. *)valsend_nick:connection:connection_t->nick:string->unitIo.t(** Send the NICK command. *)valsend_pass:connection:connection_t->password:string->unitIo.t(** Send the PASS command. *)valsend_pong:connection:connection_t->message1:string->message2:string->unitIo.t(** Send the PONG command. *)valsend_privmsg:connection:connection_t->target:string->message:string->unitIo.t(** Send the PRIVMSG command. *)valsend_notice:connection:connection_t->target:string->message:string->unitIo.t(** Send the NOTICE command. *)valsend_quit:?msg:string->connection:connection_t->unit->unitIo.t(** Send the QUIT command. *)valsend_user:connection:connection_t->username:string->mode:int->realname:string->unitIo.t(** Send the USER command. *)valconnect:?username:string->?mode:int->?realname:string->?password:string->?sasl:bool->?config:Io.config->addr:Io.inet_addr->port:int->nick:string->unit->connection_tIo.t(** Connect to an IRC server at address [addr]. The PASS command will be
sent if [password] is not None. *)valconnect_by_name:?username:string->?mode:int->?realname:string->?password:string->?sasl:bool->?config:Io.config->server:string->port:int->nick:string->unit->connection_toptionIo.t(** Try to resolve the [server] name using DNS, otherwise behaves like
{!connect}. Returns [None] if no IP could be found for the given
name. See {!connect} for more details. *)(** Information on keeping the connection alive *)typekeepalive={mode:[`Active|`Passive];timeout:int;}valdefault_keepalive:keepalive(** Default value for keepalive: active mode with auto-reconnect *)vallisten:?keepalive:keepalive->connection:connection_t->callback:(connection_t->Irc_message.parse_result->unitIo.t)->unit->unitIo.t(** [listen connection callback] listens for incoming messages on
[connection]. All server pings are handled internally; all other
messages are passed, along with [connection], to [callback].
@param keepalive the behavior on disconnection (if the transport
supports {!Irc_transport.IO.pick} and {!Irc_transport.IO.sleep}) *)valreconnect_loop:?keepalive:keepalive->?reconnect:bool->after:int->connect:(unit->connection_toptionIo.t)->f:(connection_t->unitIo.t)->callback:(connection_t->Irc_message.parse_result->unitIo.t)->unit->unitIo.t(** A combination of {!connect} and {!listen} that, every time
the connection is terminated, tries to start a new one
after [after] seconds.
@param after time before trying to reconnect
@param connect how to reconnect
(a closure over {!connect} or {!connect_by_name})
@param callback the callback for {!listen}
@param f the function to call after connection *)endmoduleMake(Io:Irc_transport.IO)=structmoduleIo=Iotypeconnection_t={sock:Io.file_descr;buffer:Buffer.t;read_length:int;read_data:Bytes.t;(* for reading *)lines:stringQueue.t;(* lines read so far *)mutableterminated:bool;}openIoletrecreally_write~connection~data~offset~length=iflength=0thenreturn()elseIo.writeconnection.sockdataoffsetlength>>=(funchars_written->really_write~connection~data~offset:(offset+chars_written)~length:(length-chars_written))letsend_raw~connection~data=Log.debug(funk->k"send: %s"data);letformatted_data=Bytes.unsafe_of_string(Printf.sprintf"%s\r\n"data)inletlength=Bytes.lengthformatted_datainreally_write~connection~data:formatted_data~offset:0~lengthmoduleM=Irc_messageletsend~connectionmsg=send_raw~connection~data:(M.to_stringmsg)letsend_join~connection~channel=send~connection(M.join~chans:[channel]~keys:None)letsend_nick~connection~nick=send~connection(M.nicknick)letsend_auth_sasl~connection~user~password=Log.debug(funk->k"login using SASL with user=%S"user);send_raw~connection~data:"CAP REQ :sasl">>=fun()->send_raw~connection~data:"AUTHENTICATE PLAIN">>=fun()->letb64_login=Base64.encode_string@@Printf.sprintf"%s\x00%s\x00%s"useruserpasswordinletdata=Printf.sprintf"AUTHENTICATE %s"b64_logininsend_raw~connection~dataletsend_pass~connection~password=send~connection(M.passpassword)letsend_ping~connection~message1~message2=send~connection(M.ping~message1~message2)letsend_pong~connection~message1~message2=send~connection(M.pong~message1~message2)letsend_privmsg~connection~target~message=send~connection(M.privmsg~targetmessage)letsend_notice~connection~target~message=send~connection(M.notice~targetmessage)letsend_quit?(msg="")~connection()=send~connection(M.quit~msg)letsend_user~connection~username~mode~realname=letmsg=M.user~username~mode~realnameinsend~connectionmsgletmk_connection_sock=letread_length=1024in{sock=sock;buffer=Buffer.create128;read_length;read_data=Bytes.makeread_length' ';lines=Queue.create();terminated=false;}type'ainput_res=|Readof'a|Timeout|Endletrecnext_line_~timeout~connection:c:stringinput_resIo.t=ifc.terminatedthenreturnEndelseifQueue.lengthc.lines>0thenreturn(Read(Queue.popc.lines))elsebegin(* Read some data into our string. *)Io.read_with_timeout~timeoutc.sockc.read_data0c.read_length>>=function|None->returnTimeout|Some0->c.terminated<-true;returnEnd(* EOF from server - we have quit or been kicked. *)|Somelen->(* read some data, push lines into [c.lines] (if any) *)letinput=Bytes.sub_stringc.read_data0leninletlines=Irc_helpers.handle_input~buffer:c.buffer~inputinList.iter(funl->Queue.pushlc.lines)lines;next_line_~timeout~connection:cendtypenick_retry={mutablenick:string;mutabletries:int;}letwelcome_timeout=30.letmax_nick_retries=3letwait_for_welcome~start~connection~nick=letnick_try={nick=nick;tries=1}inletrecaux()=letnow=Io.time()inlettimeout=start+.welcome_timeout-.nowiniftimeout<0.5thenreturn()elsebeginifnick_try.tries>max_nick_retriesthenreturn()elsebegin(* wait a bit more *)lettimeout=int_of_float(ceiltimeout)inassert(timeout>0);(* logf "wait for welcome message (%ds)" timeout >>= fun () -> *)next_line_~timeout~connection>>=function|Timeout|End->return()|Readline->Log.debug(funk->k"read: %s"line);beginmatchM.parselinewith|Result.Ok{M.command=M.Other("001",_);_}->(* we received "RPL_WELCOME", i.e. 001 *)return()|Result.Ok{M.command=M.PING(message1,message2);_}->(* server may ask for ping at any time *)send_pong~connection~message1~message2>>=aux|Result.Ok{M.command=M.Other("433",_);_}->(* we received "ERR_NICKNAMEINUSE" *)nick_try.nick<-nick_try.nick^"_";nick_try.tries<-nick_try.tries+1;Log.err(funk->k"Nick name already in use, trying %s"nick_try.nick);send_nick~connection~nick:nick_try.nick>>=aux|_->aux()endendendinaux()>|=fun()->Log.info(funk->k"finished waiting for welcome msg")letconnect?username?(mode=0)?(realname="irc-client")?password?(sasl=true)?config~addr~port~nick()=Io.open_socket?configaddrport>>=(funsock->letconnection=mk_connection_sockinletcap_end=reffalseinbeginmatchusername,passwordwith|Someuser,Somepasswordwhensasl->cap_end:=true;send_auth_sasl~connection~user~password|_,Somepassword->send_pass~connection~password|_->return()end>>=fun()->letusername=matchusernamewithSomeu->u|None->"ocaml-irc-client"insend_nick~connection~nick>>=fun()->send_user~connection~username~mode~realname>>=fun()->beginif!cap_endthensend_raw~connection~data:"CAP END"elsereturn()end>>=fun()->wait_for_welcome~start:(Io.time())~connection~nick>>=fun()->returnconnection)letconnect_by_name?(username="irc-client")?(mode=0)?(realname="irc-client")?password?sasl?config~server~port~nick()=Io.gethostbynameserver>>=(function|[]->Io.returnNone|addr::_->connect~addr~port~username~mode~realname~nick?password?sasl?config()>>=funconnection->Io.return(Someconnection))(** Information on keeping the connection alive *)typekeepalive={mode:[`Active|`Passive];timeout:int;}letdefault_keepalive:keepalive={mode=`Active;timeout=60;}typelisten_keepalive_state={mutablelast_seen:float;mutablelast_active_ping:float;mutablefinished:bool;}(* main loop for pinging server actively *)letactive_ping_threadkeepalivestate~connection=letrecloop()=assert(keepalive.mode=`Active);letnow=Io.time()inlettime_til_ping=(maxstate.last_active_pingstate.last_seen)+.(floatkeepalive.timeout/.2.)-.nowinifstate.finishedthenIo.return()elsebegin(* send "ping" if active mode and it's been long enough *)iftime_til_ping<0.then(state.last_active_ping<-now;Log.debug(funk->k"send ping to server...");(* try to send a ping, but ignore errors *)Io.catch(fun()->send_ping~connection~message1:"ping"~message2:"")(fun_->Io.return()))else(Io.return())>>=fun()->(* sleep until the due date, then check again *)Io.sleep(int_of_floattime_til_ping+1)end>>=fun()->loop()inloop()letlisten?(keepalive=default_keepalive)~connection~callback()=(* main loop *)letreclisten_recstate=letnow=Io.time()inlettimeout=state.last_seen+.floatkeepalive.timeout-.nowinnext_line_~timeout:(int_of_float(ceiltimeout))~connection>>=function|Timeout->state.finished<-true;Log.info(funk->k"client timeout");Io.return()|End->state.finished<-true;Log.info(funk->k"connection closed");Io.return()|Readline->(* update "last_seen" field *)Log.debug(funk->k"read: %s"line);letnow=Io.time()instate.last_seen<-maxnowstate.last_seen;beginmatchM.parselinewith|Result.Ok{M.command=M.PING(message1,message2);_}->(* Handle pings without calling the callback. *)Log.debug(funk->k"reply pong to server");send_pong~connection~message1~message2|Result.Ok{M.command=M.PONG_;_}->(* active response from server *)Io.return()|result->callbackconnectionresultend>>=fun()->ifstate.finishedthenIo.return()elselisten_recstateinletstate={last_seen=Io.time();last_active_ping=Io.time();finished=false;}in(* connect, serve, etc. *)beginmatchIo.pickwith|Somepickwhenkeepalive.mode=`Active->pick[listen_recstate;active_ping_threadkeepalivestate~connection;]|_->listen_recstateendletreconnect_loop?keepalive?(reconnect=true)~after~connect~f~callback()=letrecaux()=Io.catch(fun()->connect()>>=function|None->Log.info(funk->k"could not connect");returntrue|Someconnection->fconnection>>=fun()->listen?keepalive~connection~callback()>>=fun()->Log.info(funk->k"connection terminated.");returnreconnect)(function|Exit->Log.info(funk->k"stopping the connection loop");returnfalse|e->Log.err(funk->k"reconnect_loop: exception %s"(Printexc.to_stringe));returntrue)>>=funloop->(* wait and reconnect *)Io.sleepafter>>=fun()->ifloopthen(Log.info(funk->k"try to reconnect...");aux())elsereturn()inaux()end