summaryrefslogtreecommitdiff
path: root/jakob/dynamic/capabilities/comments.scm
diff options
context:
space:
mode:
Diffstat (limited to 'jakob/dynamic/capabilities/comments.scm')
-rw-r--r--jakob/dynamic/capabilities/comments.scm210
1 files changed, 210 insertions, 0 deletions
diff --git a/jakob/dynamic/capabilities/comments.scm b/jakob/dynamic/capabilities/comments.scm
new file mode 100644
index 0000000..ee2a52d
--- /dev/null
+++ b/jakob/dynamic/capabilities/comments.scm
@@ -0,0 +1,210 @@
+;;; Copyright © 2019 - 2023 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 (jakob dynamic capabilities comments)
+ #:use-module (ice-9 match)
+ #:use-module (jakob dynamic capabilities common)
+ #:use-module (jakob dynamic captcha)
+ #:use-module (jakob dynamic config)
+ #:use-module (jakob dynamic errors)
+ #:use-module (jakob dynamic util)
+ #:use-module (json)
+ #:use-module (squee)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-19)
+ #:use-module (srfi srfi-26)
+ #:use-module (web request)
+ #:use-module (web response)
+ #:use-module (web uri)
+ #:export (get-comments
+ get-comments-by-slug
+
+ put-comment
+ put-reaction))
+
+(define conn (connect-to-postgres-paramstring (paramstring-for-dbname "jakob_comments")))
+
+(define (get-comments-by-slug slug)
+ "Internal function for querying the approved comments on a post
+
+This interface exists for dynamically generating the comment view from Haunt."
+ (define (make-internal-comment~ . args)
+ (let* ((args-needing-processing (take-right args 4))
+ (approved (list-ref args-needing-processing 0))
+ (approved (string->date approved "~Y~m~d ~H~M~S.~N"))
+ (reactions (list-ref args-needing-processing 1))
+ (reactions (if reactions
+ (with-input-from-string reactions read)
+ '()))
+ (originating-network (list-ref args-needing-processing 2))
+ (replies (list-ref args-needing-processing 3)))
+ (apply make-internal-comment
+ `(,@(drop-right args 4)
+ ,approved
+ ,reactions
+ ,replies
+ ,originating-network))))
+ (define (order-comments comments)
+ (define seen (make-hash-table))
+ (define (id comment) (first comment))
+ (define (content comment) (drop-right comment 1))
+ (define (parent comment) (last comment))
+ (define (has-children? id remaining)
+ (cond ((null? remaining) #f)
+ ((equal? id (parent (car remaining))) #t)
+ (else (has-children? id (cdr remaining)))))
+ (define (pass cur initial-comments remaining)
+ (cond ((null? initial-comments) (sort-comments (hash-ref seen 'terminal)))
+ ((null? cur) (pass (reverse remaining) (reverse remaining) (list)))
+ ((has-children? (id (car cur)) initial-comments)
+ (pass (cdr cur) initial-comments (cons (car cur) remaining)))
+ (else
+ (let* ((children (or (hash-ref seen (id (car cur))) '()))
+ (children (sort-comments children))
+ (parsed (apply make-internal-comment~ (append (content (car cur)) (list children)))))
+ ;; Remove this comment from `seen'.
+ (hash-set! seen (id (car cur)) #f)
+ (if (parent (car cur))
+ (hash-append! seen (parent (car cur)) parsed)
+ (hash-append! seen 'terminal parsed))
+ (pass (cdr cur) initial-comments remaining)))))
+ (pass comments comments '()))
+ (let* ((query "SELECT id, name, subject, email, comment, url, approved, reactions, originating_network, reply_to
+ FROM comments WHERE slug = $1 and approved IS NOT NULL")
+ (result (exec-query conn query (list slug))))
+ (if (positive? (length result))
+ (order-comments result)
+ '())))
+
+(define (get-comments request body)
+ "API endpoint handler for querying for the comments on a particular post
+
+This is a wrapper around `get-comments-by-slug'."
+ (define (normalize-record record)
+ (json-string->scm (internal-comment->json record)))
+ (let* ((query-string (uri-query (request-uri request)))
+ (params (if query-string
+ (decode-form query-string)
+ '()))
+ (slug (assoc-ref params "p")))
+ (unless slug (panic "missing `slug' query parameter"))
+ (values '((content-type . (application/json)))
+ (scm->json-string
+ (list->vector
+ (map normalize-record (get-comments-by-slug (car slug))))))))
+
+
+
+(define (put-comment request body)
+ "API endpoint handler for submitting a comment"
+ (define (request-originating-network request)
+ (cond ((from-tor? request) "tor")
+ ((from-i2p? request) "i2p")
+ (else "clearnet")))
+ (define (valid-comment? form-data)
+ (and (assoc "slug" form-data)
+ (assoc "name" form-data)
+ (assoc "comment" form-data)
+ (or (assoc "captcha" form-data)
+ (and (assoc "captcha-alt" form-data)
+ (assoc "captcha-alt-id" form-data)))
+ (assoc "captcha-id" form-data)
+ (if (and (string? (assoc-value form-data "captcha-alt"))
+ (positive? (string-length (assoc-value form-data "captcha-alt"))))
+ (validate-proof-of-work!
+ (assoc-value form-data "captcha-alt")
+ (string->number (assoc-value form-data "captcha-alt-id")))
+ (validate-captcha!
+ (assoc-value form-data "captcha")
+ (string->number (assoc-value form-data "captcha-id"))))))
+ (define (insert-comment form-data)
+ (exec-query conn
+ "INSERT INTO comments (submitted, slug, name, subject, email,
+ url, comment, reply_to, originating_network)
+ VALUES (now(), $1, $2, $3, $4, $5, $6, $7, $8);"
+ (list (assoc-value form-data "slug")
+ (assoc-value form-data "name")
+ (assoc-value form-data "subject")
+ (assoc-value form-data "email")
+ (assoc-value form-data "url")
+ (assoc-value form-data "comment")
+ (if (and (assoc-value form-data "reply-to")
+ (positive? (string-length (assoc-value form-data "reply-to"))))
+ (assoc-value form-data "reply-to")
+ #f)
+ (request-originating-network request)))
+ (values (build-response
+ #:code 307
+ #:headers '((Location . "https://jakob.space")))
+ (scm->json-string `((success . #t)))))
+ (let ((form-data (decode-form body)))
+ (unless (assoc "slug" form-data) (panic "missing param `slug'"))
+ (unless (assoc "name" form-data) (panic "missing param `name'"))
+ (unless (assoc "comment" form-data) (panic "missing param `comment'"))
+ (unless (assoc "captcha-id" form-data) (panic "missing param `captcha-id'"))
+ (unless (or (assoc "captcha" form-data)
+ (and (assoc "captcha-alt" form-data)
+ (assoc "captcha-alt-id" form-data)))
+ (panic "missing param `captcha' (or `captcha-alt' and `captcha-alt-id')"))
+ (if (and (string? (assoc-value form-data "captcha-alt"))
+ (positive? (string-length (assoc-value form-data "captcha-alt"))))
+ ;; Alternate captcha fields specified; take the code path that validates
+ ;; a proof-of-work.
+ (unless (validate-proof-of-work!
+ (assoc-value form-data "captcha-alt")
+ (string->number (assoc-value form-data "captcha-alt-id")))
+ (panic "proof-of-work not acceptable"))
+ ;; Alternate captcha fields not specified, so take the normal code path
+ ;; where we validate a captcha response.
+ (unless (validate-captcha!
+ (assoc-value form-data "captcha")
+ (string->number (assoc-value form-data "captcha-id")))
+ (panic "captcha incorrect")))
+ (insert-comment form-data)))
+
+
+
+(define (add-reaction reactions reaction)
+ (with-output-to-string
+ (lambda ()
+ (let ((parsed (call-with-input-string reactions read)))
+ (write (acons-normalize reaction
+ (if (assoc reaction parsed) (+ 1 (assoc-value parsed reaction)) 1)
+ parsed))))))
+
+(define (put-reaction request body)
+ (define (set-reactions id reactions)
+ (exec-query conn "UPDATE comments SET reactions = $1 WHERE id = $2"
+ (list reactions id)))
+ (define (comment-reactions id)
+ (let* ((query "SELECT reactions FROM comments WHERE id = $1")
+ (result (exec-query conn query (list id))))
+ ;; It could be NULL, in which case we want the empty list instead.
+ (if (positive? (length result)) (or (caar result) "()") #f)))
+ (define (valid-reaction? form-data)
+ (and (assoc "id" form-data)
+ (assoc "reaction" form-data)))
+ (let ((form-data (decode-form body)))
+ (unless (assoc "id" form-data) (panic "missing param `id'"))
+ (unless (assoc "reaction" form-data) (panic "missing param `reaction'"))
+ (let* ((id (assoc-value form-data "id"))
+ (reaction (assoc-value form-data "reaction"))
+ (reactions (comment-reactions id)))
+ (unless id (panic "no such comment"))
+ (unless (emoji? reaction) (panic "invalid reaction"))
+ (set-reactions id (add-reaction reactions reaction))
+ (values '((content-type . (application/json)))
+ (scm->json-string `((success . #t)))))))