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.
1 Answer 1
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 toircbot-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 justdefine
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.
-
6\$\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\$Jonathan Schuster– Jonathan Schuster2014年09月22日 20:57:13 +00:00Commented 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\$Simon Forsberg– Simon Forsberg2014年09月22日 21:01:49 +00:00Commented Sep 22, 2014 at 21:01
-
1\$\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 commonACTION
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\$Alexis King– Alexis King2014年09月22日 22:57:05 +00:00Commented Sep 22, 2014 at 22:57
box
es? Racketstruct
s 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\$#:mutable
existed when I wrote this. \$\endgroup\$