Add support for subscribing to comment threads
This commit is contained in:
parent
725437d06f
commit
527e995c1c
|
@ -38,25 +38,40 @@ func (b *Blog) SettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
redisPort, _ := strconv.Atoi(r.FormValue("redis-port"))
|
redisPort, _ := strconv.Atoi(r.FormValue("redis-port"))
|
||||||
redisDB, _ := strconv.Atoi(r.FormValue("redis-db"))
|
redisDB, _ := strconv.Atoi(r.FormValue("redis-db"))
|
||||||
|
mailPort, _ := strconv.Atoi(r.FormValue("mail-port"))
|
||||||
form := &forms.Settings{
|
form := &forms.Settings{
|
||||||
Title: r.FormValue("title"),
|
Title: r.FormValue("title"),
|
||||||
AdminEmail: r.FormValue("admin-email"),
|
AdminEmail: r.FormValue("admin-email"),
|
||||||
|
URL: r.FormValue("url"),
|
||||||
RedisEnabled: r.FormValue("redis-enabled") == "true",
|
RedisEnabled: r.FormValue("redis-enabled") == "true",
|
||||||
RedisHost: r.FormValue("redis-host"),
|
RedisHost: r.FormValue("redis-host"),
|
||||||
RedisPort: redisPort,
|
RedisPort: redisPort,
|
||||||
RedisDB: redisDB,
|
RedisDB: redisDB,
|
||||||
RedisPrefix: r.FormValue("redis-prefix"),
|
RedisPrefix: r.FormValue("redis-prefix"),
|
||||||
|
MailEnabled: r.FormValue("mail-enabled") == "true",
|
||||||
|
MailSender: r.FormValue("mail-sender"),
|
||||||
|
MailHost: r.FormValue("mail-host"),
|
||||||
|
MailPort: mailPort,
|
||||||
|
MailUsername: r.FormValue("mail-username"),
|
||||||
|
MailPassword: r.FormValue("mail-password"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy form values into the settings struct for display, in case of
|
// Copy form values into the settings struct for display, in case of
|
||||||
// any validation errors.
|
// any validation errors.
|
||||||
settings.Site.Title = form.Title
|
settings.Site.Title = form.Title
|
||||||
settings.Site.AdminEmail = form.AdminEmail
|
settings.Site.AdminEmail = form.AdminEmail
|
||||||
|
settings.Site.URL = form.URL
|
||||||
settings.Redis.Enabled = form.RedisEnabled
|
settings.Redis.Enabled = form.RedisEnabled
|
||||||
settings.Redis.Host = form.RedisHost
|
settings.Redis.Host = form.RedisHost
|
||||||
settings.Redis.Port = form.RedisPort
|
settings.Redis.Port = form.RedisPort
|
||||||
settings.Redis.DB = form.RedisDB
|
settings.Redis.DB = form.RedisDB
|
||||||
settings.Redis.Prefix = form.RedisPrefix
|
settings.Redis.Prefix = form.RedisPrefix
|
||||||
|
settings.Mail.Enabled = form.MailEnabled
|
||||||
|
settings.Mail.Sender = form.MailSender
|
||||||
|
settings.Mail.Host = form.MailHost
|
||||||
|
settings.Mail.Port = form.MailPort
|
||||||
|
settings.Mail.Username = form.MailUsername
|
||||||
|
settings.Mail.Password = form.MailPassword
|
||||||
err := form.Validate()
|
err := form.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.Error = err
|
v.Error = err
|
||||||
|
|
39
core/blog.go
39
core/blog.go
|
@ -3,6 +3,7 @@ package core
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -11,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/kirsle/blog/core/models/comments"
|
||||||
"github.com/kirsle/blog/core/models/posts"
|
"github.com/kirsle/blog/core/models/posts"
|
||||||
"github.com/kirsle/blog/core/models/users"
|
"github.com/kirsle/blog/core/models/users"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
|
@ -18,11 +20,12 @@ import (
|
||||||
|
|
||||||
// PostMeta associates a Post with injected metadata.
|
// PostMeta associates a Post with injected metadata.
|
||||||
type PostMeta struct {
|
type PostMeta struct {
|
||||||
Post *posts.Post
|
Post *posts.Post
|
||||||
Rendered template.HTML
|
Rendered template.HTML
|
||||||
Author *users.User
|
Author *users.User
|
||||||
IndexView bool
|
NumComments int
|
||||||
Snipped bool
|
IndexView bool
|
||||||
|
Snipped bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Archive holds data for a piece of the blog archive.
|
// Archive holds data for a piece of the blog archive.
|
||||||
|
@ -201,10 +204,17 @@ func (b *Blog) PartialIndex(w http.ResponseWriter, r *http.Request,
|
||||||
author = users.DeletedUser()
|
author = users.DeletedUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count the comments on this post.
|
||||||
|
var numComments int
|
||||||
|
if thread, err := comments.Load(fmt.Sprintf("post-%d", post.ID)); err == nil {
|
||||||
|
numComments = len(thread.Comments)
|
||||||
|
}
|
||||||
|
|
||||||
view = append(view, PostMeta{
|
view = append(view, PostMeta{
|
||||||
Post: post,
|
Post: post,
|
||||||
Rendered: rendered,
|
Rendered: rendered,
|
||||||
Author: author,
|
Author: author,
|
||||||
|
NumComments: numComments,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,7 +296,7 @@ func (b *Blog) viewPost(w http.ResponseWriter, r *http.Request, fragment string)
|
||||||
// RenderPost renders a blog post as a partial template and returns the HTML.
|
// RenderPost renders a blog post as a partial template and returns the HTML.
|
||||||
// If indexView is true, the blog headers will be hyperlinked to the dedicated
|
// If indexView is true, the blog headers will be hyperlinked to the dedicated
|
||||||
// entry view page.
|
// entry view page.
|
||||||
func (b *Blog) RenderPost(p *posts.Post, indexView bool) template.HTML {
|
func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) template.HTML {
|
||||||
// Look up the author's information.
|
// Look up the author's information.
|
||||||
author, err := users.LoadReadonly(p.AuthorID)
|
author, err := users.LoadReadonly(p.AuthorID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -327,11 +337,12 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool) template.HTML {
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := PostMeta{
|
meta := PostMeta{
|
||||||
Post: p,
|
Post: p,
|
||||||
Rendered: rendered,
|
Rendered: rendered,
|
||||||
Author: author,
|
Author: author,
|
||||||
IndexView: indexView,
|
IndexView: indexView,
|
||||||
Snipped: snipped,
|
Snipped: snipped,
|
||||||
|
NumComments: numComments,
|
||||||
}
|
}
|
||||||
output := bytes.Buffer{}
|
output := bytes.Buffer{}
|
||||||
err = t.Execute(&output, meta)
|
err = t.Execute(&output, meta)
|
||||||
|
|
156
core/comments.go
156
core/comments.go
|
@ -2,9 +2,10 @@ package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -17,24 +18,19 @@ import (
|
||||||
// CommentRoutes attaches the comment routes to the app.
|
// CommentRoutes attaches the comment routes to the app.
|
||||||
func (b *Blog) CommentRoutes(r *mux.Router) {
|
func (b *Blog) CommentRoutes(r *mux.Router) {
|
||||||
r.HandleFunc("/comments", b.CommentHandler)
|
r.HandleFunc("/comments", b.CommentHandler)
|
||||||
r.HandleFunc("/comments/edit", b.EditCommentHandler)
|
r.HandleFunc("/comments/subscription", b.SubscriptionHandler)
|
||||||
r.HandleFunc("/comments/delete", b.DeleteCommentHandler)
|
r.HandleFunc("/comments/quick-delete", b.QuickDeleteHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommentMeta is the template variables for comment threads.
|
// CommentMeta is the template variables for comment threads.
|
||||||
type CommentMeta struct {
|
type CommentMeta struct {
|
||||||
IsAuthenticated bool
|
NewComment comments.Comment
|
||||||
ID string
|
ID string
|
||||||
OriginURL string // URL where original comment thread appeared
|
OriginURL string // URL where original comment thread appeared
|
||||||
Subject string // email subject
|
Subject string // email subject
|
||||||
Thread *comments.Thread
|
Thread *comments.Thread
|
||||||
Authors map[int]*users.User
|
Authors map[int]*users.User
|
||||||
CSRF string
|
CSRF string
|
||||||
|
|
||||||
// Cached name and email of the user.
|
|
||||||
Name string
|
|
||||||
Email string
|
|
||||||
EditToken string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderComments renders a comment form partial and returns the HTML.
|
// RenderComments renders a comment form partial and returns the HTML.
|
||||||
|
@ -73,12 +69,9 @@ func (b *Blog) RenderComments(session *sessions.Session, csrfToken, url, subject
|
||||||
|
|
||||||
// Look up the author username.
|
// Look up the author username.
|
||||||
if c.UserID > 0 {
|
if c.UserID > 0 {
|
||||||
log.Warn("Has USERID %d", c.UserID)
|
|
||||||
if _, ok := userMap[c.UserID]; !ok {
|
if _, ok := userMap[c.UserID]; !ok {
|
||||||
log.Warn("not in map")
|
|
||||||
if user, err := users.Load(c.UserID); err == nil {
|
if user, err := users.Load(c.UserID); err == nil {
|
||||||
userMap[c.UserID] = user
|
userMap[c.UserID] = user
|
||||||
log.Warn("is now!")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,8 +87,6 @@ func (b *Blog) RenderComments(session *sessions.Session, csrfToken, url, subject
|
||||||
if isAdmin || (len(c.EditToken) > 0 && c.EditToken == editToken) {
|
if isAdmin || (len(c.EditToken) > 0 && c.EditToken == editToken) {
|
||||||
c.Editable = true
|
c.Editable = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%v\n", c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the template snippet.
|
// Get the template snippet.
|
||||||
|
@ -120,22 +111,22 @@ func (b *Blog) RenderComments(session *sessions.Session, csrfToken, url, subject
|
||||||
}
|
}
|
||||||
|
|
||||||
v := CommentMeta{
|
v := CommentMeta{
|
||||||
ID: thread.ID,
|
ID: thread.ID,
|
||||||
OriginURL: url,
|
OriginURL: url,
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
CSRF: csrfToken,
|
CSRF: csrfToken,
|
||||||
Thread: &thread,
|
Thread: &thread,
|
||||||
IsAuthenticated: isAuthenticated,
|
NewComment: comments.Comment{
|
||||||
Name: name,
|
Name: name,
|
||||||
Email: email,
|
Email: email,
|
||||||
EditToken: editToken,
|
IsAuthenticated: isAuthenticated,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
output := bytes.Buffer{}
|
output := bytes.Buffer{}
|
||||||
err = t.Execute(&output, v)
|
err = t.Execute(&output, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
return template.HTML(err.Error())
|
||||||
return template.HTML("[error executing template in comments/comments.partial]")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return template.HTML(output.String())
|
return template.HTML(output.String())
|
||||||
|
@ -228,8 +219,24 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
c.UserID = currentUser.ID
|
c.UserID = currentUser.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append their comment.
|
||||||
t.Post(c)
|
t.Post(c)
|
||||||
|
b.NotifyComment(c)
|
||||||
|
|
||||||
|
// Are they subscribing to future comments?
|
||||||
|
if c.Subscribe && len(c.Email) > 0 {
|
||||||
|
if _, err := mail.ParseAddress(c.Email); err == nil {
|
||||||
|
m := comments.LoadMailingList()
|
||||||
|
m.Subscribe(t.ID, c.Email)
|
||||||
|
b.FlashAndRedirect(w, r, c.OriginURL,
|
||||||
|
"Comment posted, and you've been subscribed to "+
|
||||||
|
"future comments on this page.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
b.FlashAndRedirect(w, r, c.OriginURL, "Comment posted!")
|
b.FlashAndRedirect(w, r, c.OriginURL, "Comment posted!")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,49 +248,60 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
b.RenderTemplate(w, r, "comments/index.gohtml", v)
|
b.RenderTemplate(w, r, "comments/index.gohtml", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditCommentHandler for editing comments.
|
// SubscriptionHandler to opt out of subscriptions.
|
||||||
func (b *Blog) EditCommentHandler(w http.ResponseWriter, r *http.Request) {
|
func (b *Blog) SubscriptionHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var (
|
|
||||||
threadID = r.URL.Query().Get("t")
|
|
||||||
deleteToken = r.URL.Query().Get("d")
|
|
||||||
originURL = r.URL.Query().Get("o")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Our edit token.
|
|
||||||
editToken := b.GetEditToken(w, r)
|
|
||||||
|
|
||||||
// Search for the comment.
|
|
||||||
thread, err := comments.Load(threadID)
|
|
||||||
if err != nil {
|
|
||||||
b.FlashAndRedirect(w, r, "/", "That comment thread was not found.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
comment, err := thread.FindByDeleteToken(deleteToken)
|
|
||||||
if err != nil {
|
|
||||||
b.FlashAndRedirect(w, r, "/", "That comment was not found.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// And can we edit it?
|
|
||||||
if comment.EditToken != editToken {
|
|
||||||
b.Forbidden(w, r, "Your edit token is not valid for that comment.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
comment.ThreadID = thread.ID
|
|
||||||
comment.OriginURL = originURL
|
|
||||||
|
|
||||||
v := NewVars()
|
v := NewVars()
|
||||||
v.Data["Thread"] = thread
|
|
||||||
v.Data["Comment"] = comment
|
|
||||||
v.Data["Editing"] = true
|
|
||||||
|
|
||||||
b.RenderTemplate(w, r, "comments/index.gohtml", v)
|
// POST to unsubscribe from all threads.
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
email := r.FormValue("email")
|
||||||
|
if email == "" {
|
||||||
|
v.Error = errors.New("email address is required to unsubscribe from comment threads")
|
||||||
|
} else if _, err := mail.ParseAddress(email); err != nil {
|
||||||
|
v.Error = errors.New("invalid email address")
|
||||||
|
}
|
||||||
|
|
||||||
|
m := comments.LoadMailingList()
|
||||||
|
m.UnsubscribeAll(email)
|
||||||
|
b.FlashAndRedirect(w, r, "/comments/subscription",
|
||||||
|
"You have been unsubscribed from all mailing lists.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET to unsubscribe from a single thread.
|
||||||
|
thread := r.URL.Query().Get("t")
|
||||||
|
email := r.URL.Query().Get("e")
|
||||||
|
if thread != "" && email != "" {
|
||||||
|
m := comments.LoadMailingList()
|
||||||
|
m.Unsubscribe(thread, email)
|
||||||
|
b.FlashAndRedirect(w, r, "/comments/subscription", "You have been unsubscribed successfully.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.RenderTemplate(w, r, "comments/subscription.gohtml", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCommentHandler for editing comments.
|
// QuickDeleteHandler allows the admin to quickly delete spam without logging in.
|
||||||
func (b *Blog) DeleteCommentHandler(w http.ResponseWriter, r *http.Request) {
|
func (b *Blog) QuickDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
thread := r.URL.Query().Get("t")
|
||||||
|
token := r.URL.Query().Get("d")
|
||||||
|
if thread == "" || token == "" {
|
||||||
|
b.BadRequest(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := comments.Load(thread)
|
||||||
|
if err != nil {
|
||||||
|
b.BadRequest(w, r, "Comment thread does not exist.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, err := t.FindByDeleteToken(token); err == nil {
|
||||||
|
t.Delete(c.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.FlashAndRedirect(w, r, "/", "Comment deleted!")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEditToken gets or generates an edit token from the user's session, which
|
// GetEditToken gets or generates an edit token from the user's session, which
|
||||||
|
|
|
@ -9,11 +9,18 @@ import (
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
Title string
|
Title string
|
||||||
AdminEmail string
|
AdminEmail string
|
||||||
|
URL string
|
||||||
RedisEnabled bool
|
RedisEnabled bool
|
||||||
RedisHost string
|
RedisHost string
|
||||||
RedisPort int
|
RedisPort int
|
||||||
RedisDB int
|
RedisDB int
|
||||||
RedisPrefix string
|
RedisPrefix string
|
||||||
|
MailEnabled bool
|
||||||
|
MailSender string
|
||||||
|
MailHost string
|
||||||
|
MailPort int
|
||||||
|
MailUsername string
|
||||||
|
MailPassword string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the form.
|
// Validate the form.
|
||||||
|
|
121
core/mail.go
Normal file
121
core/mail.go
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/models/comments"
|
||||||
|
"github.com/kirsle/blog/core/models/settings"
|
||||||
|
gomail "gopkg.in/gomail.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Email configuration.
|
||||||
|
type Email struct {
|
||||||
|
To string
|
||||||
|
Admin bool /* admin view of the email */
|
||||||
|
Subject string
|
||||||
|
UnsubscribeURL string
|
||||||
|
Data map[string]interface{}
|
||||||
|
|
||||||
|
Template string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEmail sends an email.
|
||||||
|
func (b *Blog) SendEmail(email Email) {
|
||||||
|
s, _ := settings.Load()
|
||||||
|
if !s.Mail.Enabled || s.Mail.Host == "" || s.Mail.Port == 0 || s.Mail.Sender == "" {
|
||||||
|
log.Info("Suppressing email: not completely configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the template.
|
||||||
|
tmpl, err := b.ResolvePath(email.Template)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("SendEmail: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the template to HTML.
|
||||||
|
var html bytes.Buffer
|
||||||
|
t := template.New(tmpl.Basename)
|
||||||
|
t, err = template.ParseFiles(tmpl.Absolute)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("SendEmail: template parsing error: %s", err.Error())
|
||||||
|
}
|
||||||
|
err = t.ExecuteTemplate(&html, tmpl.Basename, email)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("SendEmail: template execution error: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
m := gomail.NewMessage()
|
||||||
|
m.SetHeader("From", s.Mail.Sender)
|
||||||
|
m.SetHeader("To", email.To)
|
||||||
|
m.SetHeader("Subject", email.Subject)
|
||||||
|
m.SetBody("text/html", html.String())
|
||||||
|
|
||||||
|
d := gomail.NewDialer(s.Mail.Host, s.Mail.Port, s.Mail.Username, s.Mail.Password)
|
||||||
|
if b.Debug {
|
||||||
|
d.TLSConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := d.DialAndSend(m); err != nil {
|
||||||
|
log.Error("SendEmail: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyComment sends notification emails about comments.
|
||||||
|
func (b *Blog) NotifyComment(c *comments.Comment) {
|
||||||
|
s, _ := settings.Load()
|
||||||
|
if s.Site.URL == "" {
|
||||||
|
log.Error("Can't send comment notification because the site URL is not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the email payload.
|
||||||
|
email := Email{
|
||||||
|
Template: ".email/comment.gohtml",
|
||||||
|
Subject: "Comment Added: " + c.Subject,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Name": c.Name,
|
||||||
|
"Subject": c.Subject,
|
||||||
|
"Body": template.HTML(b.RenderMarkdown(c.Body)),
|
||||||
|
"URL": strings.Trim(s.Site.URL, "/") + c.OriginURL,
|
||||||
|
"QuickDelete": fmt.Sprintf("%s/comments/quick-delete?t=%s&d=%s",
|
||||||
|
strings.Trim(s.Site.URL, "/"),
|
||||||
|
url.QueryEscape(c.ThreadID),
|
||||||
|
url.QueryEscape(c.DeleteToken),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email the site admins.
|
||||||
|
config, _ := settings.Load()
|
||||||
|
if config.Site.AdminEmail != "" {
|
||||||
|
email.To = config.Site.AdminEmail
|
||||||
|
email.Admin = true
|
||||||
|
log.Info("Mail site admin '%s' about comment notification on '%s'", email.To, c.ThreadID)
|
||||||
|
b.SendEmail(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email the subscribers.
|
||||||
|
email.Admin = false
|
||||||
|
m := comments.LoadMailingList()
|
||||||
|
for _, to := range m.List(c.ThreadID) {
|
||||||
|
if to == c.Email {
|
||||||
|
continue // don't email yourself
|
||||||
|
}
|
||||||
|
email.To = to
|
||||||
|
email.UnsubscribeURL = fmt.Sprintf("%s/comments/subscription?t=%s&e=%s",
|
||||||
|
strings.Trim(s.Site.URL, "/"),
|
||||||
|
url.QueryEscape(c.ThreadID),
|
||||||
|
url.QueryEscape(to),
|
||||||
|
)
|
||||||
|
log.Info("Mail subscriber '%s' about comment notification on '%s'", email.To, c.ThreadID)
|
||||||
|
b.SendEmail(email)
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,9 +54,10 @@ type Comment struct {
|
||||||
Trap2 string `json:"-"`
|
Trap2 string `json:"-"`
|
||||||
|
|
||||||
// Even privater fields.
|
// Even privater fields.
|
||||||
Username string `json:"-"`
|
IsAuthenticated bool `json:"-"`
|
||||||
Editable bool `json:"-"`
|
Username string `json:"-"`
|
||||||
Editing bool `json:"-"`
|
Editable bool `json:"-"`
|
||||||
|
Editing bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New initializes a new comment thread.
|
// New initializes a new comment thread.
|
||||||
|
@ -104,8 +105,6 @@ func (t *Thread) Post(c *Comment) error {
|
||||||
|
|
||||||
t.Comments = append(t.Comments, c)
|
t.Comments = append(t.Comments, c)
|
||||||
DB.Commit(fmt.Sprintf("comments/threads/%s", t.ID), t)
|
DB.Commit(fmt.Sprintf("comments/threads/%s", t.ID), t)
|
||||||
|
|
||||||
// TODO: handle subscriptions.
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +142,6 @@ func (t *Thread) Delete(id string) error {
|
||||||
// FindByDeleteToken finds a comment by its deletion token.
|
// FindByDeleteToken finds a comment by its deletion token.
|
||||||
func (t *Thread) FindByDeleteToken(token string) (*Comment, error) {
|
func (t *Thread) FindByDeleteToken(token string) (*Comment, error) {
|
||||||
for _, c := range t.Comments {
|
for _, c := range t.Comments {
|
||||||
fmt.Printf("%s <> %s\n", c.DeleteToken, token)
|
|
||||||
if c.DeleteToken == token {
|
if c.DeleteToken == token {
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
79
core/models/comments/subscribers.go
Normal file
79
core/models/comments/subscribers.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package comments
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// ListDBName is the path to the singleton mailing list manager.
|
||||||
|
const ListDBName = "comments/mailing-list"
|
||||||
|
|
||||||
|
// MailingList manages subscription data for all comment threads.
|
||||||
|
type MailingList struct {
|
||||||
|
Threads map[string]Subscription
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscription is the data for a single thread's subscribers.
|
||||||
|
type Subscription struct {
|
||||||
|
Emails map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadMailingList loads the mailing list, or initializes it if it doesn't exist.
|
||||||
|
func LoadMailingList() *MailingList {
|
||||||
|
m := &MailingList{
|
||||||
|
Threads: map[string]Subscription{},
|
||||||
|
}
|
||||||
|
DB.Get(ListDBName, &m)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to a comment thread.
|
||||||
|
func (m *MailingList) Subscribe(thread, email string) error {
|
||||||
|
email = strings.ToLower(email)
|
||||||
|
t := m.initThread(thread)
|
||||||
|
t.Emails[email] = true
|
||||||
|
return DB.Commit(ListDBName, &m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the subscribers for a thread.
|
||||||
|
func (m *MailingList) List(thread string) []string {
|
||||||
|
t := m.initThread(thread)
|
||||||
|
result := []string{}
|
||||||
|
for email := range t.Emails {
|
||||||
|
result = append(result, email)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe from a comment thread. Returns true if the removal was
|
||||||
|
// successful; false indicates the email was not subscribed.
|
||||||
|
func (m *MailingList) Unsubscribe(thread, email string) bool {
|
||||||
|
email = strings.ToLower(email)
|
||||||
|
t := m.initThread(thread)
|
||||||
|
if _, ok := t.Emails[email]; ok {
|
||||||
|
delete(t.Emails, email)
|
||||||
|
DB.Commit(ListDBName, &m)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsubscribeAll removes the email from all mailing lists.
|
||||||
|
func (m *MailingList) UnsubscribeAll(email string) bool {
|
||||||
|
var any bool
|
||||||
|
email = strings.ToLower(email)
|
||||||
|
for thread := range m.Threads {
|
||||||
|
if m.Unsubscribe(thread, email) {
|
||||||
|
any = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return any
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize a thread structure.
|
||||||
|
func (m *MailingList) initThread(thread string) Subscription {
|
||||||
|
if _, ok := m.Threads[thread]; !ok {
|
||||||
|
m.Threads[thread] = Subscription{
|
||||||
|
Emails: map[string]bool{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.Threads[thread]
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ type Settings struct {
|
||||||
Site struct {
|
Site struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
AdminEmail string `json:"adminEmail"`
|
AdminEmail string `json:"adminEmail"`
|
||||||
|
URL string `json:"url"`
|
||||||
} `json:"site"`
|
} `json:"site"`
|
||||||
|
|
||||||
// Security-related settings.
|
// Security-related settings.
|
||||||
|
@ -37,7 +38,14 @@ type Settings struct {
|
||||||
} `json:"redis"`
|
} `json:"redis"`
|
||||||
|
|
||||||
// Mail settings
|
// Mail settings
|
||||||
Mail struct{} `json:"mail,omitempty"`
|
Mail struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Sender string `json:"sender"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
} `json:"mail,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defaults returns default settings. The app initially sets this on
|
// Defaults returns default settings. The app initially sets this on
|
||||||
|
@ -52,6 +60,8 @@ func Defaults() *Settings {
|
||||||
s.Redis.Host = "localhost"
|
s.Redis.Host = "localhost"
|
||||||
s.Redis.Port = 6379
|
s.Redis.Port = 6379
|
||||||
s.Redis.DB = 0
|
s.Redis.DB = 0
|
||||||
|
s.Mail.Host = "localhost"
|
||||||
|
s.Mail.Port = 25
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ type Filepath struct {
|
||||||
// possible with a file extension injected.
|
// possible with a file extension injected.
|
||||||
// (i.e. "/about" -> "about.html")
|
// (i.e. "/about" -> "about.html")
|
||||||
URI string
|
URI string
|
||||||
|
Basename string
|
||||||
Relative string // Relative path including document root (i.e. "root/about.html")
|
Relative string // Relative path including document root (i.e. "root/about.html")
|
||||||
Absolute string // Absolute path on disk (i.e. "/opt/blog/root/about.html")
|
Absolute string // Absolute path on disk (i.e. "/opt/blog/root/about.html")
|
||||||
}
|
}
|
||||||
|
@ -92,6 +93,7 @@ func (b *Blog) ResolvePath(path string) (Filepath, error) {
|
||||||
// Resolve the file path.
|
// Resolve the file path.
|
||||||
relPath := filepath.Join(root, path)
|
relPath := filepath.Join(root, path)
|
||||||
absPath, err := filepath.Abs(relPath)
|
absPath, err := filepath.Abs(relPath)
|
||||||
|
basename := filepath.Base(relPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("%v", err)
|
log.Error("%v", err)
|
||||||
}
|
}
|
||||||
|
@ -101,7 +103,7 @@ func (b *Blog) ResolvePath(path string) (Filepath, error) {
|
||||||
// Found an exact hit?
|
// Found an exact hit?
|
||||||
if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() {
|
if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() {
|
||||||
debug("Exact filepath found: %s", absPath)
|
debug("Exact filepath found: %s", absPath)
|
||||||
return Filepath{path, relPath, absPath}, nil
|
return Filepath{path, basename, relPath, absPath}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try some supported suffixes.
|
// Try some supported suffixes.
|
||||||
|
@ -117,7 +119,7 @@ func (b *Blog) ResolvePath(path string) (Filepath, error) {
|
||||||
test := absPath + suffix
|
test := absPath + suffix
|
||||||
if stat, err := os.Stat(test); !os.IsNotExist(err) && !stat.IsDir() {
|
if stat, err := os.Stat(test); !os.IsNotExist(err) && !stat.IsDir() {
|
||||||
debug("Filepath found via suffix %s: %s", suffix, test)
|
debug("Filepath found via suffix %s: %s", suffix, test)
|
||||||
return Filepath{path + suffix, relPath + suffix, test}, nil
|
return Filepath{path + suffix, basename + suffix, relPath + suffix, test}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
61
root/.email/comment.gohtml
Normal file
61
root/.email/comment.gohtml
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="x-apple-disable-message-reformatting"><!-- Disable auto-scale in iOS 10 Mail -->
|
||||||
|
<title>{{ .Subject }}</title>
|
||||||
|
</head>
|
||||||
|
<body width="100%" bgcolor="#FFFFFF" color="#000000" style="margin: 0; mso-line-height-rule: exactly;">
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<table width="90%" cellspacing="0" cellpadding="8" style="border: 1px solid #000000">
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="top" bgcolor="#C0C0C0">
|
||||||
|
<font face="Helvetica,Arial,Verdana-sans-serif" size="6" color="#000000">
|
||||||
|
<b>{{ .Subject }}</b>
|
||||||
|
</font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="top" bgcolor="#FEFEFE">
|
||||||
|
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000">
|
||||||
|
{{ if not .Admin }}
|
||||||
|
Hello,<br><br>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ or .Data.Name "Anonymous" }} has left a comment on: {{ .Data.Subject }}
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
{{ .Data.Body }}
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
To view this comment, please go to <a href="{{ .Data.URL }}" target="_blank">{{ .Data.URL }}</a>.
|
||||||
|
|
||||||
|
{{ if .Admin }}
|
||||||
|
<br><br>
|
||||||
|
Was this comment spam? <a href="{{ .Data.QuickDelete }}" target="_blank">Delete it</a>.
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if .UnsubscribeURL }}
|
||||||
|
<br><br>
|
||||||
|
To unsubscribe from this message, visit <a href="{{ .UnsubscribeURL }}" target="_blank">{{ .UnsubscribeURL }}</a>
|
||||||
|
{{ end }}
|
||||||
|
</font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="top" bgcolor="#C0C0C0">
|
||||||
|
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000">
|
||||||
|
This e-mail was automatically generated; do not reply to it.
|
||||||
|
</font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -26,6 +26,20 @@
|
||||||
placeholder="name@domain.com">
|
placeholder="name@domain.com">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="admin-email">URL Root</label>
|
||||||
|
<small class="text-muted d-block">
|
||||||
|
The base absolute URL to your website. This is used to generate
|
||||||
|
emails such as comment notifications. If not provided, these
|
||||||
|
emails will not be sent.
|
||||||
|
</small>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="url"
|
||||||
|
value="{{ .Site.URL }}"
|
||||||
|
placeholder="https://www.example.com/">
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Redis Cache</h3>
|
<h3>Redis Cache</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -79,6 +93,64 @@
|
||||||
placeholder="blog:">
|
placeholder="blog:">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>Email Settings</h3>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
name="mail-enabled"
|
||||||
|
value="true"
|
||||||
|
{{ if .Mail.Enabled }}checked{{ end }}>
|
||||||
|
Enable email to be sent by this site
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mail-sender">Sender Address</label>
|
||||||
|
<input type="email"
|
||||||
|
name="mail-sender"
|
||||||
|
id="mail-sender"
|
||||||
|
class="form-control"
|
||||||
|
value="{{ .Mail.Sender }}"
|
||||||
|
placeholder="no-reply@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mail-host">SMTP Host</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="mail-host"
|
||||||
|
id="mail-host"
|
||||||
|
value="{{ .Mail.Host }}"
|
||||||
|
placeholder="localhost">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mail-port">SMTP Port</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="mail-port"
|
||||||
|
id="mail-port"
|
||||||
|
value="{{ .Mail.Port }}"
|
||||||
|
placeholder="25">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mail-username">SMTP Username</label>
|
||||||
|
<small class="text-muted">(optional)</small>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="mail-username"
|
||||||
|
value="{{ .Mail.Username }}"
|
||||||
|
placeholder="">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mail-password">SMTP Password</label>
|
||||||
|
<small class="text-muted">(optional)</small>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="mail-password"
|
||||||
|
value="{{ .Mail.Password }}"
|
||||||
|
placeholder="">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||||
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
|
|
||||||
{{ $p := .Data.Post }}
|
{{ $p := .Data.Post }}
|
||||||
{{ RenderPost $p false }}
|
{{ RenderPost $p false 0 }}
|
||||||
|
|
||||||
{{ if and .LoggedIn .CurrentUser.Admin }}
|
{{ if and .LoggedIn .CurrentUser.Admin }}
|
||||||
<small>
|
<small>
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if $p.EnableComments }}
|
{{ if $p.EnableComments }}
|
||||||
<h2 class="mt-4">Comments</h2>
|
<h2 id="comments" class="mt-4">Comments</h2>
|
||||||
|
|
||||||
{{ $idStr := printf "%d" $p.ID}}
|
{{ $idStr := printf "%d" $p.ID}}
|
||||||
{{ RenderComments $p.Title "post" $idStr }}
|
{{ RenderComments $p.Title "post" $idStr }}
|
||||||
|
|
|
@ -36,13 +36,19 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ if not .IndexView }}<hr>{{ end }}
|
|
||||||
|
|
||||||
{{ if $p.Tags }}
|
{{ if $p.Tags }}
|
||||||
<em class="text-muted float-left pr-3">Tags:</em>
|
<em class="text-muted float-left pr-3">Tags:</em>
|
||||||
<ul class="list-inline">
|
<ul class="list-inline">
|
||||||
{{ range $p.Tags }}
|
{{ range $p.Tags }}
|
||||||
<li class="list-inline-item"><a href="/tagged/{{ . }}">{{ . }}</a></li>
|
<li class="list-inline-item text-muted"><em><a href="/tagged/{{ . }}">{{ . }}</a></em></li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if .IndexView }}
|
||||||
|
<em class="text-muted">
|
||||||
|
<a href="/{{ $p.Fragment }}#comments">{{ .NumComments }} comment{{ if ne .NumComments 1 }}s{{ end }}</a>
|
||||||
|
|
|
||||||
|
<a href="/{{ $p.Fragment }}">Permalink</a>
|
||||||
|
</em>
|
||||||
|
{{ end }}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
{{ range .Data.View }}
|
{{ range .Data.View }}
|
||||||
{{ $p := .Post }}
|
{{ $p := .Post }}
|
||||||
{{ RenderPost $p true }}
|
{{ RenderPost $p true .NumComments }}
|
||||||
|
|
||||||
{{ if and $.LoggedIn $.CurrentUser.Admin }}
|
{{ if and $.LoggedIn $.CurrentUser.Admin }}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
|
|
@ -36,6 +36,10 @@ label.form-check-label {
|
||||||
button {
|
button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.text-muted a {
|
||||||
|
color: #868e96 !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Top nav
|
* Top nav
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
There is 1 comment on this page.
|
There is 1 comment on this page.
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
There are {{ len $t.Comments }} comments on this page.
|
There are {{ len $t.Comments }} comments on this page.
|
||||||
{{- end -}}
|
{{- end }}
|
||||||
<a href="#add-comment">Add your comment.</a>
|
<a href="#add-comment">Add yours.</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{{ range $t.Comments }}
|
{{ range $t.Comments }}
|
||||||
|
@ -22,69 +22,7 @@
|
||||||
<input type="hidden" name="subject" value="{{ .Subject }}">
|
<input type="hidden" name="subject" value="{{ .Subject }}">
|
||||||
<input type="hidden" name="origin" value="{{ .OriginURL }}">
|
<input type="hidden" name="origin" value="{{ .OriginURL }}">
|
||||||
|
|
||||||
{{ if not .IsAuthenticated }}
|
{{ template "comment-form" .NewComment }}
|
||||||
<div class="form-group row">
|
|
||||||
<label for="name" class="col-2 col-form-label">Your name:</label>
|
|
||||||
<div class="col-10">
|
|
||||||
<input type="text"
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
class="form-control"
|
|
||||||
value="{{ .Name }}"
|
|
||||||
placeholder="Anonymous">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row">
|
|
||||||
<label for="email" class="col-2 col-form-label">Your email:</label>
|
|
||||||
<div class="col-10">
|
|
||||||
<input type="email"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
class="form-control"
|
|
||||||
aria-describedby="emailHelp"
|
|
||||||
value="{{ .Email }}"
|
|
||||||
placeholder="(optional)">
|
|
||||||
<small id="emailHelp" class="form-text text-muted">
|
|
||||||
Used for your <a href="https://en.gravatar.com/" target="_blank">Gravatar</a>
|
|
||||||
and optional thread subscription. <a href="/privacy">Privacy policy.</a>
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<label class="form-check-label pl-0">
|
|
||||||
<input type="checkbox" name="subscribe" value="true">
|
|
||||||
<small>Notify me of future comments on this page.</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="body">Message:</label>
|
|
||||||
<textarea
|
|
||||||
name="body"
|
|
||||||
id="body"
|
|
||||||
cols="40" rows="10"
|
|
||||||
aria-describedby="bodyHelp"
|
|
||||||
class="form-control"></textarea>
|
|
||||||
<small id="bodyHelp" class="form-text text-muted">
|
|
||||||
You may format your message using
|
|
||||||
<a href="https://daringfireball.net/projects/markdown/syntax">Markdown</a>
|
|
||||||
syntax.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" style="display: none">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Sanity Check</div>
|
|
||||||
<div class="card-body">
|
|
||||||
If you happen to be able to see these fields, do not change
|
|
||||||
their values.
|
|
||||||
|
|
||||||
<input type="text" name="url" value="http://" class="form-control" placeholder="Website">
|
|
||||||
<textarea name="comment" cols="80" rows="10" class="form-control" placeholder="Comment"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
name="submit"
|
name="submit"
|
||||||
|
|
|
@ -51,3 +51,71 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "comment-form" }}
|
||||||
|
{{ if not .IsAuthenticated }}
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="name" class="col-2 col-form-label">Your name:</label>
|
||||||
|
<div class="col-10">
|
||||||
|
<input type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
class="form-control"
|
||||||
|
value="{{ .Name }}"
|
||||||
|
placeholder="Anonymous">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="email" class="col-2 col-form-label">Your email:</label>
|
||||||
|
<div class="col-10">
|
||||||
|
<input type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
class="form-control"
|
||||||
|
aria-describedby="emailHelp"
|
||||||
|
value="{{ .Email }}"
|
||||||
|
placeholder="(optional)">
|
||||||
|
<small id="emailHelp" class="form-text text-muted">
|
||||||
|
Used for your <a href="https://en.gravatar.com/" target="_blank">Gravatar</a>
|
||||||
|
and optional thread subscription. <a href="/comments/subscription" target="_blank">Privacy policy.</a>
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="form-check-label pl-0">
|
||||||
|
<input type="checkbox"{{ if .Subscribe }} checked{{ end }}
|
||||||
|
name="subscribe"
|
||||||
|
value="true">
|
||||||
|
<small>Notify me of future comments on this page.</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="body">Message:</label>
|
||||||
|
<textarea
|
||||||
|
name="body"
|
||||||
|
id="body"
|
||||||
|
cols="40" rows="10"
|
||||||
|
aria-describedby="bodyHelp"
|
||||||
|
class="form-control">{{ .Body }}</textarea>
|
||||||
|
<small id="bodyHelp" class="form-text text-muted">
|
||||||
|
You may format your message using
|
||||||
|
<a href="https://daringfireball.net/projects/markdown/syntax">Markdown</a>
|
||||||
|
syntax.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="display: none">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Sanity Check</div>
|
||||||
|
<div class="card-body">
|
||||||
|
If you happen to be able to see these fields, do not change
|
||||||
|
their values.
|
||||||
|
|
||||||
|
<input type="text" name="url" value="http://" class="form-control" placeholder="Website">
|
||||||
|
<textarea name="comment" cols="80" rows="10" class="form-control" placeholder="Comment"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
|
|
||||||
{{ with .Data.Comment }}
|
{{ with .Data.Comment }}
|
||||||
<form action="/comments" method="POST">
|
<form action="/comments" method="POST">
|
||||||
<input type="hidden" name="_csrf" value="{{ $.CSRF }}">
|
<input type="hidden" name="_csrf" value="{{ $.CSRF }}">
|
||||||
<input type="hidden" name="thread" value="{{ .ThreadID }}">
|
<input type="hidden" name="thread" value="{{ .ThreadID }}">
|
||||||
<input type="hidden" name="subject" value="{{ .Subject }}">
|
<input type="hidden" name="subject" value="{{ .Subject }}">
|
||||||
|
@ -39,75 +39,7 @@
|
||||||
</button>
|
</button>
|
||||||
<a href="{{ .OriginURL }}" class="btn btn-primary">Cancel</a>
|
<a href="{{ .OriginURL }}" class="btn btn-primary">Cancel</a>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
{{ if not $.CurrentUser.IsAuthenticated }}
|
{{ template "comment-form" . }}
|
||||||
<div class="form-group row">
|
|
||||||
<label for="name" class="col-2 col-form-label">Your name:</label>
|
|
||||||
<div class="col-10">
|
|
||||||
{{ if and $.CurrentUser.IsAuthenticated }}
|
|
||||||
{{ $.CurrentUser.Name }}
|
|
||||||
{{ else }}
|
|
||||||
<input type="text"
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
class="form-control"
|
|
||||||
value="{{ .Name }}"
|
|
||||||
placeholder="Anonymous">
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row">
|
|
||||||
<label for="email" class="col-2 col-form-label">Your email:</label>
|
|
||||||
<div class="col-10">
|
|
||||||
<input type="email"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
class="form-control"
|
|
||||||
aria-describedby="emailHelp"
|
|
||||||
value="{{ .Email }}"
|
|
||||||
placeholder="(optional)">
|
|
||||||
<small id="emailHelp" class="form-text text-muted">
|
|
||||||
Used for your <a href="https://en.gravatar.com/" target="_blank">Gravatar</a>
|
|
||||||
and optional thread subscription. <a href="/privacy">Privacy policy.</a>
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<label class="form-check-label pl-0">
|
|
||||||
<input type="checkbox"{{ if .Subscribe }} checked{{ end }}
|
|
||||||
name="subscribe"
|
|
||||||
value="true">
|
|
||||||
<small>Notify me of future comments on this page.</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="body">Message:</label>
|
|
||||||
<textarea
|
|
||||||
name="body"
|
|
||||||
id="body"
|
|
||||||
cols="40" rows="10"
|
|
||||||
aria-describedby="bodyHelp"
|
|
||||||
class="form-control">{{ .Body }}</textarea>
|
|
||||||
<small id="bodyHelp" class="form-text text-muted">
|
|
||||||
You may format your message using
|
|
||||||
<a href="https://daringfireball.net/projects/markdown/syntax">Markdown</a>
|
|
||||||
syntax.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" style="display: none">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Sanity Check</div>
|
|
||||||
<div class="card-body">
|
|
||||||
If you happen to be able to see these fields, do not change
|
|
||||||
their values.
|
|
||||||
|
|
||||||
<input type="text" name="url" value="http://" class="form-control" placeholder="Website">
|
|
||||||
<textarea name="comment" cols="80" rows="10" class="form-control" placeholder="Comment"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" name="submit" value="preview" class="btn btn-primary">
|
<button type="submit" name="submit" value="preview" class="btn btn-primary">
|
||||||
Refresh Preview
|
Refresh Preview
|
||||||
|
@ -116,7 +48,7 @@
|
||||||
Post Comment
|
Post Comment
|
||||||
</button>
|
</button>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</form>
|
</form>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
50
root/comments/subscription.gohtml
Normal file
50
root/comments/subscription.gohtml
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{{ define "title" }}Manage Comment Subscriptions{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
|
||||||
|
<h1>Comment Subscriptions</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
When leaving comments on this website, you may <em>optionally</em>
|
||||||
|
subscribe to get email notifications when new comments are added to
|
||||||
|
the same thread. This way you can get notified when your question
|
||||||
|
has been answered, for example.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Privacy Policy</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This web blog is open source software, so the following is true as far as
|
||||||
|
the web blog software itself is concerned. Please check with the site
|
||||||
|
administrator for their personal privacy policy.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Your email address is used for the following purposes by this web blog:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Showing your <a href="https://www.gravatar.com/" target="_blank">Gravatar</a> next to your comment.</li>
|
||||||
|
<li>With your permission: sending you notifications about future comments on the page.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Unsubscribe</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To unsubscribe from individual comment threads, use the "Unsubscribe" links
|
||||||
|
in the emails. Or, to unsubcribe from <strong>all</strong> comment threads,
|
||||||
|
enter your email address below.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form class="form-inline" action="/comments/subscription" method="POST">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
||||||
|
<label class="sr-only" for="email">Email address</label>
|
||||||
|
<input type="email"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
class="form-control mr-2"
|
||||||
|
placeholder="name@domain.com">
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Unsubscribe</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{ end }}
|
Loading…
Reference in New Issue
Block a user