f6a94d39d6
Add tests to check the LastReplyID for a topic before and after a reply is deleted.
232 lines
6.3 KiB
Go
232 lines
6.3 KiB
Go
/*
|
|
*
|
|
* Reply Resources File
|
|
* Copyright Azareal 2016 - 2020
|
|
*
|
|
*/
|
|
package common
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"html"
|
|
"strconv"
|
|
"time"
|
|
|
|
qgen "github.com/Azareal/Gosora/query_gen"
|
|
)
|
|
|
|
type ReplyUser struct {
|
|
Reply
|
|
|
|
ContentHtml string
|
|
UserLink string
|
|
CreatedByName string
|
|
Avatar string
|
|
MicroAvatar string
|
|
ClassName string
|
|
Tag string
|
|
URL string
|
|
//URLPrefix string
|
|
//URLName string
|
|
Group int
|
|
Level int
|
|
ActionIcon string
|
|
|
|
Attachments []*MiniAttachment
|
|
Deletable bool
|
|
}
|
|
|
|
type Reply struct {
|
|
ID int
|
|
ParentID int
|
|
Content string
|
|
CreatedBy int
|
|
//Group int
|
|
CreatedAt time.Time
|
|
LastEdit int
|
|
LastEditBy int
|
|
ContentLines int
|
|
IP string
|
|
Liked bool
|
|
LikeCount int
|
|
AttachCount uint16
|
|
ActionType string
|
|
}
|
|
|
|
var ErrAlreadyLiked = errors.New("You already liked this!")
|
|
var replyStmts ReplyStmts
|
|
|
|
type ReplyStmts struct {
|
|
isLiked *sql.Stmt
|
|
createLike *sql.Stmt
|
|
edit *sql.Stmt
|
|
setPoll *sql.Stmt
|
|
delete *sql.Stmt
|
|
addLikesToReply *sql.Stmt
|
|
removeRepliesFromTopic *sql.Stmt
|
|
deleteLikesForReply *sql.Stmt
|
|
deleteActivity *sql.Stmt
|
|
deleteActivitySubs *sql.Stmt
|
|
|
|
updateTopicReplies *sql.Stmt
|
|
updateTopicReplies2 *sql.Stmt
|
|
|
|
getAidsOfReply *sql.Stmt
|
|
}
|
|
|
|
func init() {
|
|
DbInits.Add(func(acc *qgen.Accumulator) error {
|
|
re := "replies"
|
|
replyStmts = ReplyStmts{
|
|
isLiked: acc.Select("likes").Columns("targetItem").Where("sentBy=? and targetItem=? and targetType='replies'").Prepare(),
|
|
createLike: acc.Insert("likes").Columns("weight,targetItem,targetType,sentBy,createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(),
|
|
edit: acc.Update(re).Set("content=?,parsed_content=?").Where("rid=? AND poll=0").Prepare(),
|
|
setPoll: acc.Update(re).Set("poll=?").Where("rid=? AND poll=0").Prepare(),
|
|
delete: acc.Delete(re).Where("rid=?").Prepare(),
|
|
addLikesToReply: acc.Update(re).Set("likeCount=likeCount+?").Where("rid=?").Prepare(),
|
|
removeRepliesFromTopic: acc.Update("topics").Set("postCount=postCount-?").Where("tid=?").Prepare(),
|
|
deleteLikesForReply: acc.Delete("likes").Where("targetItem=? AND targetType='replies'").Prepare(),
|
|
deleteActivity: acc.Delete("activity_stream").Where("elementID=? AND elementType='post'").Prepare(),
|
|
deleteActivitySubs: acc.Delete("activity_subscriptions").Where("targetID=? AND targetType='post'").Prepare(),
|
|
|
|
// TODO: Optimise this to avoid firing an update if it's not the last reply in a topic. We will need to set lastReplyID properly in other places and in the patcher first so we can use it here.
|
|
updateTopicReplies: acc.RawPrepare("UPDATE topics t INNER JOIN replies r ON t.tid=r.tid SET t.lastReplyBy=r.createdBy, t.lastReplyAt=r.createdAt, t.lastReplyID=r.rid WHERE t.tid=? ORDER BY r.rid DESC"),
|
|
updateTopicReplies2: acc.Update("topics").Set("lastReplyAt=createdAt,lastReplyBy=createdBy,lastReplyID=0").Where("postCount=1 AND tid=?").Prepare(),
|
|
|
|
getAidsOfReply: acc.Select("attachments").Columns("attachID").Where("originID=? AND originTable='replies'").Prepare(),
|
|
}
|
|
return acc.FirstError()
|
|
})
|
|
}
|
|
|
|
// TODO: Write tests for this
|
|
// TODO: Wrap these queries in a transaction to make sure the state is consistent
|
|
func (r *Reply) Like(uid int) (err error) {
|
|
var rid int // unused, just here to avoid mutating reply.ID
|
|
err = replyStmts.isLiked.QueryRow(uid, r.ID).Scan(&rid)
|
|
if err != nil && err != ErrNoRows {
|
|
return err
|
|
} else if err != ErrNoRows {
|
|
return ErrAlreadyLiked
|
|
}
|
|
|
|
score := 1
|
|
_, err = replyStmts.createLike.Exec(score, r.ID, "replies", uid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = replyStmts.addLikesToReply.Exec(1, r.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = userStmts.incLiked.Exec(1, uid)
|
|
_ = Rstore.GetCache().Remove(r.ID)
|
|
return err
|
|
}
|
|
|
|
// TODO: Use a transaction
|
|
func (r *Reply) Unlike(uid int) error {
|
|
err := Likes.Delete(r.ID, "replies")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = replyStmts.addLikesToReply.Exec(-1, r.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = userStmts.decLiked.Exec(1, uid)
|
|
_ = Rstore.GetCache().Remove(r.ID)
|
|
return err
|
|
}
|
|
|
|
// TODO: Refresh topic list?
|
|
func (r *Reply) Delete() error {
|
|
creator, err := Users.Get(r.CreatedBy)
|
|
if err == nil {
|
|
err = creator.DecreasePostStats(WordCount(r.Content), false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if err != ErrNoRows {
|
|
return err
|
|
}
|
|
|
|
_, err = replyStmts.delete.Exec(r.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// TODO: Move this bit to *Topic
|
|
_, err = replyStmts.removeRepliesFromTopic.Exec(1, r.ParentID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = replyStmts.updateTopicReplies.Exec(r.ParentID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = replyStmts.updateTopicReplies2.Exec(r.ParentID)
|
|
tc := Topics.GetCache()
|
|
if tc != nil {
|
|
tc.Remove(r.ParentID)
|
|
}
|
|
_ = Rstore.GetCache().Remove(r.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = replyStmts.deleteLikesForReply.Exec(r.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = handleReplyAttachments(r.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = Activity.DeleteByParamsExtra("reply", r.ParentID, "topic", strconv.Itoa(r.ID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = replyStmts.deleteActivitySubs.Exec(r.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = replyStmts.deleteActivity.Exec(r.ID)
|
|
return err
|
|
}
|
|
|
|
func (r *Reply) SetPost(content string) error {
|
|
topic, err := r.Topic()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
content = PreparseMessage(html.UnescapeString(content))
|
|
parsedContent := ParseMessage(content, topic.ParentID, "forums", nil, nil)
|
|
_, err = replyStmts.edit.Exec(content, parsedContent, r.ID) // TODO: Sniff if this changed anything to see if we hit an existing poll
|
|
_ = Rstore.GetCache().Remove(r.ID)
|
|
return err
|
|
}
|
|
|
|
// TODO: Write tests for this
|
|
func (r *Reply) SetPoll(pollID int) error {
|
|
_, err := replyStmts.setPoll.Exec(pollID, r.ID) // TODO: Sniff if this changed anything to see if we hit a poll
|
|
_ = Rstore.GetCache().Remove(r.ID)
|
|
return err
|
|
}
|
|
|
|
func (r *Reply) Topic() (*Topic, error) {
|
|
return Topics.Get(r.ParentID)
|
|
}
|
|
|
|
func (r *Reply) GetID() int {
|
|
return r.ID
|
|
}
|
|
|
|
func (r *Reply) GetTable() string {
|
|
return "replies"
|
|
}
|
|
|
|
// Copy gives you a non-pointer concurrency safe copy of the reply
|
|
func (r *Reply) Copy() Reply {
|
|
return *r
|
|
}
|