code.gitea.io/gitea@v1.22.3/models/activities/action.go (about)

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