Add user profile tests, refactor user API methods (#3030)

This adds tests for `/profile`.
Also, as a first change in this regard, refactors the methods defined on
the `UserInternalAPI` to not use structs as the request/response
parameters.
This commit is contained in:
Till 2023-04-03 20:19:26 +02:00 committed by GitHub
parent 4cb9cd7842
commit c2db38d295
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 391 additions and 258 deletions

View file

@ -22,8 +22,6 @@ import (
"encoding/json"
"errors"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
userapi "github.com/matrix-org/dendrite/userapi/api"
)
@ -150,6 +148,10 @@ type ASLocationResponse struct {
Fields json.RawMessage `json:"fields"`
}
// ErrProfileNotExists is returned when trying to lookup a user's profile that
// doesn't exist locally.
var ErrProfileNotExists = errors.New("no known profile for given user ID")
// RetrieveUserProfile is a wrapper that queries both the local database and
// application services for a given user's profile
// TODO: Remove this, it's called from federationapi and clientapi but is a pure function
@ -157,25 +159,11 @@ func RetrieveUserProfile(
ctx context.Context,
userID string,
asAPI AppServiceInternalAPI,
profileAPI userapi.ClientUserAPI,
profileAPI userapi.ProfileAPI,
) (*authtypes.Profile, error) {
localpart, _, err := gomatrixserverlib.SplitID('@', userID)
if err != nil {
return nil, err
}
// Try to query the user from the local database
res := &userapi.QueryProfileResponse{}
err = profileAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: userID}, res)
if err != nil {
return nil, err
}
profile := &authtypes.Profile{
Localpart: localpart,
DisplayName: res.DisplayName,
AvatarURL: res.AvatarURL,
}
if res.UserExists {
profile, err := profileAPI.QueryProfile(ctx, userID)
if err == nil {
return profile, nil
}
@ -188,19 +176,15 @@ func RetrieveUserProfile(
// If no user exists, return
if !userResp.UserIDExists {
return nil, errors.New("no known profile for given user ID")
return nil, ErrProfileNotExists
}
// Try to query the user from the local database again
err = profileAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: userID}, res)
profile, err = profileAPI.QueryProfile(ctx, userID)
if err != nil {
return nil, err
}
// profile should not be nil at this point
return &authtypes.Profile{
Localpart: localpart,
DisplayName: res.DisplayName,
AvatarURL: res.AvatarURL,
}, nil
return profile, nil
}

View file

@ -261,6 +261,25 @@ func TestAdminEvacuateRoom(t *testing.T) {
}
})
}
// Wait for the FS API to have consumed every message
js, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
timeout := time.After(time.Second)
for {
select {
case <-timeout:
t.Fatalf("FS API didn't process all events in time")
default:
}
info, err := js.ConsumerInfo(cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent), cfg.Global.JetStream.Durable("FederationAPIRoomServerConsumer")+"Pull")
if err != nil {
time.Sleep(time.Millisecond * 10)
continue
}
if info.NumPending == 0 && info.NumAckPending == 0 {
break
}
}
})
}

View file

