+(: b->n (-> Bytes (Option Number)))
+(define (b->n b)
+ (string->number (bytes->string/utf-8 b)))
+
+(: mon->num (-> Bytes Natural))
+(define/match (mon->num mon)
+ [(#"Jan") 1]
+ [(#"Feb") 2]
+ [(#"Mar") 3]
+ [(#"Apr") 4]
+ [(#"May") 5]
+ [(#"Jun") 6]
+ [(#"Jul") 7]
+ [(#"Aug") 8]
+ [(#"Sep") 9]
+ [(#"Oct") 10]
+ [(#"Nov") 11]
+ [(#"Dec") 12])
+
+(: rfc2822->epoch (-> Bytes (Option Nonnegative-Integer)))
+(define (rfc2822->epoch timestamp)
+ (match (regexp-match re-rfc2822 timestamp)
+ [(list _ _ dd mo yyyy HH MM SS)
+ #:when (and dd mo yyyy HH MM SS)
+ (find-seconds (b->n SS)
+ (b->n MM)
+ (b->n HH)
+ (b->n dd)
+ (mon->num mo)
+ (b->n yyyy)
+ #f)]
+ [_
+ #f]))
+
+(: user-agent String)
+(define user-agent
+ (let*
+ ([prog-name "tt"]
+ [prog-version (info:#%info-lookup 'version)]
+ [prog-uri "https://github.com/xandkar/tt"]
+ [user-peer-file (build-path tt-home-dir "me")]
+ [user
+ (if (file-exists? user-peer-file)
+ (match (first (file->peers user-peer-file))
+ [(Peer #f u) (format "+~a" (url->string u) )]
+ [(Peer n u) (format "+~a; @~a" (url->string u) n)])
+ (format "+~a" prog-uri))])
+ (format "~a/~a (~a)" prog-name prog-version user)))
+
+(: header-get (-> (Listof Bytes) Bytes (Option Bytes)))
+(define (header-get headers name)
+ (match (filter-map (curry extract-field name) headers)
+ [(list val) val]
+ [_ #f]))
+
+(: uri-download (-> Positive-Float Url Void))
+(define (uri-download timeout u)
+ (define cached-object-path (url->cache-object-path u))
+ (define cached-etag-path (url->cache-etag-path u))
+ (define cached-lmod-path (url->cache-lmod-path u))
+ (define u-str (url->string u))
+ (log-debug "uri-download ~v into ~v" u-str cached-object-path)
+ (define timeout-chan (make-channel))
+ (define result-chan (make-channel))
+ (define timeout-thread
+ (thread (λ ()
+ ; Doing this instead of sync/timeout to distinguish error values,
+ ; rather than just have #f to work with.
+ (sleep timeout)
+ (channel-put timeout-chan (cons 'error 'timeout)))))
+ (define result-thread
+ (thread (λ ()
+ ; XXX We timeout getting a response, but body download could
+ ; also take a long time and we might want to time that out as
+ ; well, but then we may end-up with partially downloaded
+ ; objects. But that could happen anyway if the server drops the
+ ; connection for whatever reason.
+ ;
+ ; Maybe that is OK once we start treating the
+ ; downloaded object as an addition to the stored set of
+ ; messages, rather than the final set of messages.
+
+ ; TODO message db
+ ; - 1st try can just be an in-memory set that gets written-to
+ ; and read-from disk as a whole.
+ (define result
+ (with-handlers
+ ([exn:fail? (λ (e) (cons 'error (cons 'net-error e)))])
+ (define-values (status-line headers body-input)
+ (http-sendrecv/url
+ u
+ #:headers (list (format "User-Agent: ~a" user-agent))))
+ (cons 'ok (Resp status-line headers body-input))))
+ (channel-put result-chan result))))
+ (define result
+ (sync timeout-chan
+ result-chan))
+ (kill-thread result-thread)
+ (kill-thread timeout-thread)
+ (match result
+ [(cons 'error 'timeout)
+ (log-error "Download failed: timeout. URL:~v" u-str)]
+ [(cons 'error (cons 'net-error e))
+ (log-error "Download failed. Network error. URL:~v EXN:~v" u-str e)]
+ [(cons 'ok (Resp status-line headers body-input))
+ (log-debug "headers: ~v" headers)
+ (log-debug "status-line: ~v" status-line)
+ (define status
+ (string->number (second (string-split (bytes->string/utf-8 status-line)))))
+ (log-debug "status: ~v" status)
+ ; TODO Handle redirects
+ (match status
+ [200
+ (let* ([etag (header-get headers #"ETag")]
+ [lmod (header-get headers #"Last-Modified")]
+ [lmod-curr (if lmod (rfc2822->epoch lmod) #f)]
+ [lmod-prev (if (file-exists? cached-lmod-path)
+ (rfc2822->epoch (file->bytes cached-lmod-path))
+ #f)])
+ (log-debug "lmod-curr:~v lmod-prev:~v" lmod-curr lmod-prev)
+ (unless (or (and etag
+ (file-exists? cached-etag-path)
+ (bytes=? etag (file->bytes cached-etag-path))
+ (begin
+ (log-info "ETags match, skipping the rest of ~v" u-str)
+ #t))
+ (and lmod-curr
+ lmod-prev
+ (<= lmod-curr lmod-prev)
+ (begin
+ (log-info "Last-Modified <= current skipping the rest of ~v" u-str)
+ #t)))
+ (begin
+ (log-info
+ "Downloading the rest of ~v. ETag: ~a, Last-Modified: ~v"
+ u-str etag lmod)
+ (make-parent-directory* cached-object-path)
+ (make-parent-directory* cached-etag-path)
+ (make-parent-directory* cached-lmod-path)
+ (call-with-output-file cached-object-path
+ (curry copy-port body-input)
+ #:exists 'replace)
+ (when etag
+ (display-to-file etag cached-etag-path #:exists 'replace))
+ (when lmod
+ (display-to-file lmod cached-lmod-path #:exists 'replace))))
+ (close-input-port body-input))]
+ [_
+ (log-error "HTTP error URL:~a status:~a" u-str status)])]))
+
+(: timeline-print (-> Out-Format (Listof Msg) Void))