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  }