mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-11-04 00:11:04 +00:00 
			
		
		
		
	[MODERATION] user blocking
- Add the ability to block a user via their profile page. - This will unstar their repositories and visa versa. - Blocked users cannot create issues or pull requests on your the doer's repositories (mind that this is not the case for organizations). - Blocked users cannot comment on the doer's opened issues or pull requests. - Blocked users cannot add reactions to doer's comments. - Blocked users cannot cause a notification trough mentioning the doer. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/540 (cherry picked from commit687d852480) (cherry picked from commit0c32a4fde5) (cherry picked from commit1791130e3c) (cherry picked from commit37858b7e8f) (cherry picked from commita3e2bfd7e9) (cherry picked from commit7009b9fe87) Conflicts: https://codeberg.org/forgejo/forgejo/pulls/1014 routers/web/user/profile.go templates/user/profile.tmpl (cherry picked from commitb2aec34791)
This commit is contained in:
		
					parent
					
						
							
								ad2fedb693
							
						
					
				
			
			
				commit
				
					
						e2f1b73752
					
				
			
		
					 39 changed files with 651 additions and 52 deletions
				
			
		| 
						 | 
					@ -580,7 +580,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if repoChanged {
 | 
							if repoChanged {
 | 
				
			||||||
			// Add feeds for user self and all watchers.
 | 
								// Add feeds for user self and all watchers.
 | 
				
			||||||
			watchers, err = repo_model.GetWatchers(ctx, act.RepoID)
 | 
								watchers, err = repo_model.GetWatchersExcludeBlocked(ctx, act.RepoID, act.ActUserID)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				return fmt.Errorf("get watchers: %w", err)
 | 
									return fmt.Errorf("get watchers: %w", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -235,6 +235,15 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
 | 
				
			||||||
		for _, id := range issueUnWatches {
 | 
							for _, id := range issueUnWatches {
 | 
				
			||||||
			toNotify.Remove(id)
 | 
								toNotify.Remove(id)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Remove users who have the notification author blocked.
 | 
				
			||||||
 | 
							blockedAuthorIDs, err := user_model.ListBlockedByUsersID(ctx, notificationAuthorID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for _, id := range blockedAuthorIDs {
 | 
				
			||||||
 | 
								toNotify.Remove(id)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = issue.LoadRepo(ctx)
 | 
						err = issue.LoadRepo(ctx)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										5
									
								
								models/fixtures/forgejo_blocked_user.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								models/fixtures/forgejo_blocked_user.yml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 1
 | 
				
			||||||
 | 
					  user_id: 4
 | 
				
			||||||
 | 
					  block_id: 1
 | 
				
			||||||
 | 
					  created_unix: 1671607299
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@ import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						forgejo_v1_20 "code.gitea.io/gitea/models/forgejo_migrations/v1_20"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/git"
 | 
						"code.gitea.io/gitea/modules/git"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
| 
						 | 
					@ -34,7 +35,9 @@ func NewMigration(desc string, fn func(*xorm.Engine) error) *Migration {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// This is a sequence of additional Forgejo migrations.
 | 
					// This is a sequence of additional Forgejo migrations.
 | 
				
			||||||
// Add new migrations to the bottom of the list.
 | 
					// Add new migrations to the bottom of the list.
 | 
				
			||||||
var migrations = []*Migration{}
 | 
					var migrations = []*Migration{
 | 
				
			||||||
 | 
						NewMigration("Add Forgejo Blocked Users table", forgejo_v1_20.AddForgejoBlockedUser),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetCurrentDBVersion returns the current Forgejo database version.
 | 
					// GetCurrentDBVersion returns the current Forgejo database version.
 | 
				
			||||||
func GetCurrentDBVersion(x *xorm.Engine) (int64, error) {
 | 
					func GetCurrentDBVersion(x *xorm.Engine) (int64, error) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										21
									
								
								models/forgejo_migrations/v1_20/v1.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								models/forgejo_migrations/v1_20/v1.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package forgejo_v1_20 //nolint:revive
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/xorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func AddForgejoBlockedUser(x *xorm.Engine) error {
 | 
				
			||||||
 | 
						type ForgejoBlockedUser struct {
 | 
				
			||||||
 | 
							ID          int64              `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
							BlockID     int64              `xorm:"index"`
 | 
				
			||||||
 | 
							UserID      int64              `xorm:"index"`
 | 
				
			||||||
 | 
							CreatedUnix timeutil.TimeStamp `xorm:"created"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return x.Sync(new(ForgejoBlockedUser))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -452,6 +452,8 @@ func TestIssue_ResolveMentions(t *testing.T) {
 | 
				
			||||||
	testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{})
 | 
						testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{})
 | 
				
			||||||
	// Public repo, doer
 | 
						// Public repo, doer
 | 
				
			||||||
	testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{})
 | 
						testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{})
 | 
				
			||||||
 | 
						// Public repo, blocked user
 | 
				
			||||||
 | 
						testSuccess("user2", "repo1", "user1", []string{"user4"}, []int64{})
 | 
				
			||||||
	// Private repo, team member
 | 
						// Private repo, team member
 | 
				
			||||||
	testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2})
 | 
						testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2})
 | 
				
			||||||
	// Private repo, not a team member
 | 
						// Private repo, not a team member
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -608,9 +608,11 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
 | 
				
			||||||
				teamusers := make([]*user_model.User, 0, 20)
 | 
									teamusers := make([]*user_model.User, 0, 20)
 | 
				
			||||||
				if err := db.GetEngine(ctx).
 | 
									if err := db.GetEngine(ctx).
 | 
				
			||||||
					Join("INNER", "team_user", "team_user.uid = `user`.id").
 | 
										Join("INNER", "team_user", "team_user.uid = `user`.id").
 | 
				
			||||||
 | 
										Join("LEFT", "forgejo_blocked_user", "forgejo_blocked_user.user_id = `user`.id").
 | 
				
			||||||
					In("`team_user`.team_id", checked).
 | 
										In("`team_user`.team_id", checked).
 | 
				
			||||||
					And("`user`.is_active = ?", true).
 | 
										And("`user`.is_active = ?", true).
 | 
				
			||||||
					And("`user`.prohibit_login = ?", false).
 | 
										And("`user`.prohibit_login = ?", false).
 | 
				
			||||||
 | 
										And(builder.Or(builder.IsNull{"`forgejo_blocked_user`.block_id"}, builder.Neq{"`forgejo_blocked_user`.block_id": doer.ID})).
 | 
				
			||||||
					Find(&teamusers); err != nil {
 | 
										Find(&teamusers); err != nil {
 | 
				
			||||||
					return nil, fmt.Errorf("get teams users: %w", err)
 | 
										return nil, fmt.Errorf("get teams users: %w", err)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
| 
						 | 
					@ -644,8 +646,10 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unchecked := make([]*user_model.User, 0, len(mentionUsers))
 | 
						unchecked := make([]*user_model.User, 0, len(mentionUsers))
 | 
				
			||||||
	if err := db.GetEngine(ctx).
 | 
						if err := db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Join("LEFT", "forgejo_blocked_user", "forgejo_blocked_user.user_id = `user`.id").
 | 
				
			||||||
		Where("`user`.is_active = ?", true).
 | 
							Where("`user`.is_active = ?", true).
 | 
				
			||||||
		And("`user`.prohibit_login = ?", false).
 | 
							And("`user`.prohibit_login = ?", false).
 | 
				
			||||||
 | 
							And(builder.Or(builder.IsNull{"`forgejo_blocked_user`.block_id"}, builder.Neq{"`forgejo_blocked_user`.block_id": doer.ID})).
 | 
				
			||||||
		In("`user`.lower_name", mentionUsers).
 | 
							In("`user`.lower_name", mentionUsers).
 | 
				
			||||||
		Find(&unchecked); err != nil {
 | 
							Find(&unchecked); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("find mentioned users: %w", err)
 | 
							return nil, fmt.Errorf("find mentioned users: %w", err)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -218,12 +218,12 @@ type ReactionOptions struct {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateReaction creates reaction for issue or comment.
 | 
					// CreateReaction creates reaction for issue or comment.
 | 
				
			||||||
func CreateReaction(opts *ReactionOptions) (*Reaction, error) {
 | 
					func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) {
 | 
				
			||||||
	if !setting.UI.ReactionsLookup.Contains(opts.Type) {
 | 
						if !setting.UI.ReactionsLookup.Contains(opts.Type) {
 | 
				
			||||||
		return nil, ErrForbiddenIssueReaction{opts.Type}
 | 
							return nil, ErrForbiddenIssueReaction{opts.Type}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx, committer, err := db.TxContext(db.DefaultContext)
 | 
						ctx, committer, err := db.TxContext(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -240,25 +240,6 @@ func CreateReaction(opts *ReactionOptions) (*Reaction, error) {
 | 
				
			||||||
	return reaction, nil
 | 
						return reaction, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateIssueReaction creates a reaction on issue.
 | 
					 | 
				
			||||||
func CreateIssueReaction(doerID, issueID int64, content string) (*Reaction, error) {
 | 
					 | 
				
			||||||
	return CreateReaction(&ReactionOptions{
 | 
					 | 
				
			||||||
		Type:    content,
 | 
					 | 
				
			||||||
		DoerID:  doerID,
 | 
					 | 
				
			||||||
		IssueID: issueID,
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// CreateCommentReaction creates a reaction on comment.
 | 
					 | 
				
			||||||
func CreateCommentReaction(doerID, issueID, commentID int64, content string) (*Reaction, error) {
 | 
					 | 
				
			||||||
	return CreateReaction(&ReactionOptions{
 | 
					 | 
				
			||||||
		Type:      content,
 | 
					 | 
				
			||||||
		DoerID:    doerID,
 | 
					 | 
				
			||||||
		IssueID:   issueID,
 | 
					 | 
				
			||||||
		CommentID: commentID,
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// DeleteReaction deletes reaction for issue or comment.
 | 
					// DeleteReaction deletes reaction for issue or comment.
 | 
				
			||||||
func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
 | 
					func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
 | 
				
			||||||
	reaction := &Reaction{
 | 
						reaction := &Reaction{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,11 +19,14 @@ import (
 | 
				
			||||||
func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
 | 
					func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
 | 
				
			||||||
	var reaction *issues_model.Reaction
 | 
						var reaction *issues_model.Reaction
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	if commentID == 0 {
 | 
						// NOTE: This doesn't do user blocking checking.
 | 
				
			||||||
		reaction, err = issues_model.CreateIssueReaction(doerID, issueID, content)
 | 
						reaction, err = issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{
 | 
				
			||||||
	} else {
 | 
							DoerID:    doerID,
 | 
				
			||||||
		reaction, err = issues_model.CreateCommentReaction(doerID, issueID, commentID, content)
 | 
							IssueID:   issueID,
 | 
				
			||||||
	}
 | 
							CommentID: commentID,
 | 
				
			||||||
 | 
							Type:      content,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.NotNil(t, reaction)
 | 
						assert.NotNil(t, reaction)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -49,7 +52,7 @@ func TestIssueAddDuplicateReaction(t *testing.T) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addReaction(t, user1.ID, issue1ID, 0, "heart")
 | 
						addReaction(t, user1.ID, issue1ID, 0, "heart")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	reaction, err := issues_model.CreateReaction(&issues_model.ReactionOptions{
 | 
						reaction, err := issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{
 | 
				
			||||||
		DoerID:  user1.ID,
 | 
							DoerID:  user1.ID,
 | 
				
			||||||
		IssueID: issue1ID,
 | 
							IssueID: issue1ID,
 | 
				
			||||||
		Type:    "heart",
 | 
							Type:    "heart",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,8 @@ import (
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/builder"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// WatchMode specifies what kind of watch the user has on a repository
 | 
					// WatchMode specifies what kind of watch the user has on a repository
 | 
				
			||||||
| 
						 | 
					@ -142,6 +144,21 @@ func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
 | 
				
			||||||
		Find(&watches)
 | 
							Find(&watches)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetWatchersExcludeBlocked returns all watchers of given repository, whereby
 | 
				
			||||||
 | 
					// the doer isn't blocked by one of the watchers.
 | 
				
			||||||
 | 
					func GetWatchersExcludeBlocked(ctx context.Context, repoID, doerID int64) ([]*Watch, error) {
 | 
				
			||||||
 | 
						watches := make([]*Watch, 0, 10)
 | 
				
			||||||
 | 
						return watches, db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Join("INNER", "`user`", "`user`.id = `watch`.user_id").
 | 
				
			||||||
 | 
							Join("LEFT", "forgejo_blocked_user", "forgejo_blocked_user.user_id = `watch`.user_id").
 | 
				
			||||||
 | 
							Where("`watch`.repo_id=?", repoID).
 | 
				
			||||||
 | 
							And("`watch`.mode<>?", WatchModeDont).
 | 
				
			||||||
 | 
							And("`user`.is_active=?", true).
 | 
				
			||||||
 | 
							And("`user`.prohibit_login=?", false).
 | 
				
			||||||
 | 
							And(builder.Or(builder.IsNull{"`forgejo_blocked_user`.block_id"}, builder.Neq{"`forgejo_blocked_user`.block_id": doerID})).
 | 
				
			||||||
 | 
							Find(&watches)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
 | 
					// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
 | 
				
			||||||
// but avoids joining with `user` for performance reasons
 | 
					// but avoids joining with `user` for performance reasons
 | 
				
			||||||
// User permissions must be verified elsewhere if required
 | 
					// User permissions must be verified elsewhere if required
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,6 +43,24 @@ func TestGetWatchers(t *testing.T) {
 | 
				
			||||||
	assert.Len(t, watches, 0)
 | 
						assert.Len(t, watches, 0)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetWatchersExcludeBlocked(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
				
			||||||
 | 
						watches, err := repo_model.GetWatchersExcludeBlocked(db.DefaultContext, repo.ID, 1)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// One watchers are inactive and one watcher is blocked, thus minus 2
 | 
				
			||||||
 | 
						assert.Len(t, watches, repo.NumWatches-2)
 | 
				
			||||||
 | 
						for _, watch := range watches {
 | 
				
			||||||
 | 
							assert.EqualValues(t, repo.ID, watch.RepoID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						watches, err = repo_model.GetWatchersExcludeBlocked(db.DefaultContext, unittest.NonexistentID, 1)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, watches, 0)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestRepository_GetWatchers(t *testing.T) {
 | 
					func TestRepository_GetWatchers(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										78
									
								
								models/user/block.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								models/user/block.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,78 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ErrBlockedByUser defines an error stating that the user is not allowed to perform the action because they are blocked.
 | 
				
			||||||
 | 
					var ErrBlockedByUser = errors.New("user is blocked by the poster or repository owner")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockedUser represents a blocked user entry.
 | 
				
			||||||
 | 
					type BlockedUser struct {
 | 
				
			||||||
 | 
						ID int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
						// UID of the one who got blocked.
 | 
				
			||||||
 | 
						BlockID int64 `xorm:"index"`
 | 
				
			||||||
 | 
						// UID of the one who did the block action.
 | 
				
			||||||
 | 
						UserID int64 `xorm:"index"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						CreatedUnix timeutil.TimeStamp `xorm:"created"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TableName provides the real table name
 | 
				
			||||||
 | 
					func (*BlockedUser) TableName() string {
 | 
				
			||||||
 | 
						return "forgejo_blocked_user"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						db.RegisterModel(new(BlockedUser))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsBlocked returns if userID has blocked blockID.
 | 
				
			||||||
 | 
					func IsBlocked(ctx context.Context, userID, blockID int64) bool {
 | 
				
			||||||
 | 
						has, _ := db.GetEngine(ctx).Exist(&BlockedUser{UserID: userID, BlockID: blockID})
 | 
				
			||||||
 | 
						return has
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsBlockedMultiple returns if one of the userIDs has blocked blockID.
 | 
				
			||||||
 | 
					func IsBlockedMultiple(ctx context.Context, userIDs []int64, blockID int64) bool {
 | 
				
			||||||
 | 
						has, _ := db.GetEngine(ctx).In("user_id", userIDs).Exist(&BlockedUser{BlockID: blockID})
 | 
				
			||||||
 | 
						return has
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnblockUser removes the blocked user entry.
 | 
				
			||||||
 | 
					func UnblockUser(ctx context.Context, userID, blockID int64) error {
 | 
				
			||||||
 | 
						_, err := db.GetEngine(ctx).Delete(&BlockedUser{UserID: userID, BlockID: blockID})
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListBlockedUsers returns the users that the user has blocked.
 | 
				
			||||||
 | 
					func ListBlockedUsers(ctx context.Context, userID int64) ([]*User, error) {
 | 
				
			||||||
 | 
						users := make([]*User, 0, 8)
 | 
				
			||||||
 | 
						err := db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Select("`user`.*").
 | 
				
			||||||
 | 
							Join("INNER", "forgejo_blocked_user", "`user`.id=`forgejo_blocked_user`.block_id").
 | 
				
			||||||
 | 
							Where("`forgejo_blocked_user`.user_id=?", userID).
 | 
				
			||||||
 | 
							Find(&users)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return users, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListBlockedByUsersID returns the ids of the users that blocked the user.
 | 
				
			||||||
 | 
					func ListBlockedByUsersID(ctx context.Context, userID int64) ([]int64, error) {
 | 
				
			||||||
 | 
						users := make([]int64, 0, 8)
 | 
				
			||||||
 | 
						err := db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Table("user").
 | 
				
			||||||
 | 
							Select("`user`.id").
 | 
				
			||||||
 | 
							Join("INNER", "forgejo_blocked_user", "`user`.id=`forgejo_blocked_user`.user_id").
 | 
				
			||||||
 | 
							Where("`forgejo_blocked_user`.block_id=?", userID).
 | 
				
			||||||
 | 
							Find(&users)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return users, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										63
									
								
								models/user/block_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								models/user/block_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,63 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package user_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIsBlocked(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
						assert.True(t, user_model.IsBlocked(db.DefaultContext, 4, 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Simple test cases to ensure the function can also respond with false.
 | 
				
			||||||
 | 
						assert.False(t, user_model.IsBlocked(db.DefaultContext, 1, 1))
 | 
				
			||||||
 | 
						assert.False(t, user_model.IsBlocked(db.DefaultContext, 3, 2))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIsBlockedMultiple(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
						assert.True(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{4}, 1))
 | 
				
			||||||
 | 
						assert.True(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{4, 3, 4, 5}, 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Simple test cases to ensure the function can also respond with false.
 | 
				
			||||||
 | 
						assert.False(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{1}, 1))
 | 
				
			||||||
 | 
						assert.False(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{3, 4, 1}, 2))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestUnblockUser(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
						assert.True(t, user_model.IsBlocked(db.DefaultContext, 4, 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, user_model.UnblockUser(db.DefaultContext, 4, 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Simple test cases to ensure the function can also respond with false.
 | 
				
			||||||
 | 
						assert.False(t, user_model.IsBlocked(db.DefaultContext, 4, 1))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestListBlockedUsers(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						blockedUsers, err := user_model.ListBlockedUsers(db.DefaultContext, 4)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						if assert.Len(t, blockedUsers, 1) {
 | 
				
			||||||
 | 
							assert.EqualValues(t, 1, blockedUsers[0].ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestListBlockedByUsersID(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						blockedByUserIDs, err := user_model.ListBlockedByUsersID(db.DefaultContext, 1)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						if assert.Len(t, blockedByUserIDs, 1) {
 | 
				
			||||||
 | 
							assert.EqualValues(t, 4, blockedByUserIDs[0])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,8 @@
 | 
				
			||||||
package user
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -27,12 +29,12 @@ func IsFollowing(userID, followID int64) bool {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// FollowUser marks someone be another's follower.
 | 
					// FollowUser marks someone be another's follower.
 | 
				
			||||||
func FollowUser(userID, followID int64) (err error) {
 | 
					func FollowUser(ctx context.Context, userID, followID int64) (err error) {
 | 
				
			||||||
	if userID == followID || IsFollowing(userID, followID) {
 | 
						if userID == followID || IsFollowing(userID, followID) {
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx, committer, err := db.TxContext(db.DefaultContext)
 | 
						ctx, committer, err := db.TxContext(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -53,12 +55,12 @@ func FollowUser(userID, followID int64) (err error) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UnfollowUser unmarks someone as another's follower.
 | 
					// UnfollowUser unmarks someone as another's follower.
 | 
				
			||||||
func UnfollowUser(userID, followID int64) (err error) {
 | 
					func UnfollowUser(ctx context.Context, userID, followID int64) (err error) {
 | 
				
			||||||
	if userID == followID || !IsFollowing(userID, followID) {
 | 
						if userID == followID || !IsFollowing(userID, followID) {
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx, committer, err := db.TxContext(db.DefaultContext)
 | 
						ctx, committer, err := db.TxContext(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -449,13 +449,13 @@ func TestFollowUser(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	testSuccess := func(followerID, followedID int64) {
 | 
						testSuccess := func(followerID, followedID int64) {
 | 
				
			||||||
		assert.NoError(t, user_model.FollowUser(followerID, followedID))
 | 
							assert.NoError(t, user_model.FollowUser(db.DefaultContext, followerID, followedID))
 | 
				
			||||||
		unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
 | 
							unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	testSuccess(4, 2)
 | 
						testSuccess(4, 2)
 | 
				
			||||||
	testSuccess(5, 2)
 | 
						testSuccess(5, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.NoError(t, user_model.FollowUser(2, 2))
 | 
						assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unittest.CheckConsistencyFor(t, &user_model.User{})
 | 
						unittest.CheckConsistencyFor(t, &user_model.User{})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -464,7 +464,7 @@ func TestUnfollowUser(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	testSuccess := func(followerID, followedID int64) {
 | 
						testSuccess := func(followerID, followedID int64) {
 | 
				
			||||||
		assert.NoError(t, user_model.UnfollowUser(followerID, followedID))
 | 
							assert.NoError(t, user_model.UnfollowUser(db.DefaultContext, followerID, followedID))
 | 
				
			||||||
		unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
 | 
							unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	testSuccess(4, 2)
 | 
						testSuccess(4, 2)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -596,11 +596,17 @@ overview = Overview
 | 
				
			||||||
following = Following
 | 
					following = Following
 | 
				
			||||||
follow = Follow
 | 
					follow = Follow
 | 
				
			||||||
unfollow = Unfollow
 | 
					unfollow = Unfollow
 | 
				
			||||||
 | 
					block = Block
 | 
				
			||||||
 | 
					unblock = Unblock
 | 
				
			||||||
heatmap.loading = Loading Heatmap…
 | 
					heatmap.loading = Loading Heatmap…
 | 
				
			||||||
user_bio = Biography
 | 
					user_bio = Biography
 | 
				
			||||||
disabled_public_activity = This user has disabled the public visibility of the activity.
 | 
					disabled_public_activity = This user has disabled the public visibility of the activity.
 | 
				
			||||||
email_visibility.limited = Your email address is visible to all authenticated users
 | 
					email_visibility.limited = Your email address is visible to all authenticated users
 | 
				
			||||||
email_visibility.private = Your email address is only visible to you and administrators
 | 
					email_visibility.private = Your email address is only visible to you and administrators
 | 
				
			||||||
 | 
					block_user = Block User
 | 
				
			||||||
 | 
					block_user.detail = Please understand that if you block this user, other actions will be taken. Such as:
 | 
				
			||||||
 | 
					block_user.detail_1 = You are being unfollowed from this user.
 | 
				
			||||||
 | 
					block_user.detail_2 = This user cannot interact with your repositories, created issues and comments.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
form.name_reserved = The username "%s" is reserved.
 | 
					form.name_reserved = The username "%s" is reserved.
 | 
				
			||||||
form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username.
 | 
					form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username.
 | 
				
			||||||
| 
						 | 
					@ -624,6 +630,7 @@ account_link = Linked Accounts
 | 
				
			||||||
organization = Organizations
 | 
					organization = Organizations
 | 
				
			||||||
uid = Uid
 | 
					uid = Uid
 | 
				
			||||||
webauthn = Security Keys
 | 
					webauthn = Security Keys
 | 
				
			||||||
 | 
					blocked_users = Blocked Users
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public_profile = Public Profile
 | 
					public_profile = Public Profile
 | 
				
			||||||
biography_placeholder = Tell us a little bit about yourself
 | 
					biography_placeholder = Tell us a little bit about yourself
 | 
				
			||||||
| 
						 | 
					@ -1636,6 +1643,7 @@ issues.content_history.delete_from_history = Delete from history
 | 
				
			||||||
issues.content_history.delete_from_history_confirm = Delete from history?
 | 
					issues.content_history.delete_from_history_confirm = Delete from history?
 | 
				
			||||||
issues.content_history.options = Options
 | 
					issues.content_history.options = Options
 | 
				
			||||||
issues.reference_link = Reference: %s
 | 
					issues.reference_link = Reference: %s
 | 
				
			||||||
 | 
					issues.blocked_by_user = You cannot create a issue on this repository because you are blocked by the repository owner.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
compare.compare_base = base
 | 
					compare.compare_base = base
 | 
				
			||||||
compare.compare_head = compare
 | 
					compare.compare_head = compare
 | 
				
			||||||
| 
						 | 
					@ -1708,6 +1716,7 @@ pulls.reject_count_n = "%d change requests"
 | 
				
			||||||
pulls.waiting_count_1 = "%d waiting review"
 | 
					pulls.waiting_count_1 = "%d waiting review"
 | 
				
			||||||
pulls.waiting_count_n = "%d waiting reviews"
 | 
					pulls.waiting_count_n = "%d waiting reviews"
 | 
				
			||||||
pulls.wrong_commit_id = "commit id must be a commit id on the target branch"
 | 
					pulls.wrong_commit_id = "commit id must be a commit id on the target branch"
 | 
				
			||||||
 | 
					pulls.blocked_by_user = You cannot create a pull request on this repository because you are blocked by the repository owner.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
 | 
					pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
 | 
				
			||||||
pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually.
 | 
					pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@
 | 
				
			||||||
package repo
 | 
					package repo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
| 
						 | 
					@ -652,7 +653,10 @@ func CreateIssue(ctx *context.APIContext) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
 | 
						if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
 | 
				
			||||||
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "BlockedByUser", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -364,7 +364,11 @@ func CreateIssueComment(ctx *context.APIContext) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
 | 
						comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
 | 
								ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,11 +8,13 @@ import (
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
						issues_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
	api "code.gitea.io/gitea/modules/structs"
 | 
						api "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web"
 | 
						"code.gitea.io/gitea/modules/web"
 | 
				
			||||||
	"code.gitea.io/gitea/routers/api/v1/utils"
 | 
						"code.gitea.io/gitea/routers/api/v1/utils"
 | 
				
			||||||
	"code.gitea.io/gitea/services/convert"
 | 
						"code.gitea.io/gitea/services/convert"
 | 
				
			||||||
 | 
						issue_service "code.gitea.io/gitea/services/issue"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetIssueCommentReactions list reactions of a comment from an issue
 | 
					// GetIssueCommentReactions list reactions of a comment from an issue
 | 
				
			||||||
| 
						 | 
					@ -196,9 +198,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if isCreateType {
 | 
						if isCreateType {
 | 
				
			||||||
		// PostIssueCommentReaction part
 | 
							// PostIssueCommentReaction part
 | 
				
			||||||
		reaction, err := issues_model.CreateCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
 | 
							reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment.Issue, comment, form.Reaction)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
								if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
				ctx.Error(http.StatusForbidden, err.Error(), err)
 | 
									ctx.Error(http.StatusForbidden, err.Error(), err)
 | 
				
			||||||
			} else if issues_model.IsErrReactionAlreadyExist(err) {
 | 
								} else if issues_model.IsErrReactionAlreadyExist(err) {
 | 
				
			||||||
				ctx.JSON(http.StatusOK, api.Reaction{
 | 
									ctx.JSON(http.StatusOK, api.Reaction{
 | 
				
			||||||
| 
						 | 
					@ -406,9 +408,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if isCreateType {
 | 
						if isCreateType {
 | 
				
			||||||
		// PostIssueReaction part
 | 
							// PostIssueReaction part
 | 
				
			||||||
		reaction, err := issues_model.CreateIssueReaction(ctx.Doer.ID, issue.ID, form.Reaction)
 | 
							reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
								if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
				ctx.Error(http.StatusForbidden, err.Error(), err)
 | 
									ctx.Error(http.StatusForbidden, err.Error(), err)
 | 
				
			||||||
			} else if issues_model.IsErrReactionAlreadyExist(err) {
 | 
								} else if issues_model.IsErrReactionAlreadyExist(err) {
 | 
				
			||||||
				ctx.JSON(http.StatusOK, api.Reaction{
 | 
									ctx.JSON(http.StatusOK, api.Reaction{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -418,7 +418,10 @@ func CreatePullRequest(ctx *context.APIContext) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
 | 
						if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
 | 
				
			||||||
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "BlockedByUser", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -218,7 +218,7 @@ func Follow(ctx *context.APIContext) {
 | 
				
			||||||
	//   "204":
 | 
						//   "204":
 | 
				
			||||||
	//     "$ref": "#/responses/empty"
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := user_model.FollowUser(ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
 | 
						if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "FollowUser", err)
 | 
							ctx.Error(http.StatusInternalServerError, "FollowUser", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -240,7 +240,7 @@ func Unfollow(ctx *context.APIContext) {
 | 
				
			||||||
	//   "204":
 | 
						//   "204":
 | 
				
			||||||
	//     "$ref": "#/responses/empty"
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := user_model.UnfollowUser(ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
 | 
						if err := user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "UnfollowUser", err)
 | 
							ctx.Error(http.StatusInternalServerError, "UnfollowUser", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1168,7 +1168,10 @@ func NewIssuePost(ctx *context.Context) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
 | 
						if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
 | 
				
			||||||
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.RenderWithErr(ctx.Tr("repo.issues.blocked_by_user"), tplIssueNew, form)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -3096,7 +3099,7 @@ func ChangeIssueReaction(ctx *context.Context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch ctx.Params(":action") {
 | 
						switch ctx.Params(":action") {
 | 
				
			||||||
	case "react":
 | 
						case "react":
 | 
				
			||||||
		reaction, err := issues_model.CreateIssueReaction(ctx.Doer.ID, issue.ID, form.Content)
 | 
							reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
								if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
				
			||||||
				ctx.ServerError("ChangeIssueReaction", err)
 | 
									ctx.ServerError("ChangeIssueReaction", err)
 | 
				
			||||||
| 
						 | 
					@ -3198,7 +3201,7 @@ func ChangeCommentReaction(ctx *context.Context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch ctx.Params(":action") {
 | 
						switch ctx.Params(":action") {
 | 
				
			||||||
	case "react":
 | 
						case "react":
 | 
				
			||||||
		reaction, err := issues_model.CreateCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content)
 | 
							reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment.Issue, comment, form.Content)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
								if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
				
			||||||
				ctx.ServerError("ChangeIssueReaction", err)
 | 
									ctx.ServerError("ChangeIssueReaction", err)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1298,7 +1298,11 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 | 
				
			||||||
	// instead of 500.
 | 
						// instead of 500.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
 | 
						if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
 | 
				
			||||||
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("repo.pulls.blocked_by_user"))
 | 
				
			||||||
 | 
								ctx.Redirect(ctx.Link)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		} else if git.IsErrPushRejected(err) {
 | 
							} else if git.IsErrPushRejected(err) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,6 +31,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 | 
				
			||||||
	prepareContextForCommonProfile(ctx)
 | 
						prepareContextForCommonProfile(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx.Doer.ID, ctx.ContextUser.ID)
 | 
						ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
 | 
						ctx.Data["IsBlocked"] = ctx.Doer != nil && user_model.IsBlocked(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
	ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
 | 
						ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Show OpenID URIs
 | 
						// Show OpenID URIs
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,7 @@ import (
 | 
				
			||||||
	"code.gitea.io/gitea/routers/web/feed"
 | 
						"code.gitea.io/gitea/routers/web/feed"
 | 
				
			||||||
	"code.gitea.io/gitea/routers/web/org"
 | 
						"code.gitea.io/gitea/routers/web/org"
 | 
				
			||||||
	shared_user "code.gitea.io/gitea/routers/web/shared/user"
 | 
						shared_user "code.gitea.io/gitea/routers/web/shared/user"
 | 
				
			||||||
 | 
						user_service "code.gitea.io/gitea/services/user"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// OwnerProfile render profile page for a user or a organization (aka, repo owner)
 | 
					// OwnerProfile render profile page for a user or a organization (aka, repo owner)
 | 
				
			||||||
| 
						 | 
					@ -279,11 +280,17 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileGi
 | 
				
			||||||
// Action response for follow/unfollow user request
 | 
					// Action response for follow/unfollow user request
 | 
				
			||||||
func Action(ctx *context.Context) {
 | 
					func Action(ctx *context.Context) {
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
 | 
						var redirectViaJSON bool
 | 
				
			||||||
	switch ctx.FormString("action") {
 | 
						switch ctx.FormString("action") {
 | 
				
			||||||
	case "follow":
 | 
						case "follow":
 | 
				
			||||||
		err = user_model.FollowUser(ctx.Doer.ID, ctx.ContextUser.ID)
 | 
							err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
	case "unfollow":
 | 
						case "unfollow":
 | 
				
			||||||
		err = user_model.UnfollowUser(ctx.Doer.ID, ctx.ContextUser.ID)
 | 
							err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
 | 
						case "block":
 | 
				
			||||||
 | 
							err = user_service.BlockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
 | 
							redirectViaJSON = true
 | 
				
			||||||
 | 
						case "unblock":
 | 
				
			||||||
 | 
							err = user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
| 
						 | 
					@ -291,5 +298,13 @@ func Action(ctx *context.Context) {
 | 
				
			||||||
		ctx.JSONError(fmt.Sprintf("Action %q failed", ctx.FormString("action")))
 | 
							ctx.JSONError(fmt.Sprintf("Action %q failed", ctx.FormString("action")))
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if redirectViaJSON {
 | 
				
			||||||
 | 
							ctx.JSON(http.StatusOK, map[string]interface{}{
 | 
				
			||||||
 | 
								"redirect": ctx.ContextUser.HomeLink(),
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.JSONOK()
 | 
						ctx.JSONOK()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										34
									
								
								routers/web/user/setting/blocked_users.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								routers/web/user/setting/blocked_users.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package setting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockedUsers render the blocked users list page.
 | 
				
			||||||
 | 
					func BlockedUsers(ctx *context.Context) {
 | 
				
			||||||
 | 
						ctx.Data["Title"] = ctx.Tr("settings.blocked_users")
 | 
				
			||||||
 | 
						ctx.Data["PageIsBlockedUsers"] = true
 | 
				
			||||||
 | 
						ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/blocked_users"
 | 
				
			||||||
 | 
						ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/blocked_users"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Doer.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("ListBlockedUsers", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Data["BlockedUsers"] = blockedUsers
 | 
				
			||||||
 | 
						ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -522,6 +522,8 @@ func registerRoutes(m *web.Route) {
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
			addWebhookEditRoutes()
 | 
								addWebhookEditRoutes()
 | 
				
			||||||
		}, webhooksEnabled)
 | 
							}, webhooksEnabled)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							m.Get("/blocked_users", user_setting.BlockedUsers)
 | 
				
			||||||
	}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
 | 
						}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	m.Group("/user", func() {
 | 
						m.Group("/user", func() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,6 +66,11 @@ func CreateRefComment(doer *user_model.User, repo *repo_model.Repository, issue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateIssueComment creates a plain issue comment.
 | 
					// CreateIssueComment creates a plain issue comment.
 | 
				
			||||||
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) {
 | 
					func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) {
 | 
				
			||||||
 | 
						// Check if doer is blocked by the poster of the issue.
 | 
				
			||||||
 | 
						if user_model.IsBlocked(ctx, issue.PosterID, doer.ID) {
 | 
				
			||||||
 | 
							return nil, user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	comment, err := CreateComment(ctx, &issues_model.CreateCommentOptions{
 | 
						comment, err := CreateComment(ctx, &issues_model.CreateCommentOptions{
 | 
				
			||||||
		Type:        issues_model.CommentTypeComment,
 | 
							Type:        issues_model.CommentTypeComment,
 | 
				
			||||||
		Doer:        doer,
 | 
							Doer:        doer,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,6 +22,11 @@ import (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewIssue creates new issue with labels for repository.
 | 
					// NewIssue creates new issue with labels for repository.
 | 
				
			||||||
func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
 | 
					func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
 | 
				
			||||||
 | 
						// Check if the user is not blocked by the repo's owner.
 | 
				
			||||||
 | 
						if user_model.IsBlocked(ctx, repo.OwnerID, issue.PosterID) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := issues_model.NewIssue(repo, issue, labelIDs, uuids); err != nil {
 | 
						if err := issues_model.NewIssue(repo, issue, labelIDs, uuids); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										47
									
								
								services/issue/reaction.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								services/issue/reaction.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					package issue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						issues_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateIssueReaction creates a reaction on issue.
 | 
				
			||||||
 | 
					func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) {
 | 
				
			||||||
 | 
						if err := issue.LoadRepo(ctx); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if the doer is blocked by the issue's poster or repository owner.
 | 
				
			||||||
 | 
						if user_model.IsBlockedMultiple(ctx, []int64{issue.PosterID, issue.Repo.OwnerID}, doer.ID) {
 | 
				
			||||||
 | 
							return nil, user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
 | 
				
			||||||
 | 
							Type:    content,
 | 
				
			||||||
 | 
							DoerID:  doer.ID,
 | 
				
			||||||
 | 
							IssueID: issue.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateCommentReaction creates a reaction on comment.
 | 
				
			||||||
 | 
					func CreateCommentReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) {
 | 
				
			||||||
 | 
						if err := issue.LoadRepo(ctx); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if the doer is blocked by the issue's poster, the comment's poster or repository owner.
 | 
				
			||||||
 | 
						if user_model.IsBlockedMultiple(ctx, []int64{comment.PosterID, issue.PosterID, issue.Repo.OwnerID}, doer.ID) {
 | 
				
			||||||
 | 
							return nil, user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
 | 
				
			||||||
 | 
							Type:      content,
 | 
				
			||||||
 | 
							DoerID:    doer.ID,
 | 
				
			||||||
 | 
							IssueID:   issue.ID,
 | 
				
			||||||
 | 
							CommentID: comment.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,11 @@ var pullWorkingPool = sync.NewExclusivePool()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewPullRequest creates new pull request with labels for repository.
 | 
					// NewPullRequest creates new pull request with labels for repository.
 | 
				
			||||||
func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
 | 
					func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
 | 
				
			||||||
 | 
						// Check if the doer is not blocked by the repository's owner.
 | 
				
			||||||
 | 
						if user_model.IsBlocked(ctx, repo.OwnerID, pull.PosterID) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := TestPatch(pr); err != nil {
 | 
						if err := TestPatch(pr); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										40
									
								
								services/user/block.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								services/user/block.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockUser adds a blocked user entry for userID to block blockID.
 | 
				
			||||||
 | 
					// TODO: Figure out if instance admins should be immune to blocking.
 | 
				
			||||||
 | 
					// TODO: Add more mechanism like removing blocked user as collaborator on
 | 
				
			||||||
 | 
					// repositories where the user is an owner.
 | 
				
			||||||
 | 
					func BlockUser(ctx context.Context, userID, blockID int64) error {
 | 
				
			||||||
 | 
						if userID == blockID || user_model.IsBlocked(ctx, userID, blockID) {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx, committer, err := db.TxContext(ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer committer.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add the blocked user entry.
 | 
				
			||||||
 | 
						_, err = db.GetEngine(ctx).Insert(&user_model.BlockedUser{UserID: userID, BlockID: blockID})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Unfollow the user from block's perspective.
 | 
				
			||||||
 | 
						err = user_model.UnfollowUser(ctx, blockID, userID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return committer.Commit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -90,6 +90,8 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
 | 
				
			||||||
		&pull_model.AutoMerge{DoerID: u.ID},
 | 
							&pull_model.AutoMerge{DoerID: u.ID},
 | 
				
			||||||
		&pull_model.ReviewState{UserID: u.ID},
 | 
							&pull_model.ReviewState{UserID: u.ID},
 | 
				
			||||||
		&user_model.Redirect{RedirectUserID: u.ID},
 | 
							&user_model.Redirect{RedirectUserID: u.ID},
 | 
				
			||||||
 | 
							&user_model.BlockedUser{BlockID: u.ID},
 | 
				
			||||||
 | 
							&user_model.BlockedUser{UserID: u.ID},
 | 
				
			||||||
	); err != nil {
 | 
						); err != nil {
 | 
				
			||||||
		return fmt.Errorf("deleteBeans: %w", err)
 | 
							return fmt.Errorf("deleteBeans: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -108,6 +108,18 @@
 | 
				
			||||||
					</button>
 | 
										</button>
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
			</li>
 | 
								</li>
 | 
				
			||||||
 | 
								<li class="block">
 | 
				
			||||||
 | 
									{{if $.IsBlocked}}
 | 
				
			||||||
 | 
										<button class="ui basic red button link-action" data-url="{{.ContextUser.HomeLink}}?action=unblock&redirect_to={{$.Link}}">
 | 
				
			||||||
 | 
											{{svg "octicon-person"}} {{.locale.Tr "user.unblock"}}
 | 
				
			||||||
 | 
										</button>
 | 
				
			||||||
 | 
									{{else}}
 | 
				
			||||||
 | 
										<button type="submit" class="ui basic orange button delete-button"
 | 
				
			||||||
 | 
										data-modal-id="block-user" data-url="{{.ContextUser.HomeLink}}?action=block">
 | 
				
			||||||
 | 
											{{svg "octicon-blocked"}} {{.locale.Tr "user.block"}}
 | 
				
			||||||
 | 
										</button>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
								</li>
 | 
				
			||||||
			{{end}}
 | 
								{{end}}
 | 
				
			||||||
		</ul>
 | 
							</ul>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,4 +41,19 @@
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					<div class="ui small basic delete modal" id="block-user">
 | 
				
			||||||
 | 
						<div class="ui icon header">
 | 
				
			||||||
 | 
							{{svg "octicon-blocked" 16 "blocked inside"}}
 | 
				
			||||||
 | 
							{{$.locale.Tr "user.block_user"}}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="content">
 | 
				
			||||||
 | 
							<p>{{$.locale.Tr "user.block_user.detail"}}</p>
 | 
				
			||||||
 | 
							<ul>
 | 
				
			||||||
 | 
									<li>{{$.locale.Tr "user.block_user.detail_1"}}</li>
 | 
				
			||||||
 | 
									<li>{{$.locale.Tr "user.block_user.detail_2"}}</li>
 | 
				
			||||||
 | 
							</ul>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						{{template "base/modal_actions_confirm" .}}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{{template "base/footer" .}}
 | 
					{{template "base/footer" .}}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								templates/user/settings/blocked_users.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								templates/user/settings/blocked_users.tmpl
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings blocked_users")}}
 | 
				
			||||||
 | 
						<div class="user-setting-content">
 | 
				
			||||||
 | 
							<h4 class="ui top attached header">
 | 
				
			||||||
 | 
								{{.locale.Tr "settings.blocked_users"}}
 | 
				
			||||||
 | 
							</h4>
 | 
				
			||||||
 | 
							<div class="ui attached segment">
 | 
				
			||||||
 | 
								<div class="ui blocked-user list gt-mt-0">
 | 
				
			||||||
 | 
									{{range .BlockedUsers}}
 | 
				
			||||||
 | 
										<div class="item">
 | 
				
			||||||
 | 
											{{avatar $.Context . 28 "gt-mr-3"}}<a href="{{.HomeLink}}">{{.Name}}</a>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					{{template "user/settings/layout_footer" .}}
 | 
				
			||||||
| 
						 | 
					@ -51,5 +51,8 @@
 | 
				
			||||||
		<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
 | 
							<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
 | 
				
			||||||
			{{.locale.Tr "settings.repos"}}
 | 
								{{.locale.Tr "settings.repos"}}
 | 
				
			||||||
		</a>
 | 
							</a>
 | 
				
			||||||
 | 
							<a class="{{if .PageIsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
 | 
				
			||||||
 | 
								{{.locale.Tr "settings.blocked_users"}}
 | 
				
			||||||
 | 
							</a>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										158
									
								
								tests/integration/block_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								tests/integration/block_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,158 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						issue_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/translation"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/tests"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockUser(t *testing.T, doer, blockedUser *user_model.User) {
 | 
				
			||||||
 | 
						t.Helper()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session := loginUser(t, doer.Name)
 | 
				
			||||||
 | 
						req := NewRequestWithValues(t, "POST", "/"+blockedUser.Name, map[string]string{
 | 
				
			||||||
 | 
							"_csrf":  GetCSRF(t, session, "/"+blockedUser.Name),
 | 
				
			||||||
 | 
							"action": "block",
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type redirect struct {
 | 
				
			||||||
 | 
							Redirect string `json:"redirect"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var respBody redirect
 | 
				
			||||||
 | 
						DecodeJSON(t, resp, &respBody)
 | 
				
			||||||
 | 
						assert.EqualValues(t, "/"+blockedUser.Name, respBody.Redirect)
 | 
				
			||||||
 | 
						assert.EqualValues(t, true, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID}))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestBlockUser(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 8})
 | 
				
			||||||
 | 
						blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						BlockUser(t, doer, blockedUser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Unblock user.
 | 
				
			||||||
 | 
						session := loginUser(t, doer.Name)
 | 
				
			||||||
 | 
						req := NewRequestWithValues(t, "POST", "/"+blockedUser.Name, map[string]string{
 | 
				
			||||||
 | 
							"_csrf":  GetCSRF(t, session, "/"+blockedUser.Name),
 | 
				
			||||||
 | 
							"action": "unblock",
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						resp := session.MakeRequest(t, req, http.StatusSeeOther)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						loc := resp.Header().Get("Location")
 | 
				
			||||||
 | 
						assert.EqualValues(t, "/"+blockedUser.Name, loc)
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestBlockIssueCreation(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
						blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: doer.ID})
 | 
				
			||||||
 | 
						BlockUser(t, doer, blockedUser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session := loginUser(t, blockedUser.Name)
 | 
				
			||||||
 | 
						req := NewRequest(t, "GET", "/"+repo.OwnerName+"/"+repo.Name+"/issues/new")
 | 
				
			||||||
 | 
						resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						htmlDoc := NewHTMLParser(t, resp.Body)
 | 
				
			||||||
 | 
						link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
 | 
				
			||||||
 | 
						assert.True(t, exists)
 | 
				
			||||||
 | 
						req = NewRequestWithValues(t, "POST", link, map[string]string{
 | 
				
			||||||
 | 
							"_csrf":   htmlDoc.GetCSRF(),
 | 
				
			||||||
 | 
							"title":   "Title",
 | 
				
			||||||
 | 
							"content": "Hello!",
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
						htmlDoc = NewHTMLParser(t, resp.Body)
 | 
				
			||||||
 | 
						assert.Contains(t,
 | 
				
			||||||
 | 
							htmlDoc.doc.Find(".ui.negative.message").Text(),
 | 
				
			||||||
 | 
							translation.NewLocale("en-US").Tr("repo.issues.blocked_by_user"),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestBlockIssueReaction(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
						blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
 | 
				
			||||||
 | 
						issue := unittest.AssertExistsAndLoadBean(t, &issue_model.Issue{ID: 4, PosterID: doer.ID, RepoID: repo.ID})
 | 
				
			||||||
 | 
						issueURL := fmt.Sprintf("/%s/%s/issues/%d", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), issue.Index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						BlockUser(t, doer, blockedUser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session := loginUser(t, blockedUser.Name)
 | 
				
			||||||
 | 
						req := NewRequest(t, "GET", issueURL)
 | 
				
			||||||
 | 
						resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
						htmlDoc := NewHTMLParser(t, resp.Body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
 | 
				
			||||||
 | 
							"_csrf":   htmlDoc.GetCSRF(),
 | 
				
			||||||
 | 
							"content": "eyes",
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
						type reactionResponse struct {
 | 
				
			||||||
 | 
							Empty bool `json:"empty"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var respBody reactionResponse
 | 
				
			||||||
 | 
						DecodeJSON(t, resp, &respBody)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.EqualValues(t, true, respBody.Empty)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestBlockCommentReaction(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 | 
				
			||||||
 | 
						blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
				
			||||||
 | 
						issue := unittest.AssertExistsAndLoadBean(t, &issue_model.Issue{ID: 1, RepoID: repo.ID})
 | 
				
			||||||
 | 
						comment := unittest.AssertExistsAndLoadBean(t, &issue_model.Comment{ID: 3, PosterID: doer.ID, IssueID: issue.ID})
 | 
				
			||||||
 | 
						_ = comment.LoadIssue(db.DefaultContext)
 | 
				
			||||||
 | 
						issueURL := fmt.Sprintf("/%s/%s/issues/%d", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), issue.Index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						BlockUser(t, doer, blockedUser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session := loginUser(t, blockedUser.Name)
 | 
				
			||||||
 | 
						req := NewRequest(t, "GET", issueURL)
 | 
				
			||||||
 | 
						resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
						htmlDoc := NewHTMLParser(t, resp.Body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req = NewRequestWithValues(t, "POST", path.Join(repo.Link(), "/comments/", strconv.FormatInt(comment.ID, 10), "/reactions/react"), map[string]string{
 | 
				
			||||||
 | 
							"_csrf":   htmlDoc.GetCSRF(),
 | 
				
			||||||
 | 
							"content": "eyes",
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
						type reactionResponse struct {
 | 
				
			||||||
 | 
							Empty bool `json:"empty"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var respBody reactionResponse
 | 
				
			||||||
 | 
						DecodeJSON(t, resp, &respBody)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.EqualValues(t, true, respBody.Empty)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,11 @@
 | 
				
			||||||
  margin-right: 5px;
 | 
					  margin-right: 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.user.profile .ui.card .extra.content > ul > li.follow .ui.button {
 | 
					.user.profile .ui.card .extra.content > ul > li.follow .ui.button,
 | 
				
			||||||
 | 
					.user.profile .ui.card .extra.content > ul > li.block .ui.button {
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue