diff options
author | dan <[email protected]> | 2023-05-14 21:53:17 -0400 |
---|---|---|
committer | dan <[email protected]> | 2023-05-14 21:53:17 -0400 |
commit | 55d4b75ee6af971d79fcdabdec7969f224f09dca (patch) | |
tree | ca409cab965a6e0b023d19b6462e54b3b00d81cd | |
download | simple-comments-widget-55d4b75ee6af971d79fcdabdec7969f224f09dca.tar.gz simple-comments-widget-55d4b75ee6af971d79fcdabdec7969f224f09dca.tar.bz2 simple-comments-widget-55d4b75ee6af971d79fcdabdec7969f224f09dca.zip |
init
-rw-r--r-- | client.js | 84 | ||||
-rw-r--r-- | main.scm | 156 | ||||
-rw-r--r-- | makefile | 12 |
3 files changed, 252 insertions, 0 deletions
diff --git a/client.js b/client.js new file mode 100644 index 0000000..c2d041c --- /dev/null +++ b/client.js @@ -0,0 +1,84 @@ +/* Config */ +const baseUrl = 'https://comments.dotemgo.com/comments'; +const pageId = '{{site-url}}{{uri}}'; +const commentsUrl = `${baseUrl}?page_url=${pageId}`; + + +/* State */ +let comments = []; + +/* GUI components */ + +const deleteButton = x => + `<button style='float:right;' onclick='handleDelete("${x.id}", "${x.deletion_key}")'>`+ + `delete</button>`; + +const commentFmt = x => + `<div class='comment' id='comment${x.id}'>${x.content}<br/>${x.created_at}`+ + `${x.deletion_key ? deleteButton(x) :''}</div>`; + +function commentsRender() { + commentsthread.innerHTML = + `<div id='commentsthreadinner'> + ${comments.map(commentFmt).join('')} + </div>`; +} + + +/* API calls */ + +// GET comments +function fetchComments() { + fetch(commentsUrl) + .then(r => r.ok && r.json()) + .then(xs => { + comments = xs; + if (comments.length) { + commentsRender(); + } + }) +} + +// POST new comment +function createComment(comment) { + const xs = new BigUint64Array(1); + crypto.getRandomValues(xs); + comment.deletion_key = xs[0].toString(); + fetch(baseUrl, { + method: 'POST', + body: JSON.stringify(comment), + headers: {'content-type': 'application/json'} + }).then(r => r.ok && r.text()) + .then(id => { + const newComment = {id, created_at: 'just now', ...comment} + comments = [newComment, ...comments]; + commentsRender(); + }) +} + +/* Action Handlers */ + +function handleCommentFormSubmit() { + if (commentcontent.value !== "") { + createComment({page_url: pageId, content: commentcontent.value}); + commentcontent.value = ''; + } +} + +// DELETE comment +function handleDelete(commentId, deletionKey) { + console.log('deleting:',{commentId,deletionKey}); + fetch( + `${baseUrl}?comment=${commentId}&deletion_key=${deletionKey}`, + {method: 'DELETE'} + ).then(r => { + if (r.ok) { + comments = comments.filter(x => x.id !== commentId); + commentsRender(); + } + }); +} + + +/* init */ +fetchComments(); diff --git a/main.scm b/main.scm new file mode 100644 index 0000000..9d65032 --- /dev/null +++ b/main.scm @@ -0,0 +1,156 @@ +(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_url 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-url 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_url, content, deletion_key) + VALUES (?, ?, ?, ?)" + id page-url content-or-empty deletion-key) + id)) + +; turns db row into list of key-value pairs +(define (comment-row->alist id page-url content created-at) + `((id . ,id) + (page_url . ,page-url) + (created_at . ,created-at) + (content . ,content))) + +; selects all comments with the given page-url +(define (get-comments page-url) + (map-row comment-row->alist db + "SELECT c.id, c.page_url, c.content, c.created_at + FROM comments c + WHERE c.page_url = ? + ORDER BY c.created_at DESC" + page-url + )) + +; 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_url query string parameter +(define (get-page-url) + (get-req-var 'page_url)) + +; 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-url (json-value-ref json-body 'page_url)] + [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-url content deletion-key)) + headers: base-headers))))) + + (GET (/ "comments") ,(lambda () + (let* ([page-url (get-page-url)] + [comments (list->vector (get-comments page-url))]) + (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) diff --git a/makefile b/makefile new file mode 100644 index 0000000..a42daab --- /dev/null +++ b/makefile @@ -0,0 +1,12 @@ +build: ./main.scm + mkdir -p ./build + chicken-csc -static ./main.scm \ + -L -lsqlite3 + mv ./main ./build/main +run: build + ./build/main + +clean: + rm -r build/ + + |