diff --git a/models/activities/action.go b/models/activities/action.go index 532667d495798..3d68ee8e6dc7f 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -7,21 +7,14 @@ package activities import ( "context" "fmt" - "net/url" - "path" "strconv" - "strings" "time" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" - access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" @@ -176,329 +169,6 @@ func (a *Action) TableIndices() []*schemas.Index { return indices } -// GetOpType gets the ActionType of this action. -func (a *Action) GetOpType() ActionType { - return a.OpType -} - -// LoadActUser loads a.ActUser -func (a *Action) LoadActUser(ctx context.Context) { - if a.ActUser != nil { - return - } - var err error - a.ActUser, err = user_model.GetUserByID(ctx, a.ActUserID) - if err == nil { - return - } else if user_model.IsErrUserNotExist(err) { - a.ActUser = user_model.NewGhostUser() - } else { - log.Error("GetUserByID(%d): %v", a.ActUserID, err) - } -} - -func (a *Action) loadRepo(ctx context.Context) { - if a.Repo != nil { - return - } - var err error - a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID) - if err != nil { - log.Error("repo_model.GetRepositoryByID(%d): %v", a.RepoID, err) - } -} - -// GetActFullName gets the action's user full name. -func (a *Action) GetActFullName(ctx context.Context) string { - a.LoadActUser(ctx) - return a.ActUser.FullName -} - -// GetActUserName gets the action's user name. -func (a *Action) GetActUserName(ctx context.Context) string { - a.LoadActUser(ctx) - return a.ActUser.Name -} - -// ShortActUserName gets the action's user name trimmed to max 20 -// chars. -func (a *Action) ShortActUserName(ctx context.Context) string { - return base.EllipsisString(a.GetActUserName(ctx), 20) -} - -// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. -func (a *Action) GetActDisplayName(ctx context.Context) string { - if setting.UI.DefaultShowFullName { - trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx)) - if len(trimmedFullName) > 0 { - return trimmedFullName - } - } - return a.ShortActUserName(ctx) -} - -// GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME -func (a *Action) GetActDisplayNameTitle(ctx context.Context) string { - if setting.UI.DefaultShowFullName { - return a.ShortActUserName(ctx) - } - return a.GetActFullName(ctx) -} - -// GetRepoUserName returns the name of the action repository owner. -func (a *Action) GetRepoUserName(ctx context.Context) string { - a.loadRepo(ctx) - return a.Repo.OwnerName -} - -// ShortRepoUserName returns the name of the action repository owner -// trimmed to max 20 chars. -func (a *Action) ShortRepoUserName(ctx context.Context) string { - return base.EllipsisString(a.GetRepoUserName(ctx), 20) -} - -// GetRepoName returns the name of the action repository. -func (a *Action) GetRepoName(ctx context.Context) string { - a.loadRepo(ctx) - return a.Repo.Name -} - -// ShortRepoName returns the name of the action repository -// trimmed to max 33 chars. -func (a *Action) ShortRepoName(ctx context.Context) string { - return base.EllipsisString(a.GetRepoName(ctx), 33) -} - -// GetRepoPath returns the virtual path to the action repository. -func (a *Action) GetRepoPath(ctx context.Context) string { - return path.Join(a.GetRepoUserName(ctx), a.GetRepoName(ctx)) -} - -// ShortRepoPath returns the virtual path to the action repository -// trimmed to max 20 + 1 + 33 chars. -func (a *Action) ShortRepoPath(ctx context.Context) string { - return path.Join(a.ShortRepoUserName(ctx), a.ShortRepoName(ctx)) -} - -// GetRepoLink returns relative link to action repository. -func (a *Action) GetRepoLink(ctx context.Context) string { - // path.Join will skip empty strings - return path.Join(setting.AppSubURL, "/", url.PathEscape(a.GetRepoUserName(ctx)), url.PathEscape(a.GetRepoName(ctx))) -} - -// GetRepoAbsoluteLink returns the absolute link to action repository. -func (a *Action) GetRepoAbsoluteLink(ctx context.Context) string { - return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx)) -} - -func (a *Action) loadComment(ctx context.Context) (err error) { - if a.CommentID == 0 || a.Comment != nil { - return nil - } - a.Comment, err = issues_model.GetCommentByID(ctx, a.CommentID) - return err -} - -// GetCommentHTMLURL returns link to action comment. -func (a *Action) GetCommentHTMLURL(ctx context.Context) string { - if a == nil { - return "#" - } - _ = a.loadComment(ctx) - if a.Comment != nil { - return a.Comment.HTMLURL(ctx) - } - - if err := a.LoadIssue(ctx); err != nil || a.Issue == nil { - return "#" - } - if err := a.Issue.LoadRepo(ctx); err != nil { - return "#" - } - - return a.Issue.HTMLURL() -} - -// GetCommentLink returns link to action comment. -func (a *Action) GetCommentLink(ctx context.Context) string { - if a == nil { - return "#" - } - _ = a.loadComment(ctx) - if a.Comment != nil { - return a.Comment.Link(ctx) - } - - if err := a.LoadIssue(ctx); err != nil || a.Issue == nil { - return "#" - } - if err := a.Issue.LoadRepo(ctx); err != nil { - return "#" - } - - return a.Issue.Link() -} - -// GetBranch returns the action's repository branch. -func (a *Action) GetBranch() string { - return strings.TrimPrefix(a.RefName, git.BranchPrefix) -} - -// GetRefLink returns the action's ref link. -func (a *Action) GetRefLink(ctx context.Context) string { - return git.RefURL(a.GetRepoLink(ctx), a.RefName) -} - -// GetTag returns the action's repository tag. -func (a *Action) GetTag() string { - return strings.TrimPrefix(a.RefName, git.TagPrefix) -} - -// GetContent returns the action's content. -func (a *Action) GetContent() string { - return a.Content -} - -// GetCreate returns the action creation time. -func (a *Action) GetCreate() time.Time { - return a.CreatedUnix.AsTime() -} - -func (a *Action) IsIssueEvent() bool { - return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request") -} - -// GetIssueInfos returns a list of associated information with the action. -func (a *Action) GetIssueInfos() []string { - // make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length - ret := strings.SplitN(a.Content, "|", 3) - for len(ret) < 3 { - ret = append(ret, "") - } - return ret -} - -func (a *Action) getIssueIndex() int64 { - infos := a.GetIssueInfos() - if len(infos) == 0 { - return 0 - } - index, _ := strconv.ParseInt(infos[0], 10, 64) - return index -} - -func (a *Action) LoadIssue(ctx context.Context) error { - if a.Issue != nil { - return nil - } - if index := a.getIssueIndex(); index > 0 { - issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) - if err != nil { - return err - } - a.Issue = issue - a.Issue.Repo = a.Repo - } - return nil -} - -// GetIssueTitle returns the title of first issue associated with the action. -func (a *Action) GetIssueTitle(ctx context.Context) string { - if err := a.LoadIssue(ctx); err != nil { - log.Error("LoadIssue: %v", err) - return "<500 when get issue>" - } - if a.Issue == nil { - return "" - } - return a.Issue.Title -} - -// GetIssueContent returns the content of first issue associated with this action. -func (a *Action) GetIssueContent(ctx context.Context) string { - if err := a.LoadIssue(ctx); err != nil { - log.Error("LoadIssue: %v", err) - return "<500 when get issue>" - } - if a.Issue == nil { - return "" - } - return a.Issue.Content -} - -// GetFeedsOptions options for retrieving feeds -type GetFeedsOptions struct { - db.ListOptions - RequestedUser *user_model.User // the user we want activity for - RequestedTeam *organization.Team // the team we want activity for - RequestedRepo *repo_model.Repository // the repo we want activity for - Actor *user_model.User // the user viewing the activity - IncludePrivate bool // include private actions - OnlyPerformedBy bool // only actions performed by requested user - IncludeDeleted bool // include deleted actions - Date string // the day we want activity for: YYYY-MM-DD -} - -// GetFeeds returns actions according to the provided options -func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) { - if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil { - return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") - } - - cond, err := activityQueryCondition(ctx, opts) - if err != nil { - return nil, 0, err - } - - actions := make([]*Action, 0, opts.PageSize) - var count int64 - - if opts.Page < 10 { // TODO: why it's 10 but other values? It's an experience value. - sess := db.GetEngine(ctx).Where(cond). - Select("`action`.*"). // this line will avoid select other joined table's columns - Join("INNER", "repository", "`repository`.id = `action`.repo_id") - - opts.SetDefaultValues() - sess = db.SetSessionPagination(sess, &opts) - - count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions) - if err != nil { - return nil, 0, fmt.Errorf("FindAndCount: %w", err) - } - } else { - // First, only query which IDs are necessary, and only then query all actions to speed up the overall query - sess := db.GetEngine(ctx).Where(cond). - Select("`action`.id"). - Join("INNER", "repository", "`repository`.id = `action`.repo_id") - - opts.SetDefaultValues() - sess = db.SetSessionPagination(sess, &opts) - - actionIDs := make([]int64, 0, opts.PageSize) - if err := sess.Table("action").Desc("`action`.created_unix").Find(&actionIDs); err != nil { - return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err) - } - - count, err = db.GetEngine(ctx).Where(cond). - Table("action"). - Cols("`action`.id"). - Join("INNER", "repository", "`repository`.id = `action`.repo_id").Count() - if err != nil { - return nil, 0, fmt.Errorf("Count: %w", err) - } - - if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil { - return nil, 0, fmt.Errorf("Find: %w", err) - } - } - - if err := ActionList(actions).LoadAttributes(ctx); err != nil { - return nil, 0, fmt.Errorf("LoadAttributes: %w", err) - } - - return actions, count, nil -} - // ActivityReadable return whether doer can read activities of user func ActivityReadable(user, doer *user_model.User) bool { return !user.KeepActivityPrivate || @@ -600,140 +270,6 @@ func activityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder. return cond, nil } -// DeleteOldActions deletes all old actions from database. -func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error) { - if olderThan <= 0 { - return nil - } - - _, err = db.GetEngine(ctx).Where("created_unix < ?", time.Now().Add(-olderThan).Unix()).Delete(&Action{}) - return err -} - -// NotifyWatchers creates batch of actions for every watcher. -// It could insert duplicate actions for a repository action, like this: -// * Original action: UserID=1 (the real actor), ActUserID=1 -// * Organization action: UserID=100 (the repo's org), ActUserID=1 -// * Watcher action: UserID=20 (a user who is watching a repo), ActUserID=1 -func NotifyWatchers(ctx context.Context, actions ...*Action) error { - var watchers []*repo_model.Watch - var repo *repo_model.Repository - var err error - var permCode []bool - var permIssue []bool - var permPR []bool - - e := db.GetEngine(ctx) - - for _, act := range actions { - repoChanged := repo == nil || repo.ID != act.RepoID - - if repoChanged { - // Add feeds for user self and all watchers. - watchers, err = repo_model.GetWatchers(ctx, act.RepoID) - if err != nil { - return fmt.Errorf("get watchers: %w", err) - } - } - - // Add feed for actioner. - act.UserID = act.ActUserID - if _, err = e.Insert(act); err != nil { - return fmt.Errorf("insert new actioner: %w", err) - } - - if repoChanged { - act.loadRepo(ctx) - repo = act.Repo - - // check repo owner exist. - if err := act.Repo.LoadOwner(ctx); err != nil { - return fmt.Errorf("can't get repo owner: %w", err) - } - } else if act.Repo == nil { - act.Repo = repo - } - - // Add feed for organization - if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID { - act.ID = 0 - act.UserID = act.Repo.Owner.ID - if err = db.Insert(ctx, act); err != nil { - return fmt.Errorf("insert new actioner: %w", err) - } - } - - if repoChanged { - permCode = make([]bool, len(watchers)) - permIssue = make([]bool, len(watchers)) - permPR = make([]bool, len(watchers)) - for i, watcher := range watchers { - user, err := user_model.GetUserByID(ctx, watcher.UserID) - if err != nil { - permCode[i] = false - permIssue[i] = false - permPR[i] = false - continue - } - perm, err := access_model.GetUserRepoPermission(ctx, repo, user) - if err != nil { - permCode[i] = false - permIssue[i] = false - permPR[i] = false - continue - } - permCode[i] = perm.CanRead(unit.TypeCode) - permIssue[i] = perm.CanRead(unit.TypeIssues) - permPR[i] = perm.CanRead(unit.TypePullRequests) - } - } - - for i, watcher := range watchers { - if act.ActUserID == watcher.UserID { - continue - } - act.ID = 0 - act.UserID = watcher.UserID - act.Repo.Units = nil - - switch act.OpType { - case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionPublishRelease, ActionDeleteBranch: - if !permCode[i] { - continue - } - case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue: - if !permIssue[i] { - continue - } - case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest, ActionAutoMergePullRequest: - if !permPR[i] { - continue - } - } - - if err = db.Insert(ctx, act); err != nil { - return fmt.Errorf("insert new action: %w", err) - } - } - } - return nil -} - -// NotifyWatchersActions creates batch of actions for every watcher. -func NotifyWatchersActions(ctx context.Context, acts []*Action) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - for _, act := range acts { - if err := NotifyWatchers(ctx, act); err != nil { - return err - } - } - return committer.Commit() -} - // DeleteIssueActions delete all actions related with issueID func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) error { // delete actions assigned to this issue @@ -765,23 +301,3 @@ func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) Delete(&Action{}) return err } - -// CountActionCreatedUnixString count actions where created_unix is an empty string -func CountActionCreatedUnixString(ctx context.Context) (int64, error) { - if setting.Database.Type.IsSQLite3() { - return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(Action)) - } - return 0, nil -} - -// FixActionCreatedUnixString set created_unix to zero if it is an empty string -func FixActionCreatedUnixString(ctx context.Context) (int64, error) { - if setting.Database.Type.IsSQLite3() { - res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ""`) - if err != nil { - return 0, err - } - return res.RowsAffected() - } - return 0, nil -} diff --git a/models/activities/user_activity.go b/models/activities/user_activity.go new file mode 100644 index 0000000000000..6252192694b00 --- /dev/null +++ b/models/activities/user_activity.go @@ -0,0 +1,479 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package activities + +import ( + "context" + "fmt" + "net/url" + "path" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm/schemas" +) + +// UserActivity represents a user activity. Don't add any xorm index comment after the fields. +// All the indexes are defined in the TableIndices method +type UserActivity struct { + ID int64 `xorm:"pk autoincr"` + OpType ActionType + ActUserID int64 // Action user id. + ActUser *user_model.User `xorm:"-"` + RepoID int64 + Repo *repo_model.Repository `xorm:"-"` + CommentID int64 + Comment *issues_model.Comment `xorm:"-"` + Issue *issues_model.Issue `xorm:"-"` // get the issue id from content + IsDeleted bool `xorm:"NOT NULL DEFAULT false"` + RefName string + IsPrivate bool `xorm:"NOT NULL DEFAULT false"` + Content string `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` +} + +func init() { + db.RegisterModel(new(UserActivity)) +} + +// TableIndices implements xorm's TableIndices interface +func (a *UserActivity) TableIndices() []*schemas.Index { + repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType) + repoIndex.AddColumn("repo_id", "user_id", "is_deleted") + + actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType) + actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted") + + cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType) + cudIndex.AddColumn("created_unix", "user_id", "is_deleted") + + indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex} + + return indices +} + +// GetOpType gets the ActionType of this action. +func (a *UserActivity) GetOpType() ActionType { + return a.OpType +} + +// LoadActUser loads a.ActUser +func (a *UserActivity) LoadActUser(ctx context.Context) { + if a.ActUser != nil { + return + } + var err error + a.ActUser, err = user_model.GetUserByID(ctx, a.ActUserID) + if err == nil { + return + } else if user_model.IsErrUserNotExist(err) { + a.ActUser = user_model.NewGhostUser() + } else { + log.Error("GetUserByID(%d): %v", a.ActUserID, err) + } +} + +func (a *UserActivity) loadRepo(ctx context.Context) { + if a.Repo != nil { + return + } + var err error + a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID) + if err != nil { + log.Error("repo_model.GetRepositoryByID(%d): %v", a.RepoID, err) + } +} + +// GetActFullName gets the action's user full name. +func (a *UserActivity) GetActFullName(ctx context.Context) string { + a.LoadActUser(ctx) + return a.ActUser.FullName +} + +// GetActUserName gets the action's user name. +func (a *UserActivity) GetActUserName(ctx context.Context) string { + a.LoadActUser(ctx) + return a.ActUser.Name +} + +// ShortActUserName gets the action's user name trimmed to max 20 +// chars. +func (a *UserActivity) ShortActUserName(ctx context.Context) string { + return base.EllipsisString(a.GetActUserName(ctx), 20) +} + +// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. +func (a *UserActivity) GetActDisplayName(ctx context.Context) string { + if setting.UI.DefaultShowFullName { + trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx)) + if len(trimmedFullName) > 0 { + return trimmedFullName + } + } + return a.ShortActUserName(ctx) +} + +// GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME +func (a *UserActivity) GetActDisplayNameTitle(ctx context.Context) string { + if setting.UI.DefaultShowFullName { + return a.ShortActUserName(ctx) + } + return a.GetActFullName(ctx) +} + +// GetRepoUserName returns the name of the action repository owner. +func (a *UserActivity) GetRepoUserName(ctx context.Context) string { + a.loadRepo(ctx) + return a.Repo.OwnerName +} + +// ShortRepoUserName returns the name of the action repository owner +// trimmed to max 20 chars. +func (a *UserActivity) ShortRepoUserName(ctx context.Context) string { + return base.EllipsisString(a.GetRepoUserName(ctx), 20) +} + +// GetRepoName returns the name of the action repository. +func (a *UserActivity) GetRepoName(ctx context.Context) string { + a.loadRepo(ctx) + return a.Repo.Name +} + +// ShortRepoName returns the name of the action repository +// trimmed to max 33 chars. +func (a *UserActivity) ShortRepoName(ctx context.Context) string { + return base.EllipsisString(a.GetRepoName(ctx), 33) +} + +// GetRepoPath returns the virtual path to the action repository. +func (a *UserActivity) GetRepoPath(ctx context.Context) string { + return path.Join(a.GetRepoUserName(ctx), a.GetRepoName(ctx)) +} + +// ShortRepoPath returns the virtual path to the action repository +// trimmed to max 20 + 1 + 33 chars. +func (a *UserActivity) ShortRepoPath(ctx context.Context) string { + return path.Join(a.ShortRepoUserName(ctx), a.ShortRepoName(ctx)) +} + +// GetRepoLink returns relative link to action repository. +func (a *UserActivity) GetRepoLink(ctx context.Context) string { + // path.Join will skip empty strings + return path.Join(setting.AppSubURL, "/", url.PathEscape(a.GetRepoUserName(ctx)), url.PathEscape(a.GetRepoName(ctx))) +} + +// GetRepoAbsoluteLink returns the absolute link to action repository. +func (a *UserActivity) GetRepoAbsoluteLink(ctx context.Context) string { + return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx)) +} + +// GetBranch returns the action's repository branch. +func (a *UserActivity) GetBranch() string { + return strings.TrimPrefix(a.RefName, git.BranchPrefix) +} + +// GetRefLink returns the action's ref link. +func (a *UserActivity) GetRefLink(ctx context.Context) string { + return git.RefURL(a.GetRepoLink(ctx), a.RefName) +} + +// GetTag returns the action's repository tag. +func (a *UserActivity) GetTag() string { + return strings.TrimPrefix(a.RefName, git.TagPrefix) +} + +// GetContent returns the action's content. +func (a *UserActivity) GetContent() string { + return a.Content +} + +// GetCreate returns the action creation time. +func (a *UserActivity) GetCreate() time.Time { + return a.CreatedUnix.AsTime() +} + +func (a *UserActivity) IsIssueEvent() bool { + return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request") +} + +// GetIssueInfos returns a list of associated information with the action. +func (a *UserActivity) GetIssueInfos() []string { + // make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length + ret := strings.SplitN(a.Content, "|", 3) + for len(ret) < 3 { + ret = append(ret, "") + } + return ret +} + +func (a *UserActivity) getIssueIndex() int64 { + infos := a.GetIssueInfos() + if len(infos) == 0 { + return 0 + } + index, _ := strconv.ParseInt(infos[0], 10, 64) + return index +} + +func (a *UserActivity) LoadRepo(ctx context.Context) error { + if a.Repo != nil { + return nil + } + var err error + a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID) + return err +} + +func (a *UserActivity) loadComment(ctx context.Context) (err error) { + if a.CommentID == 0 || a.Comment != nil { + return nil + } + a.Comment, err = issues_model.GetCommentByID(ctx, a.CommentID) + return err +} + +// GetCommentHTMLURL returns link to action comment. +func (a *UserActivity) GetCommentHTMLURL(ctx context.Context) string { + if a == nil { + return "#" + } + _ = a.loadComment(ctx) + if a.Comment != nil { + return a.Comment.HTMLURL(ctx) + } + + if err := a.LoadIssue(ctx); err != nil || a.Issue == nil { + return "#" + } + if err := a.Issue.LoadRepo(ctx); err != nil { + return "#" + } + + return a.Issue.HTMLURL() +} + +// GetCommentLink returns link to action comment. +func (a *UserActivity) GetCommentLink(ctx context.Context) string { + if a == nil { + return "#" + } + _ = a.loadComment(ctx) + if a.Comment != nil { + return a.Comment.Link(ctx) + } + + if err := a.LoadIssue(ctx); err != nil || a.Issue == nil { + return "#" + } + if err := a.Issue.LoadRepo(ctx); err != nil { + return "#" + } + + return a.Issue.Link() +} + +func (a *UserActivity) LoadIssue(ctx context.Context) error { + if a.Issue != nil { + return nil + } + if index := a.getIssueIndex(); index > 0 { + issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) + if err != nil { + return err + } + a.Issue = issue + a.Issue.Repo = a.Repo + } + return nil +} + +// GetIssueTitle returns the title of first issue associated with the action. +func (a *UserActivity) GetIssueTitle(ctx context.Context) string { + if err := a.LoadIssue(ctx); err != nil { + log.Error("LoadIssue: %v", err) + return "<500 when get issue>" + } + if a.Issue == nil { + return "" + } + return a.Issue.Title +} + +// GetIssueContent returns the content of first issue associated with this action. +func (a *UserActivity) GetIssueContent(ctx context.Context) string { + if err := a.LoadIssue(ctx); err != nil { + log.Error("LoadIssue: %v", err) + return "<500 when get issue>" + } + if a.Issue == nil { + return "" + } + return a.Issue.Content +} + +func NotifyWatchers(ctx context.Context, activity *UserActivity) error { + watchers, err := repo_model.GetWatchers(ctx, activity.RepoID) + if err != nil { + return err + } + return db.WithTx(ctx, func(ctx context.Context) error { + return notifyWatchers(ctx, activity, watchers) + }) +} + +// notifyWatchers creates batch of actions for every watcher. +// It could insert duplicate actions for a repository action, like this: +// * Original action: UserID=1 (the real actor), ActUserID=1 +// * Organization action: UserID=100 (the repo's org), ActUserID=1 +// * Watcher action: UserID=20 (a user who is watching a repo), ActUserID=1 +func notifyWatchers(ctx context.Context, activity *UserActivity, watchers []*repo_model.Watch) error { + var err error + var permCode []bool + var permIssue []bool + var permPR []bool + + // Add activity + if err = db.Insert(ctx, activity); err != nil { + return fmt.Errorf("insert new action: %w", err) + } + + // Add feed for actioner. + if err := db.Insert(ctx, &UserFeed{ + UserID: activity.ActUserID, + ActivityID: activity.ID, + }); err != nil { + return fmt.Errorf("insert new actioner: %w", err) + } + + if err := activity.LoadRepo(ctx); err != nil { + return err + } + + // check repo owner exist. + if err := activity.Repo.LoadOwner(ctx); err != nil { + return fmt.Errorf("can't get repo owner: %w", err) + } + + // Add feed for organization + if activity.Repo.Owner.IsOrganization() && activity.ActUserID != activity.Repo.Owner.ID { + if err = db.Insert(ctx, &UserFeed{ + UserID: activity.Repo.Owner.ID, + ActivityID: activity.ID, + }); err != nil { + return fmt.Errorf("insert new actioner: %w", err) + } + } + + permCode = make([]bool, len(watchers)) + permIssue = make([]bool, len(watchers)) + permPR = make([]bool, len(watchers)) + for i, watcher := range watchers { + user, err := user_model.GetUserByID(ctx, watcher.UserID) + if err != nil { + permCode[i] = false + permIssue[i] = false + permPR[i] = false + continue + } + perm, err := access_model.GetUserRepoPermission(ctx, activity.Repo, user) + if err != nil { + permCode[i] = false + permIssue[i] = false + permPR[i] = false + continue + } + permCode[i] = perm.CanRead(unit.TypeCode) + permIssue[i] = perm.CanRead(unit.TypeIssues) + permPR[i] = perm.CanRead(unit.TypePullRequests) + } + + for i, watcher := range watchers { + if activity.ActUserID == watcher.UserID { + continue + } + + switch activity.OpType { + case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionPublishRelease, ActionDeleteBranch: + if !permCode[i] { + continue + } + case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue: + if !permIssue[i] { + continue + } + case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest, ActionAutoMergePullRequest: + if !permPR[i] { + continue + } + } + + if err = db.Insert(ctx, &UserFeed{ + UserID: watcher.UserID, + ActivityID: activity.ID, + }); err != nil { + return fmt.Errorf("insert new action: %w", err) + } + } + + return nil +} + +// NotifyWatchersActions creates batch of actions for every watcher. +func NotifyWatchersActions(ctx context.Context, activities []*UserActivity) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + watchersCache := make(map[int64][]*repo_model.Watch, len(activities)) + for _, activity := range activities { + watchers, ok := watchersCache[activity.RepoID] + if !ok { + watchers, err = repo_model.GetWatchers(ctx, activity.RepoID) + if err != nil { + return err + } + watchersCache[activity.RepoID] = watchers + } + + if err := notifyWatchers(ctx, activity, watchers); err != nil { + return err + } + } + return committer.Commit() +} + +// FixUserActivityCreatedUnixString set created_unix to zero if it is an empty string +func FixUserActivityCreatedUnixString(ctx context.Context) (int64, error) { + if setting.Database.Type.IsSQLite3() { + res, err := db.GetEngine(ctx).Exec(`UPDATE user_activity SET created_unix = 0 WHERE created_unix = ""`) + if err != nil { + return 0, err + } + return res.RowsAffected() + } + return 0, nil +} + +// CountActivitiesCreatedUnixString count actions where created_unix is an empty string +func CountActivitiesCreatedUnixString(ctx context.Context) (int64, error) { + if setting.Database.Type.IsSQLite3() { + return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(UserActivity)) + } + return 0, nil +} diff --git a/models/activities/action_list.go b/models/activities/user_activity_list.go similarity index 51% rename from models/activities/action_list.go rename to models/activities/user_activity_list.go index aafb7f8a26c57..444716d37f429 100644 --- a/models/activities/action_list.go +++ b/models/activities/user_activity_list.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" @@ -18,16 +19,16 @@ import ( "xorm.io/builder" ) -// ActionList defines a list of actions -type ActionList []*Action +// UserActivityList defines a list of UserActivity +type UserActivityList []*UserActivity -func (actions ActionList) getUserIDs() []int64 { - return container.FilterSlice(actions, func(action *Action) (int64, bool) { +func (actions UserActivityList) getUserIDs() []int64 { + return container.FilterSlice(actions, func(action *UserActivity) (int64, bool) { return action.ActUserID, true }) } -func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_model.User, error) { +func (actions UserActivityList) LoadActUsers(ctx context.Context) (map[int64]*user_model.User, error) { if len(actions) == 0 { return nil, nil } @@ -47,13 +48,13 @@ func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_mod return userMaps, nil } -func (actions ActionList) getRepoIDs() []int64 { - return container.FilterSlice(actions, func(action *Action) (int64, bool) { +func (actions UserActivityList) getRepoIDs() []int64 { + return container.FilterSlice(actions, func(action *UserActivity) (int64, bool) { return action.RepoID, true }) } -func (actions ActionList) LoadRepositories(ctx context.Context) error { +func (actions UserActivityList) LoadRepositories(ctx context.Context) error { if len(actions) == 0 { return nil } @@ -71,12 +72,12 @@ func (actions ActionList) LoadRepositories(ctx context.Context) error { return repos.LoadUnits(ctx) } -func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]*user_model.User) (err error) { +func (actions UserActivityList) loadRepoOwner(ctx context.Context, userMap map[int64]*user_model.User) (err error) { if userMap == nil { userMap = make(map[int64]*user_model.User) } - missingUserIDs := container.FilterSlice(actions, func(action *Action) (int64, bool) { + missingUserIDs := container.FilterSlice(actions, func(action *UserActivity) (int64, bool) { if action.Repo == nil { return 0, false } @@ -103,7 +104,7 @@ func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]* } // LoadAttributes loads all attributes -func (actions ActionList) LoadAttributes(ctx context.Context) error { +func (actions UserActivityList) LoadAttributes(ctx context.Context) error { // the load sequence cannot be changed because of the dependencies userMap, err := actions.LoadActUsers(ctx) if err != nil { @@ -121,7 +122,7 @@ func (actions ActionList) LoadAttributes(ctx context.Context) error { return actions.LoadComments(ctx) } -func (actions ActionList) LoadComments(ctx context.Context) error { +func (actions UserActivityList) LoadComments(ctx context.Context) error { if len(actions) == 0 { return nil } @@ -152,7 +153,7 @@ func (actions ActionList) LoadComments(ctx context.Context) error { return nil } -func (actions ActionList) LoadIssues(ctx context.Context) error { +func (actions UserActivityList) LoadIssues(ctx context.Context) error { if len(actions) == 0 { return nil } @@ -201,3 +202,76 @@ func (actions ActionList) LoadIssues(ctx context.Context) error { } return nil } + +// GetFeedsOptions options for retrieving feeds +type GetFeedsOptions struct { + db.ListOptions + RequestedUser *user_model.User // the user we want activity for + RequestedTeam *organization.Team // the team we want activity for + RequestedRepo *repo_model.Repository // the repo we want activity for + Actor *user_model.User // the user viewing the activity + IncludePrivate bool // include private actions + OnlyPerformedBy bool // only actions performed by requested user + IncludeDeleted bool // include deleted actions + Date string // the day we want activity for: YYYY-MM-DD +} + +// GetFeeds returns actions according to the provided options +func GetFeeds(ctx context.Context, opts GetFeedsOptions) (UserActivityList, int64, error) { + if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil { + return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") + } + + cond, err := activityQueryCondition(ctx, opts) + if err != nil { + return nil, 0, err + } + + actions := make([]*UserActivity, 0, opts.PageSize) + var count int64 + + if opts.Page < 10 { // TODO: why it's 10 but other values? It's an experience value. + sess := db.GetEngine(ctx).Where(cond). + Select("`user_activity`.*"). // this line will avoid select other joined table's columns + Join("INNER", "repository", "`repository`.id = `user_activity`.repo_id") + + opts.SetDefaultValues() + sess = db.SetSessionPagination(sess, &opts) + + count, err = sess.Desc("`user_activity`.created_unix").FindAndCount(&actions) + if err != nil { + return nil, 0, fmt.Errorf("FindAndCount: %w", err) + } + } else { + // First, only query which IDs are necessary, and only then query all actions to speed up the overall query + sess := db.GetEngine(ctx).Where(cond). + Select("`user_activity`.id"). + Join("INNER", "repository", "`repository`.id = `user_activity`.repo_id") + + opts.SetDefaultValues() + sess = db.SetSessionPagination(sess, &opts) + + actionIDs := make([]int64, 0, opts.PageSize) + if err := sess.Table("action").Desc("`user_activity`.created_unix").Find(&actionIDs); err != nil { + return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err) + } + + count, err = db.GetEngine(ctx).Where(cond). + Table("action"). + Cols("`user_activity`.id"). + Join("INNER", "repository", "`repository`.id = `user_activity`.repo_id").Count() + if err != nil { + return nil, 0, fmt.Errorf("Count: %w", err) + } + + if err := db.GetEngine(ctx).In("`user_activity`.id", actionIDs).Desc("`user_activity`.created_unix").Find(&actions); err != nil { + return nil, 0, fmt.Errorf("Find: %w", err) + } + } + + if err := UserActivityList(actions).LoadAttributes(ctx); err != nil { + return nil, 0, fmt.Errorf("LoadAttributes: %w", err) + } + + return actions, count, nil +} diff --git a/models/activities/user_feed.go b/models/activities/user_feed.go new file mode 100644 index 0000000000000..fa25488df6b59 --- /dev/null +++ b/models/activities/user_feed.go @@ -0,0 +1,29 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package activities + +import ( + "context" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" +) + +type UserFeed struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"UNIQUE(s)"` // Receiver user id. + ActivityID int64 `xorm:"UNIQUE(s)"` // refer to action table + CreatedUnix timeutil.TimeStamp `xorm:"created"` +} + +// DeleteOldUserFeeds deletes all old actions from database. +func DeleteOldUserFeeds(ctx context.Context, olderThan time.Duration) (err error) { + if olderThan <= 0 { + return nil + } + + _, err = db.GetEngine(ctx).Where("created_unix < ?", time.Now().Add(-olderThan).Unix()).Delete(&UserFeed{}) + return err +} diff --git a/models/migrations/v1_23/v305.go b/models/migrations/v1_23/v305.go new file mode 100644 index 0000000000000..6ba91616dbc7c --- /dev/null +++ b/models/migrations/v1_23/v305.go @@ -0,0 +1,56 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import ( + "fmt" + + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +// SplitActionTableAsTwoTables splits the action table as two tables. +func SplitActionTableAsTwoTables(x *xorm.Engine) error { + // 1 create new table + type UserFeed struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"UNIQUE(s)"` // Receiver user id. + ActivityID int64 `xorm:"UNIQUE(s)"` // refer to action table + CreatedUnix timeutil.TimeStamp `xorm:"created"` + } + + type UserActivity struct { + ID int64 `xorm:"pk autoincr"` + OpType int + ActUserID int64 // Action user id. + RepoID int64 + CommentID int64 + IsDeleted bool `xorm:"NOT NULL DEFAULT false"` + RefName string + IsPrivate bool `xorm:"NOT NULL DEFAULT false"` + Content string `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` + } + + if err := x.Sync(new(UserFeed), new(UserActivity)); err != nil { + return err + } + + // 2 copy data from action table to new table + if _, err := x.Exec("INSERT INTO `user_feed` (`user_id`, `action_id`, `created_unix`) SELECT `user_id`, `id`, `created_unix` FROM `action`"); err != nil { + return err + } + + // 3 merge records from action table + if _, err := x.Exec("INSERT INTO `user_activity` (`op_type`, `act_user_id`, `repo_id`, `comment_id`, `is_deleted`, `ref_name`, `is_private`, `content`, `created_unix`) SELECT `op_type`, `act_user_id`, `repo_id`, `comment_id`, `is_deleted`, `ref_name`, `is_private`, `content`, `created_unix` FROM `action`"); err != nil { + return err + } + + // 4 update user_feed table to update action_id because of the merge records + + // 5 drop column from action table + + return fmt.Errorf("not implemented") +} diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go index afc10915163bc..f4bf87d6a99c7 100644 --- a/modules/templates/util_avatar.go +++ b/modules/templates/util_avatar.go @@ -63,7 +63,7 @@ func (au *AvatarUtils) Avatar(item any, others ...any) template.HTML { } // AvatarByAction renders user avatars from action. args: action, size (int), class (string) -func (au *AvatarUtils) AvatarByAction(action *activities_model.Action, others ...any) template.HTML { +func (au *AvatarUtils) AvatarByAction(action *activities_model.UserActivity, others ...any) template.HTML { action.LoadActUser(au.ctx) return au.Avatar(action.ActUser, others...) } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index e848d95181094..f9a14e1c34f46 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -454,5 +454,10 @@ func ListOrgActivityFeeds(ctx *context.APIContext) { } ctx.SetTotalCountHeader(count) - ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer)) + doer := ctx.Doer + if doer == nil { + doer = user_model.NewGhostUser() + } + + ctx.JSON(http.StatusOK, convert.ToActivities(ctx, doer.ID, feeds, ctx.Doer)) } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 1bcec8fcf7e72..5976d95ed4023 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -1308,5 +1308,10 @@ func ListRepoActivityFeeds(ctx *context.APIContext) { } ctx.SetTotalCountHeader(count) - ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer)) + doer := ctx.Doer + if doer == nil { + doer = user_model.NewGhostUser() + } + + ctx.JSON(http.StatusOK, convert.ToActivities(ctx, doer.ID, feeds, ctx.Doer)) } diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index fedad87fc4fa5..40a105015fb47 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -214,5 +214,10 @@ func ListUserActivityFeeds(ctx *context.APIContext) { } ctx.SetTotalCountHeader(count) - ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer)) + doer := ctx.Doer + if doer == nil { + doer = user_model.NewGhostUser() + } + + ctx.JSON(http.StatusOK, convert.ToActivities(ctx, doer.ID, feeds, ctx.Doer)) } diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index 774644e29ae21..bb63a89d7c942 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -24,33 +24,33 @@ import ( "github.com/gorilla/feeds" ) -func toBranchLink(ctx *context.Context, act *activities_model.Action) string { +func toBranchLink(ctx *context.Context, act *activities_model.UserActivity) string { return act.GetRepoAbsoluteLink(ctx) + "/src/branch/" + util.PathEscapeSegments(act.GetBranch()) } -func toTagLink(ctx *context.Context, act *activities_model.Action) string { +func toTagLink(ctx *context.Context, act *activities_model.UserActivity) string { return act.GetRepoAbsoluteLink(ctx) + "/src/tag/" + util.PathEscapeSegments(act.GetTag()) } -func toIssueLink(ctx *context.Context, act *activities_model.Action) string { +func toIssueLink(ctx *context.Context, act *activities_model.UserActivity) string { return act.GetRepoAbsoluteLink(ctx) + "/issues/" + url.PathEscape(act.GetIssueInfos()[0]) } -func toPullLink(ctx *context.Context, act *activities_model.Action) string { +func toPullLink(ctx *context.Context, act *activities_model.UserActivity) string { return act.GetRepoAbsoluteLink(ctx) + "/pulls/" + url.PathEscape(act.GetIssueInfos()[0]) } -func toSrcLink(ctx *context.Context, act *activities_model.Action) string { +func toSrcLink(ctx *context.Context, act *activities_model.UserActivity) string { return act.GetRepoAbsoluteLink(ctx) + "/src/" + util.PathEscapeSegments(act.GetBranch()) } -func toReleaseLink(ctx *context.Context, act *activities_model.Action) string { +func toReleaseLink(ctx *context.Context, act *activities_model.UserActivity) string { return act.GetRepoAbsoluteLink(ctx) + "/releases/tag/" + util.PathEscapeSegments(act.GetBranch()) } // renderMarkdown creates a minimal markdown render context from an action. // If rendering fails, the original markdown text is returned -func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) template.HTML { +func renderMarkdown(ctx *context.Context, act *activities_model.UserActivity, content string) template.HTML { markdownCtx := &markup.RenderContext{ Ctx: ctx, Links: markup.Links{ @@ -70,10 +70,12 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content } // feedActionsToFeedItems convert gitea's Action feed to feeds Item -func feedActionsToFeedItems(ctx *context.Context, actions activities_model.ActionList) (items []*feeds.Item, err error) { - for _, act := range actions { - act.LoadActUser(ctx) +func feedActionsToFeedItems(ctx *context.Context, actions activities_model.UserActivityList) (items []*feeds.Item, err error) { + if _, err := actions.LoadActUsers(ctx); err != nil { + return nil, err + } + for _, act := range actions { // TODO: the code seems quite strange (maybe not right) // sometimes it uses text content but sometimes it uses HTML content // it should clearly defines which kind of content it should use for the feed items: plan text or rich HTML diff --git a/services/convert/activity.go b/services/convert/activity.go index 01fef73e589fa..dbbe8e48a6a6f 100644 --- a/services/convert/activity.go +++ b/services/convert/activity.go @@ -14,7 +14,7 @@ import ( api "code.gitea.io/gitea/modules/structs" ) -func ToActivity(ctx context.Context, ac *activities_model.Action, doer *user_model.User) *api.Activity { +func ToActivity(ctx context.Context, watcherUserID int64, ac *activities_model.UserActivity, doer *user_model.User) *api.Activity { p, err := access_model.GetUserRepoPermission(ctx, ac.Repo, doer) if err != nil { log.Error("GetUserRepoPermission[%d]: %v", ac.RepoID, err) @@ -23,7 +23,7 @@ func ToActivity(ctx context.Context, ac *activities_model.Action, doer *user_mod result := &api.Activity{ ID: ac.ID, - UserID: ac.UserID, + UserID: watcherUserID, OpType: ac.OpType.String(), ActUserID: ac.ActUserID, ActUser: ToUser(ctx, ac.ActUser, doer), @@ -43,10 +43,10 @@ func ToActivity(ctx context.Context, ac *activities_model.Action, doer *user_mod return result } -func ToActivities(ctx context.Context, al activities_model.ActionList, doer *user_model.User) []*api.Activity { +func ToActivities(ctx context.Context, watcherUserID int64, al activities_model.UserActivityList, doer *user_model.User) []*api.Activity { result := make([]*api.Activity, 0, len(al)) for _, ac := range al { - result = append(result, ToActivity(ctx, ac, doer)) + result = append(result, ToActivity(ctx, watcherUserID, ac, doer)) } return result } diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go index 0018c5facc5d7..75a1f8a092be8 100644 --- a/services/cron/tasks_extended.go +++ b/services/cron/tasks_extended.go @@ -135,7 +135,7 @@ func registerDeleteOldActions() { OlderThan: 365 * 24 * time.Hour, }, func(ctx context.Context, _ *user_model.User, config Config) error { olderThanConfig := config.(*OlderThanConfig) - return activities_model.DeleteOldActions(ctx, olderThanConfig.OlderThan) + return activities_model.DeleteOldUserFeeds(ctx, olderThanConfig.OlderThan) }) } diff --git a/services/doctor/dbconsistency.go b/services/doctor/dbconsistency.go index 7cb7445148a0b..083865d97de53 100644 --- a/services/doctor/dbconsistency.go +++ b/services/doctor/dbconsistency.go @@ -141,9 +141,9 @@ func prepareDBConsistencyChecks() []consistencyCheck { FixedMessage: "Removed", }, { - Name: "Action with created_unix set as an empty string", - Counter: activities_model.CountActionCreatedUnixString, - Fixer: activities_model.FixActionCreatedUnixString, + Name: "User Activities with created_unix set as an empty string", + Counter: activities_model.CountActivitiesCreatedUnixString, + Fixer: activities_model.FixUserActivityCreatedUnixString, FixedMessage: "Set to zero", }, { diff --git a/services/feed/action.go b/services/feed/action.go index 83daaa1438fd5..84858fd597ce6 100644 --- a/services/feed/action.go +++ b/services/feed/action.go @@ -49,7 +49,7 @@ func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue } repo := issue.Repo - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: issue.Poster.ID, ActUser: issue.Poster, OpType: activities_model.ActionCreateIssue, @@ -66,7 +66,7 @@ func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, closeOrReopen bool) { // Compose comment action, could be plain comment, close or reopen issue/pull request. // This object will be used to notify watchers in the end of function. - act := &activities_model.Action{ + act := &activities_model.UserActivity{ ActUserID: doer.ID, ActUser: doer, Content: fmt.Sprintf("%d|%s", issue.Index, ""), @@ -99,7 +99,7 @@ func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { - act := &activities_model.Action{ + act := &activities_model.UserActivity{ ActUserID: doer.ID, ActUser: doer, RepoID: issue.Repo.ID, @@ -145,7 +145,7 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model. return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: pull.Issue.Poster.ID, ActUser: pull.Issue.Poster, OpType: activities_model.ActionCreatePullRequest, @@ -159,7 +159,7 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model. } func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldRepoName string) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionRenameRepo, @@ -173,7 +173,7 @@ func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model. } func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionTransferRepo, @@ -187,7 +187,7 @@ func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_mode } func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionCreateRepo, @@ -200,7 +200,7 @@ func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_mod } func (a *actionNotifier) ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionCreateRepo, @@ -222,11 +222,11 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model return } - actions := make([]*activities_model.Action, 0, 10) + actions := make([]*activities_model.UserActivity, 0, 10) for _, lines := range review.CodeComments { for _, comments := range lines { for _, comm := range comments { - actions = append(actions, &activities_model.Action{ + actions = append(actions, &activities_model.UserActivity{ ActUserID: review.Reviewer.ID, ActUser: review.Reviewer, Content: fmt.Sprintf("%d|%s", review.Issue.Index, strings.Split(comm.Content, "\n")[0]), @@ -242,7 +242,7 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model } if review.Type != issues_model.ReviewTypeComment || strings.TrimSpace(comment.Content) != "" { - action := &activities_model.Action{ + action := &activities_model.UserActivity{ ActUserID: review.Reviewer.ID, ActUser: review.Reviewer, Content: fmt.Sprintf("%d|%s", review.Issue.Index, strings.Split(comment.Content, "\n")[0]), @@ -271,7 +271,7 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model } func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionMergePullRequest, @@ -285,7 +285,7 @@ func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.Us } func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionAutoMergePullRequest, @@ -303,7 +303,7 @@ func (*actionNotifier) NotifyPullRevieweDismiss(ctx context.Context, doer *user_ if len(review.OriginalAuthor) > 0 { reviewerName = review.OriginalAuthor } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionPullReviewDismissed, @@ -337,7 +337,7 @@ func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.Use opType = activities_model.ActionDeleteBranch } - if err = activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err = activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: pusher.ID, ActUser: pusher, OpType: opType, @@ -357,7 +357,7 @@ func (a *actionNotifier) CreateRef(ctx context.Context, doer *user_model.User, r // has sent same action in `PushCommits`, so skip it. return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: doer.ID, ActUser: doer, OpType: opType, @@ -376,7 +376,7 @@ func (a *actionNotifier) DeleteRef(ctx context.Context, doer *user_model.User, r // has sent same action in `PushCommits`, so skip it. return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: doer.ID, ActUser: doer, OpType: opType, @@ -396,7 +396,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncPush, @@ -411,7 +411,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model } func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncCreate, @@ -425,7 +425,7 @@ func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.Use } func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncDelete, @@ -443,7 +443,7 @@ func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release log.Error("LoadAttributes: %v", err) return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.UserActivity{ ActUserID: rel.PublisherID, ActUser: rel.Publisher, OpType: activities_model.ActionPublishRelease,