github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/reply.go (about) 1 /* 2 * 3 * Reply Resources File 4 * Copyright Azareal 2016 - 2020 5 * 6 */ 7 package common 8 9 import ( 10 "database/sql" 11 "errors" 12 "html" 13 "strconv" 14 "time" 15 16 qgen "github.com/Azareal/Gosora/query_gen" 17 ) 18 19 type ReplyUser struct { 20 Reply 21 22 ContentHtml string 23 UserLink string 24 CreatedByName string 25 Avatar string 26 MicroAvatar string 27 ClassName string 28 Tag string 29 URL string 30 //URLPrefix string 31 //URLName string 32 Group int 33 Level int 34 ActionIcon string 35 36 Attachments []*MiniAttachment 37 Deletable bool 38 } 39 40 type Reply struct { 41 ID int 42 ParentID int 43 Content string 44 CreatedBy int 45 //Group int 46 CreatedAt time.Time 47 LastEdit int 48 LastEditBy int 49 ContentLines int 50 IP string 51 Liked bool 52 LikeCount int 53 AttachCount uint16 54 ActionType string 55 } 56 57 var ErrAlreadyLiked = errors.New("You already liked this!") 58 var replyStmts ReplyStmts 59 60 type ReplyStmts struct { 61 isLiked *sql.Stmt 62 createLike *sql.Stmt 63 edit *sql.Stmt 64 setPoll *sql.Stmt 65 delete *sql.Stmt 66 addLikesToReply *sql.Stmt 67 removeRepliesFromTopic *sql.Stmt 68 deleteLikesForReply *sql.Stmt 69 deleteActivity *sql.Stmt 70 deleteActivitySubs *sql.Stmt 71 72 updateTopicReplies *sql.Stmt 73 updateTopicReplies2 *sql.Stmt 74 75 getAidsOfReply *sql.Stmt 76 } 77 78 func init() { 79 DbInits.Add(func(acc *qgen.Accumulator) error { 80 re := "replies" 81 replyStmts = ReplyStmts{ 82 isLiked: acc.Select("likes").Columns("targetItem").Where("sentBy=? and targetItem=? and targetType='replies'").Prepare(), 83 createLike: acc.Insert("likes").Columns("weight,targetItem,targetType,sentBy,createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(), 84 edit: acc.Update(re).Set("content=?,parsed_content=?").Where("rid=? AND poll=0").Prepare(), 85 setPoll: acc.Update(re).Set("poll=?").Where("rid=? AND poll=0").Prepare(), 86 delete: acc.Delete(re).Where("rid=?").Prepare(), 87 addLikesToReply: acc.Update(re).Set("likeCount=likeCount+?").Where("rid=?").Prepare(), 88 removeRepliesFromTopic: acc.Update("topics").Set("postCount=postCount-?").Where("tid=?").Prepare(), 89 deleteLikesForReply: acc.Delete("likes").Where("targetItem=? AND targetType='replies'").Prepare(), 90 deleteActivity: acc.Delete("activity_stream").Where("elementID=? AND elementType='post'").Prepare(), 91 deleteActivitySubs: acc.Delete("activity_subscriptions").Where("targetID=? AND targetType='post'").Prepare(), 92 93 // 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. 94 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"), 95 updateTopicReplies2: acc.Update("topics").Set("lastReplyAt=createdAt,lastReplyBy=createdBy,lastReplyID=0").Where("postCount=1 AND tid=?").Prepare(), 96 97 getAidsOfReply: acc.Select("attachments").Columns("attachID").Where("originID=? AND originTable='replies'").Prepare(), 98 } 99 return acc.FirstError() 100 }) 101 } 102 103 // TODO: Write tests for this 104 // TODO: Wrap these queries in a transaction to make sure the state is consistent 105 func (r *Reply) Like(uid int) (err error) { 106 var rid int // unused, just here to avoid mutating reply.ID 107 err = replyStmts.isLiked.QueryRow(uid, r.ID).Scan(&rid) 108 if err != nil && err != ErrNoRows { 109 return err 110 } else if err != ErrNoRows { 111 return ErrAlreadyLiked 112 } 113 114 score := 1 115 _, err = replyStmts.createLike.Exec(score, r.ID, "replies", uid) 116 if err != nil { 117 return err 118 } 119 _, err = replyStmts.addLikesToReply.Exec(1, r.ID) 120 if err != nil { 121 return err 122 } 123 _, err = userStmts.incLiked.Exec(1, uid) 124 _ = Rstore.GetCache().Remove(r.ID) 125 return err 126 } 127 128 // TODO: Use a transaction 129 func (r *Reply) Unlike(uid int) error { 130 err := Likes.Delete(r.ID, "replies") 131 if err != nil { 132 return err 133 } 134 _, err = replyStmts.addLikesToReply.Exec(-1, r.ID) 135 if err != nil { 136 return err 137 } 138 _, err = userStmts.decLiked.Exec(1, uid) 139 _ = Rstore.GetCache().Remove(r.ID) 140 return err 141 } 142 143 // TODO: Refresh topic list? 144 func (r *Reply) Delete() error { 145 creator, err := Users.Get(r.CreatedBy) 146 if err == nil { 147 err = creator.DecreasePostStats(WordCount(r.Content), false) 148 if err != nil { 149 return err 150 } 151 } else if err != ErrNoRows { 152 return err 153 } 154 155 _, err = replyStmts.delete.Exec(r.ID) 156 if err != nil { 157 return err 158 } 159 // TODO: Move this bit to *Topic 160 _, err = replyStmts.removeRepliesFromTopic.Exec(1, r.ParentID) 161 if err != nil { 162 return err 163 } 164 _, err = replyStmts.updateTopicReplies.Exec(r.ParentID) 165 if err != nil { 166 return err 167 } 168 _, err = replyStmts.updateTopicReplies2.Exec(r.ParentID) 169 tc := Topics.GetCache() 170 if tc != nil { 171 tc.Remove(r.ParentID) 172 } 173 _ = Rstore.GetCache().Remove(r.ID) 174 if err != nil { 175 return err 176 } 177 _, err = replyStmts.deleteLikesForReply.Exec(r.ID) 178 if err != nil { 179 return err 180 } 181 err = handleReplyAttachments(r.ID) 182 if err != nil { 183 return err 184 } 185 err = Activity.DeleteByParamsExtra("reply", r.ParentID, "topic", strconv.Itoa(r.ID)) 186 if err != nil { 187 return err 188 } 189 _, err = replyStmts.deleteActivitySubs.Exec(r.ID) 190 if err != nil { 191 return err 192 } 193 _, err = replyStmts.deleteActivity.Exec(r.ID) 194 return err 195 } 196 197 func (r *Reply) SetPost(content string) error { 198 topic, err := r.Topic() 199 if err != nil { 200 return err 201 } 202 content = PreparseMessage(html.UnescapeString(content)) 203 parsedContent := ParseMessage(content, topic.ParentID, "forums", nil, nil) 204 _, err = replyStmts.edit.Exec(content, parsedContent, r.ID) // TODO: Sniff if this changed anything to see if we hit an existing poll 205 _ = Rstore.GetCache().Remove(r.ID) 206 return err 207 } 208 209 // TODO: Write tests for this 210 func (r *Reply) SetPoll(pollID int) error { 211 _, err := replyStmts.setPoll.Exec(pollID, r.ID) // TODO: Sniff if this changed anything to see if we hit a poll 212 _ = Rstore.GetCache().Remove(r.ID) 213 return err 214 } 215 216 func (r *Reply) Topic() (*Topic, error) { 217 return Topics.Get(r.ParentID) 218 } 219 220 func (r *Reply) GetID() int { 221 return r.ID 222 } 223 224 func (r *Reply) GetTable() string { 225 return "replies" 226 } 227 228 // Copy gives you a non-pointer concurrency safe copy of the reply 229 func (r *Reply) Copy() Reply { 230 return *r 231 }