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