gitea源码

reaction.go 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "bytes"
  6. "context"
  7. "fmt"
  8. "strings"
  9. "code.gitea.io/gitea/models/db"
  10. repo_model "code.gitea.io/gitea/models/repo"
  11. user_model "code.gitea.io/gitea/models/user"
  12. "code.gitea.io/gitea/modules/container"
  13. "code.gitea.io/gitea/modules/setting"
  14. "code.gitea.io/gitea/modules/timeutil"
  15. "code.gitea.io/gitea/modules/util"
  16. "xorm.io/builder"
  17. )
  18. // ErrForbiddenIssueReaction is used when a forbidden reaction was try to created
  19. type ErrForbiddenIssueReaction struct {
  20. Reaction string
  21. }
  22. // IsErrForbiddenIssueReaction checks if an error is a ErrForbiddenIssueReaction.
  23. func IsErrForbiddenIssueReaction(err error) bool {
  24. _, ok := err.(ErrForbiddenIssueReaction)
  25. return ok
  26. }
  27. func (err ErrForbiddenIssueReaction) Error() string {
  28. return fmt.Sprintf("'%s' is not an allowed reaction", err.Reaction)
  29. }
  30. func (err ErrForbiddenIssueReaction) Unwrap() error {
  31. return util.ErrPermissionDenied
  32. }
  33. // ErrReactionAlreadyExist is used when a existing reaction was try to created
  34. type ErrReactionAlreadyExist struct {
  35. Reaction string
  36. }
  37. // IsErrReactionAlreadyExist checks if an error is a ErrReactionAlreadyExist.
  38. func IsErrReactionAlreadyExist(err error) bool {
  39. _, ok := err.(ErrReactionAlreadyExist)
  40. return ok
  41. }
  42. func (err ErrReactionAlreadyExist) Error() string {
  43. return fmt.Sprintf("reaction '%s' already exists", err.Reaction)
  44. }
  45. func (err ErrReactionAlreadyExist) Unwrap() error {
  46. return util.ErrAlreadyExist
  47. }
  48. // Reaction represents a reactions on issues and comments.
  49. type Reaction struct {
  50. ID int64 `xorm:"pk autoincr"`
  51. Type string `xorm:"INDEX UNIQUE(s) NOT NULL"`
  52. IssueID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
  53. CommentID int64 `xorm:"INDEX UNIQUE(s)"`
  54. UserID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
  55. OriginalAuthorID int64 `xorm:"INDEX UNIQUE(s) NOT NULL DEFAULT(0)"`
  56. OriginalAuthor string `xorm:"INDEX UNIQUE(s)"`
  57. User *user_model.User `xorm:"-"`
  58. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  59. }
  60. // LoadUser load user of reaction
  61. func (r *Reaction) LoadUser(ctx context.Context) (*user_model.User, error) {
  62. if r.User != nil {
  63. return r.User, nil
  64. }
  65. user, err := user_model.GetUserByID(ctx, r.UserID)
  66. if err != nil {
  67. return nil, err
  68. }
  69. r.User = user
  70. return user, nil
  71. }
  72. // RemapExternalUser ExternalUserRemappable interface
  73. func (r *Reaction) RemapExternalUser(externalName string, externalID, userID int64) error {
  74. r.OriginalAuthor = externalName
  75. r.OriginalAuthorID = externalID
  76. r.UserID = userID
  77. return nil
  78. }
  79. // GetUserID ExternalUserRemappable interface
  80. func (r *Reaction) GetUserID() int64 { return r.UserID }
  81. // GetExternalName ExternalUserRemappable interface
  82. func (r *Reaction) GetExternalName() string { return r.OriginalAuthor }
  83. // GetExternalID ExternalUserRemappable interface
  84. func (r *Reaction) GetExternalID() int64 { return r.OriginalAuthorID }
  85. func init() {
  86. db.RegisterModel(new(Reaction))
  87. }
  88. // FindReactionsOptions describes the conditions to Find reactions
  89. type FindReactionsOptions struct {
  90. db.ListOptions
  91. IssueID int64
  92. CommentID int64
  93. UserID int64
  94. Reaction string
  95. }
  96. func (opts *FindReactionsOptions) toConds() builder.Cond {
  97. // If Issue ID is set add to Query
  98. cond := builder.NewCond()
  99. if opts.IssueID > 0 {
  100. cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueID})
  101. }
  102. // If CommentID is > 0 add to Query
  103. // If it is 0 Query ignore CommentID to select
  104. // If it is -1 it explicit search of Issue Reactions where CommentID = 0
  105. if opts.CommentID > 0 {
  106. cond = cond.And(builder.Eq{"reaction.comment_id": opts.CommentID})
  107. } else if opts.CommentID == -1 {
  108. cond = cond.And(builder.Eq{"reaction.comment_id": 0})
  109. }
  110. if opts.UserID > 0 {
  111. cond = cond.And(builder.Eq{
  112. "reaction.user_id": opts.UserID,
  113. "reaction.original_author_id": 0,
  114. })
  115. }
  116. if opts.Reaction != "" {
  117. cond = cond.And(builder.Eq{"reaction.type": opts.Reaction})
  118. }
  119. return cond
  120. }
  121. // FindCommentReactions returns a ReactionList of all reactions from an comment
  122. func FindCommentReactions(ctx context.Context, issueID, commentID int64) (ReactionList, int64, error) {
  123. return FindReactions(ctx, FindReactionsOptions{
  124. IssueID: issueID,
  125. CommentID: commentID,
  126. })
  127. }
  128. // FindIssueReactions returns a ReactionList of all reactions from an issue
  129. func FindIssueReactions(ctx context.Context, issueID int64, listOptions db.ListOptions) (ReactionList, int64, error) {
  130. return FindReactions(ctx, FindReactionsOptions{
  131. ListOptions: listOptions,
  132. IssueID: issueID,
  133. CommentID: -1,
  134. })
  135. }
  136. // FindReactions returns a ReactionList of all reactions from an issue or a comment
  137. func FindReactions(ctx context.Context, opts FindReactionsOptions) (ReactionList, int64, error) {
  138. sess := db.GetEngine(ctx).
  139. Where(opts.toConds()).
  140. In("reaction.`type`", setting.UI.Reactions).
  141. Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id")
  142. if opts.Page > 0 {
  143. sess = db.SetSessionPagination(sess, &opts)
  144. reactions := make([]*Reaction, 0, opts.PageSize)
  145. count, err := sess.FindAndCount(&reactions)
  146. return reactions, count, err
  147. }
  148. reactions := make([]*Reaction, 0, 10)
  149. count, err := sess.FindAndCount(&reactions)
  150. return reactions, count, err
  151. }
  152. func createReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) {
  153. reaction := &Reaction{
  154. Type: opts.Type,
  155. UserID: opts.DoerID,
  156. IssueID: opts.IssueID,
  157. CommentID: opts.CommentID,
  158. }
  159. findOpts := FindReactionsOptions{
  160. IssueID: opts.IssueID,
  161. CommentID: opts.CommentID,
  162. Reaction: opts.Type,
  163. UserID: opts.DoerID,
  164. }
  165. if findOpts.CommentID == 0 {
  166. // explicit search of Issue Reactions where CommentID = 0
  167. findOpts.CommentID = -1
  168. }
  169. existingR, _, err := FindReactions(ctx, findOpts)
  170. if err != nil {
  171. return nil, err
  172. }
  173. if len(existingR) > 0 {
  174. return existingR[0], ErrReactionAlreadyExist{Reaction: opts.Type}
  175. }
  176. if err := db.Insert(ctx, reaction); err != nil {
  177. return nil, err
  178. }
  179. return reaction, nil
  180. }
  181. // ReactionOptions defines options for creating or deleting reactions
  182. type ReactionOptions struct {
  183. Type string
  184. DoerID int64
  185. IssueID int64
  186. CommentID int64
  187. }
  188. // CreateReaction creates reaction for issue or comment.
  189. func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) {
  190. if !setting.UI.ReactionsLookup.Contains(opts.Type) {
  191. return nil, ErrForbiddenIssueReaction{opts.Type}
  192. }
  193. return db.WithTx2(ctx, func(ctx context.Context) (*Reaction, error) {
  194. return createReaction(ctx, opts)
  195. })
  196. }
  197. // DeleteReaction deletes reaction for issue or comment.
  198. func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
  199. reaction := &Reaction{
  200. Type: opts.Type,
  201. UserID: opts.DoerID,
  202. IssueID: opts.IssueID,
  203. CommentID: opts.CommentID,
  204. }
  205. sess := db.GetEngine(ctx).Where("original_author_id = 0")
  206. if opts.CommentID == -1 {
  207. reaction.CommentID = 0
  208. sess.MustCols("comment_id")
  209. }
  210. _, err := sess.Delete(reaction)
  211. return err
  212. }
  213. // DeleteIssueReaction deletes a reaction on issue.
  214. func DeleteIssueReaction(ctx context.Context, doerID, issueID int64, content string) error {
  215. return DeleteReaction(ctx, &ReactionOptions{
  216. Type: content,
  217. DoerID: doerID,
  218. IssueID: issueID,
  219. CommentID: -1,
  220. })
  221. }
  222. // DeleteCommentReaction deletes a reaction on comment.
  223. func DeleteCommentReaction(ctx context.Context, doerID, issueID, commentID int64, content string) error {
  224. return DeleteReaction(ctx, &ReactionOptions{
  225. Type: content,
  226. DoerID: doerID,
  227. IssueID: issueID,
  228. CommentID: commentID,
  229. })
  230. }
  231. // ReactionList represents list of reactions
  232. type ReactionList []*Reaction
  233. // HasUser check if user has reacted
  234. func (list ReactionList) HasUser(userID int64) bool {
  235. if userID == 0 {
  236. return false
  237. }
  238. for _, reaction := range list {
  239. if reaction.OriginalAuthor == "" && reaction.UserID == userID {
  240. return true
  241. }
  242. }
  243. return false
  244. }
  245. // GroupByType returns reactions grouped by type
  246. func (list ReactionList) GroupByType() map[string]ReactionList {
  247. reactions := make(map[string]ReactionList)
  248. for _, reaction := range list {
  249. reactions[reaction.Type] = append(reactions[reaction.Type], reaction)
  250. }
  251. return reactions
  252. }
  253. func (list ReactionList) getUserIDs() []int64 {
  254. return container.FilterSlice(list, func(reaction *Reaction) (int64, bool) {
  255. if reaction.OriginalAuthor != "" {
  256. return 0, false
  257. }
  258. return reaction.UserID, true
  259. })
  260. }
  261. func valuesUser(m map[int64]*user_model.User) []*user_model.User {
  262. values := make([]*user_model.User, 0, len(m))
  263. for _, v := range m {
  264. values = append(values, v)
  265. }
  266. return values
  267. }
  268. // newMigrationOriginalUser creates and returns a fake user for external user
  269. func newMigrationOriginalUser(name string) *user_model.User {
  270. return &user_model.User{ID: 0, Name: name, LowerName: strings.ToLower(name)}
  271. }
  272. // LoadUsers loads reactions' all users
  273. func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Repository) ([]*user_model.User, error) {
  274. if len(list) == 0 {
  275. return nil, nil
  276. }
  277. userIDs := list.getUserIDs()
  278. userMaps := make(map[int64]*user_model.User, len(userIDs))
  279. err := db.GetEngine(ctx).
  280. In("id", userIDs).
  281. Find(&userMaps)
  282. if err != nil {
  283. return nil, fmt.Errorf("find user: %w", err)
  284. }
  285. for _, reaction := range list {
  286. if reaction.OriginalAuthor != "" {
  287. reaction.User = newMigrationOriginalUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name()))
  288. } else if user, ok := userMaps[reaction.UserID]; ok {
  289. reaction.User = user
  290. } else {
  291. reaction.User = user_model.NewGhostUser()
  292. }
  293. }
  294. return valuesUser(userMaps), nil
  295. }
  296. // GetFirstUsers returns first reacted user display names separated by comma
  297. func (list ReactionList) GetFirstUsers() string {
  298. var buffer bytes.Buffer
  299. rem := setting.UI.ReactionMaxUserNum
  300. for _, reaction := range list {
  301. if buffer.Len() > 0 {
  302. buffer.WriteString(", ")
  303. }
  304. buffer.WriteString(reaction.User.Name)
  305. if rem--; rem == 0 {
  306. break
  307. }
  308. }
  309. return buffer.String()
  310. }
  311. // GetMoreUserCount returns count of not shown users in reaction tooltip
  312. func (list ReactionList) GetMoreUserCount() int {
  313. if len(list) <= setting.UI.ReactionMaxUserNum {
  314. return 0
  315. }
  316. return len(list) - setting.UI.ReactionMaxUserNum
  317. }