gophertype/pkg/controllers/comments.go

328 lines
9.1 KiB
Go

package controllers
import (
"bytes"
"fmt"
"html/template"
"net/http"
"strconv"
"strings"
"git.kirsle.net/apps/gophertype/pkg/authentication"
"git.kirsle.net/apps/gophertype/pkg/glue"
"git.kirsle.net/apps/gophertype/pkg/mail"
"git.kirsle.net/apps/gophertype/pkg/models"
"git.kirsle.net/apps/gophertype/pkg/responses"
"git.kirsle.net/apps/gophertype/pkg/session"
"github.com/albrow/forms"
uuid "github.com/satori/go.uuid"
)
func init() {
glue.Register(glue.Endpoint{
Path: "/comments",
Methods: []string{"GET", "POST"},
Handler: PostComment,
})
glue.Register(glue.Endpoint{
Path: "/comments/subscription",
Methods: []string{"GET", "POST"},
Handler: ManageSubscription,
})
glue.Register(glue.Endpoint{
Path: "/comments/quick-delete",
Methods: []string{"GET"},
Handler: CommentQuickDelete,
})
}
// RenderComments returns the partial comments HTML to embed on a page.
func RenderComments(w http.ResponseWriter, r *http.Request, subject string, ids ...string) template.HTML {
thread := strings.Join(ids, "-")
return renderComments(w, r, subject, thread, false)
}
// RenderCommentsRO returns a read-only comment view.
func RenderCommentsRO(w http.ResponseWriter, r *http.Request, ids ...string) template.HTML {
thread := strings.Join(ids, "-")
return renderComments(w, r, "", thread, true)
}
// RenderComment renders the HTML partial for a single comment in the thread.
func RenderComment(w http.ResponseWriter, r *http.Request, com models.Comment, originURL string, editable bool) template.HTML {
var (
html = bytes.NewBuffer([]byte{})
v = responses.NewTemplateVars(html, r)
editToken = getEditToken(w, r)
)
v.V["Comment"] = com
v.V["Editable"] = editable && (com.EditToken == editToken || authentication.LoggedIn(r))
v.V["OriginURL"] = originURL
responses.PartialTemplate(html, r, "_builtin/comments/entry.partial.gohtml", v)
return template.HTML(html.String())
}
// RenderCommentForm renders the comment entry form HTML onto a page.
func RenderCommentForm(r *http.Request, com models.Comment, subject, threadID, originURL string) template.HTML {
var (
html = bytes.NewBuffer([]byte{})
v = responses.NewTemplateVars(html, r)
)
v.V["Comment"] = com
v.V["Subject"] = subject
v.V["ThreadID"] = threadID
v.V["OriginURL"] = originURL
responses.PartialTemplate(html, r, "_builtin/comments/form.partial.gohtml", v)
return template.HTML(html.String())
}
// renderComments is the internal logic for both RenderComments and RenderCommentsRO.
func renderComments(w http.ResponseWriter, r *http.Request, subject string, thread string, readonly bool) template.HTML {
var (
html = bytes.NewBuffer([]byte{})
v = responses.NewTemplateVars(w, r)
ses = session.Get(r)
)
comments, err := models.Comments.GetThread(thread)
if err != nil {
return template.HTML(fmt.Sprintf("[comment error: %s]", err))
}
// Load their cached name and email from any previous comments the user posted.
name, _ := ses.Values["c.name"].(string)
email, _ := ses.Values["c.email"].(string)
editToken, _ := ses.Values["c.token"].(string)
// Logged in? Populate defaults from the user info.
if currentUser, err := authentication.CurrentUser(r); err == nil {
name = currentUser.Name
email = currentUser.Email
}
// v.V["posts"] = posts.Posts
v.V["Readonly"] = readonly
v.V["Subject"] = subject
v.V["ThreadID"] = thread
v.V["Comments"] = comments
v.V["OriginURL"] = r.URL.Path
v.V["NewComment"] = models.Comment{
Name: name,
Email: email,
EditToken: editToken,
}
responses.PartialTemplate(html, r, "_builtin/comments/comments.partial.gohtml", v)
return template.HTML(html.String())
}
// ReadComments handles the GET /comments for viewing recently added comments
// side wide.
func ReadComments(w http.ResponseWriter, r *http.Request) {
var (
v = responses.NewTemplateVars(w, r)
query = r.URL.Query()
)
// Query parameters.
page, _ := strconv.Atoi(query.Get("page"))
perPage, _ := strconv.Atoi(query.Get("per_page"))
// Get the comments.
comments, err := models.Comments.GetRecent(page, perPage)
if err != nil {
responses.Error(w, r, http.StatusInternalServerError, err.Error())
return
}
v.V["PagedComments"] = comments
responses.RenderTemplate(w, r, "_builtin/comments/recent.gohtml", v)
}
// PostComment handles all of the top-level blog index routes
// for Post, Preview, Delete comments.
func PostComment(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
ReadComments(w, r)
return
}
var (
v = responses.NewTemplateVars(w, r)
editToken = getEditToken(w, r)
comment = models.Comments.New()
ses = session.Get(r)
editing bool // true if editing an existing comment
)
// Check if the user is logged in.
var loggedIn bool
currentUser, err := authentication.CurrentUser(r)
loggedIn = err == nil
// Get form parameters.
form, _ := forms.Parse(r)
v.V["Subject"] = form.Get("subject")
v.V["ThreadID"] = form.Get("thread")
v.V["OriginURL"] = form.Get("origin")
// Are they editing an existing post ID?
if id := form.GetInt("id"); id > 0 {
// Load the comment from DB.
com, err := models.Comments.Load(id)
if err != nil {
responses.NotFound(w, r)
return
}
// Verify the user's EditToken matches this comment.
if editToken != com.EditToken && !loggedIn {
responses.Forbidden(w, r, "You do not have permission to edit that comment.")
return
}
editing = true
comment = com
}
comment.EditToken = editToken
for {
// Validate form parameters.
val := form.Validator()
val.Require("body")
if !form.GetBool("editing") {
comment.ParseForm(form)
}
if val.HasErrors() {
v.ValidationError = val.ErrorMap()
break
}
// Cache their name and email in their session, for future requests.
ses.Values["c.email"] = form.Get("email")
ses.Values["c.name"] = form.Get("name")
ses.Save(r, w)
switch form.Get("submit") {
case "delete":
v.V["deleting"] = true
case "confirm-delete":
// Delete the comment.
err := comment.Delete()
if err != nil {
session.Flash(w, r, "Error deleting the comment: %s", err)
} else {
session.Flash(w, r, "Comment has been deleted!")
}
responses.Redirect(w, r, form.Get("origin"))
return
case "preview":
v.V["preview"] = comment.HTML()
case "post":
// If we're logged in, tag our user ID with this post.
if loggedIn && !editing {
comment.UserID = currentUser.ID
}
// Store the OriginURL for this comment.
comment.OriginURL = form.Get("origin")
// Post their comment.
err := comment.Save()
if err != nil {
session.Flash(w, r, "Error posting comment: %s", err)
responses.Redirect(w, r, form.Get("origin"))
return
}
// Notify site admins and subscribers by email.
go mail.NotifyComment(v.V["Subject"].(string), v.V["OriginURL"].(string), comment)
session.Flash(w, r, "Your comment has been added!")
responses.Redirect(w, r, form.Get("origin"))
return
}
break
}
v.V["NewComment"] = comment
responses.RenderTemplate(w, r, "_builtin/comments/preview.gohtml", v)
}
// ManageSubscription helps users unsubscribe from comment threads.
func ManageSubscription(w http.ResponseWriter, r *http.Request) {
var (
v = responses.NewTemplateVars(w, r)
query = r.URL.Query()
qThread = query.Get("t")
qEmail = strings.ToLower(strings.TrimSpace(query.Get("e")))
qAll = query.Get("all")
)
// Are we unsubscribing from all?
if qEmail != "" && qAll != "" {
err := models.Comments.UnsubscribeFromAll(qEmail)
if err != nil {
session.Flash(w, r, "Error unsubscribing you: %s", err)
} else {
session.Flash(w, r, "Success: '%s' has been unsubscribed from ALL comment threads.", qEmail)
}
responses.Redirect(w, r, "/comments/subscription")
return
}
// Is there a thread and email in the query string?
if qThread != "" && qEmail != "" {
err := models.Comments.UnsubscribeThread(qThread, qEmail)
if err != nil {
session.Flash(w, r, "Error unsubscribing you: %s", err)
} else {
session.Flash(w, r, "Success: '%s' has been unsubscribed from this comment thread.", qEmail)
}
responses.Redirect(w, r, "/comments/subscription")
return
}
responses.RenderTemplate(w, r, "_builtin/comments/subscription.gohtml", v)
}
// CommentQuickDelete handles quick-delete links to remove spam comments.
func CommentQuickDelete(w http.ResponseWriter, r *http.Request) {
// Query parameters.
var (
deleteToken = r.URL.Query().Get("d")
nextURL = r.URL.Query().Get("next")
)
// Look up the comment by thread and quick-delete token.
comment, err := models.Comments.LoadByDeleteToken(deleteToken)
if err != nil {
responses.Forbidden(w, r, "Comment by Delete Token not found.")
return
}
comment.Delete()
session.Flash(w, r, "Comment has been quick-deleted!")
if nextURL == "" {
nextURL = "/"
}
responses.Redirect(w, r, nextURL)
}
// getEditToken gets the edit token from the user's session.
func getEditToken(w http.ResponseWriter, r *http.Request) string {
ses := session.Get(r)
if token, ok := ses.Values["c.token"].(string); ok && len(token) > 0 {
return token
}
token := uuid.NewV4().String()
ses.Values["c.token"] = token
ses.Save(r, w)
return token
}