+++ /dev/null
----
-postid: 000
-title: Feedbot [iii]: the IRC bot
-date: April 25, 2019
-author: Lucian Mogoșanu
-tags: tech, tmsr
----
-
-All the basic building blocks pertaining to RSS functionality --
-i.e. the [checker][feedbot-i] and the [announcer][feedbot-ii] -- being
-in place, we conclude the [Feedbot][feedbot] series with:
-
-* The [V patch][feedbot-irc.vpatch]; and
-* my [seal][feedbot-irc.vpatch.spyked.sig].
-
-The patch in this episode introduces the following changes:
-
-* Some methods are removed, due to their retrospective uselessness;
- some methods are added, due to them being found useful for both IRC
- implementation and maintenance operations.
-* Auxiliary functions are provided in order to handle IRC operation
- 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][trilemabot] commands.
-
-The remainder of this post will detail the last bullet above. The
-reader is however encouraged to read the [V patch][feedbot-irc.vpatch]
-in its entirety, to get a broader image of the changes.
-
-**VII. State management**
-
-Feedbot state is a list of the form:
-
-~~~~ {.commonlisp}
-(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 `load-state` methods:
-
-~~~~ {.commonlisp}
-(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-load-state ((bot feedbot) &optional (path "state.sexp"))
- "Load bot state from disk location given by `path'.
-
-This method throws an error condition if the bot contains pre-existing
-state, i.e. `feed-db' and `msg-queue' are non-NIL."
- (with-slots (feed-db msg-queue) bot
- (assert (not feed-db))
- (assert (not msg-queue)))
- (feedbot-reload-state bot path))
-~~~~
-
-**VIII. Ircbot glue**
-
-IRC glue:
-
-**a\.** implements [entry announcer][announcer] functionality:
-
-~~~~ {.commonlisp}
-(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:
-
-~~~~ {.commonlisp}
-(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
-
-~~~~ {.commonlisp}
-(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
-
-~~~~ {.commonlisp}
-(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][feedbot-manual] for more details.
-
-~~~~ {.commonlisp}
-(trilemabot-define-cmd (:help bot message target arguments)
- (declare (ignore arguments))
- (ircbot-send-message bot (response-rcpt bot message target)
- "http://thetarpit.org/posts/y05/081-feedbot-manual.html"))
-~~~~
-
-**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 being mostly
-Feedbot mechanism. Of 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 are more
-indicative of brain-fixing rather than any imagined code-fixing.
-
-Bug fixes and other comments are, as usual, more than welcome.
-
-[feedbot-i]: /posts/y05/08a-feedbot-i.html
-[feedbot-ii]: /posts/y05/08b-feedbot-ii.html
-[feedbot]: http://btcbase.org/log-search?q=feedbot
-[feedbot-irc.vpatch]: TODO
-[feedbot-irc.vpatch.spyked.sig]
-[trilemabot]: /posts/y05/078-trilemabot-ii.html
-[announcer]: /posts/y05/08b-feedbot-ii.html#selection-249.0-252.0
-[feedbot-manual]: /posts/y05/081-feedbot-manual.html
--- /dev/null
+---
+postid: 08c
+title: Feedbot [iii]: the IRC bot
+date: April 25, 2019
+author: Lucian Mogoșanu
+tags: tech, tmsr
+---
+
+All the basic building blocks pertaining to RSS functionality --
+i.e. the [checker][feedbot-i] and the [announcer][feedbot-ii] -- being
+in place, we conclude the [Feedbot][feedbot] series with:
+
+* The [V patch][feedbot-irc.vpatch] adding IRC functionality; and
+* my [seal][feedbot-irc.vpatch.spyked.sig] for it.
+
+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][trilemabot].
+
+The remainder of this post will detail the last bullet above. The
+reader is however encouraged to read the [V patch][feedbot-irc.vpatch]
+in its entirety, to get a broader image of the changes.
+
+**VII. State management**
+
+Feedbot state is a list of the form:
+
+~~~~ {.commonlisp}
+(feed-db msg-queue)
+~~~~
+
+where `feed-db` is a [feed db][feed-db] and `msg-queue` is a
+[message queue][msg-queue].
+
+Feedbot state is kept persistent using principally the feedbot
+`save-state` and `reload-state` methods:
+
+~~~~ {.commonlisp}
+(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][announcer] functionality:
+
+~~~~ {.commonlisp}
+(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][rpl-welcome]:
+
+~~~~ {.commonlisp}
+(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][rpl-ison]:
+
+~~~~ {.commonlisp}
+(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}][ircbot-connect]
+routines:
+
+~~~~ {.commonlisp}
+(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][feedbot-manual] for more details. For
+example, the `list` command:
+
+~~~~ {.commonlisp}
+(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.
+
+[feedbot-i]: /posts/y05/08a-feedbot-i.html
+[feedbot-ii]: /posts/y05/08b-feedbot-ii.html
+[feedbot]: http://btcbase.org/log-search?q=feedbot
+[feedbot-irc.vpatch]: http://lucian.mogosanu.ro/src/botworks/v/patches/feedbot-irc.vpatch
+[feedbot-irc.vpatch.spyked.sig]: http://lucian.mogosanu.ro/src/botworks/v/seals/feedbot-irc.vpatch.spyked.sig
+[feed-db]: /posts/y05/08a-feedbot-i.html#selection-74.0-74.1
+[msg-queue]: /posts/y05/08b-feedbot-ii.html#selection-58.0-58.1
+[trilemabot]: /posts/y05/078-trilemabot-ii.html
+[announcer]: /posts/y05/08b-feedbot-ii.html#selection-249.0-252.0
+[rpl-welcome]: https://archive.is/oKbeB#selection-2399.225-2399.340
+[rpl-ison]: https://archive.is/oKbeB#selection-2419.765-2419.913
+[ircbot-connect]: http://btcbase.org/patches/ircbot-genesis#L100
+[feedbot-manual]: /posts/y05/081-feedbot-manual.html