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  }