WIP: Add user blocking
TODO: Frontend: - Disable reactions where needed. (The check is already in the code). - Disable comment form where needed. (The check is already in the code). - Add UI to list users who you have blocked. - Add block confirmation screen and list what it implies. Backend: - Add check to not allow a user to mention someone they got blocked by.
This commit is contained in:
parent
d3ebc9449c
commit
7cd8d1410f
|
@ -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",
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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).Get(&BlockedUser{UserID: userID, 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
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -447,13 +447,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{})
|
||||||
}
|
}
|
||||||
|
@ -462,7 +462,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)
|
||||||
|
|
|
@ -544,6 +544,8 @@ projects = Projects
|
||||||
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.
|
||||||
|
@ -1545,6 +1547,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_poster = You cannot comment on this issue, because the poster has blocked you.
|
||||||
|
|
||||||
compare.compare_base = base
|
compare.compare_base = base
|
||||||
compare.compare_head = compare
|
compare.compare_head = compare
|
||||||
|
|
|
@ -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 {
|
||||||
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
if errors.Is(err, issue_service.ErrBlockedByUser) {
|
||||||
|
ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
|
||||||
|
} else {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"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 +197,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, issue_service.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 +407,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, issue_service.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{
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -2774,7 +2774,12 @@ func NewComment(ctx *context.Context) {
|
||||||
|
|
||||||
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
|
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("CreateIssueComment", err)
|
if errors.Is(err, issue_service.ErrBlockedByUser) {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.blocked_by_poster"))
|
||||||
|
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index))
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("CreateIssueComment", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2914,7 +2919,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)
|
||||||
|
@ -3016,7 +3021,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)
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"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"
|
||||||
|
user_service "code.gitea.io/gitea/services/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Profile render user's profile page
|
// Profile render user's profile page
|
||||||
|
@ -57,8 +58,10 @@ func Profile(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var isFollowing bool
|
var isFollowing bool
|
||||||
|
var isBlocked bool
|
||||||
if ctx.Doer != nil {
|
if ctx.Doer != nil {
|
||||||
isFollowing = user_model.IsFollowing(ctx.Doer.ID, ctx.ContextUser.ID)
|
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()
|
ctx.Data["Title"] = ctx.ContextUser.DisplayName()
|
||||||
|
@ -66,6 +69,7 @@ func Profile(ctx *context.Context) {
|
||||||
ctx.Data["Owner"] = ctx.ContextUser
|
ctx.Data["Owner"] = ctx.ContextUser
|
||||||
ctx.Data["OpenIDs"] = openIDs
|
ctx.Data["OpenIDs"] = openIDs
|
||||||
ctx.Data["IsFollowing"] = isFollowing
|
ctx.Data["IsFollowing"] = isFollowing
|
||||||
|
ctx.Data["IsBlocked"] = isBlocked
|
||||||
|
|
||||||
if setting.Service.EnableUserHeatmap {
|
if setting.Service.EnableUserHeatmap {
|
||||||
data, err := activities_model.GetUserHeatmapDataByUser(ctx.ContextUser, ctx.Doer)
|
data, err := activities_model.GetUserHeatmapDataByUser(ctx.ContextUser, ctx.Doer)
|
||||||
|
@ -318,9 +322,13 @@ func Action(ctx *context.Context) {
|
||||||
var err error
|
var err error
|
||||||
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)
|
||||||
|
case "unblock":
|
||||||
|
err = user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -5,6 +5,7 @@ package issue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
@ -64,8 +65,15 @@ func CreateRefComment(doer *user_model.User, repo *repo_model.Repository, issue
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrBlockedByUser = errors.New("user is blocked by the poster")
|
||||||
|
|
||||||
// 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, ErrBlockedByUser
|
||||||
|
}
|
||||||
|
|
||||||
comment, err := CreateComment(&issues_model.CreateCommentOptions{
|
comment, err := CreateComment(&issues_model.CreateCommentOptions{
|
||||||
Type: issues_model.CommentTypeComment,
|
Type: issues_model.CommentTypeComment,
|
||||||
Doer: doer,
|
Doer: doer,
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
// 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) {
|
||||||
|
// Check if the doer is blocked by the issuer's poster.
|
||||||
|
if user_model.IsBlocked(ctx, issue.PosterID, doer.ID) {
|
||||||
|
return nil, 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) {
|
||||||
|
// Check if the doer is blocked by the issuer's poster or by the comment's poster.
|
||||||
|
if user_model.IsBlocked(ctx, comment.PosterID, doer.ID) || user_model.IsBlocked(ctx, issue.PosterID, doer.ID) {
|
||||||
|
return nil, ErrBlockedByUser
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
|
||||||
|
Type: content,
|
||||||
|
DoerID: doer.ID,
|
||||||
|
IssueID: issue.ID,
|
||||||
|
CommentID: comment.ID,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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.
|
||||||
|
func BlockUser(ctx context.Context, userID, blockID int64) error {
|
||||||
|
if userID == blockID || user_model.IsBlocked(ctx, userID, blockID) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// TODO: Figure out if instance admins should be immune to blocking.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add more mechanism like removing blocked user as collaborator on
|
||||||
|
// repositories where the user is an owner.
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,19 @@
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
</li>
|
</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 method="post" action="{{.Link}}?action=block&redirect_to={{$.Link}}">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<button type="submit" class="ui basic orange button">{{svg "octicon-blocked"}} {{.locale.Tr "user.block"}}</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -36,8 +36,12 @@
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.follow {
|
&.follow,
|
||||||
|
&.block {
|
||||||
.ui.button {
|
.ui.button {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue