aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordan <[email protected]>2023-05-14 21:53:17 -0400
committerdan <[email protected]>2023-05-14 21:53:17 -0400
commit55d4b75ee6af971d79fcdabdec7969f224f09dca (patch)
treeca409cab965a6e0b023d19b6462e54b3b00d81cd
downloadsimple-comments-widget-55d4b75ee6af971d79fcdabdec7969f224f09dca.tar.gz
simple-comments-widget-55d4b75ee6af971d79fcdabdec7969f224f09dca.tar.bz2
simple-comments-widget-55d4b75ee6af971d79fcdabdec7969f224f09dca.zip
init
-rw-r--r--client.js84
-rw-r--r--main.scm156
-rw-r--r--makefile12
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/
+
+