(import scheme (chicken base) (chicken port) (chicken random) (chicken time) (chicken process-context) spiffy intarweb uri-common sqlite3 medea srfi-1 ;list functions ) ;; db open and create (define db (open-database (or (get-environment-variable "DB_FILE") "/tmp/comments.db"))) ; Creates table automatically on initial run (execute db "CREATE TABLE IF NOT EXISTS comments ( id TEXT PRIMARY KEY, page_id TEXT, content TEXT, deletion_key TEXT, created_at DATETIME default current_timestamp )") ; creates a new almost-certainly-unique & chronologically ordered id (define (gen-id) (+ (pseudo-random-integer 65536) (* (current-seconds) 100000))) ; inserts comment into db (define (create-comment page-id content deletion-key) (let ([id (gen-id)] [content-or-empty (if (or (not content) (eof-object? content)) "" content)]) (execute db "INSERT INTO comments (id, page_id, content, deletion_key) VALUES (?, ?, ?, ?)" id page-id content-or-empty deletion-key) id)) ; turns db row into list of key-value pairs (define (comment-row->alist id page-id content created-at) `((id . ,id) (page_id . ,page-id) (created_at . ,created-at) (content . ,content))) ; selects all comments with the given page-id (define (get-comments page-id) (map-row comment-row->alist db "SELECT c.id, c.page_id, c.content, c.created_at FROM comments c WHERE c.page_id = ? ORDER BY c.created_at DESC" page-id )) ; deletes any comment with the comment-id and deletion-key given, if any exist (define (delete-comment comment-id deletion-key) (execute db "DELETE FROM comments WHERE id = ? AND deletion_key = ?" comment-id deletion-key)) ; get query string parameter (define (get-req-var k) (alist-ref k (uri-query (request-uri (current-request))))) ; get page_id query string parameter (define (get-page-id) (get-req-var 'page_id)) ; read current-request body as json. Arrays represented as lists, objects as alists (define (read-json-body) (read-json (request-port (current-request)) consume-trailing-whitespace: #f)) ; get alist value with key of k (define (json-value-ref json-alist k) (and (list? json-alist) (alist-ref k json-alist equal? #f))) ; headers to always add to responses (define base-headers '((access-control-allow-origin *) (access-control-allow-credentials true) (access-control-allow-methods GET POST OPTIONS DELETE) (access-control-allow-headers content-type))) ; list of allowed routes, first item is method, second is uri, third is handler function (define routes `( (OPTIONS (/ "comments") ,(lambda () (send-response status: 'ok headers: base-headers))) (POST (/ "comments") ,(lambda () (let* ([json-body (read-json-body)] [page-id (json-value-ref json-body 'page_id)] [content (json-value-ref json-body 'content)] [deletion-key (json-value-ref json-body 'deletion_key)] [bot? (not (equal? "no" (json-value-ref json-body 'bot)))]) (if bot? (send-status 'bad-request) (send-response status: 'ok body: (number->string (create-comment page-id content deletion-key)) headers: base-headers))))) (GET (/ "comments") ,(lambda () (let* ([page-id (get-page-id)] [comments (list->vector (get-comments page-id))]) (send-response headers: (cons '(content-type application/json) base-headers) status: 'ok body: (with-output-to-string (lambda () (write-json comments)))) ))) (DELETE (/ "comments") ,(lambda () (let ( [comment-id (get-req-var 'comment)] [deletion-key (get-req-var 'deletion_key)]) (delete-comment comment-id deletion-key) (send-response status: 'no-content headers: base-headers) ))) )) ; find route with matching uri and method, or return #f (define (find-route uri method) (find (lambda (r) (and (equal? method (first r)) (equal? (uri-path uri) (second r)))) routes)) ; handle a new HTTP request (define (handle continue) (let* ([req (current-request)] [uri (request-uri req)] [method (request-method req)] [route (find-route uri method)]) (if route ((third route)) (begin (display uri) (display method) (send-status 'not-found "Page Not Found"))))) (root-path ".") (vhost-map `((".*" . ,handle))) (start-server port: 7060)