Merge branch 'master' into single-event-retrieval-671

Signed-off-by: Alex Chen <minecnly@gmail.com>
This commit is contained in:
Cnly 2019-08-07 01:55:12 +08:00
commit 111828222f
9 changed files with 148 additions and 38 deletions

View file

@ -20,13 +20,13 @@ package api
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"net/http" "net/http"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/dendrite/common"
commonHTTP "github.com/matrix-org/dendrite/common/http" commonHTTP "github.com/matrix-org/dendrite/common/http"
opentracing "github.com/opentracing/opentracing-go" opentracing "github.com/opentracing/opentracing-go"
) )
@ -164,7 +164,7 @@ func RetrieveUserProfile(
// If no user exists, return // If no user exists, return
if !userResp.UserIDExists { if !userResp.UserIDExists {
return nil, errors.New("no known profile for given user ID") return nil, common.ErrProfileNoExists
} }
// Try to query the user from the local database again // Try to query the user from the local database again

View file

@ -14,7 +14,7 @@
package authtypes package authtypes
// Profile represents the profile for a Matrix account on this home server. // Profile represents the profile for a Matrix account.
type Profile struct { type Profile struct {
Localpart string Localpart string
DisplayName string DisplayName string

View file

@ -30,43 +30,61 @@ import (
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrix"
"github.com/matrix-org/util" "github.com/matrix-org/util"
) )
// GetProfile implements GET /profile/{userID} // GetProfile implements GET /profile/{userID}
func GetProfile( func GetProfile(
req *http.Request, accountDB *accounts.Database, userID string, asAPI appserviceAPI.AppServiceQueryAPI, req *http.Request, accountDB *accounts.Database, cfg *config.Dendrite,
userID string,
asAPI appserviceAPI.AppServiceQueryAPI,
federation *gomatrixserverlib.FederationClient,
) util.JSONResponse { ) util.JSONResponse {
profile, err := appserviceAPI.RetrieveUserProfile(req.Context(), userID, asAPI, accountDB) profile, err := getProfile(req.Context(), accountDB, cfg, userID, asAPI, federation)
if err != nil { if err != nil {
if err == common.ErrProfileNoExists {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
}
}
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} }
res := common.ProfileResponse{
AvatarURL: profile.AvatarURL,
DisplayName: profile.DisplayName,
}
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusOK, Code: http.StatusOK,
JSON: res, JSON: common.ProfileResponse{
AvatarURL: profile.AvatarURL,
DisplayName: profile.DisplayName,
},
} }
} }
// GetAvatarURL implements GET /profile/{userID}/avatar_url // GetAvatarURL implements GET /profile/{userID}/avatar_url
func GetAvatarURL( func GetAvatarURL(
req *http.Request, accountDB *accounts.Database, userID string, asAPI appserviceAPI.AppServiceQueryAPI, req *http.Request, accountDB *accounts.Database, cfg *config.Dendrite,
userID string, asAPI appserviceAPI.AppServiceQueryAPI,
federation *gomatrixserverlib.FederationClient,
) util.JSONResponse { ) util.JSONResponse {
profile, err := appserviceAPI.RetrieveUserProfile(req.Context(), userID, asAPI, accountDB) profile, err := getProfile(req.Context(), accountDB, cfg, userID, asAPI, federation)
if err != nil { if err != nil {
if err == common.ErrProfileNoExists {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
}
}
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} }
res := common.AvatarURL{
AvatarURL: profile.AvatarURL,
}
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusOK, Code: http.StatusOK,
JSON: res, JSON: common.AvatarURL{
AvatarURL: profile.AvatarURL,
},
} }
} }
@ -152,18 +170,27 @@ func SetAvatarURL(
// GetDisplayName implements GET /profile/{userID}/displayname // GetDisplayName implements GET /profile/{userID}/displayname
func GetDisplayName( func GetDisplayName(
req *http.Request, accountDB *accounts.Database, userID string, asAPI appserviceAPI.AppServiceQueryAPI, req *http.Request, accountDB *accounts.Database, cfg *config.Dendrite,
userID string, asAPI appserviceAPI.AppServiceQueryAPI,
federation *gomatrixserverlib.FederationClient,
) util.JSONResponse { ) util.JSONResponse {
profile, err := appserviceAPI.RetrieveUserProfile(req.Context(), userID, asAPI, accountDB) profile, err := getProfile(req.Context(), accountDB, cfg, userID, asAPI, federation)
if err != nil { if err != nil {
if err == common.ErrProfileNoExists {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
}
}
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} }
res := common.DisplayName{
DisplayName: profile.DisplayName,
}
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusOK, Code: http.StatusOK,
JSON: res, JSON: common.DisplayName{
DisplayName: profile.DisplayName,
},
} }
} }
@ -247,6 +274,48 @@ func SetDisplayName(
} }
} }
// 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
// common.ErrProfileNoExists when the profile doesn't exist.
func getProfile(
ctx context.Context, accountDB *accounts.Database, cfg *config.Dendrite,
userID string,
asAPI appserviceAPI.AppServiceQueryAPI,
federation *gomatrixserverlib.FederationClient,
) (*authtypes.Profile, error) {
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
if err != nil {
return nil, err
}
if domain != cfg.Matrix.ServerName {
profile, fedErr := federation.LookupProfile(ctx, domain, userID, "")
if fedErr != nil {
if x, ok := fedErr.(gomatrix.HTTPError); ok {
if x.Code == http.StatusNotFound {
return nil, common.ErrProfileNoExists
}
}
return nil, fedErr
}
return &authtypes.Profile{
Localpart: localpart,
DisplayName: profile.DisplayName,
AvatarURL: profile.AvatarURL,
}, nil
}
profile, err := appserviceAPI.RetrieveUserProfile(ctx, userID, asAPI, accountDB)
if err != nil {
return nil, err
}
return profile, nil
}
func buildMembershipEvents( func buildMembershipEvents(
ctx context.Context, ctx context.Context,
memberships []authtypes.Membership, memberships []authtypes.Membership,

View file

@ -292,7 +292,7 @@ func Setup(
if err != nil { if err != nil {
return util.ErrorResponse(err) return util.ErrorResponse(err)
} }
return GetProfile(req, accountDB, vars["userID"], asAPI) return GetProfile(req, accountDB, &cfg, vars["userID"], asAPI, federation)
}), }),
).Methods(http.MethodGet, http.MethodOptions) ).Methods(http.MethodGet, http.MethodOptions)
@ -302,7 +302,7 @@ func Setup(
if err != nil { if err != nil {
return util.ErrorResponse(err) return util.ErrorResponse(err)
} }
return GetAvatarURL(req, accountDB, vars["userID"], asAPI) return GetAvatarURL(req, accountDB, &cfg, vars["userID"], asAPI, federation)
}), }),
).Methods(http.MethodGet, http.MethodOptions) ).Methods(http.MethodGet, http.MethodOptions)
@ -324,7 +324,7 @@ func Setup(
if err != nil { if err != nil {
return util.ErrorResponse(err) return util.ErrorResponse(err)
} }
return GetDisplayName(req, accountDB, vars["userID"], asAPI) return GetDisplayName(req, accountDB, &cfg, vars["userID"], asAPI, federation)
}), }),
).Methods(http.MethodGet, http.MethodOptions) ).Methods(http.MethodGet, http.MethodOptions)

