mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-31 03:14:17 +09:00
fix(user): answer the follow of a bot user with the profile card (#39980)
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
1 parent
ac11887b02
commit
7361ade2e5
8 files changed
+67
-24
No files matched your search
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -32,6 +33,9 @@ func IsFollowing(ctx context.Context, userID, followID int64) bool {
|
||||
|
||||
// FollowUser marks someone be another's follower.
|
||||
func FollowUser(ctx context.Context, user, follow *User) (err error) {
|
||||
if !follow.IsFollowable() {
|
||||
return util.NewInvalidArgumentErrorf("user %d can not be followed", follow.ID)
|
||||
}
|
||||
if user.ID == follow.ID || IsFollowing(ctx, user.ID, follow.ID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
+13
-1
@@ -441,6 +441,18 @@ func (u *User) IsIndividual() bool {
|
||||
return u.ID > 0 && u.Type == UserTypeIndividual
|
||||
}
|
||||
|
||||
// IsFollowable returns whether other users can follow this user (individuals, bots and organizations)
|
||||
func (u *User) IsFollowable() bool {
|
||||
if u.ID <= 0 {
|
||||
return false
|
||||
}
|
||||
switch u.Type {
|
||||
case UserTypeIndividual, UserTypeBot, UserTypeOrganization:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsTypeBot returns whether the user is of type bot
|
||||
func (u *User) IsTypeBot() bool {
|
||||
return u.Type == UserTypeBot
|
||||
@@ -1053,7 +1065,7 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
return users, err
|
||||
}
|
||||
|
||||
// Glossaries for XxxUser:
|
||||
// HINT: USER-TYPE-GLOSSARY: Glossaries for XxxUser:
|
||||
// * Possible: The user may or may not exist in the database, but we can still return a valid user object (e.g. Ghost user) for the given ID.
|
||||
// * Individual: The user is a real person, not an organization or a bot.
|
||||
// * Doer: The user is the one performing an action (individual, bot or system)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
user_model "gitea.dev/models/user"
|
||||
@@ -212,7 +211,6 @@ func CheckFollowing(ctx *context.APIContext) {
|
||||
checkUserFollowing(ctx, ctx.ContextUser, target.ID)
|
||||
}
|
||||
|
||||
// Follow follow a user
|
||||
func Follow(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /user/following/{username} user userCurrentPutFollow
|
||||
// ---
|
||||
@@ -230,17 +228,12 @@ func Follow(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err.Error())
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Unfollow unfollow a user
|
||||
func Unfollow(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /user/following/{username} user userCurrentDeleteFollow
|
||||
// ---
|
||||
@@ -258,7 +251,7 @@ func Unfollow(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
if err := user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
activities_model "gitea.dev/models/activities"
|
||||
@@ -33,7 +32,7 @@ const (
|
||||
|
||||
// OwnerProfile render profile page for a user or a organization (aka, repo owner)
|
||||
func OwnerProfile(ctx *context.Context) {
|
||||
ctx.Data["EnableFeed"] = setting.Other.EnableFeed
|
||||
// HINT: USER-TYPE-GLOSSARY: in the future it needs to handle more user types correctly
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
org.Home(ctx)
|
||||
} else {
|
||||
@@ -313,25 +312,22 @@ func ActionUserFollow(ctx *context.Context) {
|
||||
case "unfollow":
|
||||
err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("Failed to apply action %q: %v", ctx.FormString("action"), err)
|
||||
ctx.HTTPError(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
|
||||
ctx.ServerError("ActionUserFollow", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
if ctx.ContextUser.IsIndividual() {
|
||||
ctx.HTML(http.StatusOK, tplProfileBigAvatar)
|
||||
return
|
||||
} else if ctx.ContextUser.IsOrganization() {
|
||||
|
||||
// HINT: USER-TYPE-GLOSSARY: follow the logic in OwnerProfile
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
ctx.Data["Org"] = ctx.ContextUser
|
||||
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
ctx.Data["IsFollowing"] = user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
ctx.HTML(http.StatusOK, tplFollowUnfollow)
|
||||
return
|
||||
} else {
|
||||
ctx.HTML(http.StatusOK, tplProfileBigAvatar)
|
||||
}
|
||||
log.Error("Failed to apply action %q: unsupported context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type)
|
||||
ctx.HTTPError(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
|
||||
}
|
||||
@@ -166,11 +166,16 @@ func (ctx *Context) buildUserErrorMessage(msg string, err error) (userErrorMsg s
|
||||
|
||||
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
|
||||
// If the error is controlled by our error system, a related 404 page can be displayed instead.
|
||||
// TODO: refactor this function like APIErrorAuto in the future, and handle more error types and http codes.
|
||||
func (ctx *Context) ServerError(logMsg string, logErr error) {
|
||||
if errors.Is(logErr, util.ErrNotExist) {
|
||||
ctx.notFoundInternal(1, logMsg, logErr)
|
||||
return
|
||||
}
|
||||
if errMsg, httpCode := util.ErrorUnwrapForUser(logErr); errMsg != "" {
|
||||
ctx.HTTPError(httpCode, errMsg)
|
||||
return
|
||||
}
|
||||
ctx.serverErrorInternal(1, logMsg, logErr)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@ import (
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIFollow(t *testing.T) {
|
||||
@@ -41,6 +43,13 @@ func TestAPIFollow(t *testing.T) {
|
||||
req = NewRequest(t, "PUT", "/api/v1/user/following/user2").
|
||||
AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteUser))
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
// reserved users can't be followed
|
||||
_, err := db.GetEngine(t.Context()).Insert(&user_model.User{Name: "reserved-user", LowerName: "reserved-user", Type: user_model.UserTypeUserReserved})
|
||||
require.NoError(t, err)
|
||||
req = NewRequest(t, "PUT", "/api/v1/user/following/reserved-user").
|
||||
AddTokenAuth(token2)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
for _, list := range []struct {
|
||||
|
||||
@@ -602,7 +602,7 @@ func TestIssueReaction(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
|
||||
"content": "8ball",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusInternalServerError)
|
||||
session.MakeRequest(t, req, http.StatusForbidden)
|
||||
req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
|
||||
"content": "eyes",
|
||||
})
|
||||
|
||||
@@ -22,11 +22,13 @@ import (
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("ViewUser", testViewUser)
|
||||
t.Run("FollowUser", testUserFollow)
|
||||
t.Run("RenameInvalidUsername", testRenameInvalidUsername)
|
||||
t.Run("RenameReservedUsername", testRenameReservedUsername)
|
||||
t.Run("ViewLimitedAndPrivateUserAndRename", testViewLimitedAndPrivateUserAndRename)
|
||||
@@ -385,3 +387,25 @@ func testUserLocationMapLink(t *testing.T) {
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
AssertHTMLElement(t, htmlDoc, `a[href="https://example/foo/A%2Fb"]`, true)
|
||||
}
|
||||
|
||||
func testUserFollow(t *testing.T) {
|
||||
bot := &user_model.User{
|
||||
Name: "follow-bot",
|
||||
Email: "follow-bot@example.com",
|
||||
Type: user_model.UserTypeBot,
|
||||
}
|
||||
require.NoError(t, user_model.CreateUser(t.Context(), bot, &user_model.Meta{}))
|
||||
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
|
||||
session := loginUser(t, doer.Name)
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/"+bot.Name, map[string]string{"action": "follow"})
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), `id="profile-avatar-card"`)
|
||||
assert.True(t, user_model.IsFollowing(t.Context(), doer.ID, bot.ID))
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/"+bot.Name, map[string]string{"action": "unfollow"})
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), `id="profile-avatar-card"`)
|
||||
assert.False(t, user_model.IsFollowing(t.Context(), doer.ID, bot.ID))
|
||||
}
|
||||
Reference in new issue
Block a user