为了帮助自己学习Racket,我将为Node.js编写的简单JavaScript ircbot模块移植到了Racket。 Racket版本构建在Racket irc软件包之上,因此可以处理低级代码。我的模块仅提供了一些实用程序功能,以使其更易于实现IRC机器人。

我的代码如下。我知道这很长,可能还不太可读,对此我深表歉意。我没有在寻找非常具体的东西,但是我想知道我是否以任何明显的方式违反了该语言的任何主要约定。

#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)))


在这种情况更清楚了,这是一个使用上述模块实现的极其简单的机器人。

#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)


该机器人将标准聊天消息和CTCP操作打印到stdout,响应!rb hello!rb random <n>命令。

评论

我可能会误会,但是:您真的需要使用盒子吗?球拍结构可以是可变的(可以使用#:mutable随意更改所有字段,也可以仅更改特定字段)。所以你可以直接(set!-irc-connection-XXX-handlers x(some-mod(irc-connection-XXX-handlers x))),而不是(set-box!(irc-connection-XXX-handlers x)( -mod(取消包装(irc-connection-XXX-handlers x))))。

@GregHendershott你说的很对,当我写这篇文章时,我没有意识到#:mutable的存在。

#1 楼

在简短阅读后,下面是我的评论:首先,如果您打算使该库适合一般使用,请在提供的功能的评论中添加文档。有关示例,请参见《球拍风格指南》。另外,如果您打算使它成为更通用的库,请不要包括默认参数(例如ircbot-connect),除非它们对所有用户有意义。大多数用户将连接到端口6667,但是大多数用户将不希望使用昵称“ racketircbot”
我同意Greg Hendershott的观点,我不是不需要结构中的框-只需使用#:mutable关键字
将代码分解为逻辑复杂的较短函数(例如,PRIVMSG处理应为单独的函数)。这是对所有语言(不仅仅是Racket)的一种好习惯。
使用咖喱似乎很尴尬,但是我还没有足够深入地阅读代码来确定是否需要
channels的参数无论是列表还是单个通道都令我感到奇怪-至少要对此进行记录,但请考虑将其更改为仅包含列表。
您的代码在某些地方非常必要,而Racket赞成使用更实用的风格。当您发现自己使用ircbot-say时,尝试找到一种仅使用set!的方法,并且仅定义每个变量一次。

我以功能更丰富的形式重写了define(尽管它仍然不完美):

(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))])))


但是,否则,我认为您的代码看起来像是Racket-y。 ircbot-say中的match分支正是我编写ircbot-listen软件包时所设想的。

评论


\ $ \ begingroup \ $
附言很高兴收到您对irc软件包本身的任何反馈。我发布它只是作为功能更完善的库的起点,我很想知道您认为应该包含哪些其他功能或发现的错误。
\ $ \ endgroup \ $
–乔纳森·舒斯特(Jonathan Schuster)
2014年9月22日20:57在

\ $ \ begingroup \ $
欢迎使用代码审查,我对这种语言一无所知,但我会扩展它的链接,以便您获得一些投票!
\ $ \ endgroup \ $
–西蒙·福斯伯格
2014年9月22日21:01在

\ $ \ begingroup \ $
非常感谢!我当然同意您所概述的一切。至于irc包本身,它似乎运行良好,而且我还没有遇到任何错误。我唯一想看到的是对CTCP消息的内置支持,主要是为了能够更轻松地处理极其常见的ACTION命令。但是,否则,我想不出什么。对模式和颜色/样式格式的支持会很好,但是无论如何可能都不是特别实用。
\ $ \ endgroup \ $
–亚历克西斯·金(Alexis King)
2014年9月22日下午22:57