Feedbot [iii]: the IRC bot
All the basic building blocks pertaining to RSS functionality -- i.e. the checker and the announcer -- being in place, we conclude the Feedbot series with:
The patch in this episode introduces the following changes:
- Some methods are removed, due to their retrospective uselessness; some methods are added, due to their being found useful for both IRC implementation and maintenance operations.
- Auxiliary functions are provided in order to handle IRC-specific functionality and Feedbot command argument parsing.
- Most importantly, functionality is added to: manage the bot state; glue to Ircbot and adapt the announcer to IRC; and implement Trilemabot commands.
The remainder of this post will detail the last bullet above. The reader is however encouraged to read the V patch in its entirety, to get a broader image of the changes.
VII. State management
Feedbot state is a list of the form:
(feed-db msg-queue)
where feed-db
is a feed db and msg-queue
is a message queue.
Feedbot state is kept persistent using principally the feedbot save-state
and reload-state
methods:
(defmethod feedbot-save-state ((bot feedbot) &optional (path "state.sexp"))
"Save bot state to disk location given by `path'."
(let ((feed-db (with-feed-db (feed-db) bot feed-db))
(msg-queue (with-msg-queue (msg-queue) bot msg-queue)))
(with-open-file (out path
:direction :output
:if-does-not-exist :create
:if-exists :supersede)
(write (list feed-db msg-queue) :stream out)
nil)))
(defmethod feedbot-reload-state ((bot feedbot) &optional (path "state.sexp"))
"Reload bot state from disk location given by `path'."
(let ((state (with-open-file (in path :direction :input)
(read in))))
(with-feed-db (feed-db) bot
(setf feed-db (car state)))
(with-msg-queue (msg-queue) bot
(setf msg-queue (cadr state)))
nil))
VIII. Ircbot glue
IRC glue:
a. implements entry announcer functionality:
(defun announce-irc! (bot msg)
(announce-stdout! msg)
(let ((rcpt (get-msg-to! msg))
(entry (get-msg-entry! msg)))
(ircbot-send-message bot rcpt
(format nil "~a << ~a -- ~a~%"
(get-entry-link entry)
(get-msg-feed-title! msg)
(get-entry-title entry)))))
b. starts the checker and announcer threads on rpl_welcome:
(defun feedbot-rpl_welcome (bot message)
(declare (ignore message))
(feedbot-start-checker-thread bot)
(feedbot-start-announcer-thread bot))
c. sends messages to online nicks on rpl_ison:
(defun feedbot-rpl_ison (bot message)
;; Only when our reply contains some nicks...
(when (cdr (arguments message))
(let ((nicks (parse-ison (cadr (arguments message)))))
;; Process messages...
(feedbot-process-msg-queue
bot #'(lambda (msg)
;; Only when msg not send and :to is online...
(when (and (not (get-msg-sent! msg))
(member (get-msg-to! msg) nicks
:test #'string=))
;; Wait a bit
(sleep *announce-delay*)
;; Announce and mark as sent.
(announce-irc! bot msg)
(set-msg-sent! msg)))))))
d. implements ircbot-{connect,disconnect} routines:
(defmethod ircbot-connect :after ((bot feedbot))
(feedbot-load-state bot)
(let ((conn (ircbot-connection bot)))
(add-hook conn 'irc-rpl_welcome-message
#'(lambda (message)
(feedbot-rpl_welcome bot message)))
(add-hook conn 'irc-rpl_ison-message
#'(lambda (message)
(feedbot-rpl_ison bot message)))))
(defmethod ircbot-disconnect :after ((bot feedbot)
&optional (quit-msg "feedbot out"))
(declare (ignore quit-msg))
(with-slots (db-mutex queue-mutex checker-thread announcer-thread) bot
(ignore-errors
(release-mutex db-mutex :if-not-owner :force)
(release-mutex queue-mutex :if-not-owner :force)
(terminate-thread checker-thread)
(terminate-thread announcer-thread))
(setf checker-thread nil
announcer-thread nil)
(feedbot-flush-state bot)))
IX. Trilemabot commands
Consult the feedbot manual for more details. For example, the list
command:
(trilemabot-define-cmd (:list bot message target arguments)
(declare (ignore arguments))
;; Execute everything inside a named block, to handle control-flow
;; smoothly
(block cmd-body
(let ((rcpt (response-rcpt bot message target)))
;; Never respond to list in channel
(when (channel-rcpt-p rcpt)
(return-from cmd-body))
;; Get list of feed ids for rcpt and send them one by one in a
;; separate reply each. Delay the response to avoid flooding.
(let ((ids-titles
(feedbot-select-feeds bot
:fields '(:id :title)
:where #'(lambda (feed)
(find-rcpt-in-feed!
feed rcpt)))))
(loop for val in ids-titles do
(destructuring-bind (feed-id feed-title) val
(sleep *announce-delay*)
(ircbot-send-message bot rcpt
(format nil "~a << ~a"
feed-id feed-title))))))))
Post codex: In total, Feedbot weighs circa nine hundred lines of code, of which almost a hundred comprise auxiliary functionality (parsing, sanitization and low-level IRC code), the rest containing mostly the Feedbot mechanism. Of all these, about 23% are comments and another 7% are inline function documentation strings.
The mechanism is, as far as I'm concerned, done. There are some operator-side bits missing (e.g. bulk adding/removal of feeds), but then again, some of them can be easily scripted, while others, I expect, aren't so much "missing features" as they are bits of the user's brain that require fixing.
Bug fixes and other comments are, as usual, more than welcome.