mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	RSS/Atom support for Repos (#19055)
* support for repos * refactor * advertise the feeds via meta tags * allow feed suffix and feed header * optimize performance
This commit is contained in:
		@@ -328,7 +328,7 @@ type GetFeedsOptions struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetFeeds returns actions according to the provided options
 | 
					// GetFeeds returns actions according to the provided options
 | 
				
			||||||
func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
 | 
					func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, error) {
 | 
				
			||||||
	if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
 | 
						if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
 | 
				
			||||||
		return nil, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
 | 
							return nil, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -338,7 +338,8 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
 | 
				
			|||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	sess := db.GetEngine(db.DefaultContext).Where(cond)
 | 
						e := db.GetEngine(ctx)
 | 
				
			||||||
 | 
						sess := e.Where(cond)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	opts.SetDefaultValues()
 | 
						opts.SetDefaultValues()
 | 
				
			||||||
	sess = db.SetSessionPagination(sess, &opts)
 | 
						sess = db.SetSessionPagination(sess, &opts)
 | 
				
			||||||
@@ -349,7 +350,7 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
 | 
				
			|||||||
		return nil, fmt.Errorf("Find: %v", err)
 | 
							return nil, fmt.Errorf("Find: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := ActionList(actions).LoadAttributes(); err != nil {
 | 
						if err := ActionList(actions).loadAttributes(e); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("LoadAttributes: %v", err)
 | 
							return nil, fmt.Errorf("LoadAttributes: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@ func (actions ActionList) getUserIDs() []int64 {
 | 
				
			|||||||
	return keysInt64(userIDs)
 | 
						return keysInt64(userIDs)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (actions ActionList) loadUsers(e db.Engine) ([]*user_model.User, error) {
 | 
					func (actions ActionList) loadUsers(e db.Engine) (map[int64]*user_model.User, error) {
 | 
				
			||||||
	if len(actions) == 0 {
 | 
						if len(actions) == 0 {
 | 
				
			||||||
		return nil, nil
 | 
							return nil, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -42,12 +42,7 @@ func (actions ActionList) loadUsers(e db.Engine) ([]*user_model.User, error) {
 | 
				
			|||||||
	for _, action := range actions {
 | 
						for _, action := range actions {
 | 
				
			||||||
		action.ActUser = userMaps[action.ActUserID]
 | 
							action.ActUser = userMaps[action.ActUserID]
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return valuesUser(userMaps), nil
 | 
						return userMaps, nil
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// LoadUsers loads actions' all users
 | 
					 | 
				
			||||||
func (actions ActionList) LoadUsers() ([]*user_model.User, error) {
 | 
					 | 
				
			||||||
	return actions.loadUsers(db.GetEngine(db.DefaultContext))
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (actions ActionList) getRepoIDs() []int64 {
 | 
					func (actions ActionList) getRepoIDs() []int64 {
 | 
				
			||||||
@@ -60,45 +55,57 @@ func (actions ActionList) getRepoIDs() []int64 {
 | 
				
			|||||||
	return keysInt64(repoIDs)
 | 
						return keysInt64(repoIDs)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (actions ActionList) loadRepositories(e db.Engine) ([]*repo_model.Repository, error) {
 | 
					func (actions ActionList) loadRepositories(e db.Engine) error {
 | 
				
			||||||
	if len(actions) == 0 {
 | 
						if len(actions) == 0 {
 | 
				
			||||||
		return nil, nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	repoIDs := actions.getRepoIDs()
 | 
						repoIDs := actions.getRepoIDs()
 | 
				
			||||||
	repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs))
 | 
						repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs))
 | 
				
			||||||
	err := e.
 | 
						err := e.In("id", repoIDs).Find(&repoMaps)
 | 
				
			||||||
		In("id", repoIDs).
 | 
					 | 
				
			||||||
		Find(&repoMaps)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("find repository: %v", err)
 | 
							return fmt.Errorf("find repository: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, action := range actions {
 | 
						for _, action := range actions {
 | 
				
			||||||
		action.Repo = repoMaps[action.RepoID]
 | 
							action.Repo = repoMaps[action.RepoID]
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return valuesRepository(repoMaps), nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoadRepositories loads actions' all repositories
 | 
					func (actions ActionList) loadRepoOwner(e db.Engine, userMap map[int64]*user_model.User) (err error) {
 | 
				
			||||||
func (actions ActionList) LoadRepositories() ([]*repo_model.Repository, error) {
 | 
						if userMap == nil {
 | 
				
			||||||
	return actions.loadRepositories(db.GetEngine(db.DefaultContext))
 | 
							userMap = make(map[int64]*user_model.User)
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// loadAttributes loads all attributes
 | 
					 | 
				
			||||||
func (actions ActionList) loadAttributes(e db.Engine) (err error) {
 | 
					 | 
				
			||||||
	if _, err = actions.loadUsers(e); err != nil {
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, err = actions.loadRepositories(e); err != nil {
 | 
						for _, action := range actions {
 | 
				
			||||||
		return
 | 
							repoOwner, ok := userMap[action.Repo.OwnerID]
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								repoOwner, err = user_model.GetUserByID(action.Repo.OwnerID)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									if user_model.IsErrUserNotExist(err) {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								userMap[repoOwner.ID] = repoOwner
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							action.Repo.Owner = repoOwner
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoadAttributes loads attributes of the actions
 | 
					// loadAttributes loads all attributes
 | 
				
			||||||
func (actions ActionList) LoadAttributes() error {
 | 
					func (actions ActionList) loadAttributes(e db.Engine) error {
 | 
				
			||||||
	return actions.loadAttributes(db.GetEngine(db.DefaultContext))
 | 
						userMap, err := actions.loadUsers(e)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := actions.loadRepositories(e); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return actions.loadRepoOwner(e, userMap)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ import (
 | 
				
			|||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
	"code.gitea.io/gitea/models/unittest"
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
@@ -39,7 +40,7 @@ func TestGetFeeds(t *testing.T) {
 | 
				
			|||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 | 
						user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	actions, err := GetFeeds(GetFeedsOptions{
 | 
						actions, err := GetFeeds(db.DefaultContext, GetFeedsOptions{
 | 
				
			||||||
		RequestedUser:   user,
 | 
							RequestedUser:   user,
 | 
				
			||||||
		Actor:           user,
 | 
							Actor:           user,
 | 
				
			||||||
		IncludePrivate:  true,
 | 
							IncludePrivate:  true,
 | 
				
			||||||
@@ -52,7 +53,7 @@ func TestGetFeeds(t *testing.T) {
 | 
				
			|||||||
		assert.EqualValues(t, user.ID, actions[0].UserID)
 | 
							assert.EqualValues(t, user.ID, actions[0].UserID)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	actions, err = GetFeeds(GetFeedsOptions{
 | 
						actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{
 | 
				
			||||||
		RequestedUser:   user,
 | 
							RequestedUser:   user,
 | 
				
			||||||
		Actor:           user,
 | 
							Actor:           user,
 | 
				
			||||||
		IncludePrivate:  false,
 | 
							IncludePrivate:  false,
 | 
				
			||||||
@@ -62,13 +63,54 @@ func TestGetFeeds(t *testing.T) {
 | 
				
			|||||||
	assert.Len(t, actions, 0)
 | 
						assert.Len(t, actions, 0)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetFeedsForRepos(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
						user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 | 
				
			||||||
 | 
						privRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
 | 
				
			||||||
 | 
						pubRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8}).(*repo_model.Repository)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// private repo & no login
 | 
				
			||||||
 | 
						actions, err := GetFeeds(db.DefaultContext, GetFeedsOptions{
 | 
				
			||||||
 | 
							RequestedRepo:  privRepo,
 | 
				
			||||||
 | 
							IncludePrivate: true,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, actions, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// public repo & no login
 | 
				
			||||||
 | 
						actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{
 | 
				
			||||||
 | 
							RequestedRepo:  pubRepo,
 | 
				
			||||||
 | 
							IncludePrivate: true,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, actions, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// private repo and login
 | 
				
			||||||
 | 
						actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{
 | 
				
			||||||
 | 
							RequestedRepo:  privRepo,
 | 
				
			||||||
 | 
							IncludePrivate: true,
 | 
				
			||||||
 | 
							Actor:          user,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, actions, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// public repo & login
 | 
				
			||||||
 | 
						actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{
 | 
				
			||||||
 | 
							RequestedRepo:  pubRepo,
 | 
				
			||||||
 | 
							IncludePrivate: true,
 | 
				
			||||||
 | 
							Actor:          user,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, actions, 1)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestGetFeeds2(t *testing.T) {
 | 
					func TestGetFeeds2(t *testing.T) {
 | 
				
			||||||
	// test with an organization user
 | 
						// test with an organization user
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
	org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
 | 
						org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
 | 
				
			||||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 | 
						user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	actions, err := GetFeeds(GetFeedsOptions{
 | 
						actions, err := GetFeeds(db.DefaultContext, GetFeedsOptions{
 | 
				
			||||||
		RequestedUser:   org,
 | 
							RequestedUser:   org,
 | 
				
			||||||
		Actor:           user,
 | 
							Actor:           user,
 | 
				
			||||||
		IncludePrivate:  true,
 | 
							IncludePrivate:  true,
 | 
				
			||||||
@@ -82,7 +124,7 @@ func TestGetFeeds2(t *testing.T) {
 | 
				
			|||||||
		assert.EqualValues(t, org.ID, actions[0].UserID)
 | 
							assert.EqualValues(t, org.ID, actions[0].UserID)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	actions, err = GetFeeds(GetFeedsOptions{
 | 
						actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{
 | 
				
			||||||
		RequestedUser:   org,
 | 
							RequestedUser:   org,
 | 
				
			||||||
		Actor:           user,
 | 
							Actor:           user,
 | 
				
			||||||
		IncludePrivate:  false,
 | 
							IncludePrivate:  false,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
  user_id: 2
 | 
					  user_id: 2
 | 
				
			||||||
  op_type: 12 # close issue
 | 
					  op_type: 12 # close issue
 | 
				
			||||||
  act_user_id: 2
 | 
					  act_user_id: 2
 | 
				
			||||||
  repo_id: 2
 | 
					  repo_id: 2 # private
 | 
				
			||||||
  is_private: true
 | 
					  is_private: true
 | 
				
			||||||
  created_unix: 1603228283
 | 
					  created_unix: 1603228283
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -12,7 +12,7 @@
 | 
				
			|||||||
  user_id: 3
 | 
					  user_id: 3
 | 
				
			||||||
  op_type: 2 # rename repo
 | 
					  op_type: 2 # rename repo
 | 
				
			||||||
  act_user_id: 2
 | 
					  act_user_id: 2
 | 
				
			||||||
  repo_id: 3
 | 
					  repo_id: 3 # private
 | 
				
			||||||
  is_private: true
 | 
					  is_private: true
 | 
				
			||||||
  content: oldRepoName
 | 
					  content: oldRepoName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -21,7 +21,7 @@
 | 
				
			|||||||
  user_id: 11
 | 
					  user_id: 11
 | 
				
			||||||
  op_type: 1 # create repo
 | 
					  op_type: 1 # create repo
 | 
				
			||||||
  act_user_id: 11
 | 
					  act_user_id: 11
 | 
				
			||||||
  repo_id: 9
 | 
					  repo_id: 9 # public
 | 
				
			||||||
  is_private: false
 | 
					  is_private: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
@@ -29,7 +29,7 @@
 | 
				
			|||||||
  user_id: 16
 | 
					  user_id: 16
 | 
				
			||||||
  op_type: 12 # close issue
 | 
					  op_type: 12 # close issue
 | 
				
			||||||
  act_user_id: 16
 | 
					  act_user_id: 16
 | 
				
			||||||
  repo_id: 22
 | 
					  repo_id: 22 # private
 | 
				
			||||||
  is_private: true
 | 
					  is_private: true
 | 
				
			||||||
  created_unix: 1603267920
 | 
					  created_unix: 1603267920
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -37,7 +37,7 @@
 | 
				
			|||||||
  user_id: 10
 | 
					  user_id: 10
 | 
				
			||||||
  op_type: 1 # create repo
 | 
					  op_type: 1 # create repo
 | 
				
			||||||
  act_user_id: 10
 | 
					  act_user_id: 10
 | 
				
			||||||
  repo_id: 6
 | 
					  repo_id: 6 # private
 | 
				
			||||||
  is_private: true
 | 
					  is_private: true
 | 
				
			||||||
  created_unix: 1603010100
 | 
					  created_unix: 1603010100
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -45,7 +45,7 @@
 | 
				
			|||||||
  user_id: 10
 | 
					  user_id: 10
 | 
				
			||||||
  op_type: 1 # create repo
 | 
					  op_type: 1 # create repo
 | 
				
			||||||
  act_user_id: 10
 | 
					  act_user_id: 10
 | 
				
			||||||
  repo_id: 7
 | 
					  repo_id: 7 # private
 | 
				
			||||||
  is_private: true
 | 
					  is_private: true
 | 
				
			||||||
  created_unix: 1603011300
 | 
					  created_unix: 1603011300
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -53,6 +53,6 @@
 | 
				
			|||||||
  user_id: 10
 | 
					  user_id: 10
 | 
				
			||||||
  op_type: 1 # create repo
 | 
					  op_type: 1 # create repo
 | 
				
			||||||
  act_user_id: 10
 | 
					  act_user_id: 10
 | 
				
			||||||
  repo_id: 8
 | 
					  repo_id: 8 # public
 | 
				
			||||||
  is_private: false
 | 
					  is_private: false
 | 
				
			||||||
  created_unix: 1603011540 # grouped with id:7
 | 
					  created_unix: 1603011540 # grouped with id:7
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import (
 | 
				
			|||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
	"code.gitea.io/gitea/models/unittest"
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/json"
 | 
						"code.gitea.io/gitea/modules/json"
 | 
				
			||||||
@@ -72,7 +73,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// get the action for comparison
 | 
							// get the action for comparison
 | 
				
			||||||
		actions, err := GetFeeds(GetFeedsOptions{
 | 
							actions, err := GetFeeds(db.DefaultContext, GetFeedsOptions{
 | 
				
			||||||
			RequestedUser:   user,
 | 
								RequestedUser:   user,
 | 
				
			||||||
			Actor:           doer,
 | 
								Actor:           doer,
 | 
				
			||||||
			IncludePrivate:  true,
 | 
								IncludePrivate:  true,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,10 +34,10 @@ func Auth(serviceName, userName, passwd string) (string, error) {
 | 
				
			|||||||
	if err = t.Authenticate(0); err != nil {
 | 
						if err = t.Authenticate(0); err != nil {
 | 
				
			||||||
		return "", err
 | 
							return "", err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
					
 | 
				
			||||||
	if err = t.AcctMgmt(0); err != nil {
 | 
						if err = t.AcctMgmt(0); err != nil {
 | 
				
			||||||
	  return "", err
 | 
							return "", err
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// PAM login names might suffer transformations in the PAM stack.
 | 
						// PAM login names might suffer transformations in the PAM stack.
 | 
				
			||||||
	// We should take whatever the PAM stack returns for it.
 | 
						// We should take whatever the PAM stack returns for it.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -418,6 +418,8 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
 | 
				
			|||||||
	userName := ctx.Params(":username")
 | 
						userName := ctx.Params(":username")
 | 
				
			||||||
	repoName := ctx.Params(":reponame")
 | 
						repoName := ctx.Params(":reponame")
 | 
				
			||||||
	repoName = strings.TrimSuffix(repoName, ".git")
 | 
						repoName = strings.TrimSuffix(repoName, ".git")
 | 
				
			||||||
 | 
						repoName = strings.TrimSuffix(repoName, ".rss")
 | 
				
			||||||
 | 
						repoName = strings.TrimSuffix(repoName, ".atom")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if the user is the same as the repository owner
 | 
						// Check if the user is the same as the repository owner
 | 
				
			||||||
	if ctx.IsSigned && ctx.User.LowerName == strings.ToLower(userName) {
 | 
						if ctx.IsSigned && ctx.User.LowerName == strings.ToLower(userName) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ package feed
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"html"
 | 
						"html"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
@@ -66,7 +67,7 @@ func renderMarkdown(ctx *context.Context, act *models.Action, content string) st
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// feedActionsToFeedItems convert gitea's Action feed to feeds Item
 | 
					// feedActionsToFeedItems convert gitea's Action feed to feeds Item
 | 
				
			||||||
func feedActionsToFeedItems(ctx *context.Context, actions []*models.Action) (items []*feeds.Item, err error) {
 | 
					func feedActionsToFeedItems(ctx *context.Context, actions models.ActionList) (items []*feeds.Item, err error) {
 | 
				
			||||||
	for _, act := range actions {
 | 
						for _, act := range actions {
 | 
				
			||||||
		act.LoadActUser()
 | 
							act.LoadActUser()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -247,3 +248,18 @@ func feedActionsToFeedItems(ctx *context.Context, actions []*models.Action) (ite
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	return
 | 
						return
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetFeedType return if it is a feed request and altered name and feed type.
 | 
				
			||||||
 | 
					func GetFeedType(name string, req *http.Request) (bool, string, string) {
 | 
				
			||||||
 | 
						if strings.HasSuffix(name, ".rss") ||
 | 
				
			||||||
 | 
							strings.Contains(req.Header.Get("Accept"), "application/rss+xml") {
 | 
				
			||||||
 | 
							return true, strings.TrimSuffix(name, ".rss"), "rss"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if strings.HasSuffix(name, ".atom") ||
 | 
				
			||||||
 | 
							strings.Contains(req.Header.Get("Accept"), "application/atom+xml") {
 | 
				
			||||||
 | 
							return true, strings.TrimSuffix(name, ".atom"), "atom"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return false, name, ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,48 +15,9 @@ import (
 | 
				
			|||||||
	"github.com/gorilla/feeds"
 | 
						"github.com/gorilla/feeds"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RetrieveFeeds loads feeds for the specified user
 | 
					 | 
				
			||||||
func RetrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) []*models.Action {
 | 
					 | 
				
			||||||
	actions, err := models.GetFeeds(options)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		ctx.ServerError("GetFeeds", err)
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// TODO: move load repoOwner of act.Repo into models.GetFeeds->loadAttributes()
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		userCache := map[int64]*user_model.User{options.RequestedUser.ID: options.RequestedUser}
 | 
					 | 
				
			||||||
		if ctx.User != nil {
 | 
					 | 
				
			||||||
			userCache[ctx.User.ID] = ctx.User
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		for _, act := range actions {
 | 
					 | 
				
			||||||
			if act.ActUser != nil {
 | 
					 | 
				
			||||||
				userCache[act.ActUserID] = act.ActUser
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		for _, act := range actions {
 | 
					 | 
				
			||||||
			repoOwner, ok := userCache[act.Repo.OwnerID]
 | 
					 | 
				
			||||||
			if !ok {
 | 
					 | 
				
			||||||
				repoOwner, err = user_model.GetUserByID(act.Repo.OwnerID)
 | 
					 | 
				
			||||||
				if err != nil {
 | 
					 | 
				
			||||||
					if user_model.IsErrUserNotExist(err) {
 | 
					 | 
				
			||||||
						continue
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					ctx.ServerError("GetUserByID", err)
 | 
					 | 
				
			||||||
					return nil
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				userCache[repoOwner.ID] = repoOwner
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			act.Repo.Owner = repoOwner
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return actions
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ShowUserFeed show user activity as RSS / Atom feed
 | 
					// ShowUserFeed show user activity as RSS / Atom feed
 | 
				
			||||||
func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType string) {
 | 
					func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType string) {
 | 
				
			||||||
	actions := RetrieveFeeds(ctx, models.GetFeedsOptions{
 | 
						actions, err := models.GetFeeds(ctx, models.GetFeedsOptions{
 | 
				
			||||||
		RequestedUser:   ctxUser,
 | 
							RequestedUser:   ctxUser,
 | 
				
			||||||
		Actor:           ctx.User,
 | 
							Actor:           ctx.User,
 | 
				
			||||||
		IncludePrivate:  false,
 | 
							IncludePrivate:  false,
 | 
				
			||||||
@@ -64,7 +25,8 @@ func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType str
 | 
				
			|||||||
		IncludeDeleted:  false,
 | 
							IncludeDeleted:  false,
 | 
				
			||||||
		Date:            ctx.FormString("date"),
 | 
							Date:            ctx.FormString("date"),
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	if ctx.Written() {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("GetFeeds", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -75,7 +37,6 @@ func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType str
 | 
				
			|||||||
		Created:     time.Now(),
 | 
							Created:     time.Now(),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var err error
 | 
					 | 
				
			||||||
	feed.Items, err = feedActionsToFeedItems(ctx, actions)
 | 
						feed.Items, err = feedActionsToFeedItems(ctx, actions)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("convert feed", err)
 | 
							ctx.ServerError("convert feed", err)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										44
									
								
								routers/web/feed/repo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								routers/web/feed/repo.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package feed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/gorilla/feeds"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ShowRepoFeed shows user activity on the repo as RSS / Atom feed
 | 
				
			||||||
 | 
					func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType string) {
 | 
				
			||||||
 | 
						actions, err := models.GetFeeds(ctx, models.GetFeedsOptions{
 | 
				
			||||||
 | 
							RequestedRepo:  repo,
 | 
				
			||||||
 | 
							Actor:          ctx.User,
 | 
				
			||||||
 | 
							IncludePrivate: true,
 | 
				
			||||||
 | 
							Date:           ctx.FormString("date"),
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("GetFeeds", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						feed := &feeds.Feed{
 | 
				
			||||||
 | 
							Title:       ctx.Tr("home.feed_of", repo.FullName()),
 | 
				
			||||||
 | 
							Link:        &feeds.Link{Href: repo.HTMLURL()},
 | 
				
			||||||
 | 
							Description: repo.Description,
 | 
				
			||||||
 | 
							Created:     time.Now(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						feed.Items, err = feedActionsToFeedItems(ctx, actions)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("convert feed", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						writeFeed(ctx, feed, formatType)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -38,6 +38,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/structs"
 | 
						"code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/typesniffer"
 | 
						"code.gitea.io/gitea/modules/typesniffer"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/routers/web/feed"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
@@ -691,6 +692,14 @@ func checkHomeCodeViewable(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Home render repository home page
 | 
					// Home render repository home page
 | 
				
			||||||
func Home(ctx *context.Context) {
 | 
					func Home(ctx *context.Context) {
 | 
				
			||||||
 | 
						isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req)
 | 
				
			||||||
 | 
						if isFeed {
 | 
				
			||||||
 | 
							feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Data["FeedURL"] = ctx.Repo.Repository.HTMLURL()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	checkHomeCodeViewable(ctx)
 | 
						checkHomeCodeViewable(ctx)
 | 
				
			||||||
	if ctx.Written() {
 | 
						if ctx.Written() {
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,7 +29,6 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
						"code.gitea.io/gitea/modules/markup/markdown"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
	"code.gitea.io/gitea/routers/web/feed"
 | 
					 | 
				
			||||||
	issue_service "code.gitea.io/gitea/services/issue"
 | 
						issue_service "code.gitea.io/gitea/services/issue"
 | 
				
			||||||
	pull_service "code.gitea.io/gitea/services/pull"
 | 
						pull_service "code.gitea.io/gitea/services/pull"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -131,7 +130,7 @@ func Dashboard(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["MirrorCount"] = len(mirrors)
 | 
						ctx.Data["MirrorCount"] = len(mirrors)
 | 
				
			||||||
	ctx.Data["Mirrors"] = mirrors
 | 
						ctx.Data["Mirrors"] = mirrors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.Data["Feeds"] = feed.RetrieveFeeds(ctx, models.GetFeedsOptions{
 | 
						ctx.Data["Feeds"], err = models.GetFeeds(ctx, models.GetFeedsOptions{
 | 
				
			||||||
		RequestedUser:   ctxUser,
 | 
							RequestedUser:   ctxUser,
 | 
				
			||||||
		RequestedTeam:   ctx.Org.Team,
 | 
							RequestedTeam:   ctx.Org.Team,
 | 
				
			||||||
		Actor:           ctx.User,
 | 
							Actor:           ctx.User,
 | 
				
			||||||
@@ -140,8 +139,8 @@ func Dashboard(ctx *context.Context) {
 | 
				
			|||||||
		IncludeDeleted:  false,
 | 
							IncludeDeleted:  false,
 | 
				
			||||||
		Date:            ctx.FormString("date"),
 | 
							Date:            ctx.FormString("date"),
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
	if ctx.Written() {
 | 
							ctx.ServerError("GetFeeds", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -74,19 +74,7 @@ func Profile(ctx *context.Context) {
 | 
				
			|||||||
		uname = strings.TrimSuffix(uname, ".gpg")
 | 
							uname = strings.TrimSuffix(uname, ".gpg")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	showFeedType := ""
 | 
						isShowFeed, uname, showFeedType := feed.GetFeedType(uname, ctx.Req)
 | 
				
			||||||
	if strings.HasSuffix(uname, ".rss") {
 | 
					 | 
				
			||||||
		showFeedType = "rss"
 | 
					 | 
				
			||||||
		uname = strings.TrimSuffix(uname, ".rss")
 | 
					 | 
				
			||||||
	} else if strings.Contains(ctx.Req.Header.Get("Accept"), "application/rss+xml") {
 | 
					 | 
				
			||||||
		showFeedType = "rss"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if strings.HasSuffix(uname, ".atom") {
 | 
					 | 
				
			||||||
		showFeedType = "atom"
 | 
					 | 
				
			||||||
		uname = strings.TrimSuffix(uname, ".atom")
 | 
					 | 
				
			||||||
	} else if strings.Contains(ctx.Req.Header.Get("Accept"), "application/atom+xml") {
 | 
					 | 
				
			||||||
		showFeedType = "atom"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctxUser := GetUserByName(ctx, uname)
 | 
						ctxUser := GetUserByName(ctx, uname)
 | 
				
			||||||
	if ctx.Written() {
 | 
						if ctx.Written() {
 | 
				
			||||||
@@ -95,7 +83,7 @@ func Profile(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	if ctxUser.IsOrganization() {
 | 
						if ctxUser.IsOrganization() {
 | 
				
			||||||
		// Show Org RSS feed
 | 
							// Show Org RSS feed
 | 
				
			||||||
		if len(showFeedType) != 0 {
 | 
							if isShowFeed {
 | 
				
			||||||
			feed.ShowUserFeed(ctx, ctxUser, showFeedType)
 | 
								feed.ShowUserFeed(ctx, ctxUser, showFeedType)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -123,11 +111,14 @@ func Profile(ctx *context.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Show User RSS feed
 | 
						// Show User RSS feed
 | 
				
			||||||
	if len(showFeedType) != 0 {
 | 
						if isShowFeed {
 | 
				
			||||||
		feed.ShowUserFeed(ctx, ctxUser, showFeedType)
 | 
							feed.ShowUserFeed(ctx, ctxUser, showFeedType)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// advertise feed via meta tag
 | 
				
			||||||
 | 
						ctx.Data["FeedURL"] = ctxUser.HTMLURL()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Show OpenID URIs
 | 
						// Show OpenID URIs
 | 
				
			||||||
	openIDs, err := user_model.GetUserOpenIDs(ctxUser.ID)
 | 
						openIDs, err := user_model.GetUserOpenIDs(ctxUser.ID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -259,7 +250,7 @@ func Profile(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		total = ctxUser.NumFollowing
 | 
							total = ctxUser.NumFollowing
 | 
				
			||||||
	case "activity":
 | 
						case "activity":
 | 
				
			||||||
		ctx.Data["Feeds"] = feed.RetrieveFeeds(ctx, models.GetFeedsOptions{
 | 
							ctx.Data["Feeds"], err = models.GetFeeds(ctx, models.GetFeedsOptions{
 | 
				
			||||||
			RequestedUser:   ctxUser,
 | 
								RequestedUser:   ctxUser,
 | 
				
			||||||
			Actor:           ctx.User,
 | 
								Actor:           ctx.User,
 | 
				
			||||||
			IncludePrivate:  showPrivate,
 | 
								IncludePrivate:  showPrivate,
 | 
				
			||||||
@@ -267,7 +258,8 @@ func Profile(ctx *context.Context) {
 | 
				
			|||||||
			IncludeDeleted:  false,
 | 
								IncludeDeleted:  false,
 | 
				
			||||||
			Date:            ctx.FormString("date"),
 | 
								Date:            ctx.FormString("date"),
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		if ctx.Written() {
 | 
							if err != nil {
 | 
				
			||||||
 | 
								ctx.ServerError("GetFeeds", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	case "stars":
 | 
						case "stars":
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,10 @@
 | 
				
			|||||||
{{if .GoGetImport}}
 | 
					{{if .GoGetImport}}
 | 
				
			||||||
	<meta name="go-import" content="{{.GoGetImport}} git {{.CloneLink.HTTPS}}">
 | 
						<meta name="go-import" content="{{.GoGetImport}} git {{.CloneLink.HTTPS}}">
 | 
				
			||||||
	<meta name="go-source" content="{{.GoGetImport}} _ {{.GoDocDirectory}} {{.GoDocFile}}">
 | 
						<meta name="go-source" content="{{.GoGetImport}} _ {{.GoDocDirectory}} {{.GoDocFile}}">
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
 | 
					{{if .FeedURL}}
 | 
				
			||||||
 | 
						<link rel="alternate" type="application/atom+xml" title="" href="{{.FeedURL}}.atom">
 | 
				
			||||||
 | 
						<link rel="alternate" type="application/rss+xml" title="" href="{{.FeedURL}}.rss">
 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
	<script>
 | 
						<script>
 | 
				
			||||||
		<!-- /* eslint-disable */ -->
 | 
							<!-- /* eslint-disable */ -->
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user