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