您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package activities
  5. import (
  6. "context"
  7. "fmt"
  8. "net/url"
  9. "path"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "code.gitea.io/gitea/models/db"
  14. issues_model "code.gitea.io/gitea/models/issues"
  15. "code.gitea.io/gitea/models/organization"
  16. access_model "code.gitea.io/gitea/models/perm/access"
  17. repo_model "code.gitea.io/gitea/models/repo"
  18. "code.gitea.io/gitea/models/unit"
  19. user_model "code.gitea.io/gitea/models/user"
  20. "code.gitea.io/gitea/modules/base"
  21. "code.gitea.io/gitea/modules/git"
  22. "code.gitea.io/gitea/modules/log"
  23. "code.gitea.io/gitea/modules/optional"
  24. "code.gitea.io/gitea/modules/setting"
  25. "code.gitea.io/gitea/modules/structs"
  26. "code.gitea.io/gitea/modules/timeutil"
  27. "xorm.io/builder"
  28. "xorm.io/xorm/schemas"
  29. )
  30. // ActionType represents the type of an action.
  31. type ActionType int
  32. // Possible action types.
  33. const (
  34. ActionCreateRepo ActionType = iota + 1 // 1
  35. ActionRenameRepo // 2
  36. ActionStarRepo // 3
  37. ActionWatchRepo // 4
  38. ActionCommitRepo // 5
  39. ActionCreateIssue // 6
  40. ActionCreatePullRequest // 7
  41. ActionTransferRepo // 8
  42. ActionPushTag // 9
  43. ActionCommentIssue // 10
  44. ActionMergePullRequest // 11
  45. ActionCloseIssue // 12
  46. ActionReopenIssue // 13
  47. ActionClosePullRequest // 14
  48. ActionReopenPullRequest // 15
  49. ActionDeleteTag // 16
  50. ActionDeleteBranch // 17
  51. ActionMirrorSyncPush // 18
  52. ActionMirrorSyncCreate // 19
  53. ActionMirrorSyncDelete // 20
  54. ActionApprovePullRequest // 21
  55. ActionRejectPullRequest // 22
  56. ActionCommentPull // 23
  57. ActionPublishRelease // 24
  58. ActionPullReviewDismissed // 25
  59. ActionPullRequestReadyForReview // 26
  60. ActionAutoMergePullRequest // 27
  61. )
  62. func (at ActionType) String() string {
  63. switch at {
  64. case ActionCreateRepo:
  65. return "create_repo"
  66. case ActionRenameRepo:
  67. return "rename_repo"
  68. case ActionStarRepo:
  69. return "star_repo"
  70. case ActionWatchRepo:
  71. return "watch_repo"
  72. case ActionCommitRepo:
  73. return "commit_repo"
  74. case ActionCreateIssue:
  75. return "create_issue"
  76. case ActionCreatePullRequest:
  77. return "create_pull_request"
  78. case ActionTransferRepo:
  79. return "transfer_repo"
  80. case ActionPushTag:
  81. return "push_tag"
  82. case ActionCommentIssue:
  83. return "comment_issue"
  84. case ActionMergePullRequest:
  85. return "merge_pull_request"
  86. case ActionCloseIssue:
  87. return "close_issue"
  88. case ActionReopenIssue:
  89. return "reopen_issue"
  90. case ActionClosePullRequest:
  91. return "close_pull_request"
  92. case ActionReopenPullRequest:
  93. return "reopen_pull_request"
  94. case ActionDeleteTag:
  95. return "delete_tag"
  96. case ActionDeleteBranch:
  97. return "delete_branch"
  98. case ActionMirrorSyncPush:
  99. return "mirror_sync_push"
  100. case ActionMirrorSyncCreate:
  101. return "mirror_sync_create"
  102. case ActionMirrorSyncDelete:
  103. return "mirror_sync_delete"
  104. case ActionApprovePullRequest:
  105. return "approve_pull_request"
  106. case ActionRejectPullRequest:
  107. return "reject_pull_request"
  108. case ActionCommentPull:
  109. return "comment_pull"
  110. case ActionPublishRelease:
  111. return "publish_release"
  112. case ActionPullReviewDismissed:
  113. return "pull_review_dismissed"
  114. case ActionPullRequestReadyForReview:
  115. return "pull_request_ready_for_review"
  116. case ActionAutoMergePullRequest:
  117. return "auto_merge_pull_request"
  118. default:
  119. return "action-" + strconv.Itoa(int(at))
  120. }
  121. }
  122. func (at ActionType) InActions(actions ...string) bool {
  123. for _, action := range actions {
  124. if action == at.String() {
  125. return true
  126. }
  127. }
  128. return false
  129. }
  130. // Action represents user operation type and other information to
  131. // repository. It implemented interface base.Actioner so that can be
  132. // used in template render.
  133. type Action struct {
  134. ID int64 `xorm:"pk autoincr"`
  135. UserID int64 `xorm:"INDEX"` // Receiver user id.
  136. OpType ActionType
  137. ActUserID int64 // Action user id.
  138. ActUser *user_model.User `xorm:"-"`
  139. RepoID int64
  140. Repo *repo_model.Repository `xorm:"-"`
  141. CommentID int64 `xorm:"INDEX"`
  142. Comment *issues_model.Comment `xorm:"-"`
  143. Issue *issues_model.Issue `xorm:"-"` // get the issue id from content
  144. IsDeleted bool `xorm:"NOT NULL DEFAULT false"`
  145. RefName string
  146. IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
  147. IsPrivateView bool `xorm:"-"`
  148. Content string `xorm:"TEXT"`
  149. CreatedUnix timeutil.TimeStamp `xorm:"created"`
  150. }
  151. func init() {
  152. db.RegisterModel(new(Action))
  153. }
  154. // TableIndices implements xorm's TableIndices interface
  155. func (a *Action) TableIndices() []*schemas.Index {
  156. repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType)
  157. repoIndex.AddColumn("repo_id", "user_id", "is_deleted")
  158. actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType)
  159. actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted")
  160. cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType)
  161. cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
  162. indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex}
  163. return indices
  164. }
  165. // GetOpType gets the ActionType of this action.
  166. func (a *Action) GetOpType() ActionType {
  167. return a.OpType
  168. }
  169. // LoadActUser loads a.ActUser
  170. func (a *Action) LoadActUser(ctx context.Context) {
  171. if a.ActUser != nil {
  172. return
  173. }
  174. var err error
  175. a.ActUser, err = user_model.GetUserByID(ctx, a.ActUserID)
  176. if err == nil {
  177. return
  178. } else if user_model.IsErrUserNotExist(err) {
  179. a.ActUser = user_model.NewGhostUser()
  180. } else {
  181. log.Error("GetUserByID(%d): %v", a.ActUserID, err)
  182. }
  183. }
  184. func (a *Action) loadRepo(ctx context.Context) {
  185. if a.Repo != nil {
  186. return
  187. }
  188. var err error
  189. a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID)
  190. if err != nil {
  191. log.Error("repo_model.GetRepositoryByID(%d): %v", a.RepoID, err)
  192. }
  193. }
  194. // GetActFullName gets the action's user full name.
  195. func (a *Action) GetActFullName(ctx context.Context) string {
  196. a.LoadActUser(ctx)
  197. return a.ActUser.FullName
  198. }
  199. // GetActUserName gets the action's user name.
  200. func (a *Action) GetActUserName(ctx context.Context) string {
  201. a.LoadActUser(ctx)
  202. return a.ActUser.Name
  203. }
  204. // ShortActUserName gets the action's user name trimmed to max 20
  205. // chars.
  206. func (a *Action) ShortActUserName(ctx context.Context) string {
  207. return base.EllipsisString(a.GetActUserName(ctx), 20)
  208. }
  209. // GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
  210. func (a *Action) GetActDisplayName(ctx context.Context) string {
  211. if setting.UI.DefaultShowFullName {
  212. trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx))
  213. if len(trimmedFullName) > 0 {
  214. return trimmedFullName
  215. }
  216. }
  217. return a.ShortActUserName(ctx)
  218. }
  219. // GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
  220. func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
  221. if setting.UI.DefaultShowFullName {
  222. return a.ShortActUserName(ctx)
  223. }
  224. return a.GetActFullName(ctx)
  225. }
  226. // GetRepoUserName returns the name of the action repository owner.
  227. func (a *Action) GetRepoUserName(ctx context.Context) string {
  228. a.loadRepo(ctx)
  229. return a.Repo.OwnerName
  230. }
  231. // ShortRepoUserName returns the name of the action repository owner
  232. // trimmed to max 20 chars.
  233. func (a *Action) ShortRepoUserName(ctx context.Context) string {
  234. return base.EllipsisString(a.GetRepoUserName(ctx), 20)
  235. }
  236. // GetRepoName returns the name of the action repository.
  237. func (a *Action) GetRepoName(ctx context.Context) string {
  238. a.loadRepo(ctx)
  239. return a.Repo.Name
  240. }
  241. // ShortRepoName returns the name of the action repository
  242. // trimmed to max 33 chars.
  243. func (a *Action) ShortRepoName(ctx context.Context) string {
  244. return base.EllipsisString(a.GetRepoName(ctx), 33)
  245. }
  246. // GetRepoPath returns the virtual path to the action repository.
  247. func (a *Action) GetRepoPath(ctx context.Context) string {
  248. return path.Join(a.GetRepoUserName(ctx), a.GetRepoName(ctx))
  249. }
  250. // ShortRepoPath returns the virtual path to the action repository
  251. // trimmed to max 20 + 1 + 33 chars.
  252. func (a *Action) ShortRepoPath(ctx context.Context) string {
  253. return path.Join(a.ShortRepoUserName(ctx), a.ShortRepoName(ctx))
  254. }
  255. // GetRepoLink returns relative link to action repository.
  256. func (a *Action) GetRepoLink(ctx context.Context) string {
  257. // path.Join will skip empty strings
  258. return path.Join(setting.AppSubURL, "/", url.PathEscape(a.GetRepoUserName(ctx)), url.PathEscape(a.GetRepoName(ctx)))
  259. }
  260. // GetRepoAbsoluteLink returns the absolute link to action repository.
  261. func (a *Action) GetRepoAbsoluteLink(ctx context.Context) string {
  262. return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx))
  263. }
  264. func (a *Action) loadComment(ctx context.Context) (err error) {
  265. if a.CommentID == 0 || a.Comment != nil {
  266. return nil
  267. }
  268. a.Comment, err = issues_model.GetCommentByID(ctx, a.CommentID)
  269. return err
  270. }
  271. // GetCommentHTMLURL returns link to action comment.
  272. func (a *Action) GetCommentHTMLURL(ctx context.Context) string {
  273. if a == nil {
  274. return "#"
  275. }
  276. _ = a.loadComment(ctx)
  277. if a.Comment != nil {
  278. return a.Comment.HTMLURL(ctx)
  279. }
  280. if err := a.LoadIssue(ctx); err != nil || a.Issue == nil {
  281. return "#"
  282. }
  283. if err := a.Issue.LoadRepo(ctx); err != nil {
  284. return "#"
  285. }
  286. return a.Issue.HTMLURL()
  287. }
  288. // GetCommentLink returns link to action comment.
  289. func (a *Action) GetCommentLink(ctx context.Context) string {
  290. if a == nil {
  291. return "#"
  292. }
  293. _ = a.loadComment(ctx)
  294. if a.Comment != nil {
  295. return a.Comment.Link(ctx)
  296. }
  297. if err := a.LoadIssue(ctx); err != nil || a.Issue == nil {
  298. return "#"
  299. }
  300. if err := a.Issue.LoadRepo(ctx); err != nil {
  301. return "#"
  302. }
  303. return a.Issue.Link()
  304. }
  305. // GetBranch returns the action's repository branch.
  306. func (a *Action) GetBranch() string {
  307. return strings.TrimPrefix(a.RefName, git.BranchPrefix)
  308. }
  309. // GetRefLink returns the action's ref link.
  310. func (a *Action) GetRefLink(ctx context.Context) string {
  311. return git.RefURL(a.GetRepoLink(ctx), a.RefName)
  312. }
  313. // GetTag returns the action's repository tag.
  314. func (a *Action) GetTag() string {
  315. return strings.TrimPrefix(a.RefName, git.TagPrefix)
  316. }
  317. // GetContent returns the action's content.
  318. func (a *Action) GetContent() string {
  319. return a.Content
  320. }
  321. // GetCreate returns the action creation time.
  322. func (a *Action) GetCreate() time.Time {
  323. return a.CreatedUnix.AsTime()
  324. }
  325. func (a *Action) IsIssueEvent() bool {
  326. return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request")
  327. }
  328. // GetIssueInfos returns a list of associated information with the action.
  329. func (a *Action) GetIssueInfos() []string {
  330. // make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length
  331. ret := strings.SplitN(a.Content, "|", 3)
  332. for len(ret) < 3 {
  333. ret = append(ret, "")
  334. }
  335. return ret
  336. }
  337. func (a *Action) getIssueIndex() int64 {
  338. infos := a.GetIssueInfos()
  339. if len(infos) == 0 {
  340. return 0
  341. }
  342. index, _ := strconv.ParseInt(infos[0], 10, 64)
  343. return index
  344. }
  345. func (a *Action) LoadIssue(ctx context.Context) error {
  346. if a.Issue != nil {
  347. return nil
  348. }
  349. if index := a.getIssueIndex(); index > 0 {
  350. issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
  351. if err != nil {
  352. return err
  353. }
  354. a.Issue = issue
  355. a.Issue.Repo = a.Repo
  356. }
  357. return nil
  358. }
  359. // GetIssueTitle returns the title of first issue associated with the action.
  360. func (a *Action) GetIssueTitle(ctx context.Context) string {
  361. if err := a.LoadIssue(ctx); err != nil {
  362. log.Error("LoadIssue: %v", err)
  363. return "<500 when get issue>"
  364. }
  365. if a.Issue == nil {
  366. return "<Issue not found>"
  367. }
  368. return a.Issue.Title
  369. }
  370. // GetIssueContent returns the content of first issue associated with this action.
  371. func (a *Action) GetIssueContent(ctx context.Context) string {
  372. if err := a.LoadIssue(ctx); err != nil {
  373. log.Error("LoadIssue: %v", err)
  374. return "<500 when get issue>"
  375. }
  376. if a.Issue == nil {
  377. return "<Content not found>"
  378. }
  379. return a.Issue.Content
  380. }
  381. // GetFeedsOptions options for retrieving feeds
  382. type GetFeedsOptions struct {
  383. db.ListOptions
  384. RequestedUser *user_model.User // the user we want activity for
  385. RequestedTeam *organization.Team // the team we want activity for
  386. RequestedRepo *repo_model.Repository // the repo we want activity for
  387. Actor *user_model.User // the user viewing the activity
  388. IncludePrivate bool // include private actions
  389. OnlyPerformedBy bool // only actions performed by requested user
  390. IncludeDeleted bool // include deleted actions
  391. Date string // the day we want activity for: YYYY-MM-DD
  392. }
  393. // GetFeeds returns actions according to the provided options
  394. func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) {
  395. if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
  396. return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
  397. }
  398. cond, err := activityQueryCondition(ctx, opts)
  399. if err != nil {
  400. return nil, 0, err
  401. }
  402. sess := db.GetEngine(ctx).Where(cond).
  403. Select("`action`.*"). // this line will avoid select other joined table's columns
  404. Join("INNER", "repository", "`repository`.id = `action`.repo_id")
  405. opts.SetDefaultValues()
  406. sess = db.SetSessionPagination(sess, &opts)
  407. actions := make(ActionList, 0, opts.PageSize)
  408. count, err := sess.Desc("`action`.created_unix").FindAndCount(&actions)
  409. if err != nil {
  410. return nil, 0, fmt.Errorf("FindAndCount: %w", err)
  411. }
  412. if err := actions.LoadAttributes(ctx); err != nil {
  413. return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
  414. }
  415. if opts.Actor != nil && opts.RequestedUser != nil {
  416. isPrivateForActor := !opts.Actor.IsAdmin && opts.Actor.ID != opts.RequestedUser.ID
  417. // cache user repo read permissions
  418. canReadRepo := make(map[int64]optional.Option[bool], 0)
  419. for _, action := range actions {
  420. action.IsPrivateView = isPrivateForActor && action.IsPrivate
  421. if action.IsPrivateView && action.Repo.Owner.IsOrganization() {
  422. if !canReadRepo[action.Repo.ID].Has() {
  423. perm, err := access_model.GetUserRepoPermission(ctx, action.Repo, opts.Actor)
  424. if err != nil {
  425. return nil, 0, fmt.Errorf("GetUserRepoPermission: %w", err)
  426. }
  427. canRead := perm.CanRead(unit.TypeCode)
  428. action.IsPrivateView = !canRead
  429. canReadRepo[action.Repo.ID] = optional.Option[bool]{canRead}
  430. }
  431. action.IsPrivateView = !canReadRepo[action.Repo.ID].Value()
  432. }
  433. }
  434. } else {
  435. for _, action := range actions {
  436. action.IsPrivateView = action.IsPrivate
  437. }
  438. }
  439. return actions, count, nil
  440. }
  441. // ActivityReadable return whether doer can read activities of user
  442. func ActivityReadable(user, doer *user_model.User) bool {
  443. if doer != nil && (doer.IsAdmin || user.ID == doer.ID) {
  444. return true
  445. }
  446. if user.ActivityVisibility.ShowNone() {
  447. return false
  448. }
  449. return true
  450. }
  451. func activityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) {
  452. cond := builder.NewCond()
  453. if opts.RequestedTeam != nil && opts.RequestedUser == nil {
  454. org, err := user_model.GetUserByID(ctx, opts.RequestedTeam.OrgID)
  455. if err != nil {
  456. return nil, err
  457. }
  458. opts.RequestedUser = org
  459. }
  460. // check activity visibility for actor ( similar to activityReadable() )
  461. if opts.Actor == nil {
  462. cond = cond.And(builder.In("act_user_id",
  463. builder.Select("`user`.id").Where(
  464. builder.Eq{"visibility": structs.VisibleTypePublic},
  465. ).Where(
  466. builder.Neq{"activity_visibility": structs.ActivityVisibilityNone},
  467. ).From("`user`"),
  468. ))
  469. } else if !opts.Actor.IsAdmin {
  470. uidCond := builder.Select("`user`.id").From("`user`").Where(
  471. builder.Neq{"activity_visibility": structs.ActivityVisibilityNone},
  472. ).Where(
  473. builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited),
  474. ).Or(builder.Eq{"id": opts.Actor.ID})
  475. if opts.RequestedUser != nil {
  476. if opts.RequestedUser.IsOrganization() {
  477. // An organization can always see the activities whose `act_user_id` is the same as its id.
  478. uidCond = uidCond.Or(builder.Eq{"id": opts.RequestedUser.ID})
  479. } else {
  480. // A user can always see the activities of the organizations to which the user belongs.
  481. uidCond = uidCond.Or(
  482. builder.Eq{"type": user_model.UserTypeOrganization}.
  483. And(builder.In("`user`.id", builder.Select("org_id").
  484. Where(builder.Eq{"uid": opts.RequestedUser.ID}).
  485. From("team_user"))),
  486. )
  487. }
  488. }
  489. cond = cond.And(builder.In("act_user_id", uidCond))
  490. }
  491. includePrivateRepos := opts.RequestedUser != nil && opts.RequestedUser.ActivityVisibility.ShowAll()
  492. // check readable repositories by doer/actor
  493. if !includePrivateRepos && (opts.Actor == nil || !opts.Actor.IsAdmin) {
  494. cond = cond.And(builder.In("repo_id", repo_model.AccessibleRepoIDsQuery(opts.Actor)))
  495. }
  496. if opts.RequestedRepo != nil {
  497. cond = cond.And(builder.Eq{"repo_id": opts.RequestedRepo.ID})
  498. }
  499. if opts.RequestedTeam != nil {
  500. env := organization.OrgFromUser(opts.RequestedUser).AccessibleTeamReposEnv(ctx, opts.RequestedTeam)
  501. teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos)
  502. if err != nil {
  503. return nil, fmt.Errorf("GetTeamRepositories: %w", err)
  504. }
  505. cond = cond.And(builder.In("repo_id", teamRepoIDs))
  506. }
  507. if opts.RequestedUser != nil {
  508. cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
  509. if opts.OnlyPerformedBy {
  510. cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
  511. }
  512. }
  513. if !opts.IncludePrivate {
  514. cond = cond.And(builder.Eq{"`action`.is_private": false})
  515. }
  516. if !opts.IncludeDeleted {
  517. cond = cond.And(builder.Eq{"is_deleted": false})
  518. }
  519. if opts.Date != "" {
  520. dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
  521. if err != nil {
  522. log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
  523. } else {
  524. dateHigh := dateLow.Add(86399000000000) // 23h59m59s
  525. cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
  526. cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
  527. }
  528. }
  529. return cond, nil
  530. }
  531. // DeleteOldActions deletes all old actions from database.
  532. func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error) {
  533. if olderThan <= 0 {
  534. return nil
  535. }
  536. _, err = db.GetEngine(ctx).Where("created_unix < ?", time.Now().Add(-olderThan).Unix()).Delete(&Action{})
  537. return err
  538. }
  539. // NotifyWatchers creates batch of actions for every watcher.
  540. func NotifyWatchers(ctx context.Context, actions ...*Action) error {
  541. var watchers []*repo_model.Watch
  542. var repo *repo_model.Repository
  543. var err error
  544. var permCode []bool
  545. var permIssue []bool
  546. var permPR []bool
  547. e := db.GetEngine(ctx)
  548. for _, act := range actions {
  549. repoChanged := repo == nil || repo.ID != act.RepoID
  550. if repoChanged {
  551. // Add feeds for user self and all watchers.
  552. watchers, err = repo_model.GetWatchers(ctx, act.RepoID)
  553. if err != nil {
  554. return fmt.Errorf("get watchers: %w", err)
  555. }
  556. }
  557. // Add feed for actioner.
  558. act.UserID = act.ActUserID
  559. if _, err = e.Insert(act); err != nil {
  560. return fmt.Errorf("insert new actioner: %w", err)
  561. }
  562. if repoChanged {
  563. act.loadRepo(ctx)
  564. repo = act.Repo
  565. // check repo owner exist.
  566. if err := act.Repo.LoadOwner(ctx); err != nil {
  567. return fmt.Errorf("can't get repo owner: %w", err)
  568. }
  569. } else if act.Repo == nil {
  570. act.Repo = repo
  571. }
  572. // Add feed for organization
  573. if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID {
  574. act.ID = 0
  575. act.UserID = act.Repo.Owner.ID
  576. if err = db.Insert(ctx, act); err != nil {
  577. return fmt.Errorf("insert new actioner: %w", err)
  578. }
  579. }
  580. if repoChanged {
  581. permCode = make([]bool, len(watchers))
  582. permIssue = make([]bool, len(watchers))
  583. permPR = make([]bool, len(watchers))
  584. for i, watcher := range watchers {
  585. user, err := user_model.GetUserByID(ctx, watcher.UserID)
  586. if err != nil {
  587. permCode[i] = false
  588. permIssue[i] = false
  589. permPR[i] = false
  590. continue
  591. }
  592. perm, err := access_model.GetUserRepoPermission(ctx, repo, user)
  593. if err != nil {
  594. permCode[i] = false
  595. permIssue[i] = false
  596. permPR[i] = false
  597. continue
  598. }
  599. permCode[i] = perm.CanRead(unit.TypeCode)
  600. permIssue[i] = perm.CanRead(unit.TypeIssues)
  601. permPR[i] = perm.CanRead(unit.TypePullRequests)
  602. }
  603. }
  604. for i, watcher := range watchers {
  605. if act.ActUserID == watcher.UserID {
  606. continue
  607. }
  608. act.ID = 0
  609. act.UserID = watcher.UserID
  610. act.Repo.Units = nil
  611. switch act.OpType {
  612. case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionPublishRelease, ActionDeleteBranch:
  613. if !permCode[i] {
  614. continue
  615. }
  616. case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue:
  617. if !permIssue[i] {
  618. continue
  619. }
  620. case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest, ActionAutoMergePullRequest:
  621. if !permPR[i] {
  622. continue
  623. }
  624. }
  625. if err = db.Insert(ctx, act); err != nil {
  626. return fmt.Errorf("insert new action: %w", err)
  627. }
  628. }
  629. }
  630. return nil
  631. }
  632. // NotifyWatchersActions creates batch of actions for every watcher.
  633. func NotifyWatchersActions(ctx context.Context, acts []*Action) error {
  634. ctx, committer, err := db.TxContext(ctx)
  635. if err != nil {
  636. return err
  637. }
  638. defer committer.Close()
  639. for _, act := range acts {
  640. if err := NotifyWatchers(ctx, act); err != nil {
  641. return err
  642. }
  643. }
  644. return committer.Commit()
  645. }
  646. // DeleteIssueActions delete all actions related with issueID
  647. func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) error {
  648. // delete actions assigned to this issue
  649. e := db.GetEngine(ctx)
  650. // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
  651. // so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
  652. var lastCommentID int64
  653. commentIDs := make([]int64, 0, db.DefaultMaxInSize)
  654. for {
  655. commentIDs = commentIDs[:0]
  656. err := e.Select("`id`").Table(&issues_model.Comment{}).
  657. Where(builder.Eq{"issue_id": issueID}).And("`id` > ?", lastCommentID).
  658. OrderBy("`id`").Limit(db.DefaultMaxInSize).
  659. Find(&commentIDs)
  660. if err != nil {
  661. return err
  662. } else if len(commentIDs) == 0 {
  663. break
  664. } else if _, err = db.GetEngine(ctx).In("comment_id", commentIDs).Delete(&Action{}); err != nil {
  665. return err
  666. }
  667. lastCommentID = commentIDs[len(commentIDs)-1]
  668. }
  669. _, err := e.Where("repo_id = ?", repoID).
  670. In("op_type", ActionCreateIssue, ActionCreatePullRequest).
  671. Where("content LIKE ?", strconv.FormatInt(issueIndex, 10)+"|%"). // "IssueIndex|content..."
  672. Delete(&Action{})
  673. return err
  674. }
  675. // CountActionCreatedUnixString count actions where created_unix is an empty string
  676. func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
  677. if setting.Database.Type.IsSQLite3() {
  678. return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(Action))
  679. }
  680. return 0, nil
  681. }
  682. // FixActionCreatedUnixString set created_unix to zero if it is an empty string
  683. func FixActionCreatedUnixString(ctx context.Context) (int64, error) {
  684. if setting.Database.Type.IsSQLite3() {
  685. res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ""`)
  686. if err != nil {
  687. return 0, err
  688. }
  689. return res.RowsAffected()
  690. }
  691. return 0, nil
  692. }