commit
1819609bb3
7 changed files with 498 additions and 0 deletions
@ -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) |
@ -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)])) |
|||
|
@ -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)) |
@ -0,0 +1,13 @@ |
|||
( |
|||
; database info |
|||
("database" |
|||
(("hostname" "localhost") |
|||
("port" 5984) |
|||
("user" "foo") |
|||
("pass" "bar"))) |
|||
; bot administrators |
|||
("admins" |
|||
( |
|||
("foo" 0) |
|||
) |
|||
)) |
@ -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)) |
@ -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)) |
|||
|
@ -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…
Reference in new issue