summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--dynamic/api.scm157
-rw-r--r--dynamic/capabilities/comments.scm40
-rw-r--r--dynamic/capabilities/rsvp.scm280
-rw-r--r--dynamic/logging.scm37
-rw-r--r--dynamic/schema-rsvp.sql1
-rw-r--r--dynamic/util.scm57
-rw-r--r--haunt/static/image/hourglass.gifbin0 -> 21999 bytes
-rw-r--r--haunt/static/js/rsvp.js183
8 files changed, 535 insertions, 220 deletions
diff --git a/dynamic/api.scm b/dynamic/api.scm
index 4a41040..b53be8a 100644
--- a/dynamic/api.scm
+++ b/dynamic/api.scm
@@ -14,163 +14,26 @@
;;; along with this program. If not, see
;;; <http://www.gnu.org/licenses/>.
-(add-to-load-path (dirname (current-filename)))
-;; (add-to-load-path "/home/jakob/Blog/dynamic")
-
-(use-modules (base64)
- (captcha)
- (ice-9 binary-ports)
- (json)
+(use-modules (dynamic capabilities rsvp)
+ (dynamic logging)
(srfi srfi-1)
- (srfi srfi-11)
- (srfi srfi-13)
- (srfi srfi-26)
- (squee)
- (rnrs bytevectors)
(ice-9 match)
(web server)
(web request)
(web response)
(web uri))
-;; Globals.
-
-(define challenges (make-hash-table))
-;; (define conn (connect-to-postgres-paramstring "dbname=jakob_comments"))
-(define conn (connect-to-postgres-paramstring "dbname=jakob_rsvp"))
-
-;; Util.
-
-(define (acons-list k v alist)
- "Add V to K to alist as list"
- (let ((value (assoc-ref alist k)))
- (if value
- (let ((alist (alist-delete k alist)))
- (acons k (cons v value) alist))
- (acons k (list v) alist))))
-
-(define (list->alist lst)
- "Build a alist of list based on a list of key and values.
-
- Multiple values can be associated with the same key"
- (let next ((lst lst)
- (out '()))
- (if (null? lst)
- out
- (next (cdr lst) (acons-list (caar lst) (cdar lst) out)))))
-
-(define (decode-form bv)
- "Convert BV querystring or form data to an alist"
- (define string (if (string? bv) bv (utf8->string bv)))
- (define pairs (map (cut string-split <> #\=)
- ;; semi-colon and amp can be used as pair separator
- (append-map (cut string-split <> #\;)
- (string-split string #\&))))
- (list->alist (map (match-lambda
- ((key value)
- (cons (uri-decode key) (uri-decode value)))) pairs)))
-
-(define (request-path-components request)
- (split-and-decode-uri-path (uri-path (request-uri request))))
-
(define (not-found request)
+ "Build a (somewhat) descriptive response for a non-existent resource."
(values (build-response #:code 404)
(string-append "Resource not found: "
(uri->string (request-uri request)))))
-;;
-
-(define (get-comments request body)
- (values '((content-type . (application/json)))
- (scm->json-string
- '((title . "whoa buddy")
- (author . "Jakob Kreuze")
- (date . "2022-03-27")
- (text . "bad take, bad take!")))))
-
-(define (put-comment request body)
- (display (decode-form body))
- (newline)
- (values '((content-type . (text/plain))) "Hello hacker!"))
-
-(define (make-challenge request body)
- (let-values (((uuid value image) (new-captcha)))
- (hash-set! challenges uuid value)
- (hash-for-each (lambda (x y) (display x) (newline)) challenges)
- (values `((content-type . (application/base64))
- (access-control-allow-origin . "*")
- (x-captcha-id . ,uuid))
- (base64-encode image))))
-
-;; (define (valid-invite-code invitation)
-;; (and (= (string-length))))
-
-(define (generate-vanity-code)
- (call-with-input-file "/dev/urandom"
- (lambda (port)
- (base64-encode (get-bytevector-n port 9)))))
-
-(define (post-event-rsvp request body)
- (let* ((params (json-string->scm (utf8->string body)))
- (invitation-code (assoc-ref params "id"))
- (name (assoc-ref params "name"))
- (email (assoc-ref params "email"))
- (attending (assoc-ref params "rsvp"))
- (guests (assoc-ref params "guests")))
- (cond ((not (and invitation-code name email attending guests))
- (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*")))
- (scm->json-string
- `((success . #f)
- (error . "Invalid form data")))))
- ((> 1 (length (exec-query conn "SELECT event_id FROM invitations WHERE vanity = $1" (list invitation-code))))
- (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*")))
- (scm->json-string
- `((success . #f)
- (error . "Invalid invitation code")))))
- (else
- (let* ((receipt-code (generate-vanity-code))
- (event-id (caar (exec-query conn "SELECT event_id FROM invitations WHERE vanity = $1" (list invitation-code)))))
- (exec-query conn "INSERT INTO rsvps (vanity, event_id, fullname, email, attending, guests) VALUES ($1, $2, $3, $4, $5, $6)" (list receipt-code event-id name email attending guests))
- (values '((content-type . (application/json))
- (Access-Control-Allow-Origin . "*"))
- (scm->json-string
- `((receipt . ,receipt-code)))))))))
-
-(define (get-event-info request body)
- (define (format-rsvp rsvp)
- (match rsvp
- ((name email guests)
- `((name . ,name)
- (email . ,email)
- (guests . ,guests)))))
- (let* ((params (decode-form (uri-query (request-uri request))))
- (invitation-code (car (assoc-ref params "i")))
- (invitation (exec-query conn "SELECT event_id, capabilities FROM invitations WHERE vanity = $1" (list invitation-code))))
- (if (> 1 (length invitation))
- (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*")))
- (scm->json-string
- `((success . #f)
- (error . "Invalid invitation code"))))
- (let* ((capabilities (cadar invitation))
- (event (exec-query conn "SELECT * FROM events WHERE id = $1" (list (caar invitation))))
- (rsvps (exec-query conn "SELECT fullname, email, guests FROM rsvps WHERE event_id = $1" (list (caar invitation)))))
- (match (car event)
- ((i_ title description date location)
- (values '((content-type . (application/json))
- (Access-Control-Allow-Origin . "*"))
- (scm->json-string
- `((title . ,title)
- (description . ,description)
- (image . #f)
- (date . ,date)
- (location . ,location)
- ,@(if (= 1 (logand (string->number capabilities) 1))
- `((rsvps . ,(list->vector (map format-rsvp rsvps))))
- '()))))))))))
-
(define (handle-api-request request body endpoint)
- (display (cons (request-method request) endpoint))
- (newline)
+ "Route handler for the API server."
+ (let ((method (request-method request))
+ (originating-ip (assoc-ref (request-headers request) 'X-Forwarded-For)))
+ (log-append! 'info (format #f "~a ~a (~a)" method endpoint originating-ip)))
((match (cons (request-method request) endpoint)
;; ('(GET "challenge") make-challenge)
;; ('(GET "comments") get-comments)
@@ -181,9 +44,11 @@
request body))
(define (main-request-handler request body)
- (let ((path (request-path-components request)))
+ "Server entry-point; parse `request' and defer to routing system."
+ (let* ((path-encoded (uri-path (request-uri request)))
+ (path (split-and-decode-uri-path path-encoded)))
(if (string= "api" (first path))
(handle-api-request request body (drop path 1))
(not-found request))))
-(run-server main-request-handler 'http '(#:port 8081))
+(run-server main-request-handler)
diff --git a/dynamic/capabilities/comments.scm b/dynamic/capabilities/comments.scm
new file mode 100644
index 0000000..d7dc1fa
--- /dev/null
+++ b/dynamic/capabilities/comments.scm
@@ -0,0 +1,40 @@
+;;; Copyright © 2019 - 2022 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+;;;
+;;; This program is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; This program is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(define challenges (make-hash-table))
+(define conn (connect-to-postgres-paramstring "dbname=jakob_comments"))
+
+(define (get-comments request body)
+ (values '((content-type . (application/json)))
+ (scm->json-string
+ '((title . "whoa buddy")
+ (author . "Jakob Kreuze")
+ (date . "2022-03-27")
+ (text . "bad take, bad take!")))))
+
+(define (put-comment request body)
+ (display (decode-form body))
+ (newline)
+ (values '((content-type . (text/plain))) "Hello hacker!"))
+
+(define (make-challenge request body)
+ (let-values (((uuid value image) (new-captcha)))
+ (hash-set! challenges uuid value)
+ (hash-for-each (lambda (x y) (display x) (newline)) challenges)
+ (values `((content-type . (application/base64))
+ (access-control-allow-origin . "*")
+ (x-captcha-id . ,uuid))
+ (base64-encode image))))
diff --git a/dynamic/capabilities/rsvp.scm b/dynamic/capabilities/rsvp.scm
new file mode 100644
index 0000000..527f7a1
--- /dev/null
+++ b/dynamic/capabilities/rsvp.scm
@@ -0,0 +1,280 @@
+;;; Copyright © 2019 - 2022 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+;;;
+;;; This program is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; This program is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(define-module (dynamic capabilities rsvp)
+ #:use-module (base64)
+ #:use-module (dynamic util)
+ #:use-module (ice-9 binary-ports)
+ #:use-module (ice-9 match)
+ #:use-module (json)
+ #:use-module (rnrs bytevectors)
+ #:use-module (squee)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-9)
+ #:use-module (web request)
+ #:use-module (web response)
+ #:use-module (web uri)
+ #:export (get-event-info post-event-rsvp))
+
+;; How many bytes of entropy to use when generating vanity ID's.
+(define %vanity-length (make-parameter 9))
+
+;; Path where event header images are stored.
+(define %event-image-path-fmt (make-parameter "/home/jakob/event-images/~a.png"))
+
+;; Global handle to the RSVP database.
+(define conn (connect-to-postgres-paramstring "dbname=jakob_rsvp"))
+
+
+
+(define (generate-vanity-code)
+ "Generate a random vanity ID.
+
+A vanity ID is used in the RSVP system for creating unique URLs for invitations.
+It is a base64 string, encoding `%vanity-length' bytes of randomness."
+ (call-with-input-file "/dev/urandom"
+ (lambda (port) (base64-encode (get-bytevector-n port 9)))))
+
+(define (valid-invite-code invitation)
+ "Check database to see if `invitation' exists."
+ (and (= (string-length invitation) (base64-length (%vanity-length)))
+ (positive?
+ (length
+ (exec-query conn "SELECT * FROM invitations WHERE vanity = $1"
+ (list invitation))))))
+
+(define (valid-receipt-code receipt)
+ "Check database to see if `receipt'."
+ (and (= (string-length receipt) (base64-length (%vanity-length)))
+ (positive?
+ (length
+ (exec-query conn "SELECT * FROM rsvps WHERE vanity = $1"
+ (list receipt))))))
+
+
+
+(define-record-type <rsvp-create>
+ (make-rsvp-create-parameters)
+ rsvp-create-parameters?
+ (invitation-code rsvp-create-code set-rsvp-create-code!)
+ (name rsvp-create-name set-rsvp-create-name!)
+ (email rsvp-create-email set-rsvp-create-email!)
+ (attending rsvp-create-attending set-rsvp-create-attending!)
+ (guests rsvp-create-guests set-rsvp-create-guests!))
+
+(define (params->rsvp-create params)
+ "Parse `params', an alist, into a `<rsvp-create>'."
+ (let ((res (make-rsvp-create-parameters)))
+ (set-rsvp-create-code! res (assoc-ref params "id"))
+ (set-rsvp-create-name! res (assoc-ref params "name"))
+ (set-rsvp-create-email! res (assoc-ref params "email"))
+ (set-rsvp-create-attending! res (assoc-ref params "rsvp"))
+ (set-rsvp-create-guests! res (assoc-ref params "guests"))
+ (if (any not
+ (list (rsvp-create-code res)
+ (rsvp-create-name res)
+ (rsvp-create-email res)
+ (rsvp-create-attending res)
+ (rsvp-create-guests res)))
+ #f
+ res)))
+
+(define (invitation->event-id vanity-code)
+ "For valid `vanity-code', find the corresponding event ID and capabilities."
+ (car
+ (exec-query conn "SELECT event_id, capabilities FROM invitations WHERE vanity = $1"
+ (list vanity-code))))
+
+(define (create-new-event-rsvp params)
+ "Handler for RSVP'ing to an event."
+ (let ((params (params->rsvp-create params)))
+ (cond ((not params)
+ (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*")))
+ (scm->json-string
+ `((success . #f)
+ (error . "Invalid form data")))))
+ ((not (valid-invite-code (rsvp-create-code params)))
+ (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*")))
+ (scm->json-string
+ `((success . #f)
+ (error . "Invalid invitation code")))))
+ (else
+ (let ((receipt-code (generate-vanity-code))
+ (event-id (car (invitation->event-id (rsvp-create-code params)))))
+ (exec-query conn
+ "INSERT INTO rsvps (vanity, invitation_id, event_id, fullname, email, attending, guests) VALUES ($1, $2, $3, $4, $5, $6, $7)"
+ (list receipt-code
+ (rsvp-create-code params)
+ event-id
+ (rsvp-create-name params)
+ (rsvp-create-email params)
+ (rsvp-create-attending params)
+ (rsvp-create-guests params)))
+ (values '((content-type . (application/json))
+ (Access-Control-Allow-Origin . "*"))
+ (scm->json-string
+ `((receipt . ,receipt-code)))))))))
+
+
+
+(define-record-type <rsvp-update>
+ (make-rsvp-update-parameters)
+ rsvp-update-parameters?
+ (invitation-code rsvp-update-code set-rsvp-update-code!)
+ (name rsvp-update-name set-rsvp-update-name!)
+ (email rsvp-update-email set-rsvp-update-email!)
+ (attending rsvp-update-attending set-rsvp-update-attending!)
+ (guests rsvp-update-guests set-rsvp-update-guests!))
+
+(define (params->rsvp-update params)
+ "Parse `params', an alist, into a `<rsvp-update>'."
+ (let ((res (make-rsvp-update-parameters)))
+ (set-rsvp-update-code! res (assoc-ref params "update"))
+ (set-rsvp-update-name! res (assoc-ref params "name"))
+ (set-rsvp-update-email! res (assoc-ref params "email"))
+ (set-rsvp-update-attending! res (assoc-ref params "rsvp"))
+ (set-rsvp-update-guests! res (assoc-ref params "guests"))
+ (if (any not
+ (list (rsvp-update-code res)
+ (rsvp-update-name res)
+ (rsvp-update-email res)
+ (rsvp-update-attending res)
+ (rsvp-update-guests res)))
+ #f
+ res)))
+
+(define (update-event-rsvp params)
+ "Handler for updating an RSVP to an event."
+ (let ((params (params->rsvp-update params)))
+ (cond ((not params)
+ (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*")))
+ (scm->json-string
+ `((success . #f)
+ (error . "Invalid form data")))))
+ ((not (valid-receipt-code (rsvp-update-code params)))
+ (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*")))
+ (scm->json-string
+ `((success . #f)
+ (error . "Invalid receipt code")))))
+ (else
+ (exec-query conn
+ "UPDATE rsvps SET fullname = $2, email = $3, attending = $4, guests = $5 WHERE vanity = $1"
+ (list
+ (rsvp-update-code params)
+ (rsvp-update-name params)
+ (rsvp-update-email params)
+ (rsvp-update-attending params)
+ (rsvp-update-guests params)))
+ (values '((content-type . (application/json)))
+ (scm->json-string
+ `((receipt . ,(rsvp-update-code params)))))))))
+
+
+
+(define (post-event-rsvp request body)
+ "Entry point for RSVP create/update. We dispatch on the parameters."
+ (let* ((params (json-string->scm (utf8->string body))))
+ (cond ((assoc-ref params "id") (create-new-event-rsvp params))
+ ((assoc-ref params "update") (update-event-rsvp params))
+ (else (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*")))
+ (scm->json-string
+ `((success . #f)
+ (error . "Invalid invite/update code"))))))))
+
+
+
+(define (get-event-image event-id)
+ "Return, as base64, the header image for `event-id'."
+ (call-with-input-file (format #f (%event-image-path-fmt) event-id)
+ (lambda (port)
+ (base64-encode (get-bytevector-all port)))))
+
+(define (get-event-invitation invitation-code)
+ "Handler for reading information about an event."
+ (define (format-rsvp rsvp)
+ (match rsvp
+ ((name email attending guests)
+ `((name . ,name)
+ (email . ,email)
+ (attending . ,attending)
+ (guests . ,guests)))))
+ (let* ((invitation (invitation->event-id invitation-code))
+ (capabilities (cadr invitation))
+ (event (exec-query conn "SELECT * FROM events WHERE id = $1" (list (car invitation))))
+ (rsvps (exec-query conn "SELECT fullname, email, attending, guests FROM rsvps WHERE event_id = $1" (list (car invitation)))))
+ (match (car event)
+ ((i_ title description date location)
+ (values '((content-type . (application/json))
+ (Access-Control-Allow-Origin . "*"))
+ (scm->json-string
+ `((title . ,title)
+ (description . ,description)
+ (image . ,(get-event-image (car invitation)))
+ (date . ,date)
+ (location . ,location)
+ ,@(if (= 1 (logand (string->number capabilities) 1))
+ `((rsvps . ,(list->vector (map format-rsvp rsvps))))
+ '()))))))))
+
+(define (get-event-receipt receipt-code)
+ "Handler for reading information about an event, with receipt info."
+ (define (format-rsvp rsvp)
+ (match rsvp
+ ((name email attending guests)
+ `((name . ,name)
+ (email . ,email)
+ (attending . ,attending)
+ (guests . ,guests)))))
+ (let* ((rsvp (exec-query conn "SELECT invitation_id, fullname, email, attending, guests FROM rsvps WHERE vanity = $1" (list receipt-code)))
+ (invitation (invitation->event-id (caar rsvp)))
+ (capabilities (cadr invitation))
+ (event (exec-query conn "SELECT * FROM events WHERE id = $1" (list (car invitation))))
+ (rsvps (exec-query conn "SELECT fullname, email, attending, guests FROM rsvps WHERE event_id = $1" (list (car invitation)))))
+ (match (car event)
+ ((i_ title description date location)
+ (values '((content-type . (application/json))
+ (Access-Control-Allow-Origin . "*"))
+ (scm->json-string
+ `((title . ,title)
+ (description . ,description)
+ (image . ,(get-event-image (car invitation)))
+ (date . ,date)
+ (location . ,location)
+ (name . ,(list-ref (car rsvp) 1))
+ (email . ,(list-ref (car rsvp) 2))
+ (attending . ,(list-ref (car rsvp) 3))
+ (guests . ,(list-ref (car rsvp) 4))
+ ,@(if (= 1 (logand (string->number capabilities) 1))
+ `((rsvps . ,(list->vector (map format-rsvp rsvps))))
+ '()))))))))
+
+(define (get-event-info request body)
+ "Entry point to `get-event-receipt'/`get-event-invitation'."
+ (let* ((query-string (uri-query (request-uri request)))
+ (params (if query-string
+ (decode-form query-string)
+ '()))
+ (invitation-code (assoc-ref params "i"))
+ (receipt-code (assoc-ref params "r")))
+ (cond ((and receipt-code (valid-receipt-code (car receipt-code)))
+ (get-event-receipt (car receipt-code)))
+ ((and invitation-code (valid-invite-code (car invitation-code)))
+ (get-event-invitation (car invitation-code)))
+ (else
+ (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*")))
+ (scm->json-string
+ `((success . #f)
+ (error . "Invalid invitation or receipt code"))))))))
diff --git a/dynamic/logging.scm b/dynamic/logging.scm
new file mode 100644
index 0000000..dab2464
--- /dev/null
+++ b/dynamic/logging.scm
@@ -0,0 +1,37 @@
+;;; Copyright © 2019 - 2022 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+;;;
+;;; This program is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; This program is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(define-module (dynamic logging)
+ #:use-module (ice-9 format)
+ #:use-module (srfi srfi-19)
+ #:export (log-append!))
+
+(define %log-file-name (make-parameter "/var/log/jakob-dynamic.log"))
+(define %log-level (make-parameter 'info))
+
+(define (dominates message-level baseline-level)
+ "Determine if `a' should be shown for baseline log level `b'."
+ (define log-level-hierarchy '(debug info warn error))
+ (>= (list-index log-level-hierarchy message-level)
+ (list-index log-level-hierarchy baseline-level)))
+
+(define (log-append! level message)
+ "Append `message', at `level', to the log buffer."
+ (when (dominates level (%log-level))
+ (call-with-output-file (%log-file-name)
+ (lambda (port)
+ (let ((now (date->string (current-date) "~4")))
+ (format port "[~a] ~a: ~a~%" now level message))))))
diff --git a/dynamic/schema-rsvp.sql b/dynamic/schema-rsvp.sql
index 42c8c41..3e6a21f 100644
--- a/dynamic/schema-rsvp.sql
+++ b/dynamic/schema-rsvp.sql
@@ -20,6 +20,7 @@ CREATE TABLE IF NOT EXISTS invitations (
CREATE TABLE IF NOT EXISTS rsvps (
id SERIAL,
vanity char(12) NOT NULL,
+ invitation_id char(12) NOT NULL,
event_id bigint NOT NULL,
fullname varchar(128) NOT NULL,
email varchar(256) NOT NULL,
diff --git a/dynamic/util.scm b/dynamic/util.scm
new file mode 100644
index 0000000..29a40db
--- /dev/null
+++ b/dynamic/util.scm
@@ -0,0 +1,57 @@
+;;; Copyright © 2019 - 2022 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+;;;
+;;; This program is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; This program is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(define-module (dynamic util)
+ #:use-module (ice-9 match)
+ #:use-module (rnrs bytevectors)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-26)
+ #:use-module (web uri)
+ #:export (base64-length
+ decode-form))
+
+(define (acons-list k v alist)
+ "Add V to K to alist as list"
+ (let ((value (assoc-ref alist k)))
+ (if value
+ (let ((alist (alist-delete k alist)))
+ (acons k (cons v value) alist))
+ (acons k (list v) alist))))
+
+(define (list->alist lst)
+ "Build a alist of list based on a list of key and values.
+
+ Multiple values can be associated with the same key"
+ (let next ((lst lst)
+ (out '()))
+ (if (null? lst)
+ out
+ (next (cdr lst) (acons-list (caar lst) (cdar lst) out)))))
+
+(define (decode-form bv)
+ "Convert BV querystring or form data to an alist"
+ (define string (if (string? bv) bv (utf8->string bv)))
+ (define pairs (map (cut string-split <> #\=)
+ ;; semi-colon and amp can be used as pair separator
+ (append-map (cut string-split <> #\;)
+ (string-split string #\&))))
+ (list->alist (map (match-lambda
+ ((key value)
+ (cons (uri-decode key) (uri-decode value)))) pairs)))
+
+(define (base64-length n)
+ "The length of the base64 string encoding `n' bytes."
+ (inexact->exact (* 4 (ceiling (/ n 3.0)))))
diff --git a/haunt/static/image/hourglass.gif b/haunt/static/image/hourglass.gif
new file mode 100644
index 0000000..e1bf222
--- /dev/null
+++ b/haunt/static/image/hourglass.gif
Binary files differ
diff --git a/haunt/static/js/rsvp.js b/haunt/static/js/rsvp.js
index c7b4cd8..f0a4353 100644
--- a/haunt/static/js/rsvp.js
+++ b/haunt/static/js/rsvp.js
@@ -17,15 +17,89 @@
* <http://www.gnu.org/licenses/>.
*/
+// Common nodes.
+const guestView = document.getElementById("guest-view");
+const addEntry = document.getElementById("add-entry");
+const removeEntry = document.getElementById("remove-entry");
+const submitButton = document.getElementById("submit-form");
+const form = document.getElementById("rsvp-input");
+
+// Parse out the HTML form into something we can send to the backend.
+function submitForm() {
+ let data = new FormData(form);
+ if (data.get("name").trim().length == 0) {
+ alert("You have to provide something for \"Name\".");
+ return;
+ } else if (data.get("rsvp") === null) {
+ alert("You have to choose an option for \"RSVP Status\".");
+ return;
+ }
+
+ let res = { guests: [] };
+ if (urlParams.get("i")) {
+ res["id"] = urlParams.get("i");
+ } else if (urlParams.get("r")) {
+ res["update"] = urlParams.get("r");
+ }
+ for (let pair of data.entries()) {
+ if (pair[0].startsWith("guest-")) {
+ res.guests.push(pair[1]);
+ } else if (pair[0] === "rsvp") {
+ const radioButtons = document.querySelectorAll('input[name="rsvp"]');
+ for (const radioButton of radioButtons) {
+ if (radioButton.checked) {
+ res.rsvp = radioButton.id;
+ break;
+ }
+ }
+ } else {
+ res[pair[0]] = pair[1];
+ }
+ }
+ res.guests = res.guests.join(",");
+
+ let xhr = new XMLHttpRequest();
+ xhr.open("POST", `${location.protocol}//${location.host}/api/rsvp`);
+ xhr.send(JSON.stringify(res));
+ xhr.onload = function(data) {
+ if (xhr.status != 200) {
+ alert("Failed to submit form.");
+ } else {
+ document.getElementById("rsvp").innerHTML = "";
+ let resp = JSON.parse(xhr.response);
+ let elem = document.createElement("p");
+ elem.innerHTML = "Thanks for registering! Please bookmark or save the following link:";
+ document.getElementById("rsvp").appendChild(elem);
+
+ elem = document.createElement("a");
+ elem.href = [location.protocol, '//', location.host, location.pathname].join('') + `?r=${resp.receipt}`;
+ elem.textContent = elem.href;
+ document.getElementById("rsvp").appendChild(elem);
+
+ elem = document.createElement("p");
+ elem.innerHTML = "This will enable you to edit your RSVP later.";
+ document.getElementById("rsvp").appendChild(elem);
+ }
+ }
+ xhr.onerror = function(error) {
+ throw new Error(`Request failed: ${error}`);
+ }
+}
+
+// Immediately send off an XHR to load info from the backend.
const urlParams = new URLSearchParams(window.location.search);
-if (!urlParams.get("i")) {
+if (!urlParams.get("i") && !urlParams.get("r")) {
document.getElementById("loading").remove();
let elem = document.createElement("p");
elem.innerHTML = "No invitation code provided.";
document.getElementById("rsvp").appendChild(elem);
} else {
let xhr = new XMLHttpRequest();
- xhr.open("GET", `http://localhost:8081/api/rsvp/event-info?i=${urlParams.get("i")}`);
+ if (urlParams.get("i")) {
+ xhr.open("GET", `${location.protocol}//${location.host}/api/rsvp/event-info?i=${urlParams.get("i")}`);
+ } else {
+ xhr.open("GET", `${location.protocol}//${location.host}/api/rsvp/event-info?r=${urlParams.get("r")}`);
+ }
xhr.send();
xhr.onload = function(data) {
document.getElementById("loading").remove();
@@ -42,6 +116,13 @@ if (!urlParams.get("i")) {
elem.innerHTML = response.title;
document.getElementById("event-info").appendChild(elem);
+ if (response.image !== "#f") {
+ elem = document.createElement("img");
+ elem.src = `data:image/png;base64,${response.image}`;
+ elem.style = "float: right; max-size: 200px; margin: 16px;";
+ document.getElementById("event-info").appendChild(elem);
+ }
+
elem = document.createElement("p");
elem.innerHTML = `Where: ${response.location}`;
document.getElementById("event-info").appendChild(elem);
@@ -63,7 +144,6 @@ if (!urlParams.get("i")) {
document.getElementById("rsvp-global").appendChild(elem);
for (let rsvp of response.rsvps) {
- console.log(rsvp.name);
let container = document.createElement("tr");
let field = document.createElement("td");
field.textContent = rsvp.name;
@@ -72,7 +152,7 @@ if (!urlParams.get("i")) {
field.textContent = rsvp.email;
container.append(field);
field = document.createElement("td");
- field.textContent = rsvp.guests.replace(',', ', ');
+ field.textContent = rsvp.guests.replaceAll(',', ', ');
container.append(field);
field = document.createElement("td");
field.textContent = rsvp.attending;
@@ -80,8 +160,28 @@ if (!urlParams.get("i")) {
elem.appendChild(container);
}
}
-
- console.log(response);
+
+ // Pre-filled info if this is an edit.
+ if (response.hasOwnProperty("name")) {
+ document.getElementById("name").value = response.name;
+ }
+ if (response.hasOwnProperty("email")) {
+ document.getElementById("email").value = response.email;
+ }
+ if (response.hasOwnProperty("attending")) {
+ document.getElementById(response.attending).checked = true;
+ }
+ if (response.hasOwnProperty("guests") && response.guests.trim() != "") {
+ let i = 0;
+ for (let guest of response.guests.split(",")) {
+ let elem = document.createElement("input");
+ elem.type = "text";
+ elem.name = `guest-${i}`;
+ elem.value = guest;
+ guestView.appendChild(elem);
+ i++;
+ }
+ }
}
}
xhr.onerror = function(error) {
@@ -89,12 +189,9 @@ if (!urlParams.get("i")) {
}
}
-const guestView = document.getElementById("guest-view");
-const addEntry = document.getElementById("add-entry");
-const removeEntry = document.getElementById("remove-entry");
-const submitButton = document.getElementById("submit-form");
-const form = document.getElementById("rsvp-input");
-
+// Attach event listeners to the UI.
+submitButton.addEventListener("click", submitForm);
+submitButton.addEventListener("submit", submitForm);
addEntry.addEventListener("click", () => {
let i = guestView.childNodes.length;
let elem = document.createElement("input");
@@ -102,70 +199,8 @@ addEntry.addEventListener("click", () => {
elem.name = `guest-${i}`;
guestView.appendChild(elem);
});
-
removeEntry.addEventListener("click", () => {
if (guestView.lastChild) {
guestView.lastChild.remove();
}
});
-
-function submitForm() {
- let data = new FormData(form);
- if (data.get("name").trim().length == 0) {
- alert("You have to provide something for \"Name\".");
- return;
- } else if (data.get("rsvp") === null) {
- alert("You have to choose an option for \"RSVP Status\".");
- return;
- }
-
- let res = { id: urlParams.get("i"), guests: [] };
- for (let pair of data.entries()) {
- console.log(pair[0]);
- if (pair[0].startsWith("guest-")) {
- res.guests.push(pair[1]);
- } else if (pair[0] === "rsvp") {
- const radioButtons = document.querySelectorAll('input[name="rsvp"]');
- for (const radioButton of radioButtons) {
- if (radioButton.checked) {
- res.rsvp = radioButton.id;
- break;
- }
- }
- } else {
- res[pair[0]] = pair[1];
- }
- }
- res.guests = res.guests.join(",");
- console.log(res);
-
- let xhr = new XMLHttpRequest();
- xhr.open("POST", "http://localhost:8081/api/rsvp");
- xhr.send(JSON.stringify(res));
- xhr.onload = function(data) {
- if (xhr.status != 200) {
- alert("Failed to submit form.");
- } else {
- document.getElementById("rsvp").innerHTML = "";
- let resp = JSON.parse(xhr.response);
- let elem = document.createElement("p");
- elem.innerHTML = "Thanks for registering! Please bookmark or save the following link:";
- document.getElementById("rsvp").appendChild(elem);
-
- elem = document.createElement("a");
- elem.href = [location.protocol, '//', location.host, location.pathname].join('') + `?r=${resp.receipt}`;
- elem.textContent = elem.href;
- document.getElementById("rsvp").appendChild(elem);
-
- elem = document.createElement("p");
- elem.innerHTML = "This will enable you to edit your RSVP later.";
- document.getElementById("rsvp").appendChild(elem);
- }
- }
- xhr.onerror = function(error) {
- throw new Error(`Request failed: ${error}`);
- }
-}
-
-submitButton.addEventListener("click", submitForm);
-submitButton.addEventListener("submit", submitForm);