@ -4,25 +4,30 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
"github.com/tidwall/gjson"
"github.com/matrix-org/dendrite/appservice"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/internal/caching"
"github.com/matrix-org/dendrite/internal/httputil"
"github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/roomserver"
"github.com/matrix-org/dendrite/setup/base"
"github.com/matrix-org/dendrite/setup/jetstream"
"github.com/matrix-org/dendrite/test"
"github.com/matrix-org/dendrite/test/testrig"
"github.com/matrix-org/dendrite/userapi"
uapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
"github.com/tidwall/gjson"
)
type userDevice struct {
@ -371,3 +376,227 @@ func createAccessTokens(t *testing.T, accessTokens map[*test.User]userDevice, us
}
}
}
func TestSetDisplayname(t *testing.T) {
alice := test.NewUser(t)
bob := test.NewUser(t)
notLocalUser := &test.User{ID: "@charlie:localhost", Localpart: "charlie"}
changeDisplayName := "my new display name"
testCases := []struct {
name string
user *test.User
wantOK bool
changeReq io.Reader
wantDisplayName string
}{
{
name: "invalid user",
user: &test.User{ID: "!notauser"},
},
{
name: "non-existent user",
user: &test.User{ID: "@doesnotexist:test"},
},
{
name: "non-local user is not allowed",
user: notLocalUser,
},
{
name: "existing user is allowed to change own name",
user: alice,
wantOK: true,
wantDisplayName: changeDisplayName,
},
{
name: "existing user is not allowed to change own name if name is empty",
user: bob,
wantOK: false,
wantDisplayName: "",
},
}
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
defer closeDB()
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
routers := httputil.NewRouters()
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
natsInstance := &jetstream.NATSInstance{}
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
rsAPI.SetFederationAPI(nil, nil)
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, natsInstance, rsAPI, nil)
asPI := appservice.NewInternalAPI(processCtx, cfg, natsInstance, userAPI, rsAPI)
AddPublicRoutes(processCtx, routers, cfg, natsInstance, base.CreateFederationClient(cfg, nil), rsAPI, asPI, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
accessTokens := map[*test.User]userDevice{
alice: {},
bob: {},
}
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
wantDisplayName := tc.user.Localpart
if tc.changeReq == nil {
tc.changeReq = strings.NewReader("")
}
// check profile after initial account creation
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/profile/"+tc.user.ID, strings.NewReader(""))
t.Logf("%s", req.URL.String())
routers.Client.ServeHTTP(rec, req)
if tc.wantOK && rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d", rec.Code)
}
if gotDisplayName := gjson.GetBytes(rec.Body.Bytes(), "displayname").Str; tc.wantOK && gotDisplayName != wantDisplayName {
t.Fatalf("expected displayname to be '%s', but got '%s'", wantDisplayName, gotDisplayName)
}
// now set the new display name
wantDisplayName = tc.wantDisplayName
tc.changeReq = strings.NewReader(fmt.Sprintf(`{"displayname":"%s"}`, tc.wantDisplayName))
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPut, "/_matrix/client/v3/profile/"+tc.user.ID+"/displayname", tc.changeReq)
req.Header.Set("Authorization", "Bearer "+accessTokens[tc.user].accessToken)
routers.Client.ServeHTTP(rec, req)
if tc.wantOK && rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
}
// now only get the display name
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/profile/"+tc.user.ID+"/displayname", strings.NewReader(""))
routers.Client.ServeHTTP(rec, req)
if tc.wantOK && rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
}
if gotDisplayName := gjson.GetBytes(rec.Body.Bytes(), "displayname").Str; tc.wantOK && gotDisplayName != wantDisplayName {
t.Fatalf("expected displayname to be '%s', but got '%s'", wantDisplayName, gotDisplayName)
}
})
}
})
}
func TestSetAvatarURL(t *testing.T) {
alice := test.NewUser(t)
bob := test.NewUser(t)
notLocalUser := &test.User{ID: "@charlie:localhost", Localpart: "charlie"}
changeDisplayName := "mxc://newMXID"
testCases := []struct {
name string
user *test.User
wantOK bool
changeReq io.Reader
avatar_url string
}{
{
name: "invalid user",
user: &test.User{ID: "!notauser"},
},
{
name: "non-existent user",
user: &test.User{ID: "@doesnotexist:test"},
},
{
name: "non-local user is not allowed",
user: notLocalUser,
},
{
name: "existing user is allowed to change own avatar",
user: alice,
wantOK: true,
avatar_url: changeDisplayName,
},
{
name: "existing user is not allowed to change own avatar if avatar is empty",
user: bob,
wantOK: false,
avatar_url: "",
},
}
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
defer closeDB()
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
routers := httputil.NewRouters()
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
natsInstance := &jetstream.NATSInstance{}
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
rsAPI.SetFederationAPI(nil, nil)
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, natsInstance, rsAPI, nil)
asPI := appservice.NewInternalAPI(processCtx, cfg, natsInstance, userAPI, rsAPI)
AddPublicRoutes(processCtx, routers, cfg, natsInstance, base.CreateFederationClient(cfg, nil), rsAPI, asPI, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
accessTokens := map[*test.User]userDevice{
alice: {},
bob: {},
}
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
wantAvatarURL := ""
if tc.changeReq == nil {
tc.changeReq = strings.NewReader("")
}
// check profile after initial account creation
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/profile/"+tc.user.ID, strings.NewReader(""))
t.Logf("%s", req.URL.String())
routers.Client.ServeHTTP(rec, req)
if tc.wantOK && rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d", rec.Code)
}
if gotDisplayName := gjson.GetBytes(rec.Body.Bytes(), "avatar_url").Str; tc.wantOK && gotDisplayName != wantAvatarURL {
t.Fatalf("expected displayname to be '%s', but got '%s'", wantAvatarURL, gotDisplayName)
}
// now set the new display name
wantAvatarURL = tc.avatar_url
tc.changeReq = strings.NewReader(fmt.Sprintf(`{"avatar_url":"%s"}`, tc.avatar_url))
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPut, "/_matrix/client/v3/profile/"+tc.user.ID+"/avatar_url", tc.changeReq)
req.Header.Set("Authorization", "Bearer "+accessTokens[tc.user].accessToken)
routers.Client.ServeHTTP(rec, req)
if tc.wantOK && rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
}
// now only get the display name
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/profile/"+tc.user.ID+"/avatar_url", strings.NewReader(""))
routers.Client.ServeHTTP(rec, req)
if tc.wantOK && rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
}
if gotDisplayName := gjson.GetBytes(rec.Body.Bytes(), "avatar_url").Str; tc.wantOK && gotDisplayName != wantAvatarURL {
t.Fatalf("expected displayname to be '%s', but got '%s'", wantAvatarURL, gotDisplayName)
}
})
}
})
}

