23
\$\begingroup\$

To help myself learn Racket, I ported a simple JavaScript ircbot module I wrote for Node.js to Racket. The Racket version is built atop the Racket irc package, so the low-level code is handled by that. My module simply provides some utility functions to make it easier to implement IRC bots.

My code is as follows. I know it's rather long and probably fairly unreadable, and for that I apologize. I'm not looking for anything extremely specific, but I would like to know if I'm violating any major conventions of the language in any obvious ways.

#lang racket

(provide ircbot-connect
         ircbot-say
         ircbot-listen-action
         ircbot-listen-chat
         ircbot-listen-trigger
         ircbot-listen-message
         ircbot-listen-self
         ircbot-listen-self-action
         ircbot-listen-join
         ircbot-listen-part
         ircbot-listen-quit
         ircbot-listen-nick
         ircbot-listen)

;; ---------------------------------------------------------------------------------------------------

(require (for-syntax racket/syntax))

(require racket/async-channel)
(require irc)
(require srfi/13)

(struct ircbot-connection (server nick username realname channels triggers pmtrigger
                                  action-handlers chat-handlers trigger-handlers message-handlers
                                  self-handlers self-action-handlers join-handlers part-handlers
                                  quit-handlers nick-handlers
                                  connection))

(define (ircbot-connect #:server [server "localhost"]
                        #:port [port 6667]
                        #:nick [nick "racketircbot"]
                        #:username [username "racketircbot"]
                        #:realname [realname "racketircbot"]
                        #:channels [channels (list)]
                        #:triggers [triggers (list)]
                        #:pmtrigger [pmtrigger #f])
  (define-values (connection ready-event)
    (irc-connect server port nick username realname))
  (sync ready-event)
  (for ([channel channels])
    (irc-join-channel connection channel))
  (ircbot-connection server nick username realname channels triggers pmtrigger
                     (box '()) (box '()) (box '()) (box '()) (box '())
                     (box '()) (box '()) (box '()) (box '()) (box '())
                     connection))

(define (ircbot-say connection message [channels (ircbot-connection-channels connection)])
  (when (not (list? channels))
    (set! channels (list channels)))
  (define action #f)
  (when (equal? (string-contains message "/me ") 0)
    (set! action #t)
    (set! message (substring message 4)))
  (for ([channel channels])
    (if action
        (irc-send-command (ircbot-connection-connection connection)
                          "PRIVMSG"
                          channel (format ":\u0001ACTION ~a\u0001" message))
        (irc-send-message (ircbot-connection-connection connection) channel message))))

;; ---------------------------------------------------------------------------------------------------

(define-syntax (define-ircbot-listener stx)
  (syntax-case stx ()
    [(define-ircbot-listener name)
     #`(define (#,(format-id #'name #:source #'name
                             "ircbot-listen-~a"
                             (syntax-e #'name)) connection callback)
         (set-box! (#,(format-id #'name #:source #'name
                                 "ircbot-connection-~a-handlers"
                                 (syntax-e #'name)) connection)
                   (append (unbox (#,(format-id #'name #:source #'name
                                                "ircbot-connection-~a-handlers"
                                                (syntax-e #'name)) connection))
                           (list callback))))]))

(define-ircbot-listener action)
(define-ircbot-listener chat)
(define-ircbot-listener trigger)
(define-ircbot-listener message)
(define-ircbot-listener self)
(define-ircbot-listener self-action)
(define-ircbot-listener join)
(define-ircbot-listener part)
(define-ircbot-listener quit)
(define-ircbot-listener nick)

;; ---------------------------------------------------------------------------------------------------

(define (channel? str)
  (equal? (string-ref str 0) #\#))

(define (respond connection sender recipient response)
  (irc-send-message (ircbot-connection-connection connection)
                    (if (channel? recipient) recipient sender)
                    response)
  (when (channel? recipient)
    (map (λ (el) (el (hash "text" response))) (unbox (ircbot-connection-self-handlers connection)))))

(define (ircbot-listen connection)
  (let loop ()
    (define message (async-channel-get
                     (irc-connection-incoming (ircbot-connection-connection connection))))
    (match message
      [(irc-message prefix "PRIVMSG" params _)
       (define prefix-match (regexp-match #rx"^[^!]+" prefix))
       (when prefix-match
         (define sender (first prefix-match))
         (define recipient (first params))
         (define message (second params))
         (define action-match (regexp-match #rx"^\u0001ACTION ([^\u0001]*)\u0001" message))
         (define (callback text cb)
           (cb (hash "sender" sender
                     "recipient" recipient
                     "message"  text
                     "type" (if action-match "action" "chat")
                     "private"  (not (channel? recipient)))
               (λ (response) (respond connection sender recipient response))))
         (cond
           [action-match
            (set! message (second action-match))
            ; call all message handlers
            (map (curry callback message)
                 (unbox (ircbot-connection-message-handlers connection)))
            ; call all action handlers
            (map (curry callback message)
                 (unbox (ircbot-connection-action-handlers connection)))
            ]
           [else
            ; call all message handlers
            (map (curry callback message)
                 (unbox (ircbot-connection-message-handlers connection)))
            ; call all action handlers
            (map (curry callback message)
                 (unbox (ircbot-connection-chat-handlers connection)))
            ; call trigger handlers if necessary
            (define triggered #f)
            (define args (string-split message))
            (define trigger (first args))
            (define reconstructed (string-join (rest args)))
            (when (member trigger (ircbot-connection-triggers connection))
              (set! triggered #t)
              (map (curry callback reconstructed)
                   (unbox (ircbot-connection-trigger-handlers connection))))
            ; call trigger handlers on pm if enabled
            (when (and
                   (not triggered)
                   (ircbot-connection-pmtrigger connection)
                   (not (channel? recipient)))
              (set! triggered #t)
              (map (curry callback message)
                   (unbox (ircbot-connection-trigger-handlers connection))))
            ]))]
      [(irc-message prefix "JOIN" params _)
       (define prefix-match (regexp-match #rx"^[^!]+" prefix))
       (when prefix-match
         (for ([callback (unbox (ircbot-connection-join-handlers connection))])
           (callback (hash "channel" (first params)
                           "nick" (first prefix-match)))))]
      [(irc-message prefix "PART" params _)
       (define prefix-match (regexp-match #rx"^[^!]+" prefix))
       (when prefix-match
         (for ([callback (unbox (ircbot-connection-part-handlers connection))])
           (callback (hash "channel" (first params)
                           "nick" (first prefix-match)
                           "reason" (second params)))))]
      [(irc-message prefix "QUIT" params _)
       (define prefix-match (regexp-match #rx"^[^!]+" prefix))
       (when prefix-match
         (for ([callback (unbox (ircbot-connection-quit-handlers connection))])
           (callback (hash "nick" (first prefix-match)
                           "reason" (first params)))))]
      [(irc-message prefix "NICK" params _)
       (define prefix-match (regexp-match #rx"^[^!]+" prefix))
       (when prefix-match
         (for ([callback (unbox (ircbot-connection-nick-handlers connection))])
           (callback (hash "oldnick" (first prefix-match)
                           "newnick" (first params)))))]
      [_ (void)])
    (loop)))

In case this makes it more clear, here is an extremely simple bot implemented using the above module.

#lang racket

(require "irc-bot.rkt")

(define connection (ircbot-connect #:nick "racketbot"
                                   #:username "racketbot"
                                   #:realname "RacketBot 9000"
                                   #:channels (list "#racket")
                                   #:triggers (list "!rb")
                                   #:pmtrigger #t))

; log chats
(ircbot-listen-chat connection
                    (λ (data respond)
                      (printf "<~a> ~a~n"
                              (hash-ref data "sender")
                              (hash-ref data "message"))))
; log actions
(ircbot-listen-action connection
                      (λ (data respond)
                        (printf "* ~a ~a~n"
                                (hash-ref data "sender")
                                (hash-ref data "message"))))

; respond to simple commands
(ircbot-listen-trigger connection
                       (λ (data respond)
                         (define args (string-split (hash-ref data "message")))
                         (match args
                           [(list "hello")
                            (respond (format "Hello, ~a!" (hash-ref data "sender")))]
                           [(list "random" n)
                            (respond (number->string (random (string->number n))))]
                           [_ (void)]
                           )))

(ircbot-listen connection)

The bot prints standard chat messages and CTCP actions to stdout, and it responds to the !rb hello and !rb random <n> commands.

\$\endgroup\$
2
  • \$\begingroup\$ I might be misunderstanding, but: Do you really need to use boxes? Racket structs can be mutable (all fields, or just specific fields, as you wish, using #:mutable). So you could e.g. directly (set!-irc-connection-XXX-handlers x (some-mod (irc-connection-XXX-handlers x))), as opposed to (set-box! (irc-connection-XXX-handlers x) (some-mod (unbox (irc-connection-XXX-handlers x)))). \$\endgroup\$ Commented Sep 22, 2014 at 20:04
  • \$\begingroup\$ @GregHendershott You're quite right, I didn't realize #:mutable existed when I wrote this. \$\endgroup\$ Commented Sep 22, 2014 at 22:51

1 Answer 1

16
+50
\$\begingroup\$

Here are my comments after a brief reading:

  • First, if you intend to make this library fit for general use, add documentation in comments for the provided functions. See the Racket style guide for examples
  • Also, if you intend to make this a more general library, don't include default arguments (e.g. for ircbot-connect) unless they make sense for all users. Most users will connect to port 6667, but most users will not want to use the nick "racketircbot"
  • I agree with Greg Hendershott, I don't you don't need boxes in your struct - just use the #:mutable keyword
  • Break your code up into shorter functions where the logic is complex (e.g. the PRIVMSG handling should be a separate function). This is a good practice for all languages, not just Racket.
  • The use of curry seems awkward, but I haven't read the code in-depth enough to figure out if it's needed or not
  • Allowing the channels argument to ircbot-say be either a list or a single channel strikes me as strange - document this at the very least, but consider changing it to just take a list.
  • Your code is quite imperative in places, while Racket favors a more functional style. When you find yourself using set!, try to find a way to use just define instead, and only define each variable once.

I rewrote ircbot-say in a more functional form (although it's still not perfect):

(define (ircbot-say connection message [channels (ircbot-connection-channels connection)])
  (define channel-list (if (list? channels) channels (list channels)))
  (cond ([(string-contains message "/me ")
          (for ([channel channels])
            (irc-send-command (ircbot-connection-connection connection)
                              "PRIVMSG"
                              channel (format ":\u0001ACTION ~a\u0001" (substring message 4))))]
         [else
          (for ([channel channels])
            (irc-send-message (ircbot-connection-connection connection) channel message))])))

Otherwise, though, I think your code looks Racket-y. The match branches in ircbot-listen are exactly the kind of thing I envisioned when I wrote the irc package.

\$\endgroup\$
3
  • 5
    \$\begingroup\$ P.S. I'm happy to take any feedback you have about the irc package itself. I published it just as a starting point for a more full-fledged library, and I'd love to know what other functionality you think should be included, or bugs you've found. \$\endgroup\$ Commented Sep 22, 2014 at 20:57
  • 3
    \$\begingroup\$ Welcome to Code Review, I know nothing about this language but I will spread the link to it so you might get some votes! \$\endgroup\$ Commented Sep 22, 2014 at 21:01
  • \$\begingroup\$ Thanks much for this! I certainly agree with everything you've outlined. As for the irc package itself, it seems to work well, and I haven't encountered any bugs. The only thing I'd like to see would be some built-in support for CTCP messages, mostly to be able to more easily handle the extremely common ACTION command. Otherwise, though, I can't think of anything off the top of my head. Support for modes and formatting colors/styles would be nice, but probably wouldn't be particularly practical, anyway. \$\endgroup\$ Commented Sep 22, 2014 at 22:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.