+(: paths->peers (-> (Listof String) (Listof Peer)))
+(define (paths->peers paths)
+ (let* ([paths (match paths
+ ['()
+ (let ([peer-refs-file (build-path tt-home-dir "peers")])
+ (log-debug
+ "No peer ref file paths provided, defaulting to ~v"
+ (path->string peer-refs-file))
+ (list peer-refs-file))]
+ [paths
+ (log-debug "Peer ref file paths provided: ~v" paths)
+ (map string->path paths)])]
+ [peers (append* (map file->peers paths))])
+ (log-info "Read-in ~a peers." (length peers))
+ (uniq peers)))
+
+(: mentioned-peers-in-cache (-> (Listof Peer)))
+(define (mentioned-peers-in-cache)
+ (define msgs
+ (append* (map (λ (filename)
+ (define path (build-path cache-object-dir filename))
+ (define size (/ (file-size path) 1000000.0))
+ (log-info "BEGIN parsing ~a MB from file: ~v"
+ size
+ (path->string path))
+ (define t0 (current-inexact-milliseconds))
+ (define m (filter-map
+ (λ (line)
+ (str->msg #f (cache-object-filename->url filename) line))
+ (filter-comments
+ (file->lines path))))
+ (define t1 (current-inexact-milliseconds))
+ (log-info "END parsing ~a MB in ~a seconds from file: ~v."
+ size
+ (* 0.001 (- t1 t0))
+ (path->string path))
+ (when (empty? m)
+ (log-warning "No messages found in ~a" (path->string path)))
+ m)
+ (directory-list cache-object-dir))))
+ (uniq (append* (map Msg-mentions msgs))))
+
+(: log-writer-stop (-> Thread Void))
+(define (log-writer-stop log-writer)
+ (log-message (current-logger) 'fatal 'stop "Exiting." #f)
+ (thread-wait log-writer))
+
+(: log-writer-start (-> Log-Level Thread))
+(define (log-writer-start level)
+ (let* ([logger
+ (make-logger #f #f level #f)]
+ [log-receiver
+ (make-log-receiver logger level)]
+ [log-writer
+ (thread
+ (λ ()
+ (parameterize
+ ([date-display-format 'iso-8601])
+ (let loop ()
+ (match-define (vector level msg _ topic) (sync log-receiver))
+ (unless (equal? topic 'stop)
+ (eprintf "~a [~a] ~a~n" (date->string (current-date) #t) level msg)
+ (loop))))))])
+ (current-logger logger)
+ log-writer))
+
+(module+ main
+ (let ([log-level 'info])
+ (command-line
+ #:program
+ "tt"
+ #:once-each
+ [("-d" "--debug")
+ "Enable debug log level."
+ (set! log-level 'debug)]
+ #:help-labels
+ ""
+ "and <command> is one of"
+ "r, read : Read the timeline (offline operation)."
+ "d, download : Download the timeline."
+ ; TODO Add path dynamically
+ "u, upload : Upload your twtxt file (alias to execute ~/.tt/upload)."
+ "c, crawl : Discover new peers mentioned by known peers (offline operation)."
+ ""
+ #:args (command . args)
+ (define log-writer (log-writer-start log-level))
+ (current-command-line-arguments (list->vector args))
+ (match command
+ [(or "d" "download")
+ ; Initially, 15 was fastest out of the tried: 1, 5, 10, 20. Then I
+ ; started noticing significant slowdowns. Reducing to 5 seems to help.
+ (let ([num-workers 5]
+ [timeout 10.0])
+ (command-line
+ #:program
+ "tt download"
+ #:once-each
+ [("-j" "--jobs")
+ njobs "Number of concurrent jobs."
+ (set! num-workers (string->number njobs))]
+ [("-t" "--timeout")
+ seconds "Timeout seconds per request."
+ (set! timeout (string->number seconds))]
+ #:args file-paths
+ (let ([peers (paths->peers file-paths)])
+ (define-values (_res _cpu real-ms _gc)
+ (time-apply timeline-download (list num-workers timeout peers)))
+ (log-info "Downloaded timelines from ~a peers in ~a seconds."
+ (length peers)
+ (/ real-ms 1000.0)))))]
+ [(or "u" "upload")
+ (command-line
+ #:program
+ "tt upload"
+ #:args ()
+ (if (system (path->string (build-path tt-home-dir "upload")))
+ (exit 0)
+ (exit 1)))]
+ [(or "r" "read")
+ (let ([out-format 'multi-line]
+ [order 'old->new]
+ [ts-min #f]
+ [ts-max #f])
+ (command-line
+ #:program
+ "tt read"
+ #:once-each
+ [("-r" "--rev")
+ "Reverse displayed timeline order."
+ (set! order 'new->old)]
+ [("-m" "--min")
+ m "Earliest time to display (ignore anything before it)."
+ (set! ts-min (rfc3339->epoch m))]
+ [("-x" "--max")
+ x "Latest time to display (ignore anything after it)."
+ (set! ts-max (rfc3339->epoch x))]
+ #:once-any
+ [("-s" "--short")
+ "Short output format"
+ (set! out-format 'single-line)]
+ [("-l" "--long")
+ "Long output format"
+ (set! out-format 'multi-line)]
+ #:args file-paths
+ (let* ([peers
+ (paths->peers file-paths)]
+ [timeline
+ (timeline-sort (peers->timeline peers) order)]
+ [timeline
+ (filter (λ (m) (and (if ts-min (>= (Msg-ts-epoch m)
+ ts-min)
+ #t)
+ (if ts-max (<= (Msg-ts-epoch m)
+ ts-max)
+ #t)))
+ timeline)])
+ (timeline-print out-format timeline))))]
+ [(or "c" "crawl")
+ (command-line
+ #:program
+ "tt crawl"
+ #:args ()
+ (let* ([peers-sort
+ (λ (peers) (sort peers (match-lambda**
+ [((Peer n1 _ _) (Peer n2 _ _))
+ (string<? (if n1 n1 "")
+ (if n2 n2 ""))])))]
+ [peers-all-file
+ (build-path tt-home-dir "peers-all")]
+ [peers-mentioned-file
+ (build-path tt-home-dir "peers-mentioned")]
+ [peers-parsed-file
+ (build-path tt-home-dir "peers-parsed")]
+ [peers-mentioned-curr
+ (mentioned-peers-in-cache)]
+ [peers-mentioned-prev
+ (file->peers peers-mentioned-file)]
+ [peers-mentioned
+ (peers-sort (uniq (append peers-mentioned-prev
+ peers-mentioned-curr)))]
+ [peers-all-prev
+ (file->peers peers-all-file)]
+ [peers-all
+ (list->set (append peers-mentioned
+ peers-all-prev))]
+ [peers-discovered
+ (set-subtract peers-all (list->set peers-all-prev))]
+ [peers-all
+ (peers-sort (set->list peers-all))]
+ [peers-parsed
+ (filter
+ (λ (p) (< 0 (length (peer->msgs p))))
+ peers-all)])
+ (log-info "Known peers mentioned: ~a" (length peers-mentioned))
+ (log-info "Known peers parsed ~a" (length peers-parsed))
+ (log-info "Known peers total: ~a" (length peers-all))
+ (log-info "Discovered ~a new peers:~n~a"
+ (set-count peers-discovered)
+ (pretty-format (map
+ (λ (p) (cons (Peer-nick p)
+ (url->string (Peer-uri p))))
+ (set->list peers-discovered))))
+ (peers->file peers-mentioned
+ peers-mentioned-file)
+ (peers->file peers-parsed
+ peers-parsed-file)
+ (peers->file peers-all
+ peers-all-file)))]
+ [command
+ (eprintf "Error: invalid command: ~v\n" command)
+ (eprintf "Please use the \"--help\" option to see a list of available commands.\n")
+ (exit 1)])
+ (log-writer-stop log-writer))))