b9973719a5
Preloaded a small number of users and topics. Use cache.Set instead of cache.Add in a few spots for topics to avoid issues with the counter falling out of sync with the cache length. Embed Reply in ReplyUser instead of duplicating the same fields. Add the missing AttachCount and ActionType fields to Reply. Add the missing Poll field to TopicsRow. Added the TopicUser.Replies method to better abstract the reply list generation logic. Shortened some common.s to c.s Moved memStuff out of the analytics memory panes and into analytics.js Removed the temporary preStats fix for label overflow now that we have a better solution. Added the panel_analytics_script_memory template to help de-dupe logic in the analytics memory panes. Added the Topic method to TopicsRow. Added the GetRidsForTopic method to help cache replies in a small set of scenarios. Added the ReplyCache config.json setting. Added the ReplyCacheCapacity config.json setting. Added a parser test case. Added more Reply and ReplyStore related test cases.
642 lines
18 KiB
Go
642 lines
18 KiB
Go
package routes
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
c "github.com/Azareal/Gosora/common"
|
|
"github.com/Azareal/Gosora/common/counters"
|
|
"github.com/Azareal/Gosora/common/phrases"
|
|
"github.com/Azareal/Gosora/query_gen"
|
|
)
|
|
|
|
type ReplyStmts struct {
|
|
updateAttachs *sql.Stmt
|
|
createReplyPaging *sql.Stmt
|
|
}
|
|
|
|
var replyStmts ReplyStmts
|
|
|
|
// TODO: Move this statement somewhere else
|
|
func init() {
|
|
c.DbInits.Add(func(acc *qgen.Accumulator) error {
|
|
replyStmts = ReplyStmts{
|
|
// TODO: Less race-y attachment count updates
|
|
updateAttachs: acc.Update("replies").Set("attachCount = ?").Where("rid = ?").Prepare(),
|
|
createReplyPaging: acc.Select("replies").Cols("rid").Where("rid >= ? - 1 AND tid = ?").Orderby("rid ASC").Prepare(),
|
|
}
|
|
return acc.FirstError()
|
|
})
|
|
}
|
|
|
|
type JsonReply struct {
|
|
Content string
|
|
}
|
|
|
|
func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
|
|
// TODO: Use this
|
|
js := r.FormValue("js") == "1"
|
|
tid, err := strconv.Atoi(r.PostFormValue("tid"))
|
|
if err != nil {
|
|
return c.PreErrorJSQ("Failed to convert the Topic ID", w, r, js)
|
|
}
|
|
|
|
topic, err := c.Topics.Get(tid)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("Couldn't find the parent topic", w, r, js)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
|
|
// TODO: Add hooks to make use of headerLite
|
|
lite, ferr := c.SimpleForumUserCheck(w, r, &user, topic.ParentID)
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
if !user.Perms.ViewTopic || !user.Perms.CreateReply {
|
|
return c.NoPermissionsJSQ(w, r, user, js)
|
|
}
|
|
if topic.IsClosed && !user.Perms.CloseTopic {
|
|
return c.NoPermissionsJSQ(w, r, user, js)
|
|
}
|
|
|
|
content := c.PreparseMessage(r.PostFormValue("reply-content"))
|
|
// TODO: Fully parse the post and put that in the parsed column
|
|
rid, err := c.Rstore.Create(topic, content, user.LastIP, user.ID)
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
|
|
reply, err := c.Rstore.Get(rid)
|
|
if err != nil {
|
|
return c.LocalErrorJSQ("Unable to load the reply", w, r, user, js)
|
|
}
|
|
|
|
// Handle the file attachments
|
|
// TODO: Stop duplicating this code
|
|
if user.Perms.UploadFiles {
|
|
_, rerr := uploadAttachment(w, r, user, topic.ParentID, "forums", rid, "replies", strconv.Itoa(topic.ID))
|
|
if rerr != nil {
|
|
return rerr
|
|
}
|
|
}
|
|
|
|
if r.PostFormValue("has_poll") == "1" {
|
|
var maxPollOptions = 10
|
|
var pollInputItems = make(map[int]string)
|
|
for key, values := range r.Form {
|
|
//c.DebugDetail("key: ", key)
|
|
//c.DebugDetailf("values: %+v\n", values)
|
|
for _, value := range values {
|
|
if strings.HasPrefix(key, "pollinputitem[") {
|
|
halves := strings.Split(key, "[")
|
|
if len(halves) != 2 {
|
|
return c.LocalErrorJSQ("Malformed pollinputitem", w, r, user, js)
|
|
}
|
|
halves[1] = strings.TrimSuffix(halves[1], "]")
|
|
|
|
index, err := strconv.Atoi(halves[1])
|
|
if err != nil {
|
|
return c.LocalErrorJSQ("Malformed pollinputitem", w, r, user, js)
|
|
}
|
|
|
|
// If there are duplicates, then something has gone horribly wrong, so let's ignore them, this'll likely happen during an attack
|
|
_, exists := pollInputItems[index]
|
|
// TODO: Should we use SanitiseBody instead to keep the newlines?
|
|
if !exists && len(c.SanitiseSingleLine(value)) != 0 {
|
|
pollInputItems[index] = c.SanitiseSingleLine(value)
|
|
if len(pollInputItems) >= maxPollOptions {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make sure the indices are sequential to avoid out of bounds issues
|
|
var seqPollInputItems = make(map[int]string)
|
|
for i := 0; i < len(pollInputItems); i++ {
|
|
seqPollInputItems[i] = pollInputItems[i]
|
|
}
|
|
|
|
pollType := 0 // Basic single choice
|
|
_, err := c.Polls.Create(reply, pollType, seqPollInputItems)
|
|
if err != nil {
|
|
return c.LocalErrorJSQ("Failed to add poll to reply", w, r, user, js) // TODO: Might need to be an internal error as it could leave phantom polls?
|
|
}
|
|
}
|
|
|
|
err = c.Forums.UpdateLastTopic(tid, user.ID, topic.ParentID)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
|
|
c.AddActivityAndNotifyAll(user.ID, topic.CreatedBy, "reply", "topic", tid)
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
|
|
wcount := c.WordCount(content)
|
|
err = user.IncreasePostStats(wcount, false)
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
|
|
nTopic, err := c.Topics.Get(tid)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("Couldn't find the parent topic", w, r, js)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
|
|
page := c.LastPage(nTopic.PostCount, c.Config.ItemsPerPage)
|
|
|
|
rows, err := replyStmts.createReplyPaging.Query(reply.ID, topic.ID)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var rids []int
|
|
for rows.Next() {
|
|
var rid int
|
|
err := rows.Scan(&rid)
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
rids = append(rids, rid)
|
|
}
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
if len(rids) == 0 {
|
|
return c.NotFoundJSQ(w, r, nil, js)
|
|
}
|
|
|
|
if page > 1 {
|
|
var offset int
|
|
if rids[0] == reply.ID {
|
|
offset = 1
|
|
} else if len(rids) == 2 && rids[1] == reply.ID {
|
|
offset = 2
|
|
}
|
|
page = c.LastPage(nTopic.PostCount-(len(rids)+offset), c.Config.ItemsPerPage)
|
|
}
|
|
|
|
counters.PostCounter.Bump()
|
|
skip, rerr := lite.Hooks.VhookSkippable("action_end_create_reply", reply.ID, &user)
|
|
if skip || rerr != nil {
|
|
return rerr
|
|
}
|
|
|
|
prid, _ := strconv.Atoi(r.FormValue("prid"))
|
|
if js && (prid == 0 || rids[0] == prid) {
|
|
outBytes, err := json.Marshal(JsonReply{c.ParseMessage(reply.Content, topic.ParentID, "forums")})
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
w.Write(outBytes)
|
|
} else {
|
|
var spage string
|
|
if page > 1 {
|
|
spage = "?page=" + strconv.Itoa(page)
|
|
}
|
|
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid)+spage+"#post-"+strconv.Itoa(reply.ID), http.StatusSeeOther)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TODO: Disable stat updates in posts handled by plugin_guilds
|
|
// TODO: Update the stats after edits so that we don't under or over decrement stats during deletes
|
|
func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
|
|
js := (r.PostFormValue("js") == "1")
|
|
rid, err := strconv.Atoi(srid)
|
|
if err != nil {
|
|
return c.PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, js)
|
|
}
|
|
|
|
reply, err := c.Rstore.Get(rid)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("The target reply doesn't exist.", w, r, js)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
|
|
topic, err := reply.Topic()
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("The parent topic doesn't exist.", w, r, js)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
|
|
// TODO: Add hooks to make use of headerLite
|
|
lite, ferr := c.SimpleForumUserCheck(w, r, &user, topic.ParentID)
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
if !user.Perms.ViewTopic || !user.Perms.EditReply {
|
|
return c.NoPermissionsJSQ(w, r, user, js)
|
|
}
|
|
if topic.IsClosed && !user.Perms.CloseTopic {
|
|
return c.NoPermissionsJSQ(w, r, user, js)
|
|
}
|
|
|
|
err = reply.SetPost(r.PostFormValue("edit_item"))
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("The parent topic doesn't exist.", w, r, js)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
|
|
// TODO: Avoid the load to get this faster?
|
|
reply, err = c.Rstore.Get(rid)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("The updated reply doesn't exist.", w, r, js)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
|
|
skip, rerr := lite.Hooks.VhookSkippable("action_end_edit_reply", reply.ID, &user)
|
|
if skip || rerr != nil {
|
|
return rerr
|
|
}
|
|
|
|
if !js {
|
|
http.Redirect(w, r, "/topic/"+strconv.Itoa(topic.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther)
|
|
} else {
|
|
outBytes, err := json.Marshal(JsonReply{c.ParseMessage(reply.Content, topic.ParentID, "forums")})
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, js)
|
|
}
|
|
w.Write(outBytes)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TODO: Refactor this
|
|
// TODO: Disable stat updates in posts handled by plugin_guilds
|
|
func ReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
|
|
isJs := (r.PostFormValue("isJs") == "1")
|
|
rid, err := strconv.Atoi(srid)
|
|
if err != nil {
|
|
return c.PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, isJs)
|
|
}
|
|
|
|
reply, err := c.Rstore.Get(rid)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("The reply you tried to delete doesn't exist.", w, r, isJs)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
topic, err := c.Topics.Get(reply.ParentID)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("The parent topic doesn't exist.", w, r, isJs)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
// TODO: Add hooks to make use of headerLite
|
|
lite, ferr := c.SimpleForumUserCheck(w, r, &user, topic.ParentID)
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
if !user.Perms.ViewTopic || !user.Perms.DeleteReply {
|
|
return c.NoPermissionsJSQ(w, r, user, isJs)
|
|
}
|
|
|
|
err = reply.Delete()
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
skip, rerr := lite.Hooks.VhookSkippable("action_end_delete_reply", reply.ID, &user)
|
|
if skip || rerr != nil {
|
|
return rerr
|
|
}
|
|
|
|
//log.Printf("Reply #%d was deleted by c.User #%d", rid, user.ID)
|
|
if !isJs {
|
|
http.Redirect(w, r, "/topic/"+strconv.Itoa(reply.ParentID), http.StatusSeeOther)
|
|
} else {
|
|
w.Write(successJSONBytes)
|
|
}
|
|
|
|
// ? - What happens if an error fires after a redirect...?
|
|
replyCreator, err := c.Users.Get(reply.CreatedBy)
|
|
if err == nil {
|
|
wcount := c.WordCount(reply.Content)
|
|
err = replyCreator.DecreasePostStats(wcount, false)
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
} else if err != sql.ErrNoRows {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
err = c.ModLogs.Create("delete", reply.ParentID, "reply", user.LastIP, user.ID)
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TODO: Avoid uploading this again if the attachment already exists? They'll resolve to the same hash either way, but we could save on some IO / bandwidth here
|
|
// TODO: Enforce the max request limit on all of this topic's attachments
|
|
// TODO: Test this route
|
|
func AddAttachToReplySubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
|
|
rid, err := strconv.Atoi(srid)
|
|
if err != nil {
|
|
return c.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r)
|
|
}
|
|
|
|
reply, err := c.Rstore.Get(rid)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJS("You can't attach to something which doesn't exist!", w, r)
|
|
} else if err != nil {
|
|
return c.InternalErrorJS(err, w, r)
|
|
}
|
|
|
|
topic, err := c.Topics.Get(reply.ParentID)
|
|
if err != nil {
|
|
return c.NotFoundJS(w, r)
|
|
}
|
|
|
|
lite, ferr := c.SimpleForumUserCheck(w, r, &user, topic.ParentID)
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
if !user.Perms.ViewTopic || !user.Perms.EditReply || !user.Perms.UploadFiles {
|
|
return c.NoPermissionsJS(w, r, user)
|
|
}
|
|
if topic.IsClosed && !user.Perms.CloseTopic {
|
|
return c.NoPermissionsJS(w, r, user)
|
|
}
|
|
|
|
// Handle the file attachments
|
|
pathMap, rerr := uploadAttachment(w, r, user, topic.ParentID, "forums", rid, "replies", strconv.Itoa(topic.ID))
|
|
if rerr != nil {
|
|
// TODO: This needs to be a JS error...
|
|
return rerr
|
|
}
|
|
if len(pathMap) == 0 {
|
|
return c.InternalErrorJS(errors.New("no paths for attachment add"), w, r)
|
|
}
|
|
|
|
skip, rerr := lite.Hooks.VhookSkippable("action_end_add_attach_to_reply", reply.ID, &user)
|
|
if skip || rerr != nil {
|
|
return rerr
|
|
}
|
|
|
|
var elemStr string
|
|
for path, aids := range pathMap {
|
|
elemStr += "\"" + path + "\":\"" + aids + "\","
|
|
}
|
|
if len(elemStr) > 1 {
|
|
elemStr = elemStr[:len(elemStr)-1]
|
|
}
|
|
|
|
w.Write([]byte(`{"success":"1","elems":{` + elemStr + `}}`))
|
|
return nil
|
|
}
|
|
|
|
// TODO: Reduce the amount of duplication between this and RemoveAttachFromTopicSubmit
|
|
func RemoveAttachFromReplySubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
|
|
rid, err := strconv.Atoi(srid)
|
|
if err != nil {
|
|
return c.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r)
|
|
}
|
|
|
|
reply, err := c.Rstore.Get(rid)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJS("You can't attach from something which doesn't exist!", w, r)
|
|
} else if err != nil {
|
|
return c.InternalErrorJS(err, w, r)
|
|
}
|
|
|
|
topic, err := c.Topics.Get(reply.ParentID)
|
|
if err != nil {
|
|
return c.NotFoundJS(w, r)
|
|
}
|
|
|
|
lite, ferr := c.SimpleForumUserCheck(w, r, &user, topic.ParentID)
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
if !user.Perms.ViewTopic || !user.Perms.EditReply {
|
|
return c.NoPermissionsJS(w, r, user)
|
|
}
|
|
if topic.IsClosed && !user.Perms.CloseTopic {
|
|
return c.NoPermissionsJS(w, r, user)
|
|
}
|
|
|
|
saids := strings.Split(r.PostFormValue("aids"), ",")
|
|
if len(saids) == 0 {
|
|
return c.LocalErrorJS("No aids provided", w, r)
|
|
}
|
|
for _, said := range saids {
|
|
aid, err := strconv.Atoi(said)
|
|
if err != nil {
|
|
return c.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r)
|
|
}
|
|
rerr := deleteAttachment(w, r, user, aid, true)
|
|
if rerr != nil {
|
|
// TODO: This needs to be a JS error...
|
|
return rerr
|
|
}
|
|
}
|
|
|
|
skip, rerr := lite.Hooks.VhookSkippable("action_end_remove_attach_from_reply", reply.ID, &user)
|
|
if skip || rerr != nil {
|
|
return rerr
|
|
}
|
|
|
|
w.Write(successJSONBytes)
|
|
return nil
|
|
}
|
|
|
|
// TODO: Move the profile reply routes to their own file?
|
|
func ProfileReplyCreateSubmit(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
|
|
if !user.Perms.ViewTopic || !user.Perms.CreateReply {
|
|
return c.NoPermissions(w, r, user)
|
|
}
|
|
|
|
uid, err := strconv.Atoi(r.PostFormValue("uid"))
|
|
if err != nil {
|
|
return c.LocalError("Invalid UID", w, r, user)
|
|
}
|
|
|
|
profileOwner, err := c.Users.Get(uid)
|
|
if err == sql.ErrNoRows {
|
|
return c.LocalError("The profile you're trying to post on doesn't exist.", w, r, user)
|
|
} else if err != nil {
|
|
return c.InternalError(err, w, r)
|
|
}
|
|
|
|
content := c.PreparseMessage(r.PostFormValue("reply-content"))
|
|
// TODO: Fully parse the post and store it in the parsed column
|
|
_, err = c.Prstore.Create(profileOwner.ID, content, user.ID, user.LastIP)
|
|
if err != nil {
|
|
return c.InternalError(err, w, r)
|
|
}
|
|
|
|
// ! Be careful about leaking per-route permission state with &user
|
|
alert := c.Alert{ActorID: user.ID, TargetUserID: profileOwner.ID, Event: "reply", ElementType: "user", ElementID: profileOwner.ID, Actor: &user}
|
|
err = c.AddActivityAndNotifyTarget(alert)
|
|
if err != nil {
|
|
return c.InternalError(err, w, r)
|
|
}
|
|
|
|
counters.PostCounter.Bump()
|
|
http.Redirect(w, r, "/user/"+strconv.Itoa(uid), http.StatusSeeOther)
|
|
return nil
|
|
}
|
|
|
|
func ProfileReplyEditSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
|
|
isJs := (r.PostFormValue("js") == "1")
|
|
|
|
rid, err := strconv.Atoi(srid)
|
|
if err != nil {
|
|
return c.LocalErrorJSQ("The provided Reply ID is not a valid number.", w, r, user, isJs)
|
|
}
|
|
|
|
reply, err := c.Prstore.Get(rid)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("The target reply doesn't exist.", w, r, isJs)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
creator, err := c.Users.Get(reply.CreatedBy)
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
// ? Does the admin understand that this group perm affects this?
|
|
if user.ID != creator.ID && !user.Perms.EditReply {
|
|
return c.NoPermissionsJSQ(w, r, user, isJs)
|
|
}
|
|
|
|
err = reply.SetBody(r.PostFormValue("edit_item"))
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
if !isJs {
|
|
http.Redirect(w, r, "/user/"+strconv.Itoa(creator.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther)
|
|
} else {
|
|
w.Write(successJSONBytes)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ProfileReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
|
|
isJs := (r.PostFormValue("isJs") == "1")
|
|
|
|
rid, err := strconv.Atoi(srid)
|
|
if err != nil {
|
|
return c.LocalErrorJSQ("The provided Reply ID is not a valid number.", w, r, user, isJs)
|
|
}
|
|
|
|
reply, err := c.Prstore.Get(rid)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("The target reply doesn't exist.", w, r, isJs)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
creator, err := c.Users.Get(reply.CreatedBy)
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
if user.ID != creator.ID && !user.Perms.DeleteReply {
|
|
return c.NoPermissionsJSQ(w, r, user, isJs)
|
|
}
|
|
|
|
err = reply.Delete()
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
//log.Printf("The profile post '%d' was deleted by c.User #%d", reply.ID, user.ID)
|
|
|
|
if !isJs {
|
|
//http.Redirect(w,r, "/user/" + strconv.Itoa(creator.ID), http.StatusSeeOther)
|
|
} else {
|
|
w.Write(successJSONBytes)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
|
|
isJs := (r.PostFormValue("isJs") == "1")
|
|
|
|
rid, err := strconv.Atoi(srid)
|
|
if err != nil {
|
|
return c.PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, isJs)
|
|
}
|
|
|
|
reply, err := c.Rstore.Get(rid)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("You can't like something which doesn't exist!", w, r, isJs)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
topic, err := c.Topics.Get(reply.ParentID)
|
|
if err == sql.ErrNoRows {
|
|
return c.PreErrorJSQ("The parent topic doesn't exist.", w, r, isJs)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
// TODO: Add hooks to make use of headerLite
|
|
lite, ferr := c.SimpleForumUserCheck(w, r, &user, topic.ParentID)
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
if !user.Perms.ViewTopic || !user.Perms.LikeItem {
|
|
return c.NoPermissionsJSQ(w, r, user, isJs)
|
|
}
|
|
if reply.CreatedBy == user.ID {
|
|
return c.LocalErrorJSQ("You can't like your own replies", w, r, user, isJs)
|
|
}
|
|
|
|
_, err = c.Users.Get(reply.CreatedBy)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return c.LocalErrorJSQ("The target user doesn't exist", w, r, user, isJs)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
err = reply.Like(user.ID)
|
|
if err == c.ErrAlreadyLiked {
|
|
return c.LocalErrorJSQ("You've already liked this!", w, r, user, isJs)
|
|
} else if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
// ! Be careful about leaking per-route permission state with &user
|
|
alert := c.Alert{ActorID: user.ID, TargetUserID: reply.CreatedBy, Event: "like", ElementType: "post", ElementID: rid, Actor: &user}
|
|
err = c.AddActivityAndNotifyTarget(alert)
|
|
if err != nil {
|
|
return c.InternalErrorJSQ(err, w, r, isJs)
|
|
}
|
|
|
|
skip, rerr := lite.Hooks.VhookSkippable("action_end_like_reply", reply.ID, &user)
|
|
if skip || rerr != nil {
|
|
return rerr
|
|
}
|
|
|
|
if !isJs {
|
|
http.Redirect(w, r, "/topic/"+strconv.Itoa(reply.ParentID), http.StatusSeeOther)
|
|
} else {
|
|
_, _ = w.Write(successJSONBytes)
|
|
}
|
|
return nil
|
|
}
|