View file

@ -50,7 +50,7 @@ func SendEvent(
) util.JSONResponse { ) util.JSONResponse {
if txnID != nil { if txnID != nil {
// Try to fetch response from transactionsCache // Try to fetch response from transactionsCache
if res, ok := txnCache.FetchTransaction(*txnID); ok { if res, ok := txnCache.FetchTransaction(device.AccessToken, *txnID); ok {
return *res return *res
} }
} }
@ -83,7 +83,7 @@ func SendEvent(
} }
// Add response to transactionsCache // Add response to transactionsCache
if txnID != nil { if txnID != nil {
txnCache.AddTransaction(*txnID, &res) txnCache.AddTransaction(device.AccessToken, *txnID, &res)
} }
return res return res

View file

@ -22,7 +22,14 @@ import (
// DefaultCleanupPeriod represents the default time duration after which cacheCleanService runs. // DefaultCleanupPeriod represents the default time duration after which cacheCleanService runs.
const DefaultCleanupPeriod time.Duration = 30 * time.Minute const DefaultCleanupPeriod time.Duration = 30 * time.Minute
type txnsMap map[string]*util.JSONResponse type txnsMap map[CacheKey]*util.JSONResponse
// CacheKey is the type for the key in a transactions cache.
// This is needed because the spec requires transaction IDs to have a per-access token scope.
type CacheKey struct {
AccessToken string
TxnID string
}
// Cache represents a temporary store for response entries. // Cache represents a temporary store for response entries.
// Entries are evicted after a certain period, defined by cleanupPeriod. // Entries are evicted after a certain period, defined by cleanupPeriod.
@ -50,14 +57,14 @@ func NewWithCleanupPeriod(cleanupPeriod time.Duration) *Cache {
return &t return &t
} }
// FetchTransaction looks up an entry for txnID in Cache. // FetchTransaction looks up an entry for the (accessToken, txnID) tuple in Cache.
// Looks in both the txnMaps. // Looks in both the txnMaps.
// Returns (JSON response, true) if txnID is found, else the returned bool is false. // Returns (JSON response, true) if txnID is found, else the returned bool is false.
func (t *Cache) FetchTransaction(txnID string) (*util.JSONResponse, bool) { func (t *Cache) FetchTransaction(accessToken, txnID string) (*util.JSONResponse, bool) {
t.RLock() t.RLock()
defer t.RUnlock() defer t.RUnlock()
for _, txns := range t.txnsMaps { for _, txns := range t.txnsMaps {
res, ok := txns[txnID] res, ok := txns[CacheKey{accessToken, txnID}]
if ok { if ok {
return res, true return res, true
} }
@ -65,13 +72,13 @@ func (t *Cache) FetchTransaction(txnID string) (*util.JSONResponse, bool) {
return nil, false return nil, false
} }
// AddTransaction adds an entry for txnID in Cache for later access. // AddTransaction adds an entry for the (accessToken, txnID) tuple in Cache.
// Adds to the front txnMap. // Adds to the front txnMap.
func (t *Cache) AddTransaction(txnID string, res *util.JSONResponse) { func (t *Cache) AddTransaction(accessToken, txnID string, res *util.JSONResponse) {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
t.txnsMaps[0][txnID] = res t.txnsMaps[0][CacheKey{accessToken, txnID}] = res
} }
// cacheCleanService is responsible for cleaning up entries after cleanupPeriod. // cacheCleanService is responsible for cleaning up entries after cleanupPeriod.

View file

@ -24,27 +24,54 @@ type fakeType struct {
} }
var ( var (
fakeAccessToken = "aRandomAccessToken"
fakeAccessToken2 = "anotherRandomAccessToken"
fakeTxnID = "aRandomTxnID" fakeTxnID = "aRandomTxnID"
fakeResponse = &util.JSONResponse{Code: http.StatusOK, JSON: fakeType{ID: "0"}} fakeResponse = &util.JSONResponse{
Code: http.StatusOK, JSON: fakeType{ID: "0"},
}
fakeResponse2 = &util.JSONResponse{
Code: http.StatusOK, JSON: fakeType{ID: "1"},
}
) )
// TestCache creates a New Cache and tests AddTransaction & FetchTransaction // TestCache creates a New Cache and tests AddTransaction & FetchTransaction
func TestCache(t *testing.T) { func TestCache(t *testing.T) {
fakeTxnCache := New() fakeTxnCache := New()
fakeTxnCache.AddTransaction(fakeTxnID, fakeResponse) fakeTxnCache.AddTransaction(fakeAccessToken, fakeTxnID, fakeResponse)
// Add entries for noise. // Add entries for noise.
for i := 1; i <= 100; i++ { for i := 1; i <= 100; i++ {
fakeTxnCache.AddTransaction( fakeTxnCache.AddTransaction(
fakeAccessToken,
fakeTxnID+string(i), fakeTxnID+string(i),
&util.JSONResponse{Code: http.StatusOK, JSON: fakeType{ID: string(i)}}, &util.JSONResponse{Code: http.StatusOK, JSON: fakeType{ID: string(i)}},
) )
} }
testResponse, ok := fakeTxnCache.FetchTransaction(fakeTxnID) testResponse, ok := fakeTxnCache.FetchTransaction(fakeAccessToken, fakeTxnID)
if !ok { if !ok {
t.Error("Failed to retrieve entry for txnID: ", fakeTxnID) t.Error("Failed to retrieve entry for txnID: ", fakeTxnID)
} else if testResponse.JSON != fakeResponse.JSON { } else if testResponse.JSON != fakeResponse.JSON {
t.Error("Fetched response incorrect. Expected: ", fakeResponse.JSON, " got: ", testResponse.JSON) t.Error("Fetched response incorrect. Expected: ", fakeResponse.JSON, " got: ", testResponse.JSON)
} }
} }
// TestCacheScope ensures transactions with the same transaction ID are not shared
// across multiple access tokens.
func TestCacheScope(t *testing.T) {
cache := New()
cache.AddTransaction(fakeAccessToken, fakeTxnID, fakeResponse)
cache.AddTransaction(fakeAccessToken2, fakeTxnID, fakeResponse2)
if res, ok := cache.FetchTransaction(fakeAccessToken, fakeTxnID); !ok {
t.Errorf("failed to retrieve entry for (%s, %s)", fakeAccessToken, fakeTxnID)
} else if res.JSON != fakeResponse.JSON {
t.Errorf("Wrong cache entry for (%s, %s). Expected: %v; got: %v", fakeAccessToken, fakeTxnID, fakeResponse.JSON, res.JSON)
}
if res, ok := cache.FetchTransaction(fakeAccessToken2, fakeTxnID); !ok {
t.Errorf("failed to retrieve entry for (%s, %s)", fakeAccessToken, fakeTxnID)
} else if res.JSON != fakeResponse2.JSON {
t.Errorf("Wrong cache entry for (%s, %s). Expected: %v; got: %v", fakeAccessToken, fakeTxnID, fakeResponse2.JSON, res.JSON)
}
}

View file

@ -15,9 +15,14 @@
package common package common
import ( import (
"errors"
"strconv" "strconv"
) )
// 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 // AccountData represents account data sent from the client API server to the
// sync API server // sync API server
type AccountData struct { type AccountData struct {

View file

@ -159,6 +159,7 @@ Inbound federation rejects remote attempts to kick local users to rooms
An event which redacts itself should be ignored An event which redacts itself should be ignored
A pair of events which redact each other should be ignored A pair of events which redact each other should be ignored
Full state sync includes joined rooms Full state sync includes joined rooms
A message sent after an initial sync appears in the timeline of an incremental sync.
Can add tag Can add tag
Can remove tag Can remove tag
Can list tags for a room Can list tags for a room
@ -166,5 +167,6 @@ Tags appear in an initial v2 /sync
Newly updated tags appear in an incremental v2 /sync Newly updated tags appear in an incremental v2 /sync
Deleted tags appear in an incremental v2 /sync Deleted tags appear in an incremental v2 /sync
/event/ on non world readable room does not work /event/ on non world readable room does not work
Outbound federation can query profile data
/event/ on joined room works /event/ on joined room works
/event/ does not allow access to events before the user joined /event/ does not allow access to events before the user joined