2020-12-01 23:14:54 +00:00
|
|
|
#lang racket
|
|
|
|
|
2020-12-02 08:49:57 +00:00
|
|
|
;; library to interact with the AoC API and track solutions progress
|
|
|
|
|
2020-12-01 23:14:54 +00:00
|
|
|
(require net/uri-codec net/http-client)
|
2020-12-02 08:49:57 +00:00
|
|
|
(provide aoc-fetch-input aoc-fetch-challenge aoc-submit-answer
|
|
|
|
aoc-complete? aoc-set-complete!)
|
2020-12-01 23:14:54 +00:00
|
|
|
|
|
|
|
(define *host* "adventofcode.com")
|
2020-12-02 08:49:57 +00:00
|
|
|
(define *status-file* ".status.rktd")
|
2020-12-01 23:14:54 +00:00
|
|
|
|
2020-12-02 08:49:57 +00:00
|
|
|
;; generates API paths
|
2020-12-01 23:14:54 +00:00
|
|
|
(define/contract (puzzle-path year day endpoint)
|
2020-12-02 08:02:58 +00:00
|
|
|
(-> string? string? (or/c "input" "answer" false/c) path?)
|
|
|
|
(define base (build-path "/" year "day" day))
|
|
|
|
(if endpoint (build-path base endpoint) base))
|
2020-12-01 23:14:54 +00:00
|
|
|
|
2020-12-02 08:49:57 +00:00
|
|
|
;; sets up necessary headers for API
|
2020-12-01 23:14:54 +00:00
|
|
|
(define (make-headers session)
|
2020-12-02 08:02:58 +00:00
|
|
|
(list (string-append "Cookie: session=" session)
|
|
|
|
"Content-Type: application/x-www-form-urlencoded"))
|
2020-12-01 23:14:54 +00:00
|
|
|
|
2020-12-02 08:49:57 +00:00
|
|
|
;; http request helper
|
2020-12-01 23:14:54 +00:00
|
|
|
(define (aoc-request year day endpoint session [method 'GET] [data #f])
|
|
|
|
(define (parse-headers hlist)
|
|
|
|
(for/list ([h (in-list hlist)])
|
|
|
|
(match h
|
|
|
|
[(pregexp #px"^([^:]+): (.*?)$" (list _ k v))
|
|
|
|
(cons (string->symbol (string-downcase (bytes->string/utf-8 k)))
|
|
|
|
(bytes->string/utf-8 v))]
|
|
|
|
[x (cons 'unknown x)])))
|
|
|
|
|
2020-12-02 08:02:58 +00:00
|
|
|
(define (do-request path headers method data)
|
2020-12-01 23:14:54 +00:00
|
|
|
(define-values [status headers-out content]
|
2020-12-02 08:02:58 +00:00
|
|
|
(http-sendrecv *host* path #:ssl? #t #:headers headers #:method method #:data data))
|
2020-12-01 23:14:54 +00:00
|
|
|
(define headers-out/parsed (parse-headers headers-out))
|
|
|
|
|
|
|
|
(match status
|
|
|
|
[(pregexp #px"^HTTP/1\\.[10] 200") content]
|
|
|
|
[(pregexp #px"^HTTP/1\\.[10] 302")
|
|
|
|
(define location (cdr (or (assoc 'location headers-out/parsed)
|
|
|
|
(error "got 302 with no location"))))
|
|
|
|
(printf "got redirect to ~a\n" location)
|
|
|
|
(close-input-port content)
|
2020-12-02 08:02:58 +00:00
|
|
|
(do-request location headers 'GET #f)]
|
2020-12-01 23:14:54 +00:00
|
|
|
[(pregexp #px"^HTTP/1\\.[10] 404")
|
|
|
|
(error "endpoint returned 404\n response: " (port->bytes content))]
|
|
|
|
[stat
|
|
|
|
(error "endpoint returned unexpected data\n status: " stat "\n response: "
|
|
|
|
(port->bytes content))]))
|
2020-12-02 08:02:58 +00:00
|
|
|
(do-request (path->string (puzzle-path year day endpoint)) (make-headers session) method data))
|
2020-12-01 23:14:54 +00:00
|
|
|
|
2020-12-02 08:49:57 +00:00
|
|
|
;; gets the input file for a challenge
|
2020-12-01 23:14:54 +00:00
|
|
|
(define/contract (aoc-fetch-input year day session)
|
|
|
|
(-> string? string? string? input-port?)
|
|
|
|
(aoc-request year day "input" session))
|
|
|
|
|
2020-12-02 08:49:57 +00:00
|
|
|
;; submits an answer to the server
|
2020-12-01 23:14:54 +00:00
|
|
|
(define/contract (aoc-submit-answer year day session part answer)
|
|
|
|
(-> string? string? string? (or/c 1 2 "1" "2") string? (or/c symbol? bytes?))
|
|
|
|
(define data `((level . ,(~a part))
|
|
|
|
(answer . ,answer)))
|
|
|
|
(define resp
|
|
|
|
(port->bytes (aoc-request year day "answer" session 'POST (alist->form-urlencoded data))))
|
|
|
|
|
|
|
|
(match resp
|
2020-12-02 08:49:57 +00:00
|
|
|
[(pregexp #px"That's the right answer")
|
|
|
|
(aoc-set-complete! day part)
|
|
|
|
'answer-correct]
|
2020-12-01 23:14:54 +00:00
|
|
|
[(pregexp #px"That's not the right answer") 'answer-incorrect]
|
2020-12-02 08:49:57 +00:00
|
|
|
[(pregexp #px"Did you already complete it?")
|
|
|
|
(aoc-set-complete! day part)
|
|
|
|
'already-completed]
|
2020-12-01 23:14:54 +00:00
|
|
|
[x x]))
|
2020-12-02 08:02:58 +00:00
|
|
|
|
2020-12-02 08:49:57 +00:00
|
|
|
;; fetches the HTML page for a challenge
|
2020-12-02 08:02:58 +00:00
|
|
|
(define/contract (aoc-fetch-challenge year day session)
|
|
|
|
(-> string? string? string? input-port?)
|
|
|
|
(aoc-request year day #f session))
|
2020-12-02 08:49:57 +00:00
|
|
|
|
|
|
|
;; helper to generate entries for the challenge status file
|
|
|
|
(define (day+part->key day part)
|
|
|
|
(when (string? day)
|
|
|
|
(set! day (string->number day)))
|
|
|
|
(when (string? part)
|
|
|
|
(set! part (string->number part)))
|
|
|
|
(cons day part))
|
|
|
|
|
|
|
|
;; get the challenge status file
|
|
|
|
(define (aoc-get-status)
|
|
|
|
(cond [(file-exists? *status-file*)
|
|
|
|
(call-with-input-file *status-file* read)]
|
|
|
|
[else '()]))
|
|
|
|
|
|
|
|
;; is a challenge complete already?
|
|
|
|
(define (aoc-complete? day part)
|
|
|
|
(set-member? (aoc-get-status) (day+part->key day part)))
|
|
|
|
|
|
|
|
;; mark a challenge as completed
|
|
|
|
(define (aoc-set-complete! day part)
|
|
|
|
(define status (set-add (aoc-get-status) (day+part->key day part)))
|
|
|
|
(call-with-output-file
|
|
|
|
*status-file* (lambda (out) (write status out))
|
|
|
|
#:mode 'binary #:exists 'replace))
|