You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

action.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package feed
  4. import (
  5. "context"
  6. "fmt"
  7. "path"
  8. "strings"
  9. activities_model "code.gitea.io/gitea/models/activities"
  10. issues_model "code.gitea.io/gitea/models/issues"
  11. repo_model "code.gitea.io/gitea/models/repo"
  12. user_model "code.gitea.io/gitea/models/user"
  13. "code.gitea.io/gitea/modules/git"
  14. "code.gitea.io/gitea/modules/json"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/repository"
  17. "code.gitea.io/gitea/modules/util"
  18. notify_service "code.gitea.io/gitea/services/notify"
  19. )
  20. type actionNotifier struct {
  21. notify_service.NullNotifier
  22. }
  23. var _ notify_service.Notifier = &actionNotifier{}
  24. func Init() error {
  25. notify_service.RegisterNotifier(NewNotifier())
  26. return nil
  27. }
  28. // NewNotifier create a new actionNotifier notifier
  29. func NewNotifier() notify_service.Notifier {
  30. return &actionNotifier{}
  31. }
  32. func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) {
  33. if err := issue.LoadPoster(ctx); err != nil {
  34. log.Error("issue.LoadPoster: %v", err)
  35. return
  36. }
  37. if err := issue.LoadRepo(ctx); err != nil {
  38. log.Error("issue.LoadRepo: %v", err)
  39. return
  40. }
  41. repo := issue.Repo
  42. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  43. ActUserID: issue.Poster.ID,
  44. ActUser: issue.Poster,
  45. OpType: activities_model.ActionCreateIssue,
  46. Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
  47. RepoID: repo.ID,
  48. Repo: repo,
  49. IsPrivate: repo.IsPrivate,
  50. }); err != nil {
  51. log.Error("NotifyWatchers: %v", err)
  52. }
  53. }
  54. // IssueChangeStatus notifies close or reopen issue to notifiers
  55. func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, closeOrReopen bool) {
  56. // Compose comment action, could be plain comment, close or reopen issue/pull request.
  57. // This object will be used to notify watchers in the end of function.
  58. act := &activities_model.Action{
  59. ActUserID: doer.ID,
  60. ActUser: doer,
  61. Content: fmt.Sprintf("%d|%s", issue.Index, ""),
  62. RepoID: issue.Repo.ID,
  63. Repo: issue.Repo,
  64. Comment: actionComment,
  65. CommentID: actionComment.ID,
  66. IsPrivate: issue.Repo.IsPrivate,
  67. }
  68. // Check comment type.
  69. if closeOrReopen {
  70. act.OpType = activities_model.ActionCloseIssue
  71. if issue.IsPull {
  72. act.OpType = activities_model.ActionClosePullRequest
  73. }
  74. } else {
  75. act.OpType = activities_model.ActionReopenIssue
  76. if issue.IsPull {
  77. act.OpType = activities_model.ActionReopenPullRequest
  78. }
  79. }
  80. // Notify watchers for whatever action comes in, ignore if no action type.
  81. if err := activities_model.NotifyWatchers(ctx, act); err != nil {
  82. log.Error("NotifyWatchers: %v", err)
  83. }
  84. }
  85. // CreateIssueComment notifies comment on an issue to notifiers
  86. func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
  87. issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
  88. ) {
  89. act := &activities_model.Action{
  90. ActUserID: doer.ID,
  91. ActUser: doer,
  92. RepoID: issue.Repo.ID,
  93. Repo: issue.Repo,
  94. Comment: comment,
  95. CommentID: comment.ID,
  96. IsPrivate: issue.Repo.IsPrivate,
  97. }
  98. truncatedContent, truncatedRight := util.SplitStringAtByteN(comment.Content, 200)
  99. if truncatedRight != "" {
  100. // in case the content is in a Latin family language, we remove the last broken word.
  101. lastSpaceIdx := strings.LastIndex(truncatedContent, " ")
  102. if lastSpaceIdx != -1 && (len(truncatedContent)-lastSpaceIdx < 15) {
  103. truncatedContent = truncatedContent[:lastSpaceIdx] + "…"
  104. }
  105. }
  106. act.Content = fmt.Sprintf("%d|%s", issue.Index, truncatedContent)
  107. if issue.IsPull {
  108. act.OpType = activities_model.ActionCommentPull
  109. } else {
  110. act.OpType = activities_model.ActionCommentIssue
  111. }
  112. // Notify watchers for whatever action comes in, ignore if no action type.
  113. if err := activities_model.NotifyWatchers(ctx, act); err != nil {
  114. log.Error("NotifyWatchers: %v", err)
  115. }
  116. }
  117. func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model.PullRequest, mentions []*user_model.User) {
  118. if err := pull.LoadIssue(ctx); err != nil {
  119. log.Error("pull.LoadIssue: %v", err)
  120. return
  121. }
  122. if err := pull.Issue.LoadRepo(ctx); err != nil {
  123. log.Error("pull.Issue.LoadRepo: %v", err)
  124. return
  125. }
  126. if err := pull.Issue.LoadPoster(ctx); err != nil {
  127. log.Error("pull.Issue.LoadPoster: %v", err)
  128. return
  129. }
  130. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  131. ActUserID: pull.Issue.Poster.ID,
  132. ActUser: pull.Issue.Poster,
  133. OpType: activities_model.ActionCreatePullRequest,
  134. Content: fmt.Sprintf("%d|%s", pull.Issue.Index, pull.Issue.Title),
  135. RepoID: pull.Issue.Repo.ID,
  136. Repo: pull.Issue.Repo,
  137. IsPrivate: pull.Issue.Repo.IsPrivate,
  138. }); err != nil {
  139. log.Error("NotifyWatchers: %v", err)
  140. }
  141. }
  142. func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldRepoName string) {
  143. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  144. ActUserID: doer.ID,
  145. ActUser: doer,
  146. OpType: activities_model.ActionRenameRepo,
  147. RepoID: repo.ID,
  148. Repo: repo,
  149. IsPrivate: repo.IsPrivate,
  150. Content: oldRepoName,
  151. }); err != nil {
  152. log.Error("NotifyWatchers: %v", err)
  153. }
  154. }
  155. func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
  156. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  157. ActUserID: doer.ID,
  158. ActUser: doer,
  159. OpType: activities_model.ActionTransferRepo,
  160. RepoID: repo.ID,
  161. Repo: repo,
  162. IsPrivate: repo.IsPrivate,
  163. Content: path.Join(oldOwnerName, repo.Name),
  164. }); err != nil {
  165. log.Error("NotifyWatchers: %v", err)
  166. }
  167. }
  168. func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
  169. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  170. ActUserID: doer.ID,
  171. ActUser: doer,
  172. OpType: activities_model.ActionCreateRepo,
  173. RepoID: repo.ID,
  174. Repo: repo,
  175. IsPrivate: repo.IsPrivate,
  176. }); err != nil {
  177. log.Error("notify watchers '%d/%d': %v", doer.ID, repo.ID, err)
  178. }
  179. }
  180. func (a *actionNotifier) ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) {
  181. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  182. ActUserID: doer.ID,
  183. ActUser: doer,
  184. OpType: activities_model.ActionCreateRepo,
  185. RepoID: repo.ID,
  186. Repo: repo,
  187. IsPrivate: repo.IsPrivate,
  188. }); err != nil {
  189. log.Error("notify watchers '%d/%d': %v", doer.ID, repo.ID, err)
  190. }
  191. }
  192. func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
  193. if err := review.LoadReviewer(ctx); err != nil {
  194. log.Error("LoadReviewer '%d/%d': %v", review.ID, review.ReviewerID, err)
  195. return
  196. }
  197. if err := review.LoadCodeComments(ctx); err != nil {
  198. log.Error("LoadCodeComments '%d/%d': %v", review.Reviewer.ID, review.ID, err)
  199. return
  200. }
  201. actions := make([]*activities_model.Action, 0, 10)
  202. for _, lines := range review.CodeComments {
  203. for _, comments := range lines {
  204. for _, comm := range comments {
  205. actions = append(actions, &activities_model.Action{
  206. ActUserID: review.Reviewer.ID,
  207. ActUser: review.Reviewer,
  208. Content: fmt.Sprintf("%d|%s", review.Issue.Index, strings.Split(comm.Content, "\n")[0]),
  209. OpType: activities_model.ActionCommentPull,
  210. RepoID: review.Issue.RepoID,
  211. Repo: review.Issue.Repo,
  212. IsPrivate: review.Issue.Repo.IsPrivate,
  213. Comment: comm,
  214. CommentID: comm.ID,
  215. })
  216. }
  217. }
  218. }
  219. if review.Type != issues_model.ReviewTypeComment || strings.TrimSpace(comment.Content) != "" {
  220. action := &activities_model.Action{
  221. ActUserID: review.Reviewer.ID,
  222. ActUser: review.Reviewer,
  223. Content: fmt.Sprintf("%d|%s", review.Issue.Index, strings.Split(comment.Content, "\n")[0]),
  224. RepoID: review.Issue.RepoID,
  225. Repo: review.Issue.Repo,
  226. IsPrivate: review.Issue.Repo.IsPrivate,
  227. Comment: comment,
  228. CommentID: comment.ID,
  229. }
  230. switch review.Type {
  231. case issues_model.ReviewTypeApprove:
  232. action.OpType = activities_model.ActionApprovePullRequest
  233. case issues_model.ReviewTypeReject:
  234. action.OpType = activities_model.ActionRejectPullRequest
  235. default:
  236. action.OpType = activities_model.ActionCommentPull
  237. }
  238. actions = append(actions, action)
  239. }
  240. if err := activities_model.NotifyWatchersActions(ctx, actions); err != nil {
  241. log.Error("notify watchers '%d/%d': %v", review.Reviewer.ID, review.Issue.RepoID, err)
  242. }
  243. }
  244. func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
  245. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  246. ActUserID: doer.ID,
  247. ActUser: doer,
  248. OpType: activities_model.ActionMergePullRequest,
  249. Content: fmt.Sprintf("%d|%s", pr.Issue.Index, pr.Issue.Title),
  250. RepoID: pr.Issue.Repo.ID,
  251. Repo: pr.Issue.Repo,
  252. IsPrivate: pr.Issue.Repo.IsPrivate,
  253. }); err != nil {
  254. log.Error("NotifyWatchers [%d]: %v", pr.ID, err)
  255. }
  256. }
  257. func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
  258. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  259. ActUserID: doer.ID,
  260. ActUser: doer,
  261. OpType: activities_model.ActionAutoMergePullRequest,
  262. Content: fmt.Sprintf("%d|%s", pr.Issue.Index, pr.Issue.Title),
  263. RepoID: pr.Issue.Repo.ID,
  264. Repo: pr.Issue.Repo,
  265. IsPrivate: pr.Issue.Repo.IsPrivate,
  266. }); err != nil {
  267. log.Error("NotifyWatchers [%d]: %v", pr.ID, err)
  268. }
  269. }
  270. func (*actionNotifier) NotifyPullRevieweDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
  271. reviewerName := review.Reviewer.Name
  272. if len(review.OriginalAuthor) > 0 {
  273. reviewerName = review.OriginalAuthor
  274. }
  275. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  276. ActUserID: doer.ID,
  277. ActUser: doer,
  278. OpType: activities_model.ActionPullReviewDismissed,
  279. Content: fmt.Sprintf("%d|%s|%s", review.Issue.Index, reviewerName, comment.Content),
  280. RepoID: review.Issue.Repo.ID,
  281. Repo: review.Issue.Repo,
  282. IsPrivate: review.Issue.Repo.IsPrivate,
  283. CommentID: comment.ID,
  284. Comment: comment,
  285. }); err != nil {
  286. log.Error("NotifyWatchers [%d]: %v", review.Issue.ID, err)
  287. }
  288. }
  289. func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
  290. data, err := json.Marshal(commits)
  291. if err != nil {
  292. log.Error("Marshal: %v", err)
  293. return
  294. }
  295. opType := activities_model.ActionCommitRepo
  296. // Check it's tag push or branch.
  297. if opts.RefFullName.IsTag() {
  298. opType = activities_model.ActionPushTag
  299. if opts.IsDelRef() {
  300. opType = activities_model.ActionDeleteTag
  301. }
  302. } else if opts.IsDelRef() {
  303. opType = activities_model.ActionDeleteBranch
  304. }
  305. if err = activities_model.NotifyWatchers(ctx, &activities_model.Action{
  306. ActUserID: pusher.ID,
  307. ActUser: pusher,
  308. OpType: opType,
  309. Content: string(data),
  310. RepoID: repo.ID,
  311. Repo: repo,
  312. RefName: opts.RefFullName.String(),
  313. IsPrivate: repo.IsPrivate,
  314. }); err != nil {
  315. log.Error("NotifyWatchers: %v", err)
  316. }
  317. }
  318. func (a *actionNotifier) CreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
  319. opType := activities_model.ActionCommitRepo
  320. if refFullName.IsTag() {
  321. // has sent same action in `PushCommits`, so skip it.
  322. return
  323. }
  324. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  325. ActUserID: doer.ID,
  326. ActUser: doer,
  327. OpType: opType,
  328. RepoID: repo.ID,
  329. Repo: repo,
  330. IsPrivate: repo.IsPrivate,
  331. RefName: refFullName.String(),
  332. }); err != nil {
  333. log.Error("NotifyWatchers: %v", err)
  334. }
  335. }
  336. func (a *actionNotifier) DeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
  337. opType := activities_model.ActionDeleteBranch
  338. if refFullName.IsTag() {
  339. // has sent same action in `PushCommits`, so skip it.
  340. return
  341. }
  342. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  343. ActUserID: doer.ID,
  344. ActUser: doer,
  345. OpType: opType,
  346. RepoID: repo.ID,
  347. Repo: repo,
  348. IsPrivate: repo.IsPrivate,
  349. RefName: refFullName.String(),
  350. }); err != nil {
  351. log.Error("NotifyWatchers: %v", err)
  352. }
  353. }
  354. func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
  355. data, err := json.Marshal(commits)
  356. if err != nil {
  357. log.Error("json.Marshal: %v", err)
  358. return
  359. }
  360. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  361. ActUserID: repo.OwnerID,
  362. ActUser: repo.MustOwner(ctx),
  363. OpType: activities_model.ActionMirrorSyncPush,
  364. RepoID: repo.ID,
  365. Repo: repo,
  366. IsPrivate: repo.IsPrivate,
  367. RefName: opts.RefFullName.String(),
  368. Content: string(data),
  369. }); err != nil {
  370. log.Error("NotifyWatchers: %v", err)
  371. }
  372. }
  373. func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
  374. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  375. ActUserID: repo.OwnerID,
  376. ActUser: repo.MustOwner(ctx),
  377. OpType: activities_model.ActionMirrorSyncCreate,
  378. RepoID: repo.ID,
  379. Repo: repo,
  380. IsPrivate: repo.IsPrivate,
  381. RefName: refFullName.String(),
  382. }); err != nil {
  383. log.Error("NotifyWatchers: %v", err)
  384. }
  385. }
  386. func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
  387. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  388. ActUserID: repo.OwnerID,
  389. ActUser: repo.MustOwner(ctx),
  390. OpType: activities_model.ActionMirrorSyncDelete,
  391. RepoID: repo.ID,
  392. Repo: repo,
  393. IsPrivate: repo.IsPrivate,
  394. RefName: refFullName.String(),
  395. }); err != nil {
  396. log.Error("NotifyWatchers: %v", err)
  397. }
  398. }
  399. func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release) {
  400. if err := rel.LoadAttributes(ctx); err != nil {
  401. log.Error("LoadAttributes: %v", err)
  402. return
  403. }
  404. if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
  405. ActUserID: rel.PublisherID,
  406. ActUser: rel.Publisher,
  407. OpType: activities_model.ActionPublishRelease,
  408. RepoID: rel.RepoID,
  409. Repo: rel.Repo,
  410. IsPrivate: rel.Repo.IsPrivate,
  411. Content: rel.Title,
  412. RefName: rel.TagName, // FIXME: use a full ref name?
  413. }); err != nil {
  414. log.Error("NotifyWatchers: %v", err)
  415. }
  416. }