Recently, I've started programming in Common Lisp.
My idea here is to use cl-irc, an IRC library into an event loop. This can be really useful, for example to trigger action based on time, using timers.
Creating a connection
The first step is to create a basic cl-irc:connection object on our own. This can be achieved easily with this:
(require :cl-irc)
(defun connect (server)
(cl-irc:make-connection :connection-type 'cl-irc:connection
:client-stream t
:network-stream ?
:server-name server))
This will return a cl-irc:connection object, logging to stdout
(:client-stream t) and having the server name server. Note that the
server name could be any string.
You probably noticed the ? I used as :network-stream value. This is not a
real and working value: this should be a stream established to the IRC
server you want to chat with. This is where we'll need to use
cl-async:tcp-connect
to establish a TCP connection.
As you can read in this function's documentation, all we need to pass is the server address, two callbacks for read and general events, and the :stream option to get a stream rather than a socket.
So you would do something like:
(require :cl-irc)
(require :cl-async)
(defun connection-socket-read (socket stream)
(format t "We should read the IRC message from ~a ~%" stream))
(defun connection-socket-event (ev)
(format t "Socket event: ~a~%" ev))
(defun connect (server &optional (port 6667))
(cl-irc:make-connection :connection-type 'cl-irc:connection
:client-stream t
:network-stream (as:tcp-connect server port
#'connection-socket-read
#'connection-socket-event
:stream t)
:server-name server))
(as:start-event-loop (lambda () (connect "irc.oftc.net")))
If you run this program, it will connect to the OFTC IRC server, and then
notice you each time the server is sending you a message.
Therefore our problem here is how we you treat the message read from the
stream in connection-socket-read and handle them in the name of
our connection object you used? We can't link both together at this point.
We can't build a closure, because as the time we use as:tcp-connect we don't have the cl-irc:connection instance. Also we can't change easily the read-cb parameter of our network-stream established by as:tcp-connect, simply because cl-async doesn't use to do allow that.
Building a closure
So one solution here is to hack cl-irc:make-connection so we can build an
cl-irc:connection instance without providing in advance the
network-stream, allowing us to build a closure including the
cl-irc:connection to read event for. This is what we're going to do in the
connect function.
(require :cl-irc)
(require :cl-async)
(require :flexi-streams)
(defun connection-socket-read (connection)
(loop for message = (cl-irc::read-irc-message connection)
while message
do (cl-irc:irc-message-event connection message)))
(defun connection-socket-event (ev)
(format t "Socket event: ~a~%" ev))
(defun connect (server port nickname
&key
(username nil)
(realname nil)
(password nil))
;; Build an instance of cl-irc:connection, without any network/output stream
(let* ((connection (make-instance 'cl-irc:connection
:user username
:password password
:server-name server
:server-port port
:client-stream t))
;; Use as:tcp-connect to build our network stream, and build a
;; closure calling `connection-socket-read' with our `connection'
;; as arguments
(network-stream (as:tcp-connect server port
(lambda (socket stream)
(declare (ignore socket stream))
(connection-socket-read connection))
#'connection-socket-event
:stream t)))
;; Set the network stream on the connection
(setf (cl-irc:network-stream connection) network-stream)
;; Set the output stream on the connection
(setf (cl-irc:output-stream connection)
;; This is grabbed from cl-irc:make-connection
(flexi-streams:make-flexi-stream
network-stream
:element-type 'character
:external-format '(:utf8 :eol-style :crlf)))
;; Now handle the IRC protocol authentication pass
(unless (null password)
(cl-irc:pass connection password))
(cl-irc:nick connection nickname)
(cl-irc:user- connection (or username nickname) 0 (or realname nickname))
connection))
(as:start-event-loop (lambda () (connect "irc.oftc.net" 6667 "jd-blog")))
And here we are! If we run this, we're now using an event loop to run
cl-irc. Each time the socket has something to read, the
function connection-socket-read will be called on the
non-blocking mode socket. If there's no message to be read, then the
function will exit and the loop will continue to run.
Using timers
You can now modify the last line with this:
(defun say-hello (connection)
(cl-irc:privmsg connection "#jd-blog" "Hey I read your blog!")
(as:delay (lambda () (say-hello connection)) :time 60))
(as:start-event-loop (lambda ()
(let ((connection (connect "irc.oftc.net" 6667 "jd-blog")))
(cl-irc:join connection "#jd-blog")
(say-hello connection))))
This will connect to the IRC server, join a channel and then say the same
sentence every minute.
Challenge accomplished!
And I'd like to thank Andrew Lyon, the author of cl-async, who has been incredibly helpful with my recent experimentations in this area.