birch
2024-10-12
A simple Common Lisp IRC client library
Birch
Birch is a simple Common Lisp IRC client library. It makes use of CLOS for event handling.
Dependencies
Birch is built in Common Lisp on SBCL. It depends on:
- SPLIT-SEQUENCE (Public Domain?)
- usocket (MIT)
- FLEXI-STREAMS (BSD)
- Alexandria (Public Domain)
- CL+SSL (MIT)
The tests also use Prove (MIT).
The CL+SSL requirement can be relaxed by adding :birch-no-ssl
to *features*
.
Installation
Birch can be loaded with Quicklisp:
(ql:quickload :birch)
Usage
The first step is to create a subclass of CONNECTION
:
(defclass my-connection (connection) ())
To connect to an IRC network you create an instance of your connection class. You then pass that instance to CONNECT
to actually connect to the network.
(defvar *connection* (make-instance 'my-connection :server-host "irc.example.com" :nick "mybot"))
The only required initargs for the default connection class are :SERVER-HOST
and :NICK
. The others of interest are:
:SERVER-PORT
, the port to connect to (defaults to 6667):USER
, the username to send upon initial connection (defaults to the supplied nickname):PASS
, the password to send. If not supplied no password will be sent.:REAL-NAME
, the real name to register with (defaults to "Birch IRC library")
The accessors for all of those are the name of the slot (the initarg without the ':').
Event handling in Birch is done by defining methods on HANDLE-EVENT
.
(defgeneric handle-event (connection event) (:documentation " Will be called after an IRC message has successfully been parsed and turned into an event. Most IRC messages don't result in events, should you want to handle them you can define a method on HANDLE-MESSAGE instead.") (:method-combination event))
For example, to do something when a PRIVMSG is received, you define a method specializing the first argument on your connection class and the second on the PRIVMSG-EVENT class:
(defmethod handle-event ((connection my-connection) (event privmsg-event)) (format t "Message received on ~A: ~A" (channel event) (message event)))
A list of all the events currently included in Birch is below.
Sending commands to a connection can be done in two ways. You can use the generic function /RAW
, which is basically a glorified FORMAT
, or you can use one of the built-in functions for sending often-used commands. They're all generic functions and can thus easily be extended.
RAW
can, for example, be called like this:
(/raw connection "JOIN ~A" channel)
But you should probably just use the function /JOIN
in this case.
All currently implemented commands are listed below.
To start handling messages you should call PROCESS-MESSAGE-LOOP
, which will block until the connection to the server is closed. Even then, if /QUIT
wasn't called (so the ACTIVEP
slot on the connection wasn't set to NIL
) PROCESS-MESSAGE-LOOP
will try to reconnect. You'll likely want to run this in a new thread.
Alternatively, you can call PROCESS-MESSAGE
yourself.
We have to go deeper
If you want to handle a message from the server for which there is no event you can instead define a method on HANDLE-MESSAGE
.
(defgeneric handle-message (connection prefix command params) (:documentation " Called when a raw message is returned. CONNECTION is the connection object of the connection the message was received on. PREFIX is a list of (NICK USER HOST) COMMAND is a keyword, such as :PRIVMSG or :RPL_WELCOME PARAMS is a list of parameters") (:method-combination event))
For example, to do something when RPL_WELCOME
is received, you could define a method like so:
(defmethod handle-message ((connection my-connection) prefix (command (eql :RPL_WELCOME)) params) (format t "Received RPL_WELCOME, we can now do stuff"))
The COMMAND
argument will either be the keyword-ized name of the command as defined in RFC2812, like :PRIVMSG
, or, in the case of numeric replies, the name as found here.
If you want to handle something as an event you can define a method on HANDLE-MESSAGE
that calls HANDLE-EVENT
with a newly initialized EVENT
object. The macro DEFINE-EVENT-DISPATCHER
can be of great help with that.
(defmacro define-event-dispatcher (command class &optional positional-initargs) "Defines a method on HANDLE-MESSAGE to handle messages of which the command is COMMAND. This new method will call HANDLE-EVENT with a new instance of type CLASS. POSITIONAL-INITARGS should be a list of initargs to pass to MAKE-INSTANCE, where the position of the keyword determines the IRC command parameter that will be used as a value. A NIL will cause an IRC parameter to be ignored. For example, when POSITIONAL-INITARGS is (:CHANNEL), the first parameter of the IRC message will be passed as the initial value of :CHANNEL. If POSITIONAL-INITARGS is (:CHANNEL :TARGET), the first parameter will be passed as the initial value of :CHANNEL, and the second parameter will be passed as the initial value of :TARGET. Instead of a keyword, an element of POSITIONAL-INITARGS can also be a list of the form (:KEYWORD FUNCTION), which means the value passed as the initarg will be the result of calling FUNCTION with two arguments: the connection object and the IRC parameter. Any remaining arguments will be joined together (separated by spaces) and passed as the initial value of :MESSAGE." ...)
For example, kick events are implemented like this:
(defclass kick-event (channel-event) ((target :initarg :target :initform NIL :accessor target))) (define-event-dispatcher :KICK 'kick-event ((:channel #'make-channel) (:target #'make-user)))
Events
-
EVENT
, which is a superclass of all events.EVENT
has a couple of slots, which you can access using the following accessors:NICK
, the nickname of the sender of the message. Taken from the message's prefix.USER
, the username of the sender of the message. Also taken from the message's prefix.HOST
, the hostname of the sender of the message. Also taken from the message's prefix.MESSAGE
, a string containing all of the parameters supplied with the message, separated by spaces. Note that the "trailing" parameter is not prefixed with a ':', so you can't recognize it from here.
Subclasses of
EVENT
are:QUIT-EVENT
NICK-EVENT
which adds a slot, accessed withNEW-NICK
. It contains the new nick of the person whose nick has changed.
-
CHANNEL-EVENT
, which is a superclass of all events happening on a certain channel.CHANNEL-EVENT
adds another slot, accessed withCHANNEL
. The subclasses ofCHANNEL-EVENT
are:-
PRIVMSG-EVENT
-
NOTICE-EVENT
-
JOIN-EVENT
-
PART-EVENT
-
PART-EVENT
-
KICK-EVENT
which adds a slot, accessed withTARGET
. It contains the nick of the person being kicked. -
TOPIC-EVENT
which adds a slot, accessed withNEW-TOPIC
. It contains the new topic of the channel.
-
Commands
(/pass connection password)
(/nick connection nick)
(/user connection username mode real-name)
(/join connection channel &optional key)
(/privmsg connection channel message)
(/invite connection nick channel)
(/kick connection channel nick &optional message)
(/part connection channel &optional message)
(/quit connection &optional message)
(/pong connection server-1 &optional server-2)
You shouldn't need this, as Birch automatically responds to PING.
Users and Channels
Birch keeps track of users and channels for you. The default events call MAKE-USER
and MAKE-CHANNEL
on the appropriate parameters, turning them into USER
and CHANNEL
objects with appropriate slots. To get a list of all users in a channel, call USERS
on the channel object. Similarly, to get all channels a user is in (that we know of), call CHANNELS
on a user object.
It is possible to change the class instantiated by MAKE-USER
and MAKE-CHANNEL
, by passing the desired class name to the :user-class
or :channel-class
initargs when creating the instance. This setting can also be changed after the fact with the accessors USER-CLASS
and CHANNEL-CLASS
.
It is worth noting that CONNECTION
is itself a subclass of USER
and will appear in channel user lists.
CTCP
Birch provides two utility functions for working with CTCP messages.
-
MAKE-CTCP-MESSAGE
, which takes a string as an argument and adds an ASCII 0x01 character to the start and end of it.The result of
MAKE-CTCP-MESSAGE
can be sent to a connection using PRIVMSG or NOTICE to send a CTCP query to someone. -
CTCP-MESSAGE-P
, which takes a string as an argument and checks if it starts and ends with an ASCII 0x01 character.To handle CTCP you can pass messages received through PRIVMSG or NOTICE through
CTCP-MESSAGE-P
and parse them as such if they are, in fact, CTCP messages.
Notes
-
The test coverage is not yet as good as I'd like it to be.
-
Birch does not handle errors in your event handlers, which can result in the connection staying open while the bot/client/whatever has crashed. This can be prevented, for example, by defining an
:AROUND
method onHANDLE-EVENT
, like so:(defmethod handle-event :around ((connection my-connection) (event event)) (handler-case (if (next-method-p) (call-next-method)) (serious-condition (condition) (format *trace-output* "~&Caught error: ~A~%" condition))))
HANDLE-MESSAGE
andHANDLE-EVENT
use a modified standard method combination to allow for:AROUND
methods to exist when there are no primary methods without signalling an (apparently implementation-defined) error.
License
Copyright (c) 2015 Joram Schrijver
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.