#lang racket/base (require json racket/contract racket/file racket/port racket/runtime-path racket-mimetypes web-server/dispatch web-server/http web-server/http/json web-server/servlet-env "player.rkt") (provide serve-player) (define-runtime-path public-directory "../public") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HTTP handlers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define current-player #f) (define (json-response value #:code [code 200]) (response/jsexpr value #:code code #:headers (list (header #"Cache-Control" #"no-store")))) (define (error-response exception) (json-response (hasheq 'error (exn-message exception)) #:code 400)) (define (request-jsexpr request) (let ((body (request-post-data/raw request))) (if (and body (positive? (bytes-length body))) (bytes->jsexpr body) (hasheq)))) (define (state-handler _request) (json-response (player-state->jsexpr current-player))) (define (discover-handler _request) (player-discover! current-player) (json-response (player-state->jsexpr current-player))) (define (command-handler request command) (with-handlers ((exn:fail? error-response)) (json-response (player-command! current-player command (request-jsexpr request))))) (define (agent-register-handler request) (with-handlers ((exn:fail? error-response)) (json-response (player-agent-register! current-player (request-jsexpr request))))) (define (agent-poll-handler request) (with-handlers ((exn:fail? error-response)) (json-response (player-agent-poll! current-player (request-jsexpr request))))) (define (agent-media-handler _request app-id token) (let ((file (player-agent-media current-player app-id token))) (if (and file (file-exists? file)) (response/output (λ (output) (call-with-input-file file (λ (input) (copy-port input output)))) #:mime-type (let ((mime (mimetype-for-ext file))) (if (string? mime) (string->bytes/utf-8 mime) #"application/octet-stream")) #:headers (list (header #"Content-Length" (string->bytes/utf-8 (number->string (file-size file)))) (header #"Cache-Control" #"no-store"))) (json-response (hasheq 'error "media token is invalid or expired") #:code 404)))) (define-values (dispatch _url) (dispatch-rules [("api" "state") #:method "get" state-handler] [("api" "discover") #:method "post" discover-handler] [("api" "agent" "register") #:method "post" agent-register-handler] [("api" "agent" "poll") #:method "post" agent-poll-handler] [("api" "agent" "media" (string-arg) (string-arg)) #:method "get" agent-media-handler] [("api" "command" (string-arg)) #:method "post" command-handler])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Provided functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; goal : Serve the web interface and JSON player API. ; pre : Value is a player, listen-ip is a string, and port is valid. ; post : Static files and API routes are served until the server stops. ; result : The result returned by serve/servlet. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define/contract (serve-player value #:listen-ip [listen-ip "127.0.0.1"] #:port [port 8080] #:launch-browser? [launch-browser? #t]) (->* (any/c) (#:listen-ip string? #:port exact-positive-integer? #:launch-browser? boolean?) any) (set! current-player value) (serve/servlet dispatch #:listen-ip listen-ip #:port port #:connection-close? #t #:launch-browser? launch-browser? #:quit? #f #:banner? #t #:servlet-regexp #rx"^/api(?:/|$)" #:extra-files-paths (list public-directory)))