#! /usr/bin/env racket ; vim: filetype=racket ; TODO write ; TODO caching (use cache by default, unless explicitly asked for update) ; TODO timeline limits ; TODO user-defined feed sets (a la twitter lists) ; TODO feed set operations ; TODO timeline as a result of a query (feed set op + filter expressions) ; TODO named timelines ; TODO CLI params ; TODO config files ; TODO highlight mentions ; TODO filter on mentions ; TODO highlight hashtags ; TODO filter on hashtags ; TODO hashtags as channels? initial hashtag special? ; TODO query language ; TODO concurrency ; TODO console logger colors by level ('error) ; TODO file logger ('debug) ; TODO commands: ; - r | read ; - see timeline ops above ; - w | write ; - nick expand to URI ; - q | query ; - see timeline ops above ; - see hashtag and channels above #lang racket (require racket/date) (require http-client) (require rfc3339-old) (struct msg (tm_epoch tm_rfc3339 nick text)) (struct feed (nick uri)) (define (msg-print odd m) (printf "~a \033[1;37m<~a>\033[0m \033[0;~am~a\033[0m~n" (date->string (seconds->date [msg-tm_epoch m]) #t) [msg-nick m] [if odd 36 33] [msg-text m])) (define re-msg-begin ; TODO Zulu offset. Maybe in several formats. Which ones? (pregexp "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}")) (define (str->msg nick str) (if (not (regexp-match? re-msg-begin str)) (begin (log-debug "Non-msg line from nick:~a, line:~a" nick str) #f) (let ([toks (string-split str (regexp "\t+"))]) (if (not (= 2 (length toks))) (begin (log-warning "Invalid msg line from nick:~a, msg:~a" nick str) #f) (let* ([tm_rfc3339 (list-ref toks 0)] [tok_text (list-ref toks 1)] [t (string->rfc3339-record tm_rfc3339)] ; TODO handle tz offset [tm_epoch (find-seconds [rfc3339-record:second t] [rfc3339-record:minute t] [rfc3339-record:hour t] [rfc3339-record:mday t] [rfc3339-record:month t] [rfc3339-record:year t])]) (msg tm_epoch tm_rfc3339 nick tok_text)))))) (define (str->lines str) (string-split str (regexp "[\r\n]+"))) (define (str->msgs nick str) (filter-map (λ (line) (str->msg nick line)) (str->lines str))) (define (uri-fetch uri) (log-info "GET ~a" uri) (define resp (http-get uri)) (define status (http-response-code resp)) (define body (http-response-body resp)) (log-debug "finished GET ~a status:~a body length:~a" uri status (string-length body)) ; TODO Handle redirects (if (= status 200) body (raise status))) (define (timeline-print timeline) (for ([msg timeline] [i (in-naturals)]) (msg-print (odd? i) msg))) (define (feed->msgs feed) (log-info "downloading feed nick:~a uri:~a" (feed-nick feed) (feed-uri feed)) (with-handlers ([exn:fail:network? (λ (e) (log-error "network error nick:~a uri:~a exn:~a" (feed-nick feed) (feed-uri feed) e) #f)] [integer? (λ (status) (log-error "http error nick:~a uri:~a status:~a" (feed-nick feed) (feed-uri feed) status) #f)]) (str->msgs [feed-nick feed] [uri-fetch (feed-uri feed)]))) ; TODO timeline contract : time-sorted list of messages (define (timeline feeds) (sort (append* (filter-map feed->msgs feeds)) (λ (a b) [< (msg-tm_epoch a) (msg-tm_epoch b)]))) (define (we-are-twtxt) (let* ([uri "https://raw.githubusercontent.com/mdom/we-are-twtxt/master/we-are-twtxt.txt"] [payload (uri-fetch uri)] [lines (str->lines payload)] [feeds (map (λ (line) ; TODO validation (define toks (string-split line)) (feed [list-ref toks 0] [list-ref toks 1])) lines)]) feeds)) (define (setup-logging) (define logger (make-logger #f #f 'debug #f)) (define log-chan (make-log-receiver logger 'debug)) (void (thread (λ () [date-display-format 'iso-8601] [let loop () (define data (sync log-chan)) (define level (vector-ref data 0)) (define msg (vector-ref data 1)) (define ts (date->string (current-date) #t)) (eprintf "~a [~a] ~a~n" ts level msg) (loop)]))) (current-logger logger)) (define (main) (setup-logging) (current-http-response-auto #f) (current-http-user-agent "xandkar/tt 0.0.0") (date-display-format 'rfc2822) (define feeds (we-are-twtxt)) (timeline-print (timeline feeds))) (main)