#!/usr/bin/guile !# (use-modules (ice-9 rdelim) (ice-9 textual-ports) (ice-9 format) (ice-9 control) (ice-9 sandbox) (ice-9 popen) (ice-9 regex) (rnrs exceptions)) (define +channels+ '("#tildetown" "#bots")) ;; (define +channels+ '("#bots")) (define (scdr lst) "Safe CDR. Returns CDR of lst, if it is a list. Returns lst otherwise." (if (not (pair? lst)) lst (cdr lst))) (define (scar lst) "Safe CAR, see scdr." (if (not (pair? lst)) lst (car lst))) (define (slist-ref lst index) "Safe list-ref, see scdr." (if (< (length lst) (+ index 1)) '() (list-ref lst index))) (define (slist-tail lst index) "Safe list-tail, see scdr." (if (< (length lst) (+ index 1)) '() (list-tail lst index))) (define (string-tail str index) "List-tail for strings." (list->string (list-tail (string->list str) index))) (define (intersperse sep lst) "Returns a new list with sep between each element of lst." (cond ((or (null? lst) (null? (cdr lst))) lst) (else (cons (car lst) (cons sep (intersperse sep (cdr lst))))))) (define (make-connection host port) "Connect to irc server on host:port, returns a socket for reading and writing." (let* ((sock (socket PF_INET SOCK_STREAM 0)) (con (connect sock AF_INET (car (vector-ref (gethost host) 4)) port))) sock)) (define (get-username str) (define (get-username-list lst) (cond ((null? lst) '()) ((eq? (car lst) #\!) '()) (else (cons (car lst) (get-username-list (cdr lst)))))) (list->string (cdr (get-username-list (string->list str))))) (define (pong stream) (format stream "PONG :anna\r\n")) (define (send-nick stream) (format stream "NICK anna\r\n")) (define (send-user stream) (format stream "USER anna 0.0.0.0 anna :Anna\r\n")) (define (join-channels stream) (for-each (lambda (chn) (format stream "JOIN ~a\r\n" chn)) +channels+)) (define (send-message stream chn msg) (format stream "PRIVMSG ~a :~a\r\n" chn msg)) (define (send-action stream chn action) (format stream "PRIVMSG ~a :\x01ACTION ~a\x01\r\n" chn action)) (define (input-filter str) "Remove escaped chars to avoid fooling the bot into performing bad actions." (define (remove-escaped-chars lst) (cond ((or (null? lst) (null? (cdr lst))) lst) ((eq? (car lst) #\\) (remove-escaped-chars (cddr lst))) (else (cons (car lst) (remove-escaped-chars (cdr lst)))))) (list->string (remove-escaped-chars (string->list str)))) (define (evaluate str) "Evaluate the given string in a sandbox without crashing on errors." (guard (ex (else (begin (display "error encountered!\n") 'error))) ;; << this gets printed in chat (eval-in-sandbox (read (open-input-string str))))) (define (find-documentation language search) "Find documentation for different programming languages." (cond ((null? language) "Missing langauge specifier.") ((null? (string-tokenize search)) "Missing search string.") ((eq? language 'cl) (find-cl-documentation search)) (else "Unrecognized language! Supported languages are: cl"))) (define (read-file f) (call-with-input-file f get-string-all)) (define (timezone user) (let ((path (string-append "/home/" user "/.tz"))) (if (access? path R_OK) (read-file path) (string-append user " has not set their timezone. Use `echo '' > ~/.tz' to add your timezone.")))) (define (slice start end lst) "Return the list between index start and end (start inclusive, end not inclusive)." (cond ((> start end) (error "Start index can't be more than end index.")) ((> end (length lst)) (error "End index too large.")) ((= start end) '()) (else (cons (list-ref lst start) (slice (+ 1 start) end lst))))) (define (escape-string str) "Replace all special regex chars with escaped versions." (define chars '(#\( #\) #\* #\+ #\?)) (define (member? elem lst) (cond ((null? lst) #f) ((eq? (car lst) elem) #t) (else (member? elem (cdr lst))))) (define (loop lst) (cond ((null? lst) '()) ((member? (car lst) chars) (cons #\\ (cons (car lst) (loop (cdr lst))))) (else (cons (car lst) (loop (cdr lst)))))) (list->string (loop (string->list str)))) (define (find-cl-documentation search) "Find documentation for Common Lisp." (let ((status (system* "grep" (string-append "htm#" search "\"") "cl.htm"))) (cond ((= 0 status) (let* ((port (open-input-pipe (string-append "grep htm#" search "\\\" cl.htm"))) (out (string-match "Body.*\"" (read-line port))) (sublink (list->string (slice (car (vector-ref out 1)) (- (cdr (vector-ref out 1)) 1) (string->list (vector-ref out 0)))))) (close-pipe port) (string-append "http://www.lispworks.com/documentation/lw70/CLHS/" sublink))) (else "Symbol not found in documentation.")))) (define (main-loop stream) (let* ((inl (read-line stream)) (data (string-tokenize inl)) (chn (scar (scdr (member "PRIVMSG" data)))) (filtered-data (map input-filter data))) ;; debug print (format #t "~s~%" data) (cond ((equal? (car data) "PING") (pong stream)) ((or (equal? ":!rollcall" (slist-ref data 3)) (equal? ":!anna" (slist-ref data 3))) (send-message stream chn "Hello! I respond to !rollcall, !anna, !tz , and !eval .")) ((equal? ":!eval" (slist-ref data 3)) (let ((expr (string-concatenate (intersperse " " (cdr (member ":!eval" filtered-data)))))) (send-message stream chn (evaluate expr)))) ((equal? ":!doc" (slist-ref data 3)) (let ((lang (slist-ref data 4)) (search (escape-string (string-concatenate (intersperse " " (cddr (member ":!doc" filtered-data))))))) (send-message stream chn (find-documentation (string->symbol lang) search)))) ((equal? ":!tz" (slist-ref data 3)) (send-message stream chn (if (= 4 (length data)) (timezone (get-username (car data))) (timezone (slist-ref data 4))))) ((and (= 7 (length data)) (equal? (list-tail data 4) '("ACTION" "shoots" "anna"))) (send-action stream chn "dies") (close-port stream) (exit))) (main-loop stream))) (let* ((io (make-connection "127.0.0.1" 6667))) (send-nick io) (send-user io) (join-channels io) (main-loop io))