Feedbot [iii]: the IRC bot

08c April 25, 2019 -- (tech tmsr)

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:

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.