Browse Source

first commit

master
wes 13 years ago
commit
1819609bb3
  1. 36
      botcore.rkt
  2. 39
      commands.rkt
  3. 50
      config.rkt
  4. 13
      config.sexp
  5. 124
      database.rkt
  6. 32
      irclib.rkt
  7. 204
      redis.rkt

36
botcore.rkt

@ -0,0 +1,36 @@
#lang racket
;;TCP
;;print-from-port : port -> void
;;prints out lines from the port until there are no more lines to print.
(define (print-from-port port)
(if (char-ready? port)
(let ([nextchar (read-char port)])
(printf (if (char=? #\~ nextchar)
"~~"
(string nextchar)))
(print-from-port port))
(printf "~n")))
;;send-to-port : port -> void
;;sends direct input from the user to the port.
(define (send-to-port port)
(let ([input (read-line)])
(if (not (string=? input "n"))
(begin (display (string-append input "\r\n") port)
(flush-output port)
(printf "Sending ~a~n" input))
(printf "Not sending anything.~n"))))
;;irc-connect : string number (port port -> void) -> void
;;connects to an IRC network, and begins running the provided irc handler.
(define (irc-connect server port handler)
(call-with-values (lambda () (tcp-connect server port))
(lambda (arguments) (handler (first arguments) (second arguments)))))
(define (irc-handle in out)
(print-from-port in)
(send-to-port out)
(irc-handle in out))
;(irc-connect "localhost" 6667 irc-handle)

39
commands.rkt

@ -0,0 +1,39 @@
#lang racket
(require "database.rkt")
(require "config.rkt")
;; Command Parsing
(define (string-head str)
(substring str 0 1))
(define (string-tail str)
(substring str 1 (string-length str)))
(define (cmd? cmdchar cmd)
(string=? cmdchar (string-head cmd)))
(define split-whitespace
((curry regexp-split) #rx" "))
(define (cmdparse command-char)
(compose
(lambda (command)
(match (cmd? command-char (car command))
[#f 'nil]
[#t (list (string-tail (car command))
(cdr command))]))
split-whitespace))
(define parse-exclamation
(cmdparse "!"))
(define parse-at
(cmdparse "@"))
;; Command Dispatcher
(define (dispatch nick command)
(match (parse-exclamation command)
[(list-rest "set" args) (submit-question (caar args) (string-join (cdar args) " "))]
[(list-rest "alias" args) (make-alias (cadar args) (caar args))]
[(list-rest name _) (get-question name)]))

50
config.rkt

@ -0,0 +1,50 @@
#lang racket
; Config stuff
(define inp-port
(open-input-file "./config.sexp"))
; Decides what to do with a config item
(define (config-parse item)
(match item
[(list-rest x (list-rest xs))
(update (make-hash) item)] ; make this into a new hash-table
[(list-rest xs) item] ; a flat list
[_ item])) ; any other type
; Creates a config hash table
(define (update htable settings)
(cond ((null? settings) htable)
(else (map (lambda (setting)
(hash-set! htable (first setting) (config-parse
(second setting)))) settings)
htable)))
; the actual config hash-table
(define config
(update (make-hash)
(read inp-port)))
; get database info
(define database-info
(let* [(database
(hash-ref
config
"database"))]
(lambda (key)
(hash-ref
database
key))))
; get admin info, each admin returns an int which is access level
(define admin-info
(let* [(admins
(hash-ref
config
"admins")
)]
(lambda (key)
(hash-ref
admins
key 1 ; default is 1 which is non-admin))))
(provide (all-defined-out))

13
config.sexp

@ -0,0 +1,13 @@
(
; database info
("database"
(("hostname" "localhost")
("port" 5984)
("user" "foo")
("pass" "bar")))
; bot administrators
("admins"
(
("foo" 0)
)
))

124
database.rkt

@ -0,0 +1,124 @@
#lang racket
(require (planet mordae/couchdb:1:11))
(require (planet dherman/json:4:0))
(require net/base64)
(require "redis.rkt")
(require "config.rkt")
; Couch Database operations
; object used to connect to the database
(define (database-connection database-name)
(couchdb-db
(couchdb-connect
#:host (database-info "hostname")
#:port (database-info "port")
#:user (database-info "user")
#:password (database-info "pass"))
database-name))
; gets a single uuid
(define (get-uuid)
(couchdb-get (database-connection "_uuids") ""))
; submits a question
(define couchdb-submit
(compose ((curry couchdb-put)
(database-connection "faqbot"))
; build the jsexpr (hasheq)
(lambda (question)
(hasheq '_id
(first question)
'text
(second question)))
; strip all newlines
(lambda (question)
(list (first question)
(strip-newlines
(second question))))))
; gets a question
(define couchdb-retrieve
(compose
(lambda (result)
(hash-ref result 'text))
((curry couchdb-get)
(database-connection "faqbot"))))
; strips all newlines
(define (strip-newlines text)
(regexp-replace* #rx"\n" text ""))
; Redis Database Operations
; make a command
(define make-cmd set)
; gets a specified command ID from redis
(define (get-id cmd)
(match (get cmd)
['nil 'nil]
[result (match (bytes->string/utf-8 result)
['nil 'nil]
[result (match (regexp-match #rx"^[0-9a-z]*$" result)
[#f 'nil]
[_ result])])]))
; make a command alias
(define (make-alias new-cmd old-cmd)
(match (get-id old-cmd)
['nil 'nil]
[_ (make-cmd
new-cmd
(get-id old-cmd))]))
;; Command Dispatch Functions
;; These functions take a string, parse it, and then execute the command
; associates a name with an id in Redis, then puts the question in couchdb
(define (submit-question name content)
(let* ([id (caar (hash-values [get-uuid]))])
(make-cmd name id)
(couchdb-submit (list id content))))
; gets an id associated with a name from Redis, then gets the question from couchdb
(define (get-question name)
(let* ([id (get-id name)])
(match id
['nil "No question found"]
[_ (couchdb-retrieve id)])))
;; Rate Limiting
; if |hC Current| > k, then set hC to Current, and hN to 0, and allow
; if |hC Current| < k and hN == n, then deny
; if |hC Current| < k, and hN < n, then allow, and increment hN by 1
(define (bytes->number x)
(string->number (bytes->string/utf-8 x)))
; Is there a built-in Racket function to convert byte strings to floats?
(define (time-magnitude a b)
(abs (- b (bytes->number a))))
(define (rate-check-helper mag hN n k)
(let ([hN (bytes->number hN)])
(cond
[(and (> k mag) (eq? hN n)) #f]
[(and (< mag k) (< hN n)) (hincrby "counter" "n" "1") #t]
[else (hset "counter" "n" "0")
(hset "counter" "current" (number->string (current-inexact-milliseconds)))
#t])))
(define ((rate-check n k))
(let* ([counter (hvals "counter")]
[current-time (current-inexact-milliseconds)])
(let* ([hC-Current (time-magnitude (second counter) current-time)])
(rate-check-helper hC-Current (first counter) n k))))
; Allows no more than 20 commands every 5 minutes
; resets after 5 minutes have passed from the last time it was reset
(define allowed? (rate-check 20 (* 5 60000)))
;; Admin Stuff
(provide (all-defined-out))

32
irclib.rkt

@ -0,0 +1,32 @@
#lang racket
; This module builds strings for requisite IRC commands
; PRIVMSG command
(define (privmsg target msg)
(format "PRIVMSG ~a :~a"
target
msg))
; USER command
(define (usermsg username realname)
(format
"USER ~a 0 * :~a" username realname))
; JOIN command
(define (join channel)
(format
"JOIN :~a" channel))
; PART command
(define (part channel reason)
(format
"PART ~a :~a"
channel
reason))
; QUIT command
(define (quit reason)
(format "QUIT :~a"
reason))

204
redis.rkt

@ -0,0 +1,204 @@
;;; redis.rkt
;;;
;;; Implements an interface to the redis persistent key-value
;;; database. Communicates with the database through its TCP
;;; interface.
#lang racket
(require racket/date)
(define-struct connection (in out cust))
(define (connect)
(let ([cust (make-custodian)])
(parameterize ([current-custodian cust])
(let-values ([(in out) (tcp-connect "localhost" 6379)])
(make-connection in out cust)))))
(define current-connection (make-parameter (connect)))
(define (disconnect!)
(define conn (current-connection))
(with-handlers ([exn:fail:network? void])
(close-output-port (connection-out conn)))
(with-handlers ([exn:fail:network? void])
(close-input-port (connection-in conn))))
(define send-command
(lambda commands
(define out (connection-out (current-connection)))
(fprintf out "*~a\r\n" (length commands))
(for-each (lambda (command)
(fprintf out "$~a\r\n~a\r\n"
(bytes-length
(string->bytes/utf-8
command))
command))
commands)
(flush-output out)))
(define-struct exn:redis (message))
(define (read-reply)
(define in (connection-in (current-connection)))
(match (read-bytes 1 in)
[#"-" (read-line in 'return-linefeed)]
[#"+" (read-line in 'return-linefeed)]
[#"$" (read-bulk-reply in)]
[#"*" (read-multi-bulk-reply in)]
[#":" (string->number (read-line in 'return-linefeed))]
[_ (raise (make-exn:redis (format "invalid control character: ~a"
(read-byte in))))]))
(define (read-bulk-reply in)
(flush-output)
(let ([length (string->number (read-line in 'return-linefeed))])
(match length
[-1 'nil]
[_ (begin0 (read-bytes length in)
(read-line in 'return-linefeed))])))
(define (read-multi-bulk-reply in)
(let ([length (string->number (read-line in 'return-linefeed))])
(flush-output)
(build-list length
(lambda (_) (read-reply)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COMMANDS
(define (ping)
(send-command "PING")
(read-reply))
;; Connection handling
(define (quit)
(send-command "QUIT")
(disconnect!))
(define (auth)
(send-command "AUTH")
(read-reply))
;; Commands operating on all value types
(define (exists? key)
(send-command "EXISTS" key)
(match (read-reply)
[1 #t]
[0 #f]))
(define del!
(lambda keys
(apply send-command `("DEL" ,@keys))
(read-reply)))
(define (type key)
(send-command "TYPE" key)
(string->symbol (read-reply)))
(define (keys pattern)
(send-command "KEYS" pattern)
(read-reply))
(define (randomkey)
(send-command "RANDOMKEY")
(read-reply))
(define (rename! oldkey newkey)
(send-command "RENAME" oldkey newkey)
(read-reply))
(define (renamenx! oldkey newkey)
(send-command "RENAME" oldkey newkey)
(match (read-reply)
[1 #t]
[0 #f]))
(define (dbsize)
(send-command "DBSIZE")
(read-reply))
(define (expire! key seconds)
(send-command "EXPIRE" key seconds)
(read-reply))
(define (expireat! key date)
(send-command "EXPIREAT" key (date->seconds date))
(read-reply))
(define (ttl key)
(send-command "TTL" key)
(read-reply))
(define (select key)
(send-command "SELECT" key)
(read-reply))
(define (move key dbindex)
(send-command "MOVE" key dbindex)
(read-reply))
(define (flushdb)
(send-command "FLUSHDB")
(read-reply))
(define (flushall)
(send-command "FLUSHALL")
(read-reply))
(define (set key value)
(send-command "SET" key value)
(read-reply))
(define (get key)
(send-command "GET" key)
(read-reply))
(define (getset key value)
(send-command "GETSET" key value)
(read-reply))
(define mget
(lambda keys
(apply send-command `("MGET" ,@keys)
(read-reply))))
(define (incrby key n)
(send-command "INCRBY" key n)
(read-reply))
;; Hash commands
(define (hset key field value)
(send-command "HSET" key field value)
(read-reply))
(define (hget key field)
(send-command "HGET" key field)
(read-reply))
(define (hgetall key)
(send-command "HGETALL" key)
(read-reply))
(define (hincrby key field num)
(send-command "HINCRBY" key field num)
(read-reply))
(define (hincrbyfloat key field num)
(send-command "HINCRBYFLOAT" key field num)
(read-reply))
(define (hkeys key)
(send-command "HKEYS" key)
(read-reply))
(define (hvals key)
(send-command "HVALS" key)
(read-reply))
(provide (all-defined-out))
Loading…
Cancel
Save