RSS Feed

Lisp Project of the Day


You can support this project by donating at:

Donate using PatreonDonate using Liberapay


Tests 😀
CI 🥺

Today we'll write a simple bot to keep a history of the IRC channel. IRC is a chat protocol that existed before Slack, Telegram, etc.

For the test I've installed a local lisp server on my OSX:

[poftheday:~]% brew install ngircd
Updating Homebrew...
==> Caveats
==> ngircd
To have launchd start ngircd now and restart at login:
  brew services start ngircd

[poftheday:~]% /usr/local/sbin/ngircd --nodaemon --passive

[poftheday:~]% /usr/local/sbin/ngircd --nodaemon --passive
[66510:5    0] ngIRCd 26-IDENT+IPv6+IRCPLUS+SSL+SYSLOG+ZLIB-x86_64/apple/darwin19.5.0 starting ...
[66510:6    0] Using configuration file "/usr/local/etc/ngircd.conf" ...
[66510:3    0] Can't read MOTD file "/usr/local/etc/ngircd.motd": No such file or directory
[66510:4    0] No administrative information configured but required by RFC!
[66510:6    0] ServerUID must not be root(0), using "nobody" instead.
[66510:3    0] Can't change group ID to nobody(4294967294): Operation not permitted!
[66510:3    0] Can't drop supplementary group IDs: Operation not permitted!
[66510:3    0] Can't change user ID to nobody(4294967294): Operation not permitted!
[66510:6    0] Running as user art(1345292665), group LD\Domain Users(593637566), with PID 66510.
[66510:6    0] Not running with changed root directory.
[66510:6    0] IO subsystem: kqueue (initial maxfd 100, masterfd 3).
[66510:6    0] Now listening on [0::]:6667 (socket 6).
[66510:6    0] Now listening on []:6667 (socket 8).
[66510:5    0] Server "" (on "poftheday") ready.

After that, I've installed a command line IRC client ircii and made two connections to simulate users in the #lisp channel.

Now it is time to connect our bot to the server and create a thread with the message processing loop:

POFTHEDAY> (defparameter
             (cl-irc:connect :nickname "bot"
                             :server "localhost"))

POFTHEDAY> (defparameter *thread*
             (bt:make-thread (lambda ()
                               (cl-irc:read-message-loop *conn*))
                             :name "IRC"))

POFTHEDAY> (cl-irc:join *conn* "#lisp")

While messages are processed in the thread we are free to experiment in the REPL. Let's add a hook to process messages from the channel:

POFTHEDAY> (defun on-message (msg)
             (log:info "New message" msg))

POFTHEDAY> (cl-irc:add-hook *conn*

;; Now if some of users will write to the channel,
;; the message will be logged to the screen:

; No values
 <INFO> [22:35:43] poftheday (on-message) -
UNHANDLED-EVENT:3803916943: PRIVMSG: joanna #lisp "Hello lispers!"

We can modify the on-message function to save the last message into the global variable to inspect its structure:

POFTHEDAY> *last-msg*

POFTHEDAY> (describe *)

Slots with :INSTANCE allocation:
  SOURCE                         = "joanna"
  USER                           = "~art"
  HOST                           = "localhost"
  COMMAND                        = "PRIVMSG"
  ARGUMENTS                      = ("#lisp" "Hello")
  CONNECTION                     = #<CL-IRC:CONNECTION localhost {1003918FA3}>
  RECEIVED-TIME                  = 3803917081
  RAW-MESSAGE-STRING             = ":joanna!~art@localhost PRIVMSG #lisp :Hello

;; If user sent a direct message,
;; it will have the bot's username as the first argument:

POFTHEDAY> (describe *last-msg*)

Slots with :INSTANCE allocation:
  SOURCE                         = "joanna"
  USER                           = "~art"
  HOST                           = "localhost"
  COMMAND                        = "PRIVMSG"
  ARGUMENTS                      = ("bot" "Hello. It is Joanna.")
  CONNECTION                     = #<CL-IRC:CONNECTION localhost {1003918FA3}>
  RECEIVED-TIME                  = 3803917270
  RAW-MESSAGE-STRING             = ":joanna!~art@localhost PRIVMSG bot :Hello. It is Joanna.

If you intend to make a bot which will reply to the messages, you have to choose either message's source slot or the first argument as a destination for the response.

Most probably the bug @SatoshiShinohai complained about on Twitter is caused by the wrong algorithm for choosing the response's destination.

Now let's redefine our on-message function to format log messages in an accurate way:

POFTHEDAY> (defun on-message (msg)
             (log:info "<~A> ~A"
                       (cl-irc:source msg)
                       (second (cl-irc:arguments msg)))
             ;; To let cl-irc know that we've processed the event
             ;; we need to return `t'.
             ;; Otherwise it will output "UNHANDLED-EVENT" messages.
 <INFO> [22:55:06] poftheday (on-message) - <joanna> Hello everybody!
 <INFO> [22:55:17] poftheday (on-message) - <art> Hello, Joanna!
 <INFO> [22:55:27] poftheday (on-message) -
  <joanna> What is the best book on Common Lisp for newbee?
 <INFO> [22:55:56] poftheday (on-message) - <art> Try the Practical Common Lisp.
 <INFO> [22:56:04] poftheday (on-message) - <joanna> Thanks!

If you want to make the bot which responds to the message, then use cl-irc:privmsg like this:

;; This will send a message to the channel:
POFTHEDAY> (cl-irc:privmsg *conn* "#lisp" "Hello! Bot is in the channel!")
"PRIVMSG #lisp :Hello! Bot is in the channel!

;; and this will send a private message:
POFTHEDAY> (cl-irc:privmsg *conn* "joanna" "Hi Joanna!")
"PRIVMSG joanna :Hi Joanna!

If you will download the cl-irc's sources from you'll find more sofisticated bot in the example folder.

One final note, to debug communication between lisp and IRC server set the cl-irc::*debug-p* variable to true and it will log every message send or received by the bot.

Brought to you by 40Ants under Creative Commons License