Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
484 changes: 0 additions & 484 deletions models/activities/action.go

Large diffs are not rendered by default.

479 changes: 479 additions & 0 deletions models/activities/user_activity.go

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
29 changes: 29 additions & 0 deletions models/activities/user_feed.go
Original file line number Diff line number Diff line change
@@ -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
}
56 changes: 56 additions & 0 deletions models/migrations/v1_23/v305.go
Original file line number Diff line number Diff line change
@@ -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")
}
2 changes: 1 addition & 1 deletion modules/templates/util_avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
}
Expand Down
7 changes: 6 additions & 1 deletion routers/api/v1/org/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
7 changes: 6 additions & 1 deletion routers/api/v1/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
7 changes: 6 additions & 1 deletion routers/api/v1/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
22 changes: 12 additions & 10 deletions routers/web/feed/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions services/convert/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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),
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion services/cron/tasks_extended.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}

Expand Down
Loading
Loading