code.gitea.io/gitea@v1.21.7/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  	IsDeleted   bool                   `xorm:"NOT NULL DEFAULT false"`
   152  	RefName     string
   153  	IsPrivate   bool               `xorm:"NOT NULL DEFAULT false"`
   154  	Content     string             `xorm:"TEXT"`
   155  	CreatedUnix timeutil.TimeStamp `xorm:"created"`
   156  }
   157  
   158  func init() {
   159  	db.RegisterModel(new(Action))
   160  }
   161  
   162  // TableIndices implements xorm's TableIndices interface
   163  func (a *Action) TableIndices() []*schemas.Index {
   164  	repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType)
   165  	repoIndex.AddColumn("repo_id", "user_id", "is_deleted")
   166  
   167  	actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType)
   168  	actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted")
   169  
   170  	cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType)
   171  	cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
   172  
   173  	indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex}
   174  
   175  	return indices
   176  }
   177  
   178  // GetOpType gets the ActionType of this action.
   179  func (a *Action) GetOpType() ActionType {
   180  	return a.OpType
   181  }
   182  
   183  // LoadActUser loads a.ActUser
   184  func (a *Action) LoadActUser(ctx context.Context) {
   185  	if a.ActUser != nil {
   186  		return
   187  	}
   188  	var err error
   189  	a.ActUser, err = user_model.GetUserByID(ctx, a.ActUserID)
   190  	if err == nil {
   191  		return
   192  	} else if user_model.IsErrUserNotExist(err) {
   193  		a.ActUser = user_model.NewGhostUser()
   194  	} else {
   195  		log.Error("GetUserByID(%d): %v", a.ActUserID, err)
   196  	}
   197  }
   198  
   199  func (a *Action) loadRepo(ctx context.Context) {
   200  	if a.Repo != nil {
   201  		return
   202  	}
   203  	var err error
   204  	a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID)
   205  	if err != nil {
   206  		log.Error("repo_model.GetRepositoryByID(%d): %v", a.RepoID, err)
   207  	}
   208  }
   209  
   210  // GetActFullName gets the action's user full name.
   211  func (a *Action) GetActFullName(ctx context.Context) string {
   212  	a.LoadActUser(ctx)
   213  	return a.ActUser.FullName
   214  }
   215  
   216  // GetActUserName gets the action's user name.
   217  func (a *Action) GetActUserName(ctx context.Context) string {
   218  	a.LoadActUser(ctx)
   219  	return a.ActUser.Name
   220  }
   221  
   222  // ShortActUserName gets the action's user name trimmed to max 20
   223  // chars.
   224  func (a *Action) ShortActUserName(ctx context.Context) string {
   225  	return base.EllipsisString(a.GetActUserName(ctx), 20)
   226  }
   227  
   228  // GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
   229  func (a *Action) GetDisplayName(ctx context.Context) string {
   230  	if setting.UI.DefaultShowFullName {
   231  		trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx))
   232  		if len(trimmedFullName) > 0 {
   233  			return trimmedFullName
   234  		}
   235  	}
   236  	return a.ShortActUserName(ctx)
   237  }
   238  
   239  // GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
   240  func (a *Action) GetDisplayNameTitle(ctx context.Context) string {
   241  	if setting.UI.DefaultShowFullName {
   242  		return a.ShortActUserName(ctx)
   243  	}
   244  	return a.GetActFullName(ctx)
   245  }
   246  
   247  // GetRepoUserName returns the name of the action repository owner.
   248  func (a *Action) GetRepoUserName(ctx context.Context) string {
   249  	a.loadRepo(ctx)
   250  	return a.Repo.OwnerName
   251  }
   252  
   253  // ShortRepoUserName returns the name of the action repository owner
   254  // trimmed to max 20 chars.
   255  func (a *Action) ShortRepoUserName(ctx context.Context) string {
   256  	return base.EllipsisString(a.GetRepoUserName(ctx), 20)
   257  }
   258  
   259  // GetRepoName returns the name of the action repository.
   260  func (a *Action) GetRepoName(ctx context.Context) string {
   261  	a.loadRepo(ctx)
   262  	return a.Repo.Name
   263  }
   264  
   265  // ShortRepoName returns the name of the action repository
   266  // trimmed to max 33 chars.
   267  func (a *Action) ShortRepoName(ctx context.Context) string {
   268  	return base.EllipsisString(a.GetRepoName(ctx), 33)
   269  }
   270  
   271  // GetRepoPath returns the virtual path to the action repository.
   272  func (a *Action) GetRepoPath(ctx context.Context) string {
   273  	return path.Join(a.GetRepoUserName(ctx), a.GetRepoName(ctx))
   274  }
   275  
   276  // ShortRepoPath returns the virtual path to the action repository
   277  // trimmed to max 20 + 1 + 33 chars.
   278  func (a *Action) ShortRepoPath(ctx context.Context) string {
   279  	return path.Join(a.ShortRepoUserName(ctx), a.ShortRepoName(ctx))
   280  }
   281  
   282  // GetRepoLink returns relative link to action repository.
   283  func (a *Action) GetRepoLink(ctx context.Context) string {
   284  	// path.Join will skip empty strings
   285  	return path.Join(setting.AppSubURL, "/", url.PathEscape(a.GetRepoUserName(ctx)), url.PathEscape(a.GetRepoName(ctx)))
   286  }
   287  
   288  // GetRepoAbsoluteLink returns the absolute link to action repository.
   289  func (a *Action) GetRepoAbsoluteLink(ctx context.Context) string {
   290  	return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx))
   291  }
   292  
   293  // GetCommentHTMLURL returns link to action comment.
   294  func (a *Action) GetCommentHTMLURL(ctx context.Context) string {
   295  	return a.getCommentHTMLURL(ctx)
   296  }
   297  
   298  func (a *Action) loadComment(ctx context.Context) (err error) {
   299  	if a.CommentID == 0 || a.Comment != nil {
   300  		return nil
   301  	}
   302  	a.Comment, err = issues_model.GetCommentByID(ctx, a.CommentID)
   303  	return err
   304  }
   305  
   306  func (a *Action) getCommentHTMLURL(ctx context.Context) string {
   307  	if a == nil {
   308  		return "#"
   309  	}
   310  	_ = a.loadComment(ctx)
   311  	if a.Comment != nil {
   312  		return a.Comment.HTMLURL(ctx)
   313  	}
   314  	if len(a.GetIssueInfos()) == 0 {
   315  		return "#"
   316  	}
   317  	// Return link to issue
   318  	issueIDString := a.GetIssueInfos()[0]
   319  	issueID, err := strconv.ParseInt(issueIDString, 10, 64)
   320  	if err != nil {
   321  		return "#"
   322  	}
   323  
   324  	issue, err := issues_model.GetIssueByID(ctx, issueID)
   325  	if err != nil {
   326  		return "#"
   327  	}
   328  
   329  	if err = issue.LoadRepo(ctx); err != nil {
   330  		return "#"
   331  	}
   332  
   333  	return issue.HTMLURL()
   334  }
   335  
   336  // GetCommentLink returns link to action comment.
   337  func (a *Action) GetCommentLink(ctx context.Context) string {
   338  	return a.getCommentLink(ctx)
   339  }
   340  
   341  func (a *Action) getCommentLink(ctx context.Context) string {
   342  	if a == nil {
   343  		return "#"
   344  	}
   345  	_ = a.loadComment(ctx)
   346  	if a.Comment != nil {
   347  		return a.Comment.Link(ctx)
   348  	}
   349  	if len(a.GetIssueInfos()) == 0 {
   350  		return "#"
   351  	}
   352  	// Return link to issue
   353  	issueIDString := a.GetIssueInfos()[0]
   354  	issueID, err := strconv.ParseInt(issueIDString, 10, 64)
   355  	if err != nil {
   356  		return "#"
   357  	}
   358  
   359  	issue, err := issues_model.GetIssueByID(ctx, issueID)
   360  	if err != nil {
   361  		return "#"
   362  	}
   363  
   364  	if err = issue.LoadRepo(ctx); err != nil {
   365  		return "#"
   366  	}
   367  
   368  	return issue.Link()
   369  }
   370  
   371  // GetBranch returns the action's repository branch.
   372  func (a *Action) GetBranch() string {
   373  	return strings.TrimPrefix(a.RefName, git.BranchPrefix)
   374  }
   375  
   376  // GetRefLink returns the action's ref link.
   377  func (a *Action) GetRefLink(ctx context.Context) string {
   378  	return git.RefURL(a.GetRepoLink(ctx), a.RefName)
   379  }
   380  
   381  // GetTag returns the action's repository tag.
   382  func (a *Action) GetTag() string {
   383  	return strings.TrimPrefix(a.RefName, git.TagPrefix)
   384  }
   385  
   386  // GetContent returns the action's content.
   387  func (a *Action) GetContent() string {
   388  	return a.Content
   389  }
   390  
   391  // GetCreate returns the action creation time.
   392  func (a *Action) GetCreate() time.Time {
   393  	return a.CreatedUnix.AsTime()
   394  }
   395  
   396  // GetIssueInfos returns a list of issues associated with
   397  // the action.
   398  func (a *Action) GetIssueInfos() []string {
   399  	return strings.SplitN(a.Content, "|", 3)
   400  }
   401  
   402  // GetIssueTitle returns the title of first issue associated with the action.
   403  func (a *Action) GetIssueTitle(ctx context.Context) string {
   404  	index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
   405  	issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
   406  	if err != nil {
   407  		log.Error("GetIssueByIndex: %v", err)
   408  		return "500 when get issue"
   409  	}
   410  	return issue.Title
   411  }
   412  
   413  // GetIssueContent returns the content of first issue associated with
   414  // this action.
   415  func (a *Action) GetIssueContent(ctx context.Context) string {
   416  	index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
   417  	issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
   418  	if err != nil {
   419  		log.Error("GetIssueByIndex: %v", err)
   420  		return "500 when get issue"
   421  	}
   422  	return issue.Content
   423  }
   424  
   425  // GetFeedsOptions options for retrieving feeds
   426  type GetFeedsOptions struct {
   427  	db.ListOptions
   428  	RequestedUser   *user_model.User       // the user we want activity for
   429  	RequestedTeam   *organization.Team     // the team we want activity for
   430  	RequestedRepo   *repo_model.Repository // the repo we want activity for
   431  	Actor           *user_model.User       // the user viewing the activity
   432  	IncludePrivate  bool                   // include private actions
   433  	OnlyPerformedBy bool                   // only actions performed by requested user
   434  	IncludeDeleted  bool                   // include deleted actions
   435  	Date            string                 // the day we want activity for: YYYY-MM-DD
   436  }
   437  
   438  // GetFeeds returns actions according to the provided options
   439  func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) {
   440  	if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
   441  		return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
   442  	}
   443  
   444  	cond, err := activityQueryCondition(ctx, opts)
   445  	if err != nil {
   446  		return nil, 0, err
   447  	}
   448  
   449  	sess := db.GetEngine(ctx).Where(cond).
   450  		Select("`action`.*"). // this line will avoid select other joined table's columns
   451  		Join("INNER", "repository", "`repository`.id = `action`.repo_id")
   452  
   453  	opts.SetDefaultValues()
   454  	sess = db.SetSessionPagination(sess, &opts)
   455  
   456  	actions := make([]*Action, 0, opts.PageSize)
   457  	count, err := sess.Desc("`action`.created_unix").FindAndCount(&actions)
   458  	if err != nil {
   459  		return nil, 0, fmt.Errorf("FindAndCount: %w", err)
   460  	}
   461  
   462  	if err := ActionList(actions).loadAttributes(ctx); err != nil {
   463  		return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
   464  	}
   465  
   466  	return actions, count, nil
   467  }
   468  
   469  // ActivityReadable return whether doer can read activities of user
   470  func ActivityReadable(user, doer *user_model.User) bool {
   471  	return !user.KeepActivityPrivate ||
   472  		doer != nil && (doer.IsAdmin || user.ID == doer.ID)
   473  }
   474  
   475  func activityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) {
   476  	cond := builder.NewCond()
   477  
   478  	if opts.RequestedTeam != nil && opts.RequestedUser == nil {
   479  		org, err := user_model.GetUserByID(ctx, opts.RequestedTeam.OrgID)
   480  		if err != nil {
   481  			return nil, err
   482  		}
   483  		opts.RequestedUser = org
   484  	}
   485  
   486  	// check activity visibility for actor ( similar to activityReadable() )
   487  	if opts.Actor == nil {
   488  		cond = cond.And(builder.In("act_user_id",
   489  			builder.Select("`user`.id").Where(
   490  				builder.Eq{"keep_activity_private": false, "visibility": structs.VisibleTypePublic},
   491  			).From("`user`"),
   492  		))
   493  	} else if !opts.Actor.IsAdmin {
   494  		uidCond := builder.Select("`user`.id").From("`user`").Where(
   495  			builder.Eq{"keep_activity_private": false}.
   496  				And(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))).
   497  			Or(builder.Eq{"id": opts.Actor.ID})
   498  
   499  		if opts.RequestedUser != nil {
   500  			if opts.RequestedUser.IsOrganization() {
   501  				// An organization can always see the activities whose `act_user_id` is the same as its id.
   502  				uidCond = uidCond.Or(builder.Eq{"id": opts.RequestedUser.ID})
   503  			} else {
   504  				// A user can always see the activities of the organizations to which the user belongs.
   505  				uidCond = uidCond.Or(
   506  					builder.Eq{"type": user_model.UserTypeOrganization}.
   507  						And(builder.In("`user`.id", builder.Select("org_id").
   508  							Where(builder.Eq{"uid": opts.RequestedUser.ID}).
   509  							From("team_user"))),
   510  				)
   511  			}
   512  		}
   513  
   514  		cond = cond.And(builder.In("act_user_id", uidCond))
   515  	}
   516  
   517  	// check readable repositories by doer/actor
   518  	if opts.Actor == nil || !opts.Actor.IsAdmin {
   519  		cond = cond.And(builder.In("repo_id", repo_model.AccessibleRepoIDsQuery(opts.Actor)))
   520  	}
   521  
   522  	if opts.RequestedRepo != nil {
   523  		cond = cond.And(builder.Eq{"repo_id": opts.RequestedRepo.ID})
   524  	}
   525  
   526  	if opts.RequestedTeam != nil {
   527  		env := organization.OrgFromUser(opts.RequestedUser).AccessibleTeamReposEnv(opts.RequestedTeam)
   528  		teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos)
   529  		if err != nil {
   530  			return nil, fmt.Errorf("GetTeamRepositories: %w", err)
   531  		}
   532  		cond = cond.And(builder.In("repo_id", teamRepoIDs))
   533  	}
   534  
   535  	if opts.RequestedUser != nil {
   536  		cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
   537  
   538  		if opts.OnlyPerformedBy {
   539  			cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
   540  		}
   541  	}
   542  
   543  	if !opts.IncludePrivate {
   544  		cond = cond.And(builder.Eq{"`action`.is_private": false})
   545  	}
   546  	if !opts.IncludeDeleted {
   547  		cond = cond.And(builder.Eq{"is_deleted": false})
   548  	}
   549  
   550  	if opts.Date != "" {
   551  		dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
   552  		if err != nil {
   553  			log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
   554  		} else {
   555  			dateHigh := dateLow.Add(86399000000000) // 23h59m59s
   556  
   557  			cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
   558  			cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
   559  		}
   560  	}
   561  
   562  	return cond, nil
   563  }
   564  
   565  // DeleteOldActions deletes all old actions from database.
   566  func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error) {
   567  	if olderThan <= 0 {
   568  		return nil
   569  	}
   570  
   571  	_, err = db.GetEngine(ctx).Where("created_unix < ?", time.Now().Add(-olderThan).Unix()).Delete(&Action{})
   572  	return err
   573  }
   574  
   575  // NotifyWatchers creates batch of actions for every watcher.
   576  func NotifyWatchers(ctx context.Context, actions ...*Action) error {
   577  	var watchers []*repo_model.Watch
   578  	var repo *repo_model.Repository
   579  	var err error
   580  	var permCode []bool
   581  	var permIssue []bool
   582  	var permPR []bool
   583  
   584  	e := db.GetEngine(ctx)
   585  
   586  	for _, act := range actions {
   587  		repoChanged := repo == nil || repo.ID != act.RepoID
   588  
   589  		if repoChanged {
   590  			// Add feeds for user self and all watchers.
   591  			watchers, err = repo_model.GetWatchers(ctx, act.RepoID)
   592  			if err != nil {
   593  				return fmt.Errorf("get watchers: %w", err)
   594  			}
   595  		}
   596  
   597  		// Add feed for actioner.
   598  		act.UserID = act.ActUserID
   599  		if _, err = e.Insert(act); err != nil {
   600  			return fmt.Errorf("insert new actioner: %w", err)
   601  		}
   602  
   603  		if repoChanged {
   604  			act.loadRepo(ctx)
   605  			repo = act.Repo
   606  
   607  			// check repo owner exist.
   608  			if err := act.Repo.LoadOwner(ctx); err != nil {
   609  				return fmt.Errorf("can't get repo owner: %w", err)
   610  			}
   611  		} else if act.Repo == nil {
   612  			act.Repo = repo
   613  		}
   614  
   615  		// Add feed for organization
   616  		if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID {
   617  			act.ID = 0
   618  			act.UserID = act.Repo.Owner.ID
   619  			if err = db.Insert(ctx, act); err != nil {
   620  				return fmt.Errorf("insert new actioner: %w", err)
   621  			}
   622  		}
   623  
   624  		if repoChanged {
   625  			permCode = make([]bool, len(watchers))
   626  			permIssue = make([]bool, len(watchers))
   627  			permPR = make([]bool, len(watchers))
   628  			for i, watcher := range watchers {
   629  				user, err := user_model.GetUserByID(ctx, watcher.UserID)
   630  				if err != nil {
   631  					permCode[i] = false
   632  					permIssue[i] = false
   633  					permPR[i] = false
   634  					continue
   635  				}
   636  				perm, err := access_model.GetUserRepoPermission(ctx, repo, user)
   637  				if err != nil {
   638  					permCode[i] = false
   639  					permIssue[i] = false
   640  					permPR[i] = false
   641  					continue
   642  				}
   643  				permCode[i] = perm.CanRead(unit.TypeCode)
   644  				permIssue[i] = perm.CanRead(unit.TypeIssues)
   645  				permPR[i] = perm.CanRead(unit.TypePullRequests)
   646  			}
   647  		}
   648  
   649  		for i, watcher := range watchers {
   650  			if act.ActUserID == watcher.UserID {
   651  				continue
   652  			}
   653  			act.ID = 0
   654  			act.UserID = watcher.UserID
   655  			act.Repo.Units = nil
   656  
   657  			switch act.OpType {
   658  			case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionPublishRelease, ActionDeleteBranch:
   659  				if !permCode[i] {
   660  					continue
   661  				}
   662  			case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue:
   663  				if !permIssue[i] {
   664  					continue
   665  				}
   666  			case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest, ActionAutoMergePullRequest:
   667  				if !permPR[i] {
   668  					continue
   669  				}
   670  			}
   671  
   672  			if err = db.Insert(ctx, act); err != nil {
   673  				return fmt.Errorf("insert new action: %w", err)
   674  			}
   675  		}
   676  	}
   677  	return nil
   678  }
   679  
   680  // NotifyWatchersActions creates batch of actions for every watcher.
   681  func NotifyWatchersActions(ctx context.Context, acts []*Action) error {
   682  	ctx, committer, err := db.TxContext(ctx)
   683  	if err != nil {
   684  		return err
   685  	}
   686  	defer committer.Close()
   687  	for _, act := range acts {
   688  		if err := NotifyWatchers(ctx, act); err != nil {
   689  			return err
   690  		}
   691  	}
   692  	return committer.Commit()
   693  }
   694  
   695  // DeleteIssueActions delete all actions related with issueID
   696  func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) error {
   697  	// delete actions assigned to this issue
   698  	e := db.GetEngine(ctx)
   699  
   700  	// MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
   701  	// so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
   702  	var lastCommentID int64
   703  	commentIDs := make([]int64, 0, db.DefaultMaxInSize)
   704  	for {
   705  		commentIDs = commentIDs[:0]
   706  		err := e.Select("`id`").Table(&issues_model.Comment{}).
   707  			Where(builder.Eq{"issue_id": issueID}).And("`id` > ?", lastCommentID).
   708  			OrderBy("`id`").Limit(db.DefaultMaxInSize).
   709  			Find(&commentIDs)
   710  		if err != nil {
   711  			return err
   712  		} else if len(commentIDs) == 0 {
   713  			break
   714  		} else if _, err = db.GetEngine(ctx).In("comment_id", commentIDs).Delete(&Action{}); err != nil {
   715  			return err
   716  		} else {
   717  			lastCommentID = commentIDs[len(commentIDs)-1]
   718  		}
   719  	}
   720  
   721  	_, err := e.Where("repo_id = ?", repoID).
   722  		In("op_type", ActionCreateIssue, ActionCreatePullRequest).
   723  		Where("content LIKE ?", strconv.FormatInt(issueIndex, 10)+"|%"). // "IssueIndex|content..."
   724  		Delete(&Action{})
   725  	return err
   726  }
   727  
   728  // CountActionCreatedUnixString count actions where created_unix is an empty string
   729  func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
   730  	if setting.Database.Type.IsSQLite3() {
   731  		return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(Action))
   732  	}
   733  	return 0, nil
   734  }
   735  
   736  // FixActionCreatedUnixString set created_unix to zero if it is an empty string
   737  func FixActionCreatedUnixString(ctx context.Context) (int64, error) {
   738  	if setting.Database.Type.IsSQLite3() {
   739  		res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ""`)
   740  		if err != nil {
   741  			return 0, err
   742  		}
   743  		return res.RowsAffected()
   744  	}
   745  	return 0, nil
   746  }