393 lines
12 KiB
Go
393 lines
12 KiB
Go
package forum
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
|
|
"code.nonshy.com/nonshy/website/pkg/log"
|
|
"code.nonshy.com/nonshy/website/pkg/markdown"
|
|
"code.nonshy.com/nonshy/website/pkg/models"
|
|
"code.nonshy.com/nonshy/website/pkg/photo"
|
|
"code.nonshy.com/nonshy/website/pkg/session"
|
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
|
)
|
|
|
|
// NewPost view.
|
|
func NewPost() http.HandlerFunc {
|
|
tmpl := templates.Must("forum/new_post.html")
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Query params.
|
|
var (
|
|
fragment = r.FormValue("to") // forum to (new post)
|
|
toThreadID = r.FormValue("thread") // add reply to a thread ID
|
|
quoteCommentID = r.FormValue("quote") // add reply to thread while quoting a comment
|
|
editCommentID = r.FormValue("edit") // edit your comment
|
|
intent = r.FormValue("intent") // preview or submit
|
|
photoIntent = r.FormValue("photo_intent") // upload, remove photo attachment
|
|
photoID = r.FormValue("photo_id") // existing CommentPhoto ID
|
|
title = r.FormValue("title") // for new forum post only
|
|
message = r.PostFormValue("message") // comment body
|
|
isPinned = r.PostFormValue("pinned") == "true" // owners or admins only
|
|
isExplicit = r.PostFormValue("explicit") == "true" // for thread only
|
|
isNoReply = r.PostFormValue("noreply") == "true" // for thread only
|
|
isDelete = r.FormValue("delete") == "true" // delete comment (along with edit=$id)
|
|
forum *models.Forum
|
|
thread *models.Thread // if replying to a thread
|
|
comment *models.Comment // if editing a comment
|
|
|
|
// If we are modifying a comment (post) and it's the OG post of the
|
|
// thread, we show and accept the thread settings to be updated as
|
|
// well (pinned, explicit, noreply)
|
|
isOriginalComment bool
|
|
|
|
// Attached photo object.
|
|
commentPhoto *models.CommentPhoto
|
|
)
|
|
|
|
// Get the current user.
|
|
currentUser, err := session.CurrentUser(r)
|
|
if err != nil {
|
|
session.FlashError(w, r, "Couldn't get current user: %s", err)
|
|
templates.Redirect(w, "/")
|
|
return
|
|
}
|
|
|
|
// Look up the forum itself.
|
|
if found, err := models.ForumByFragment(fragment); err != nil {
|
|
session.FlashError(w, r, "Couldn't post to forum %s: not found.", fragment)
|
|
templates.Redirect(w, "/forum")
|
|
return
|
|
} else {
|
|
forum = found
|
|
}
|
|
|
|
// Are we manipulating a reply to an existing thread?
|
|
if len(toThreadID) > 0 {
|
|
if i, err := strconv.Atoi(toThreadID); err == nil {
|
|
if found, err := models.GetThread(uint64(i)); err != nil {
|
|
session.FlashError(w, r, "Couldn't find that thread ID!")
|
|
templates.Redirect(w, fmt.Sprintf("/f/%s", forum.Fragment))
|
|
return
|
|
} else {
|
|
thread = found
|
|
}
|
|
}
|
|
}
|
|
|
|
// Does the comment have an existing Photo ID?
|
|
if len(photoID) > 0 {
|
|
if i, err := strconv.Atoi(photoID); err == nil {
|
|
if found, err := models.GetCommentPhoto(uint64(i)); err != nil {
|
|
session.FlashError(w, r, "Couldn't find comment photo ID #%d!", i)
|
|
templates.Redirect(w, fmt.Sprintf("/f/%s", forum.Fragment))
|
|
return
|
|
} else {
|
|
commentPhoto = found
|
|
}
|
|
}
|
|
}
|
|
|
|
// Are we pre-filling the message with a quotation of an existing comment?
|
|
if len(quoteCommentID) > 0 {
|
|
if i, err := strconv.Atoi(quoteCommentID); err == nil {
|
|
if comment, err := models.GetComment(uint64(i)); err == nil {
|
|
message = markdown.Quotify(comment.Message) + "\n\n"
|
|
}
|
|
}
|
|
}
|
|
|
|
// Are we editing or deleting our comment?
|
|
if len(editCommentID) > 0 {
|
|
if i, err := strconv.Atoi(editCommentID); err == nil {
|
|
if found, err := models.GetComment(uint64(i)); err == nil {
|
|
comment = found
|
|
|
|
// Verify that it is indeed OUR comment.
|
|
if currentUser.ID != comment.UserID && !currentUser.IsAdmin {
|
|
templates.ForbiddenPage(w, r)
|
|
return
|
|
}
|
|
|
|
// Initialize the form w/ the content of this message.
|
|
if r.Method == http.MethodGet {
|
|
message = comment.Message
|
|
}
|
|
|
|
// Did this comment have a picture? Load it if so.
|
|
if photos, err := comment.GetPhotos(); err == nil && len(photos) > 0 {
|
|
commentPhoto = photos[0]
|
|
}
|
|
|
|
// Is this the OG thread of the post?
|
|
if thread.CommentID == comment.ID {
|
|
isOriginalComment = true
|
|
|
|
// Restore the checkbox option form values from thread settings.
|
|
if r.Method == http.MethodGet {
|
|
isPinned = thread.Pinned
|
|
isExplicit = thread.Explicit
|
|
isNoReply = thread.NoReply
|
|
}
|
|
}
|
|
|
|
// Are we DELETING this comment?
|
|
if isDelete {
|
|
// Is there a photo attachment? Remove it, too.
|
|
if commentPhoto != nil {
|
|
if err := photo.Delete(commentPhoto.Filename); err != nil {
|
|
session.FlashError(w, r, "Error removing the photo from disk: %s", err)
|
|
}
|
|
|
|
if err := commentPhoto.Delete(); err != nil {
|
|
session.FlashError(w, r, "Couldn't remove photo from DB: %s", err)
|
|
} else {
|
|
commentPhoto = nil
|
|
}
|
|
}
|
|
|
|
if err := thread.DeleteReply(comment); err != nil {
|
|
session.FlashError(w, r, "Error deleting your post: %s", err)
|
|
} else {
|
|
session.Flash(w, r, "Your post has been deleted.")
|
|
}
|
|
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
|
return
|
|
}
|
|
} else {
|
|
// Comment not found - show the Forbidden page anyway.
|
|
templates.ForbiddenPage(w, r)
|
|
return
|
|
}
|
|
} else {
|
|
templates.NotFoundPage(w, r)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Submitting the form.
|
|
if r.Method == http.MethodPost {
|
|
// Is a photo coming along?
|
|
if forum.PermitPhotos {
|
|
// Removing or replacing?
|
|
if photoIntent == "remove" || photoIntent == "replace" {
|
|
// Remove the attached photo.
|
|
if commentPhoto == nil {
|
|
session.FlashError(w, r, "Couldn't remove photo from post: no photo found!")
|
|
} else {
|
|
photo.Delete(commentPhoto.Filename)
|
|
if err := commentPhoto.Delete(); err != nil {
|
|
session.FlashError(w, r, "Couldn't remove photo from DB: %s", err)
|
|
} else {
|
|
session.Flash(w, r, "Photo attachment %sd from this post.", photoIntent)
|
|
commentPhoto = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Uploading a new picture?
|
|
if photoIntent == "upload" || photoIntent == "replace" {
|
|
log.Info("Receiving a photo upload for forum post")
|
|
|
|
// Get their file upload.
|
|
file, header, err := r.FormFile("file")
|
|
if err != nil {
|
|
session.FlashError(w, r, "Error receiving your file: %s", err)
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
// Read the file contents.
|
|
log.Debug("Receiving uploaded file (%d bytes): %s", header.Size, header.Filename)
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, file)
|
|
|
|
filename, _, err := photo.UploadPhoto(photo.UploadConfig{
|
|
Extension: filepath.Ext(header.Filename),
|
|
Data: buf.Bytes(),
|
|
})
|
|
if err != nil {
|
|
session.FlashError(w, r, "Error in UploadPhoto: %s", err)
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
// Create the PhotoComment. If we don't have a Comment ID yet, let it be empty.
|
|
ptmpl := models.CommentPhoto{
|
|
Filename: filename,
|
|
UserID: currentUser.ID,
|
|
}
|
|
if comment != nil {
|
|
ptmpl.CommentID = comment.ID
|
|
}
|
|
|
|
// Get the filesize.
|
|
if stat, err := os.Stat(photo.DiskPath(filename)); err == nil {
|
|
ptmpl.Filesize = stat.Size()
|
|
}
|
|
|
|
// Create it in DB!
|
|
p, err := models.CreateCommentPhoto(ptmpl)
|
|
if err != nil {
|
|
session.FlashError(w, r, "Couldn't create CommentPhoto in DB: %s", err)
|
|
} else {
|
|
log.Info("New photo! %+v", p)
|
|
}
|
|
|
|
commentPhoto = p
|
|
}
|
|
}
|
|
|
|
// Default intent is preview unless told to submit.
|
|
if intent == "submit" {
|
|
// A message OR a photo is required.
|
|
if forum.PermitPhotos && message == "" && commentPhoto == nil {
|
|
session.FlashError(w, r, "A message OR photo is required for this post.")
|
|
if thread != nil {
|
|
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
|
} else if forum != nil {
|
|
templates.Redirect(w, fmt.Sprintf("/f/%s", forum.Fragment))
|
|
} else {
|
|
templates.Redirect(w, "/forum")
|
|
}
|
|
return
|
|
} else if !forum.PermitPhotos && message == "" {
|
|
session.FlashError(w, r, "A message is required for this post.")
|
|
if thread != nil {
|
|
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
|
} else if forum != nil {
|
|
templates.Redirect(w, fmt.Sprintf("/f/%s", forum.Fragment))
|
|
} else {
|
|
templates.Redirect(w, "/forum")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Are we modifying an existing comment?
|
|
if comment != nil {
|
|
comment.Message = message
|
|
|
|
// Can we update the thread props?
|
|
if isOriginalComment {
|
|
thread.Pinned = isPinned
|
|
thread.Explicit = isExplicit
|
|
thread.NoReply = isNoReply
|
|
if err := thread.Save(); err != nil {
|
|
session.FlashError(w, r, "Couldn't save thread properties: %s", err)
|
|
}
|
|
}
|
|
|
|
if err := comment.Save(); err != nil {
|
|
session.FlashError(w, r, "Couldn't save comment: %s", err)
|
|
} else {
|
|
session.Flash(w, r, "Comment updated!")
|
|
}
|
|
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
|
return
|
|
}
|
|
|
|
// Are we replying to an existing thread?
|
|
if thread != nil {
|
|
if reply, err := thread.Reply(currentUser, message); err != nil {
|
|
session.FlashError(w, r, "Couldn't add reply to thread: %s", err)
|
|
} else {
|
|
session.Flash(w, r, "Reply added to the thread!")
|
|
|
|
// If we're attaching a photo, link it to this reply CommentID.
|
|
if commentPhoto != nil {
|
|
commentPhoto.CommentID = reply.ID
|
|
if err := commentPhoto.Save(); err != nil {
|
|
log.Error("Couldn't save forum reply CommentPhoto.CommentID: %s", err)
|
|
}
|
|
}
|
|
|
|
// Notify watchers about this new post.
|
|
for _, userID := range models.GetSubscribers("threads", thread.ID) {
|
|
if userID == currentUser.ID {
|
|
continue
|
|
}
|
|
|
|
notif := &models.Notification{
|
|
UserID: userID,
|
|
AboutUser: *currentUser,
|
|
Type: models.NotificationAlsoPosted,
|
|
TableName: "threads",
|
|
TableID: thread.ID,
|
|
Message: message,
|
|
Link: fmt.Sprintf("/forum/thread/%d", thread.ID),
|
|
}
|
|
if err := models.CreateNotification(notif); err != nil {
|
|
log.Error("Couldn't create thread reply notification for subscriber %d: %s", userID, err)
|
|
}
|
|
}
|
|
|
|
// Subscribe the current user to further responses on this thread.
|
|
if _, err := models.SubscribeTo(currentUser, "threads", thread.ID); err != nil {
|
|
log.Error("Couldn't subscribe user %d to forum thread %d: %s", currentUser.ID, thread.ID, err)
|
|
}
|
|
}
|
|
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
|
return
|
|
}
|
|
|
|
// Create a new thread?
|
|
if thread, err := models.CreateThread(
|
|
currentUser,
|
|
forum.ID,
|
|
title,
|
|
message,
|
|
isPinned,
|
|
isExplicit,
|
|
isNoReply,
|
|
); err != nil {
|
|
session.FlashError(w, r, "Couldn't create thread: %s", err)
|
|
} else {
|
|
session.Flash(w, r, "Thread created!")
|
|
|
|
// If we're attaching a photo, link it to this CommentID.
|
|
if commentPhoto != nil {
|
|
commentPhoto.CommentID = thread.CommentID
|
|
if err := commentPhoto.Save(); err != nil {
|
|
log.Error("Couldn't save forum post CommentPhoto.CommentID: %s", err)
|
|
}
|
|
}
|
|
|
|
// Subscribe the current user to responses on this thread.
|
|
if _, err := models.SubscribeTo(currentUser, "threads", thread.ID); err != nil {
|
|
log.Error("Couldn't subscribe user %d to forum thread %d: %s", currentUser.ID, thread.ID, err)
|
|
}
|
|
|
|
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
var vars = map[string]interface{}{
|
|
"Forum": forum,
|
|
"Thread": thread,
|
|
"Intent": intent,
|
|
"PostTitle": title,
|
|
"EditCommentID": editCommentID,
|
|
"EditThreadSettings": isOriginalComment,
|
|
"Message": message,
|
|
|
|
// Thread settings (for editing the original comment esp.)
|
|
"IsPinned": isPinned,
|
|
"IsExplicit": isExplicit,
|
|
"IsNoReply": isNoReply,
|
|
|
|
// Attached photo.
|
|
"CommentPhoto": commentPhoto,
|
|
}
|
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
})
|
|
}
|