View file

@ -18,6 +18,7 @@ import (
"net/http"
"time"
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
@ -61,21 +62,19 @@ func JoinRoomByIDOrAlias(
// Work out our localpart for the client profile request.
// Request our profile content to populate the request content with.
res := &api.QueryProfileResponse{}
err := profileAPI.QueryProfile(req.Context(), &api.QueryProfileRequest{UserID: device.UserID}, res)
if err != nil || !res.UserExists {
if !res.UserExists {
util.GetLogger(req.Context()).Error("Unable to query user profile, no profile found.")
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.Unknown("Unable to query user profile, no profile found."),
}
}
profile, err := profileAPI.QueryProfile(req.Context(), device.UserID)
util.GetLogger(req.Context()).WithError(err).Error("UserProfileAPI.QueryProfile failed")
} else {
joinReq.Content["displayname"] = res.DisplayName
joinReq.Content["avatar_url"] = res.AvatarURL
switch err {
case nil:
joinReq.Content["displayname"] = profile.DisplayName
joinReq.Content["avatar_url"] = profile.AvatarURL
case appserviceAPI.ErrProfileNotExists:
util.GetLogger(req.Context()).Error("Unable to query user profile, no profile found.")
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.Unknown("Unable to query user profile, no profile found."),
}
default:
}
// Ask the roomserver to perform the join.

View file

@ -36,14 +36,14 @@ import (
// GetProfile implements GET /profile/{userID}
func GetProfile(
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
userID string,
asAPI appserviceAPI.AppServiceInternalAPI,
federation *gomatrixserverlib.FederationClient,
) util.JSONResponse {
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
if err != nil {
if err == eventutil.ErrProfileNoExists {
if err == appserviceAPI.ErrProfileNotExists {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
@ -56,7 +56,7 @@ func GetProfile(
return util.JSONResponse{
Code: http.StatusOK,
JSON: eventutil.ProfileResponse{
JSON: eventutil.UserProfile{
AvatarURL: profile.AvatarURL,
DisplayName: profile.DisplayName,
},
@ -65,34 +65,28 @@ func GetProfile(
// GetAvatarURL implements GET /profile/{userID}/avatar_url
func GetAvatarURL(
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
federation *gomatrixserverlib.FederationClient,
) util.JSONResponse {
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
if err != nil {
if err == eventutil.ErrProfileNoExists {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
}
}
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
return jsonerror.InternalServerError()
profile := GetProfile(req, profileAPI, cfg, userID, asAPI, federation)
p, ok := profile.JSON.(eventutil.UserProfile)
// not a profile response, so most likely an error, return that
if !ok {
return profile
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: eventutil.AvatarURL{
AvatarURL: profile.AvatarURL,
JSON: eventutil.UserProfile{
AvatarURL: p.AvatarURL,
},
}
}
// SetAvatarURL implements PUT /profile/{userID}/avatar_url
func SetAvatarURL(
req *http.Request, profileAPI userapi.ClientUserAPI,
req *http.Request, profileAPI userapi.ProfileAPI,
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
) util.JSONResponse {
if userID != device.UserID {
@ -102,7 +96,7 @@ func SetAvatarURL(
}
}
var r eventutil.AvatarURL
var r eventutil.UserProfile
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
return *resErr
}
@ -134,24 +128,20 @@ func SetAvatarURL(
}
}
setRes := &userapi.PerformSetAvatarURLResponse{}
if err = profileAPI.SetAvatarURL(req.Context(), &userapi.PerformSetAvatarURLRequest{
Localpart: localpart,
ServerName: domain,
AvatarURL: r.AvatarURL,
}, setRes); err != nil {
profile, changed, err := profileAPI.SetAvatarURL(req.Context(), localpart, domain, r.AvatarURL)
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetAvatarURL failed")
return jsonerror.InternalServerError()
}
// No need to build new membership events, since nothing changed
if !setRes.Changed {
if !changed {
return util.JSONResponse{
Code: http.StatusOK,
JSON: struct{}{},
}
}
response, err := updateProfile(req.Context(), rsAPI, device, setRes.Profile, userID, cfg, evTime)
response, err := updateProfile(req.Context(), rsAPI, device, profile, userID, cfg, evTime)
if err != nil {
return response
}
@ -164,34 +154,28 @@ func SetAvatarURL(
// GetDisplayName implements GET /profile/{userID}/displayname
func GetDisplayName(
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
federation *gomatrixserverlib.FederationClient,
) util.JSONResponse {
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
if err != nil {
if err == eventutil.ErrProfileNoExists {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
}
}
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
return jsonerror.InternalServerError()
profile := GetProfile(req, profileAPI, cfg, userID, asAPI, federation)
p, ok := profile.JSON.(eventutil.UserProfile)
// not a profile response, so most likely an error, return that
if !ok {
return profile
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: eventutil.DisplayName{
DisplayName: profile.DisplayName,
JSON: eventutil.UserProfile{
DisplayName: p.DisplayName,
},
}
}
// SetDisplayName implements PUT /profile/{userID}/displayname
func SetDisplayName(
req *http.Request, profileAPI userapi.ClientUserAPI,
req *http.Request, profileAPI userapi.ProfileAPI,
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
) util.JSONResponse {
if userID != device.UserID {
@ -201,7 +185,7 @@ func SetDisplayName(
}
}
var r eventutil.DisplayName
var r eventutil.UserProfile
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
return *resErr
}
@ -233,25 +217,20 @@ func SetDisplayName(
}
}
profileRes := &userapi.PerformUpdateDisplayNameResponse{}
err = profileAPI.SetDisplayName(req.Context(), &userapi.PerformUpdateDisplayNameRequest{
Localpart: localpart,
ServerName: domain,
DisplayName: r.DisplayName,
}, profileRes)
profile, changed, err := profileAPI.SetDisplayName(req.Context(), localpart, domain, r.DisplayName)
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetDisplayName failed")
return jsonerror.InternalServerError()
}
// No need to build new membership events, since nothing changed
if !profileRes.Changed {
if !changed {
return util.JSONResponse{
Code: http.StatusOK,
JSON: struct{}{},
}
}
response, err := updateProfile(req.Context(), rsAPI, device, profileRes.Profile, userID, cfg, evTime)
response, err := updateProfile(req.Context(), rsAPI, device, profile, userID, cfg, evTime)
if err != nil {
return response
}
@ -308,9 +287,9 @@ func updateProfile(
// getProfile gets the full profile of a user by querying the database or a
// remote homeserver.
// Returns an error when something goes wrong or specifically
// eventutil.ErrProfileNoExists when the profile doesn't exist.
// eventutil.ErrProfileNotExists when the profile doesn't exist.
func getProfile(
ctx context.Context, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
ctx context.Context, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
userID string,
asAPI appserviceAPI.AppServiceInternalAPI,
federation *gomatrixserverlib.FederationClient,
@ -325,7 +304,7 @@ func getProfile(
if fedErr != nil {
if x, ok := fedErr.(gomatrix.HTTPError); ok {
if x.Code == http.StatusNotFound {
return nil, eventutil.ErrProfileNoExists
return nil, appserviceAPI.ErrProfileNotExists
}
}

View file

@ -888,13 +888,7 @@ func completeRegistration(
}
if displayName != "" {
nameReq := userapi.PerformUpdateDisplayNameRequest{
Localpart: username,
ServerName: serverName,
DisplayName: displayName,
}
var nameRes userapi.PerformUpdateDisplayNameResponse
err = userAPI.SetDisplayName(ctx, &nameReq, &nameRes)
_, _, err = userAPI.SetDisplayName(ctx, username, serverName, displayName)
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,

View file

@ -611,11 +611,9 @@ func TestRegisterUserWithDisplayName(t *testing.T) {
assert.Equal(t, http.StatusOK, response.Code)
req := api.QueryProfileRequest{UserID: "@user:server"}
var res api.QueryProfileResponse
err := userAPI.QueryProfile(processCtx.Context(), &req, &res)
profile, err := userAPI.QueryProfile(processCtx.Context(), "@user:server")
assert.NoError(t, err)
assert.Equal(t, expectedDisplayName, res.DisplayName)
assert.Equal(t, expectedDisplayName, profile.DisplayName)
})
}
@ -662,10 +660,8 @@ func TestRegisterAdminUsingSharedSecret(t *testing.T) {
)
assert.Equal(t, http.StatusOK, response.Code)
profilReq := api.QueryProfileRequest{UserID: "@alice:server"}
var profileRes api.QueryProfileResponse
err = userAPI.QueryProfile(processCtx.Context(), &profilReq, &profileRes)
profile, err := userAPI.QueryProfile(processCtx.Context(), "@alice:server")
assert.NoError(t, err)
assert.Equal(t, expectedDisplayName, profileRes.DisplayName)
assert.Equal(t, expectedDisplayName, profile.DisplayName)
})
}

View file

@ -295,30 +295,28 @@ func getSenderDevice(
}
// Set the avatarurl for the user
avatarRes := &userapi.PerformSetAvatarURLResponse{}
if err = userAPI.SetAvatarURL(ctx, &userapi.PerformSetAvatarURLRequest{
Localpart: cfg.Matrix.ServerNotices.LocalPart,
ServerName: cfg.Matrix.ServerName,
AvatarURL: cfg.Matrix.ServerNotices.AvatarURL,
}, avatarRes); err != nil {
profile, avatarChanged, err := userAPI.SetAvatarURL(ctx,
cfg.Matrix.ServerNotices.LocalPart,
cfg.Matrix.ServerName,
cfg.Matrix.ServerNotices.AvatarURL,
)
if err != nil {
util.GetLogger(ctx).WithError(err).Error("userAPI.SetAvatarURL failed")
return nil, err
}
profile := avatarRes.Profile
// Set the displayname for the user
displayNameRes := &userapi.PerformUpdateDisplayNameResponse{}
if err = userAPI.SetDisplayName(ctx, &userapi.PerformUpdateDisplayNameRequest{
Localpart: cfg.Matrix.ServerNotices.LocalPart,
ServerName: cfg.Matrix.ServerName,
DisplayName: cfg.Matrix.ServerNotices.DisplayName,
}, displayNameRes); err != nil {
_, displayNameChanged, err := userAPI.SetDisplayName(ctx,
cfg.Matrix.ServerNotices.LocalPart,
cfg.Matrix.ServerName,
cfg.Matrix.ServerNotices.DisplayName,
)
if err != nil {
util.GetLogger(ctx).WithError(err).Error("userAPI.SetDisplayName failed")
return nil, err
}
if displayNameRes.Changed {
if displayNameChanged {
profile.DisplayName = cfg.Matrix.ServerNotices.DisplayName
}
@ -334,7 +332,7 @@ func getSenderDevice(
// We've got an existing account, return the first device of it
if len(deviceRes.Devices) > 0 {
// If there were changes to the profile, create a new membership event
if displayNameRes.Changed || avatarRes.Changed {
if displayNameChanged || avatarChanged {
_, err = updateProfile(ctx, rsAPI, &deviceRes.Devices[0], profile, accRes.Account.UserID, cfg, time.Now())
if err != nil {
return nil, err

View file

@ -209,24 +209,17 @@ func queryIDServerStoreInvite(
body *MembershipRequest, roomID string,
) (*idServerStoreInviteResponse, error) {
// Retrieve the sender's profile to get their display name
localpart, serverName, err := gomatrixserverlib.SplitID('@', device.UserID)
_, serverName, err := gomatrixserverlib.SplitID('@', device.UserID)
if err != nil {
return nil, err
}
var profile *authtypes.Profile
if cfg.Matrix.IsLocalServerName(serverName) {
res := &userapi.QueryProfileResponse{}
err = userAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: device.UserID}, res)
profile, err = userAPI.QueryProfile(ctx, device.UserID)
if err != nil {
return nil, err
}
profile = &authtypes.Profile{
Localpart: localpart,
DisplayName: res.DisplayName,
AvatarURL: res.AvatarURL,
}
} else {
profile = &authtypes.Profile{}
}

View file

@ -50,10 +50,7 @@ func GetProfile(
}
}
var profileRes userapi.QueryProfileResponse
err = userAPI.QueryProfile(httpReq.Context(), &userapi.QueryProfileRequest{
UserID: userID,
}, &profileRes)
profile, err := userAPI.QueryProfile(httpReq.Context(), userID)
if err != nil {
util.GetLogger(httpReq.Context()).WithError(err).Error("userAPI.QueryProfile failed")
return jsonerror.InternalServerError()
@ -65,21 +62,21 @@ func GetProfile(
if field != "" {
switch field {
case "displayname":
res = eventutil.DisplayName{
DisplayName: profileRes.DisplayName,
res = eventutil.UserProfile{
DisplayName: profile.DisplayName,
}
case "avatar_url":
res = eventutil.AvatarURL{
AvatarURL: profileRes.AvatarURL,
res = eventutil.UserProfile{
AvatarURL: profile.AvatarURL,
}
default:
code = http.StatusBadRequest
res = jsonerror.InvalidArgumentValue("The request body did not contain an allowed value of argument 'field'. Allowed values are either: 'avatar_url', 'displayname'.")
}
} else {
res = eventutil.ProfileResponse{
AvatarURL: profileRes.AvatarURL,
DisplayName: profileRes.DisplayName,
res = eventutil.UserProfile{
AvatarURL: profile.AvatarURL,
DisplayName: profile.DisplayName,
}
}

View file

@ -23,6 +23,7 @@ import (
"testing"
"github.com/gorilla/mux"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
fedAPI "github.com/matrix-org/dendrite/federationapi"
fedInternal "github.com/matrix-org/dendrite/federationapi/internal"
@ -43,8 +44,8 @@ type fakeUserAPI struct {
userAPI.FederationUserAPI
}
func (u *fakeUserAPI) QueryProfile(ctx context.Context, req *userAPI.QueryProfileRequest, res *userAPI.QueryProfileResponse) error {
return nil
func (u *fakeUserAPI) QueryProfile(ctx context.Context, userID string) (*authtypes.Profile, error) {
return &authtypes.Profile{}, nil
}
func TestHandleQueryProfile(t *testing.T) {

View file

@ -256,17 +256,14 @@ func createInviteFrom3PIDInvite(
StateKey: &inv.MXID,
}
var res userapi.QueryProfileResponse
err = userAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{
UserID: inv.MXID,
}, &res)
profile, err := userAPI.QueryProfile(ctx, inv.MXID)
if err != nil {
return nil, err
}
content := gomatrixserverlib.MemberContent{
AvatarURL: res.AvatarURL,
DisplayName: res.DisplayName,
AvatarURL: profile.AvatarURL,
DisplayName: profile.DisplayName,
Membership: gomatrixserverlib.Invite,
ThirdPartyInvite: &gomatrixserverlib.MemberThirdPartyInvite{
Signed: inv.Signed,

View file

@ -15,16 +15,11 @@
package eventutil
import (
"errors"
"strconv"
"github.com/matrix-org/dendrite/syncapi/types"
)
// ErrProfileNoExists is returned when trying to lookup a user's profile that
// doesn't exist locally.
var ErrProfileNoExists = errors.New("no known profile for given user ID")
// AccountData represents account data sent from the client API server to the
// sync API server
type AccountData struct {
@ -56,20 +51,10 @@ type NotificationData struct {
UnreadNotificationCount int `json:"unread_notification_count"`
}
// ProfileResponse is a struct containing all known user profile data
type ProfileResponse struct {
AvatarURL string `json:"avatar_url"`
DisplayName string `json:"displayname"`
}
// AvatarURL is a struct containing only the URL to a user's avatar
type AvatarURL struct {
AvatarURL string `json:"avatar_url"`
}
// DisplayName is a struct containing only a user's display name
type DisplayName struct {
DisplayName string `json:"displayname"`
// UserProfile is a struct containing all known user profile data
type UserProfile struct {
AvatarURL string `json:"avatar_url,omitempty"`
DisplayName string `json:"displayname,omitempty"`
}
// WeakBoolean is a type that will Unmarshal to true or false even if the encoded

View file

@ -17,6 +17,7 @@ package test
import (
"crypto/ed25519"
"fmt"
"strconv"
"sync/atomic"
"testing"
@ -47,6 +48,7 @@ var (
type User struct {
ID string
Localpart string
AccountType api.AccountType
// key ID and private key of the server who has this user, if known.
keyID gomatrixserverlib.KeyID
@ -81,6 +83,7 @@ func NewUser(t *testing.T, opts ...UserOpt) *User {
WithSigningServer(serverName, keyID, privateKey)(&u)
}
u.ID = fmt.Sprintf("@%d:%s", counter, u.srvName)
u.Localpart = strconv.Itoa(int(counter))
t.Logf("NewUser: created user %s", u.ID)
return &u
}

View file

@ -58,7 +58,7 @@ type MediaUserAPI interface {
type FederationUserAPI interface {
UploadDeviceKeysAPI
QueryOpenIDToken(ctx context.Context, req *QueryOpenIDTokenRequest, res *QueryOpenIDTokenResponse) error
QueryProfile(ctx context.Context, req *QueryProfileRequest, res *QueryProfileResponse) error
QueryProfile(ctx context.Context, userID string) (*authtypes.Profile, error)
QueryDevices(ctx context.Context, req *QueryDevicesRequest, res *QueryDevicesResponse) error
QueryKeys(ctx context.Context, req *QueryKeysRequest, res *QueryKeysResponse) error
QuerySignatures(ctx context.Context, req *QuerySignaturesRequest, res *QuerySignaturesResponse) error
@ -83,9 +83,9 @@ type ClientUserAPI interface {
LoginTokenInternalAPI
UserLoginAPI
ClientKeyAPI
ProfileAPI
QueryNumericLocalpart(ctx context.Context, req *QueryNumericLocalpartRequest, res *QueryNumericLocalpartResponse) error
QueryDevices(ctx context.Context, req *QueryDevicesRequest, res *QueryDevicesResponse) error
QueryProfile(ctx context.Context, req *QueryProfileRequest, res *QueryProfileResponse) error
QueryAccountData(ctx context.Context, req *QueryAccountDataRequest, res *QueryAccountDataResponse) error
QueryPushers(ctx context.Context, req *QueryPushersRequest, res *QueryPushersResponse) error
QueryPushRules(ctx context.Context, req *QueryPushRulesRequest, res *QueryPushRulesResponse) error
@ -100,8 +100,6 @@ type ClientUserAPI interface {
PerformPushRulesPut(ctx context.Context, req *PerformPushRulesPutRequest, res *struct{}) error
PerformAccountDeactivation(ctx context.Context, req *PerformAccountDeactivationRequest, res *PerformAccountDeactivationResponse) error
PerformOpenIDTokenCreation(ctx context.Context, req *PerformOpenIDTokenCreationRequest, res *PerformOpenIDTokenCreationResponse) error
SetAvatarURL(ctx context.Context, req *PerformSetAvatarURLRequest, res *PerformSetAvatarURLResponse) error
SetDisplayName(ctx context.Context, req *PerformUpdateDisplayNameRequest, res *PerformUpdateDisplayNameResponse) error
QueryNotifications(ctx context.Context, req *QueryNotificationsRequest, res *QueryNotificationsResponse) error
InputAccountData(ctx context.Context, req *InputAccountDataRequest, res *InputAccountDataResponse) error
PerformKeyBackup(ctx context.Context, req *PerformKeyBackupRequest, res *PerformKeyBackupResponse) error
@ -113,6 +111,12 @@ type ClientUserAPI interface {
PerformSaveThreePIDAssociation(ctx context.Context, req *PerformSaveThreePIDAssociationRequest, res *struct{}) error
}
type ProfileAPI interface {
QueryProfile(ctx context.Context, userID string) (*authtypes.Profile, error)
SetAvatarURL(ctx context.Context, localpart string, serverName gomatrixserverlib.ServerName, avatarURL string) (*authtypes.Profile, bool, error)
SetDisplayName(ctx context.Context, localpart string, serverName gomatrixserverlib.ServerName, displayName string) (*authtypes.Profile, bool, error)
}
// custom api functions required by pinecone / p2p demos
type QuerySearchProfilesAPI interface {
QuerySearchProfiles(ctx context.Context, req *QuerySearchProfilesRequest, res *QuerySearchProfilesResponse) error
@ -290,22 +294,6 @@ type QueryDevicesResponse struct {
Devices []Device
}
// QueryProfileRequest is the request for QueryProfile
type QueryProfileRequest struct {
// The user ID to query
UserID string
}
// QueryProfileResponse is the response for QueryProfile
type QueryProfileResponse struct {
// True if the user exists. Querying for a profile does not create them.
UserExists bool
// The current display name if set.
DisplayName string
// The current avatar URL if set.
AvatarURL string
}
// QuerySearchProfilesRequest is the request for QueryProfile
type QuerySearchProfilesRequest struct {
// The search string to match
@ -600,16 +588,6 @@ type Notification struct {
TS gomatrixserverlib.Timestamp `json:"ts"` // Required.
}
type PerformSetAvatarURLRequest struct {
Localpart string
ServerName gomatrixserverlib.ServerName
AvatarURL string
}
type PerformSetAvatarURLResponse struct {
Profile *authtypes.Profile `json:"profile"`
Changed bool `json:"changed"`
}
type QueryNumericLocalpartRequest struct {
ServerName gomatrixserverlib.ServerName
}
@ -638,17 +616,6 @@ type QueryAccountByPasswordResponse struct {
Exists bool
}
type PerformUpdateDisplayNameRequest struct {
Localpart string
ServerName gomatrixserverlib.ServerName
DisplayName string
}
type PerformUpdateDisplayNameResponse struct {
Profile *authtypes.Profile `json:"profile"`
Changed bool `json:"changed"`
}
type QueryLocalpartForThreePIDRequest struct {
ThreePID, Medium string
}

View file

@ -23,6 +23,8 @@ import (
"strconv"
"time"
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
fedsenderapi "github.com/matrix-org/dendrite/federationapi/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
@ -418,25 +420,26 @@ func (a *UserInternalAPI) PerformDeviceUpdate(ctx context.Context, req *api.Perf
return nil
}
func (a *UserInternalAPI) QueryProfile(ctx context.Context, req *api.QueryProfileRequest, res *api.QueryProfileResponse) error {
local, domain, err := gomatrixserverlib.SplitID('@', req.UserID)
var (
ErrIsRemoteServer = errors.New("cannot query profile of remote users")
)
func (a *UserInternalAPI) QueryProfile(ctx context.Context, userID string) (*authtypes.Profile, error) {
local, domain, err := gomatrixserverlib.SplitID('@', userID)
if err != nil {
return err
return nil, err
}
if !a.Config.Matrix.IsLocalServerName(domain) {
return fmt.Errorf("cannot query profile of remote users (server name %s)", domain)
return nil, ErrIsRemoteServer
}
prof, err := a.DB.GetProfileByLocalpart(ctx, local, domain)
if err != nil {
if err == sql.ErrNoRows {
return nil
return nil, appserviceAPI.ErrProfileNotExists
}
return err
return nil, err
}
res.UserExists = true
res.AvatarURL = prof.AvatarURL
res.DisplayName = prof.DisplayName
return nil
return prof, nil
}
func (a *UserInternalAPI) QuerySearchProfiles(ctx context.Context, req *api.QuerySearchProfilesRequest, res *api.QuerySearchProfilesResponse) error {
@ -901,11 +904,8 @@ func (a *UserInternalAPI) QueryPushRules(ctx context.Context, req *api.QueryPush
return nil
}
func (a *UserInternalAPI) SetAvatarURL(ctx context.Context, req *api.PerformSetAvatarURLRequest, res *api.PerformSetAvatarURLResponse) error {
profile, changed, err := a.DB.SetAvatarURL(ctx, req.Localpart, req.ServerName, req.AvatarURL)
res.Profile = profile
res.Changed = changed
return err
func (a *UserInternalAPI) SetAvatarURL(ctx context.Context, localpart string, serverName gomatrixserverlib.ServerName, avatarURL string) (*authtypes.Profile, bool, error) {
return a.DB.SetAvatarURL(ctx, localpart, serverName, avatarURL)
}
func (a *UserInternalAPI) QueryNumericLocalpart(ctx context.Context, req *api.QueryNumericLocalpartRequest, res *api.QueryNumericLocalpartResponse) error {
@ -939,11 +939,8 @@ func (a *UserInternalAPI) QueryAccountByPassword(ctx context.Context, req *api.Q
}
}
func (a *UserInternalAPI) SetDisplayName(ctx context.Context, req *api.PerformUpdateDisplayNameRequest, res *api.PerformUpdateDisplayNameResponse) error {
profile, changed, err := a.DB.SetDisplayName(ctx, req.Localpart, req.ServerName, req.DisplayName)
res.Profile = profile
res.Changed = changed
return err
func (a *UserInternalAPI) SetDisplayName(ctx context.Context, localpart string, serverName gomatrixserverlib.ServerName, displayName string) (*authtypes.Profile, bool, error) {
return a.DB.SetDisplayName(ctx, localpart, serverName, displayName)
}
func (a *UserInternalAPI) QueryLocalpartForThreePID(ctx context.Context, req *api.QueryLocalpartForThreePIDRequest, res *api.QueryLocalpartForThreePIDResponse) error {

View file

@ -22,6 +22,8 @@ import (
"testing"
"time"
api2 "github.com/matrix-org/dendrite/appservice/api"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/userapi/producers"
"github.com/matrix-org/gomatrixserverlib"
@ -111,33 +113,26 @@ func TestQueryProfile(t *testing.T) {
aliceDisplayName := "Alice"
testCases := []struct {
req api.QueryProfileRequest
wantRes api.QueryProfileResponse
userID string
wantRes *authtypes.Profile
wantErr error
}{
{
req: api.QueryProfileRequest{
UserID: fmt.Sprintf("@alice:%s", serverName),
},
wantRes: api.QueryProfileResponse{
UserExists: true,
AvatarURL: aliceAvatarURL,
userID: fmt.Sprintf("@alice:%s", serverName),
wantRes: &authtypes.Profile{
Localpart: "alice",
DisplayName: aliceDisplayName,
AvatarURL: aliceAvatarURL,
ServerName: string(serverName),
},
},
{
req: api.QueryProfileRequest{
UserID: fmt.Sprintf("@bob:%s", serverName),
},
wantRes: api.QueryProfileResponse{
UserExists: false,
},
userID: fmt.Sprintf("@bob:%s", serverName),
wantErr: api2.ErrProfileNotExists,
},
{
req: api.QueryProfileRequest{
UserID: "@alice:wrongdomain.com",
},
wantErr: fmt.Errorf("wrong domain"),
userID: "@alice:wrongdomain.com",
wantErr: api2.ErrProfileNotExists,
},
}
@ -147,14 +142,14 @@ func TestQueryProfile(t *testing.T) {
mode = "HTTP"
}
for _, tc := range testCases {
var gotRes api.QueryProfileResponse
gotErr := testAPI.QueryProfile(context.TODO(), &tc.req, &gotRes)
profile, gotErr := testAPI.QueryProfile(context.TODO(), tc.userID)
if tc.wantErr == nil && gotErr != nil || tc.wantErr != nil && gotErr == nil {
t.Errorf("QueryProfile %s error, got %s want %s", mode, gotErr, tc.wantErr)
continue
}
if !reflect.DeepEqual(tc.wantRes, gotRes) {
t.Errorf("QueryProfile %s response got %+v want %+v", mode, gotRes, tc.wantRes)
if !reflect.DeepEqual(tc.wantRes, profile) {
t.Errorf("QueryProfile %s response got %+v want %+v", mode, profile, tc.wantRes)
}
}
}