HN-like selfhosted forums

Code git clone ssh://git.poy.ooo:6969/~rii/district ZIP
app/comments.go / 3KiB #3f538b84
Raw
package appimport (	"database/sql"	"errors"	"fmt"	"log"	"net/http"	"strconv"	"strings"	"time"	"github.com/google/uuid"	"github.com/jackc/pgx/v5"	"github.com/nxrmqlly/district/app/httpx"	"github.com/nxrmqlly/district/auth"	"github.com/nxrmqlly/district/store")// returns a HTML fragment.func (ro *Router) handleCommentCreate(w http.ResponseWriter, r *http.Request) {	postID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)	if err != nil {		log.Println(err)		http.Error(w, "error parsing post id", http.StatusBadRequest)		return	}	sess, _ := SessionFromContext(r.Context())	if err := r.ParseForm(); err != nil {		httpx.ErrorJSON(w, http.StatusBadRequest, "invalid form")		return	}	var parentID *int64	rawPID := strings.TrimSpace(r.Form.Get("parent_id"))	log.Printf("raw parent id: %q", rawPID)	// parent id is nil = top lvl comment	if rawPID != "" {		pid, err := strconv.ParseInt(rawPID, 10, 64)		if err != nil {			log.Printf("parse parent id: %v", err)			http.Error(w, "error parsing parent_id", http.StatusBadRequest)			return		}		parentID = &pid	} else {		parentID = nil	}	body := strings.TrimSpace(r.FormValue("body"))	if body == "" {		http.Error(w, "body must not be empty", http.StatusBadRequest)		return	}	c, err := ro.queries.CreateComment(r.Context(), store.CreateCommentParams{		PostID:   postID,		ParentID: parentID,		AuthorID: sess.UserID,		Body:     body,	})	if err != nil {		log.Println(err)		if errors.Is(err, pgx.ErrNoRows) || errors.Is(err, sql.ErrNoRows) {			httpx.ErrorJSON(w, http.StatusBadRequest, "invalid parent comment")			return		}		httpx.ErrorJSON(w, http.StatusInternalServerError, "could not create comment")		return	}	if r.Header.Get("HX-Request") == "true" {		// render fragment if htmx wants to do its replacing		ro.RenderFragment(w, r, "comment", &CommentTree{			ID:             c.ID,			PostID:         c.PostID,			ParentID:       c.ParentID,			Body:           c.Body,			AuthorUsername: sess.Username,			AuthorID:       c.AuthorID,			CreatedAt:      c.CreatedAt,			DeletedAt:      c.DeletedAt,			Session: sess,			Children: nil, // a fresh comment will have no children anyways.		})		return	}	// if javascript is off or bot-user, redirect them	http.Redirect(w, r, fmt.Sprintf("/p/%d", postID), http.StatusSeeOther)}type CommentTree struct {	ID             int64	PostID         int64	ParentID       *int64 // nil for root	Body           string	AuthorUsername string	AuthorID       uuid.UUID	CreatedAt      time.Time	DeletedAt      *time.Time	Session *auth.AuthSession // convinience for ui	Children []*CommentTree}func buildCommentTree(comments []store.GetCommentsByPostRow, sess *auth.AuthSession) []*CommentTree {	byID := make(map[int64]*CommentTree, len(comments))	for _, c := range comments {		byID[c.ID] = &CommentTree{			ID:             c.ID,			PostID:         c.PostID,			ParentID:       c.ParentID,			Body:           c.Body,			AuthorUsername: c.AuthorUsername,			AuthorID:       c.AuthorID,			CreatedAt:      c.CreatedAt,			DeletedAt:      c.DeletedAt,			Session: sess,		}	}	var roots []*CommentTree	for _, c := range comments {		tree := byID[c.ID]		// root comment		if c.ParentID == nil {			roots = append(roots, tree)			continue		}		parent, ok := byID[*c.ParentID]		if !ok {			// parent hard deleted or missing = put into root			roots = append(roots, tree)			continue		}		parent.Children = append(parent.Children, tree)	}	return roots}