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.
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\$