mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-11-12 04:04:27 +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 commit00f411819f) (cherry picked from commite0c039b0e8) (cherry picked from commitb5a058ef00) (cherry picked from commit5ff5460d28)
This commit is contained in:
parent
89490dbb7b
commit
97bc6e619d
37 changed files with 656 additions and 52 deletions
|
|
@ -580,7 +580,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
|
|||
|
||||
if repoChanged {
|
||||
// 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 {
|
||||
return fmt.Errorf("get watchers: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,6 +235,15 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
|
|||
for _, id := range issueUnWatches {
|
||||
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)
|
||||
|
|
|
|||
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"
|
||||
"os"
|
||||
|
||||
forgejo_v1_20 "code.gitea.io/gitea/models/forgejo_migrations/v1_20"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"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.
|
||||
// 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.
|
||||
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))
|
||||
}
|
||||
|
|
@ -451,6 +451,8 @@ func TestIssue_ResolveMentions(t *testing.T) {
|
|||
testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{})
|
||||
// Public repo, doer
|
||||
testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{})
|
||||
// Public repo, blocked user
|
||||
testSuccess("user2", "repo1", "user1", []string{"user4"}, []int64{})
|
||||
// Private repo, team member
|
||||
testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2})
|
||||
// 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)
|
||||
if err := db.GetEngine(ctx).
|
||||
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).
|
||||
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": doer.ID})).
|
||||
Find(&teamusers); err != nil {
|
||||
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))
|
||||
if err := db.GetEngine(ctx).
|
||||
Join("LEFT", "forgejo_blocked_user", "forgejo_blocked_user.user_id = `user`.id").
|
||||
Where("`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": doer.ID})).
|
||||
In("`user`.lower_name", mentionUsers).
|
||||
Find(&unchecked); err != nil {
|
||||
return nil, fmt.Errorf("find mentioned users: %w", err)
|
||||
|
|
|
|||
|
|
@ -218,12 +218,12 @@ type ReactionOptions struct {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
return nil, ErrForbiddenIssueReaction{opts.Type}
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -240,25 +240,6 @@ func CreateReaction(opts *ReactionOptions) (*Reaction, error) {
|
|||
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.
|
||||
func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
|
||||
reaction := &Reaction{
|
||||
|
|
|
|||
|
|
@ -19,11 +19,14 @@ import (
|
|||
func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
|
||||
var reaction *issues_model.Reaction
|
||||
var err error
|
||||
if commentID == 0 {
|
||||
reaction, err = issues_model.CreateIssueReaction(doerID, issueID, content)
|
||||
} else {
|
||||
reaction, err = issues_model.CreateCommentReaction(doerID, issueID, commentID, content)
|
||||
}
|
||||
// NOTE: This doesn't do user blocking checking.
|
||||
reaction, err = issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{
|
||||
DoerID: doerID,
|
||||
IssueID: issueID,
|
||||
CommentID: commentID,
|
||||
Type: content,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, reaction)
|
||||
}
|
||||
|
|
@ -49,7 +52,7 @@ func TestIssueAddDuplicateReaction(t *testing.T) {
|
|||
|
||||
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,
|
||||
IssueID: issue1ID,
|
||||
Type: "heart",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import (
|
|||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// but avoids joining with `user` for performance reasons
|
||||
// User permissions must be verified elsewhere if required
|
||||
|
|
|
|||
|
|
@ -43,6 +43,24 @@ func TestGetWatchers(t *testing.T) {
|
|||
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) {
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
|
@ -27,12 +29,12 @@ func IsFollowing(userID, followID int64) bool {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -53,12 +55,12 @@ func FollowUser(userID, followID int64) (err error) {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -449,13 +449,13 @@ func TestFollowUser(t *testing.T) {
|
|||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
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})
|
||||
}
|
||||
testSuccess(4, 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{})
|
||||
}
|
||||
|
|
@ -464,7 +464,7 @@ func TestUnfollowUser(t *testing.T) {
|
|||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
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})
|
||||
}
|
||||
testSuccess(4, 2)
|
||||
|
|
|
|||
|
|
@ -590,11 +590,17 @@ overview = Overview
|
|||
following = Following
|
||||
follow = Follow
|
||||
unfollow = Unfollow
|
||||
block = Block
|
||||
unblock = Unblock
|
||||
heatmap.loading = Loading Heatmap…
|
||||
user_bio = Biography
|
||||
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.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_pattern_not_allowed = The pattern "%s" is not allowed in a username.
|
||||
|
|
@ -618,6 +624,7 @@ account_link = Linked Accounts
|
|||
organization = Organizations
|
||||
uid = Uid
|
||||
webauthn = Security Keys
|
||||
blocked_users = Blocked Users
|
||||
|
||||
public_profile = Public Profile
|
||||
biography_placeholder = Tell us a little bit about yourself
|
||||
|
|
@ -1629,6 +1636,7 @@ issues.content_history.delete_from_history = Delete from history
|
|||
issues.content_history.delete_from_history_confirm = Delete from history?
|
||||
issues.content_history.options = Options
|
||||
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_head = compare
|
||||
|
|
@ -1701,6 +1709,7 @@ pulls.reject_count_n = "%d change requests"
|
|||
pulls.waiting_count_1 = "%d waiting review"
|
||||
pulls.waiting_count_n = "%d waiting reviews"
|
||||
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_helper = Enable merge options in the repository settings or merge the pull request manually.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"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 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)
|
||||
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)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
||||
if errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ import (
|
|||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
)
|
||||
|
||||
// GetIssueCommentReactions list reactions of a comment from an issue
|
||||
|
|
@ -196,9 +198,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
|
|||
|
||||
if isCreateType {
|
||||
// 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 issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.Error(http.StatusForbidden, err.Error(), err)
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
ctx.JSON(http.StatusOK, api.Reaction{
|
||||
|
|
@ -406,9 +408,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
|
|||
|
||||
if isCreateType {
|
||||
// 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 issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.Error(http.StatusForbidden, err.Error(), err)
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
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 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)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ func Follow(ctx *context.APIContext) {
|
|||
// "204":
|
||||
// "$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)
|
||||
return
|
||||
}
|
||||
|
|
@ -240,7 +240,7 @@ func Unfollow(ctx *context.APIContext) {
|
|||
// "204":
|
||||
// "$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)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1162,7 +1162,10 @@ func NewIssuePost(ctx *context.Context) {
|
|||
}
|
||||
|
||||
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())
|
||||
return
|
||||
}
|
||||
|
|
@ -3061,7 +3064,7 @@ func ChangeIssueReaction(ctx *context.Context) {
|
|||
|
||||
switch ctx.Params(":action") {
|
||||
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 issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
ctx.ServerError("ChangeIssueReaction", err)
|
||||
|
|
@ -3163,7 +3166,7 @@ func ChangeCommentReaction(ctx *context.Context) {
|
|||
|
||||
switch ctx.Params(":action") {
|
||||
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 issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
ctx.ServerError("ChangeIssueReaction", err)
|
||||
|
|
|
|||
|
|
@ -1271,7 +1271,11 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
|||
// instead of 500.
|
||||
|
||||
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())
|
||||
return
|
||||
} else if git.IsErrPushRejected(err) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/web/feed"
|
||||
"code.gitea.io/gitea/routers/web/org"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
// Profile render user's profile page
|
||||
|
|
@ -58,8 +59,10 @@ func Profile(ctx *context.Context) {
|
|||
}
|
||||
|
||||
var isFollowing bool
|
||||
var isBlocked bool
|
||||
if ctx.Doer != nil {
|
||||
isFollowing = user_model.IsFollowing(ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
isBlocked = user_model.IsBlocked(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.ContextUser.DisplayName()
|
||||
|
|
@ -67,6 +70,7 @@ func Profile(ctx *context.Context) {
|
|||
ctx.Data["ContextUser"] = ctx.ContextUser
|
||||
ctx.Data["OpenIDs"] = openIDs
|
||||
ctx.Data["IsFollowing"] = isFollowing
|
||||
ctx.Data["IsBlocked"] = isBlocked
|
||||
|
||||
if setting.Service.EnableUserHeatmap {
|
||||
data, err := activities_model.GetUserHeatmapDataByUser(ctx.ContextUser, ctx.Doer)
|
||||
|
|
@ -351,17 +355,31 @@ func Profile(ctx *context.Context) {
|
|||
// Action response for follow/unfollow user request
|
||||
func Action(ctx *context.Context) {
|
||||
var err error
|
||||
var redirectViaJSON bool
|
||||
switch ctx.FormString("action") {
|
||||
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":
|
||||
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 {
|
||||
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.FormString("action")), err)
|
||||
return
|
||||
}
|
||||
|
||||
if redirectViaJSON {
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"redirect": ctx.ContextUser.HomeLink(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: We should check this URL and make sure that it's a valid Gitea URL
|
||||
ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.ContextUser.HomeLink())
|
||||
}
|
||||
|
|
|
|||
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)
|
||||
}
|
||||
|
|
@ -516,6 +516,8 @@ func registerRoutes(m *web.Route) {
|
|||
})
|
||||
addWebhookEditRoutes()
|
||||
}, webhooksEnabled)
|
||||
|
||||
m.Get("/blocked_users", user_setting.BlockedUsers)
|
||||
}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
|
||||
|
||||
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.
|
||||
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{
|
||||
Type: issues_model.CommentTypeComment,
|
||||
Doer: doer,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ import (
|
|||
|
||||
// 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 {
|
||||
// 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 {
|
||||
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.
|
||||
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 {
|
||||
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.ReviewState{UserID: u.ID},
|
||||
&user_model.Redirect{RedirectUserID: u.ID},
|
||||
&user_model.BlockedUser{BlockID: u.ID},
|
||||
&user_model.BlockedUser{UserID: u.ID},
|
||||
); err != nil {
|
||||
return fmt.Errorf("deleteBeans: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,21 @@
|
|||
</form>
|
||||
{{end}}
|
||||
</li>
|
||||
<li class="block">
|
||||
{{if $.IsBlocked}}
|
||||
<form method="post" action="{{.Link}}?action=unblock&redirect_to={{$.Link}}">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button type="submit" class="ui basic red button">{{svg "octicon-blocked"}} {{.locale.Tr "user.unblock"}}</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<form>
|
||||
<button type="submit" class="ui basic orange button delete-button"
|
||||
data-modal-id="block-user" data-url="{{.Link}}?action=block">
|
||||
{{svg "octicon-blocked"}} {{.locale.Tr "user.block"}}
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -156,4 +171,19 @@
|
|||
</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" .}}
|
||||
|
|
|
|||
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" .}}
|
||||
|
|
@ -48,5 +48,8 @@
|
|||
<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
|
||||
{{.locale.Tr "settings.repos"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
|
||||
{{.locale.Tr "settings.blocked_users"}}
|
||||
</a>
|
||||
</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;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue